C#のWPFで実行と停止(キャンセル)ボタンを試作

コンピュータ

ソースコード

ファイル名:MainWindow.xaml

<Window x:Class="ExecuteButton.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:i="clr-namespace:Microsoft.Xaml.Behaviors;assembly=Microsoft.Xaml.Behaviors"
        xmlns:interactivity="clr-namespace:Reactive.Bindings.Interactivity;assembly=ReactiveProperty.WPF"
        xmlns:local="clr-namespace:ExecuteButton"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Window.DataContext>
        <local:MainWindowViewModel />
    </Window.DataContext>
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="Closed">
            <interactivity:EventToReactiveCommand Command="{Binding WindowClosedCommand}" />
        </i:EventTrigger>
    </i:Interaction.Triggers>
    <Grid>
        <WrapPanel>
            <Button
                Width="60"
                Height="30"
                Content="{Binding ExecuteContent.Value}"
                ToolTip="実行"
                Command="{Binding ExecuteCommand}" />
        </WrapPanel>
    </Grid>
</Window>

ファイル名:MainWindowViewModel.cs

using System;
using System.ComponentModel;
using Reactive.Bindings;
using Reactive.Bindings.Extensions;
using System.Reactive.Disposables;

using System.Diagnostics;

namespace ExecuteButton;

public class MainWindowViewModel : INotifyPropertyChanged, IDisposable
{
    public event PropertyChangedEventHandler? PropertyChanged;
    protected virtual void OnPropertyChanged(string name) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
    private CompositeDisposable Disposable { get; } = [];
    public void Dispose() => Disposable.Dispose();
    public ReactiveCommand<EventArgs> WindowClosedCommand { get; }
    public ReactiveCommand ExecuteCommand { get; } = new();
    public ReactiveProperty<string> ExecuteContent { get; set; } = new("▶");

    CancellationTokenSource? cts;
    async void ExecuteAction()
    {
        if (cts is not null)
        {
            cts?.Cancel();  // キャンセルを実行。■⇒▶
            while(cts is not null) await Task.Delay(10);
            return; // 戻る
        }

        ExecuteContent.Value = "■";
        cts = new();
        var result = await Task.Run(()=>
        {
            bool result = false;
            try
            {
                for(int i=0; i<100; i++)
                {
                    cts.Token.ThrowIfCancellationRequested();
                    Task.Delay(100).Wait();               
                    Debug.Print($"i={i}");
                }
                result = true;
            }
            catch (OperationCanceledException ex)
            {
                Debug.Print($"{ex.Message}");
            }
            return result;
        }, cts.Token);

        cts?.Dispose();
        cts = null;
        ExecuteContent.Value = "▶";
    }
    public MainWindowViewModel()
    {
        WindowClosedCommand = new ReactiveCommand<EventArgs>()
            .WithSubscribe(e => Disposable.Dispose());
        
        ExecuteCommand.Subscribe(_ => ExecuteAction());
    }
}

実行


「▶」ボタンを押すと「■」に切り替わり処理が実行される。

処理を実行中に「■」を押すとキャンセルが発行され処理が停止する。

説明

前回の時点でcts.Token.ThrowIfCancellationRequested()で例外を発生させて、awaitしている呼び出し元で例外をcatchし一気に戻るものだと思っていましたが、それだとcatchすることが出来ないので、Task.Runの中で例外をキャッチしそのまま戻るようにしてみました。

また、キャンセル後即再実行するような作りになっていましたが、キャンセルが実行されると一度終了し、再度「▶」ボタンを押すことで再実行する仕様に変更しました。

試作ということでTask.Delay(100).Wait();で重たい処理を表現していますが、本番でこの辺りがどのような動作になるのか要調査といった感じです。await、async、Tokenの流れが途切れないようにすれば良さそうな感じがします。

AsyncReactiveCommand

非同期版のReactiveCommandの存在を知り、置き換えてみたところ実行中はボタンが不活性化して停止ボタンとしては使えないようです。停止ボタンを別途用意してみました。executeFlagを用意し実行と停止ボタンを切り替えるつもりだったのですが、切り替えのロジックを組んだつもりが無いのに動作していて不思議な感じがします。

ソースコード

ファイル名:MainWindow.xaml

<Window x:Class="ExecuteButton.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:i="clr-namespace:Microsoft.Xaml.Behaviors;assembly=Microsoft.Xaml.Behaviors"
        xmlns:interactivity="clr-namespace:Reactive.Bindings.Interactivity;assembly=ReactiveProperty.WPF"
        xmlns:local="clr-namespace:ExecuteButton"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Window.DataContext>
        <local:MainWindowViewModel />
    </Window.DataContext>
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="Closed">
            <interactivity:EventToReactiveCommand Command="{Binding WindowClosedCommand}" />
        </i:EventTrigger>
    </i:Interaction.Triggers>
    <Grid>
        <WrapPanel>
            <Button
                Width="60"
                Height="30"
                Content="▶"
                ToolTip="実行"
                Command="{Binding ExecuteCommand}" />
            <Button
                Width="60"
                Height="30"
                Content="■"
                ToolTip="停止"
                Command="{Binding StopCommand}" />
        </WrapPanel>
    </Grid>
</Window>

ファイル名:MainWindowViewModel.cs

using System.ComponentModel;
using Reactive.Bindings;
using System.Reactive.Disposables;

using System.Diagnostics;
using System.Reactive.Linq;

namespace ExecuteButton;

public class MainWindowViewModel : INotifyPropertyChanged, IDisposable
{
    public event PropertyChangedEventHandler? PropertyChanged;
    protected virtual void OnPropertyChanged(string name) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
    private CompositeDisposable Disposable { get; } = [];
    public void Dispose() => Disposable.Dispose();
    public ReactiveCommand<EventArgs> WindowClosedCommand { get; }
    public AsyncReactiveCommand ExecuteCommand { get; }
    public AsyncReactiveCommand StopCommand { get; }
    public ReactiveProperty<string> ExecuteContent { get; set; } = new("");

    CancellationTokenSource? cts;
    async Task<bool> ExecuteActionAsync(CancellationToken token)
    {
        var result = await Task.Run(()=>
        {
            bool result = false;
            try
            {
                for(int i=0; i<100; i++)
                {
                    token.ThrowIfCancellationRequested();
                    Task.Delay(100).Wait();               
                    Debug.Print($"i={i}");
                }
                result = true;
            }
            catch (OperationCanceledException ex)
            {
                Debug.Print($"{ex.Message}");
            }
            return result;
        }, token);
        return result;
    }
    public MainWindowViewModel()
    {
        WindowClosedCommand = new ReactiveCommand<EventArgs>()
            .WithSubscribe(e => Disposable.Dispose());
        
        var executeFlag = new ReactiveProperty<bool>(true);

        ExecuteCommand = executeFlag
            .ToAsyncReactiveCommand()
            .WithSubscribe(async () =>
            {

                cts = new();

                var result = await ExecuteActionAsync(cts.Token);

                cts?.Dispose();
                cts = null;
            });
        StopCommand = executeFlag
            .Select(e => !e)
            .ToAsyncReactiveCommand()
            .WithSubscribe(async () =>
            {
                if (cts is not null)
                {
                    cts?.Cancel();  // キャンセルを実行。
                    while(cts is not null) await Task.Delay(10);
                    return; // 戻る
                }               
            });
    }
}


ボタンは増えましたが同じ動作をしているようです。

また、「メンバー ‘ExecuteActionAsync’ はインスタンス データにアクセスしないため、static にマークできます 」とのアドバイスを頂きましたので、この部分をViewModelの外に出すことができそうです。

コメント