FC2ブログ

スポンサーサイト

上記の広告は1ヶ月以上更新のないブログに表示されています。
新しい記事を書く事で広告が消せます。

asyncとIProgressを使ってプログレスバーを操作する

 今現在、C#で非同期処理をするのであればasync awaitを使用することが多いと思います。(読み書きしやすいですし)
 また、.NET 4.5からIProgress<T>が実装され、状況通知を行う際のインターフェイスもできました。
 これらを使って処理状況をプログレスバーで通知してみます。ついでに、キャンセル処理も組み込んでみました。
 (以下のサンプルコードにはLivet ver1.2を使用しています。)
 まずは、モデルに重たい処理を行うメソッドを作成します。
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

using Livet;

namespace AsyncProgress.Models
{
    // 通知内容
    public class ProgressInfo
    {
        public ProgressInfo(int value, string message)
        {
            Value = value;
            Message = message;
        }

        public int Value { get; private set; }
        public string Message { get; private set; }
    }

    public class Model : NotificationObject
    {
        private CancellationTokenSource _CancellationTokenSource;

        // 時間がかかる処理
        public async Task HeavyWork(IProgress<ProgressInfo> progress)
        {
            using(_CancellationTokenSource = new CancellationTokenSource())
            {
                try
                {
                    await Task.Run(() =>
                    {
                        foreach (var v in Enumerable.Range(1, 100))
                        {
                            // キャンセル処理
                            _CancellationTokenSource.Token.ThrowIfCancellationRequested();

                            // 重たい処理
                            Task.Delay(100).Wait();

                            // 状況通知
                            progress.Report(new ProgressInfo(v, string.Format("処理中{0}/{1}", v, 100)));
                        }
                    }, _CancellationTokenSource.Token);

                    // 作業終了後の処理
                    progress.Report(new ProgressInfo(0, "作業終了"));
                }
                catch(OperationCanceledException)
                {
                    // キャンセルされた場合
                    progress.Report(new ProgressInfo(0, "キャンセルされました"));
                    return ;
                }
            }   
        }

        // 処理をキャンセルする
        public void Cancel()
        {
            _CancellationTokenSource.Cancel();
        }
    }
}
 ProgressInfoは処理状況を伝えるための入れ物です。プログレスバー等で状況を通知する際に必要な情報を持たせます。
 ModelクラスのHeavyWorkメソッドが重たい処理を行うメソッドです。このメソッドの役割は、1)非同期で重たい処理を行う。2)処理状況を通知する。3)キャンセルされた場合は処理を中止する。の3つです。
 非同期処理はTaskを使用して行います。単純に、重たい処理のみを行うのであればasync awaitは必要ありません。(Taskを返せばOKです。)
 ですが、今回は作業が終わった後に終了処理(作業が終了したことを通知したい)を行いたいので、重たい処理(Task)をawaitしています。こうすることで、awaitが付いたTaskの処理が開始された時点で、一旦メソッドから抜けます。その後、Taskの処理が終了したら、続きの処理(この例の場合は、作業終了の通知を行う)が実行されます。
 非同期処理を同期処理と同じような感覚でコードを書けるので読み書きしやすいコードになります。
 async awaitを使用する場合は、基本的にTask又はTask<T>を返すようにしましょう。async voidにしてしまうと処理を待機することができなくなります。(投げっぱなしになります。イベントならそれでも良いのですが、普段使う場合はいつ終わるのかがわからなくなるのでやめたほうがよいです。)

 状況通知ですが、IProgress<T>は状況通知を行うためのReportメソッドを持っています。このメソッドに現在の処理状況を格納したインスタンスを渡すだけでOKです。
 ViewModel側でこの情報を受け取って、プログレスバー等で表示させます。

 キャンセル処理は、Taskによるキャンセルと同様にCancellationTokenSourceを使用します。
 _CancellationTokenSource.Cancel()メソッドが呼ばれた場合、_CancellationTokenSource.Token.ThrowIfCancellationRequested();は例外(OperationCanceledException)を返すため、処理が中止されます。あとは、try-catchでOperationCanceledExceptionをキャッチして例外処理を行えばOKです。

 次に、ViewModelを作成します。
using System;
using Livet;
using AsyncProgress.Models;

namespace AsyncProgress.ViewModels
{
    public class MainWindowViewModel : ViewModel
    {
        private Model _Model;
        private Progress<ProgressInfo> _Progress;

        public MainWindowViewModel()
        {
            _Model = new Model();

            // 状況通知処理を定義
            _Progress = new Progress<ProgressInfo>(e =>
            {
                ProgressValue = e.Value;
                Message = e.Message;
            });

            ProgressValue = 0;
            CanHeavyWork = true;
            CanCancel = false;
        }

        #region ProgressValue変更通知プロパティ
        private double _ProgressValue;

        // 処理状況を示す数値
        public double ProgressValue
        {
            get
            { return _ProgressValue; }
            set
            { 
                if (_ProgressValue == value)
                    return;
                _ProgressValue = value;
                RaisePropertyChanged();
            }
        }
        #endregion


        #region Message変更通知プロパティ
        private string _Message;

        // 状況を示すメッセージ
        public string Message
        {
            get
            { return _Message; }
            private set
            { 
                if (_Message == value)
                    return;
                _Message = value;
                RaisePropertyChanged();
            }
        }
        #endregion


        #region CanHeavyWork変更通知プロパティ
        private bool _CanHeavyWork;

        // 処理が可能かどうか
        public bool CanHeavyWork
        {
            get
            { return _CanHeavyWork; }
            private set
            { 
                if (_CanHeavyWork == value)
                    return;
                _CanHeavyWork = value;
                RaisePropertyChanged();
            }
        }
        #endregion

        #region CanCancel変更通知プロパティ
        private bool _CanCancel;

        // キャンセル可能かどうか
        public bool CanCancel
        {
            get
            { return _CanCancel; }
            private set
            { 
                if (_CanCancel == value)
                    return;
                _CanCancel = value;
                RaisePropertyChanged();
            }
        }
        #endregion

        // 重い処理開始
        public async void RunHeavyWork()
        {
            CanHeavyWork = false;
            CanCancel = true;

            await _Model.HeavyWork(_Progress);

            CanHeavyWork = true;
            CanCancel = false;
        }

        // 処理をキャンセルする
        public void Cancel()
        {
            CanCancel = false;
            _Model.Cancel(); 
        }
    }
}
 ViewModelでは状況通知用の変数や処理を行うためのメソッド等を作ります。
 ここで、通知された処理状況を受け取ってどう処理をするかを定義します。
 Progress<T>に、Reportメソッドから通知があった場合の処理を指定します。ここでは、ProgressValueとMessageの値を更新し、Viewの表示を更新させています。
 RunHeavyWorkメソッドは処理開始ボタンにバインドさせるメソッドです。処理中に再度RunHeavyWorkメソッドが実行されないように、また、処理中のみキャンセルボタンが押せるようにするために、CanHeavyWork, CanCancelの値を制御しています。
 RunHeavyWorkがasync voidになっているのは、メソッドバインディングする場合は戻り値がvoidの必要があるためです。(実質、このメソッドはクリックイベントと同じと見なせますので、async voidでも問題ありません。)

 最後にViewを作ります。
<Window x:Class="AsyncProgress.Views.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
        xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
        xmlns:l="http://schemas.livet-mvvm.net/2011/wpf"
        xmlns:v="clr-namespace:AsyncProgress.Views"
        xmlns:vm="clr-namespace:AsyncProgress.ViewModels"
        Title="MainWindow" Height="80" Width="225">
    
    <Window.DataContext>
        <vm:MainWindowViewModel/>
    </Window.DataContext>
    
    <i:Interaction.Triggers>
        <!--Windowが閉じたタイミングでViewModelのDisposeメソッドが呼ばれます-->
        <i:EventTrigger EventName="Closed">
            <l:DataContextDisposeAction/>
        </i:EventTrigger>
    </i:Interaction.Triggers>
    
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="20"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        
        <ProgressBar Grid.Row="0" Minimum="0" Maximum="100" Value="{Binding ProgressValue}"/>
        
        <StackPanel Grid.Row="1" HorizontalAlignment="Left" Orientation="Horizontal">
            <Button Content="処理開始" IsEnabled="{Binding CanHeavyWork}">
                <i:Interaction.Triggers>
                    <i:EventTrigger EventName="Click">
                        <l:LivetCallMethodAction MethodTarget="{Binding}" MethodName="RunHeavyWork"/>
                    </i:EventTrigger>
                </i:Interaction.Triggers>
            </Button>
            <Button Content="キャンセル" IsEnabled="{Binding CanCancel}">
                <i:Interaction.Triggers>
                    <i:EventTrigger EventName="Click">
                        <l:LivetCallMethodAction MethodTarget="{Binding}" MethodName="Cancel"/>
                    </i:EventTrigger>
                </i:Interaction.Triggers>
            </Button>
            <TextBlock Text="{Binding Message}"/>
        </StackPanel>
    </Grid>
</Window>
 これを実行すると、以下の様になります。
処理中 処理中
処理終了 処理が終了した場合
キャンセルした場合 キャンセルした場合
スポンサーサイト

コメントの投稿

非公開コメント

カレンダー
09 | 2018/10 | 11
- 1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31 - - -
全記事表示リンク

全ての記事を表示する

カテゴリ
タグリスト

月別アーカイブ
11  09  08  07  06  05  04  03  02  01  12  11  10  09  08  07  06  04  03  02  01  12  11  10  09  08  07  06  05  04  03  02  01  12  11  10  09 
最新記事
リンク
最新コメント
検索フォーム
Amazon
上記広告は1ヶ月以上更新のないブログに表示されています。新しい記事を書くことで広告を消せます。