WPF MVVMサンプル(進捗表示)
1)概要
WPFでMVVMを使った簡単なサンプルを作ります。
実行ボタンを押すとプログレスバーの進捗が進みます。
キャンセルボタンで処理を中断することもできます。
MVVMライブラリにはWCT MVVM Toolkit(Microsoft.Toolkit.Mvvm)を使用します。
2)環境
- Windows 10 Version 21H1
- .NET Framework 4.7.2
- Visual Studio 2019 Version 16.10.3
- WPF
- Microsoft.Toolkit.Mvvm Version 7.0.2
- ModernWpfUI Version 0.9.4
3)プロジェクトの作成
広く普及している.NET Framework 4.7.2で作ります。
10 (1803) |
10 (1809) |
10 (1903) |
10 (1909) |
10 (2004) |
10 (20H2) |
10 (21H1) |
|
---|---|---|---|---|---|---|---|
4.7.2 | 〇 | 〇 | 〇 | 〇 | 〇 | 〇 | 〇 |
4.8 | - | - | 〇 | 〇 | 〇 | 〇 | 〇 |
5.0 | - | - | - | - | - | - | - |
.NET 5.0で作る場合はこちら↓の3)~4)を、.NET 6.0で作る場合は12-3)~12-4-a)を参考にプロジェクトを作成してください。
新規プロジェクトでWPFアプリ(.NET Framework)
を選択します。
プロジェクト名にProgressSample2
と入力し、フレームワークに.NET Framework 4.7.2
を選択します。
4)プロジェクトの設定
[ツール]-[オプション]-[NuGetパッケージマネージャー]-[既定のパッケージ管理方式]でPackageReference
を指定します。
5)NuGetパッケージ追加
以下のパッケージをNuGetで追加します。
Microsoft.Toolkit.Mvvm
ModernWpfUI
6)ファイル準備
MainWindow.xaml
を削除し、
プロジェクト直下にModels
フォルダ、ViewModels
フォルダ、Views
フォルダを作成します。
Models
フォルダにクラスHeavyWork.cs
とProgressInfo.cs
を追加します。
ViewModels
フォルダにクラスMainWindowViewModel.cs
を追加します。
Views
フォルダにウィンドウ(WPF)MainWindow.xaml
を追加します。
7)画面デザイン変更
App.xaml
に
xmlns:ui="http://schemas.modernwpf.com/2019"
を追加し、
StartupUri
をViews/MainWindow.xaml
に修正し、
Application.Resources
にResourceDictionary
を追加します。
<Application x:Class="ProgressSample2.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:ProgressSample2"
xmlns:ui="http://schemas.modernwpf.com/2019"
StartupUri="Views/MainWindow.xaml">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ui:ThemeResources />
<ui:XamlControlsResources />
<!-- Other merged dictionaries here -->
</ResourceDictionary.MergedDictionaries>
<!-- Other app resources here -->
</ResourceDictionary>
</Application.Resources>
</Application>
MainWindow.xaml
に
xmlns:ui="http://schemas.modernwpf.com/2019"
と
ui:WindowHelper.UseModernWindowStyle="True"
を追加します。
<Window x:Class="ProgressSample2.Views.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:ProgressSample2.Views"
xmlns:ui="http://schemas.modernwpf.com/2019"
ui:WindowHelper.UseModernWindowStyle="True"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
</Grid>
</Window>
8)Modelの作成
まず進捗情報を保持するクラスを作成します。
namespace ProgressSample2.Models
{
/// <summary>
/// 進捗情報
/// </summary>
public class ProgressInfo
{
/// <summary>
/// 進捗値
/// </summary>
public int ProgressValue { get; set; }
/// <summary>
/// 進捗テキスト
/// </summary>
public string ProgressText { get; set; }
/// <summary>
/// コンストラクタ
/// </summary>
/// <param name="value"></param>
/// <param name="text"></param>
public ProgressInfo(int value, string text)
{
ProgressValue = value;
ProgressText = text;
}
}
}
次に時間のかかる処理を実行するクラスを作成します。
using System;
using System.Threading;
using System.Threading.Tasks;
namespace ProgressSample2.Models
{
/// <summary>
/// 何か処理するクラス
/// </summary>
public class HeavyWork
{
private CancellationTokenSource _cancelTokenSrc;
/// <summary>
/// データ件数
/// </summary>
private static readonly int s_dataCount = 100;
/// <summary>
/// 時間のかかる処理を実行
/// </summary>
/// <param name="progress"></param>
/// <returns></returns>
public async Task ExecuteAsync(IProgress<ProgressInfo> progress)
{
using (_cancelTokenSrc = new CancellationTokenSource())
{
try
{
// 何か時間のかかる処理
for (int i = 0; i < s_dataCount; i++)
{
// キャンセルされたら例外発生
_cancelTokenSrc.Token.ThrowIfCancellationRequested();
await Task.Delay(50);
// 進捗状況通知
progress.Report(new ProgressInfo(
i + 1,
$"{i + 1} / {s_dataCount} 件処理"));
}
await Task.Delay(1000);
progress.Report(new ProgressInfo(
0,
"処理完了!"));
}
catch (OperationCanceledException)
{
progress.Report(new ProgressInfo(
0,
"処理をキャンセルしました。"));
return;
}
}
}
/// <summary>
/// 処理キャンセル
/// </summary>
public void Cancel()
{
if (_cancelTokenSrc?.IsCancellationRequested == false)
{
_cancelTokenSrc.Cancel();
}
}
}
}
9)ViewModelの作成
MainWindowViewModel.cs
を以下のように編集します。
-
ObservableObject
を継承するようにします。 - 処理中か否かを表すフラグを定義します。
- 進捗の値(数値)と進捗のテキストを定義します。
- 処理実行とキャンセルのコマンドを定義します。
using Microsoft.Toolkit.Mvvm.ComponentModel;
using Microsoft.Toolkit.Mvvm.Input;
using ProgressSample2.Models;
using System;
using System.Threading.Tasks;
namespace ProgressSample2.ViewModels
{
public class MainWindowViewModel : ObservableObject
{
private bool _isBusy;
/// <summary>
/// 処理中フラグ
/// </summary>
public bool IsBusy
{
get => _isBusy;
set => SetProperty(ref _isBusy, value);
}
private int _progressValue;
/// <summary>
/// 進捗値
/// </summary>
public int ProgressValue
{
get => _progressValue;
set => SetProperty(ref _progressValue, value);
}
private string _progressText;
/// <summary>
/// 進捗テキスト
/// </summary>
public string ProgressText
{
get => _progressText;
set => SetProperty(ref _progressText, value);
}
/// <summary>
/// 実行コマンド
/// </summary>
public IAsyncRelayCommand ExecuteCommand { get; }
/// <summary>
/// キャンセルコマンド
/// </summary>
public IRelayCommand CancelCommand { get; }
private HeavyWork _model;
/// <summary>
/// コンストラクタ
/// </summary>
public MainWindowViewModel()
{
IsBusy = false;
// 実行コマンド初期化
ExecuteCommand = new AsyncRelayCommand(OnExecuteAsync, CanExecute);
// キャンセルコマンド初期化
CancelCommand = new RelayCommand(
execute: () =>
{
_model?.Cancel();
// コマンドの実行可否 更新
UpdateCommandStatus();
},
canExecute: () => IsBusy);
}
/// <summary>
/// 実行コマンドの処理
/// </summary>
/// <returns></returns>
private async Task OnExecuteAsync()
{
IsBusy = true;
// コマンドの実行可否 更新
UpdateCommandStatus();
_model = new HeavyWork();
var p = new Progress<ProgressInfo>();
p.ProgressChanged += (sender, e) =>
{
ProgressValue = e.ProgressValue;
ProgressText = e.ProgressText;
};
// 時間のかかる処理 開始
await _model.ExecuteAsync(p);
IsBusy = false;
// コマンドの実行可否 更新
UpdateCommandStatus();
}
/// <summary>
/// 実行コマンドの実行可否
/// </summary>
/// <returns></returns>
private bool CanExecute()
{
// 処理中でなければ実行可
return !IsBusy;
}
/// <summary>
/// コマンドの実行可否 更新
/// </summary>
private void UpdateCommandStatus()
{
ExecuteCommand.NotifyCanExecuteChanged();
CancelCommand.NotifyCanExecuteChanged();
}
}
}
10)Viewの作成
MainWindow.Xaml
を開き、
Window
に
xmlns:vm~
, d:DataContext~
を追加します。
CanResizeMode~
を追加します。
Width
を350
, Height
を200
に編集します。
-
Window.DataContext
にVMを設定します。 - Gridを2行に分割します。
- 上の行に進捗表示のコントロールを配置します。
- 下の行に実行、キャンセルボタンを配置します。
<Window
x:Class="ProgressSample2.Views.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:ProgressSample2.Views"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="http://schemas.modernwpf.com/2019"
xmlns:vm="clr-namespace:ProgressSample2.ViewModels"
d:DataContext="{d:DesignInstance Type=vm:MainWindowViewModel,
IsDesignTimeCreatable=True}"
Title="MainWindow"
ResizeMode="CanResizeWithGrip"
Width="350"
Height="200"
ui:WindowHelper.UseModernWindowStyle="True"
mc:Ignorable="d">
<Window.DataContext>
<vm:MainWindowViewModel />
</Window.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<StackPanel>
<!-- 進捗テキスト -->
<TextBlock
Margin="10,10,10,3"
HorizontalAlignment="Center"
Text="{Binding ProgressText, Mode=OneWay}" />
<!-- プログレスバー -->
<ui:ProgressBar Margin="10,0,10,10" Value="{Binding ProgressValue, Mode=OneWay}" />
</StackPanel>
<StackPanel Grid.Row="1" Orientation="Horizontal">
<Button
Width="85"
Margin="10"
Command="{Binding ExecuteCommand, Mode=OneTime}"
Content="実行"
IsDefault="True"
Style="{StaticResource AccentButtonStyle}" />
<Button
Width="85"
Margin="10"
Command="{Binding CancelCommand, Mode=OneTime}"
Content="キャンセル"
IsCancel="True" />
</StackPanel>
</Grid>
</Window>
完成です。
11)実行
実行してみます。
実行ボタンを押します。
プログレスバーがいっぱいになると処理が完了します。
もう一度実行ボタンを押して、少し待ってからキャンセルボタンを押してみます。
処理が途中で止まりました。
12)参考
Discussion