WPF MVVMサンプル(進捗表示)

2021/07/13に公開

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)を参考にプロジェクトを作成してください。

https://zenn.dev/apterygiformes/articles/79a7c9e7e15106

新規プロジェクトで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.csProgressInfo.csを追加します。
ViewModelsフォルダにクラスMainWindowViewModel.csを追加します。
Viewsフォルダにウィンドウ(WPF)MainWindow.xamlを追加します。

7)画面デザイン変更

App.xaml
xmlns:ui="http://schemas.modernwpf.com/2019"を追加し、
StartupUriViews/MainWindow.xamlに修正し、
Application.ResourcesResourceDictionaryを追加します。

App.xaml
<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"を追加します。

MainWindow.xaml
<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の作成

まず進捗情報を保持するクラスを作成します。

ProgressInfo.cs
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;
        }
    }
}

次に時間のかかる処理を実行するクラスを作成します。

HeavyWork.cs
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を継承するようにします。
  • 処理中か否かを表すフラグを定義します。
  • 進捗の値(数値)と進捗のテキストを定義します。
  • 処理実行とキャンセルのコマンドを定義します。
MainWindowViewModel.cs
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~を追加します。
Width350, Height200に編集します。

  • Window.DataContextにVMを設定します。
  • Gridを2行に分割します。
  • 上の行に進捗表示のコントロールを配置します。
  • 下の行に実行、キャンセルボタンを配置します。
MainWindow.xaml
<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)参考

https://github.com/emu2021makuake/MVVMSample007

Discussion