🌖

ボタン処理中はボタンを無効化する(WinUI 3 同時実行制御)

に公開

はじめに

ボタンクリック時の処理が長時間かかる際、処理が終わるまでは再度のボタンクリックを禁止したい(ボタンを無効化したい)ことがあります。例えば、再入可能「ではない」関数を呼びだしている場合や、2 度目以降の実行が無意味な場合などです。

逆に、ボタンクリックを禁止せずに複数同時実行させても良い場合もあります。

本稿では WinUI 3 + MVVM Toolkit における同時実行制御(排他制御)について整理しました。仕組みの理解のために基本から整理していますが、結論だけ知りたい場合は「基本」を飛ばして「簡略化」をご覧ください。

同時実行禁止

基本(同時実行禁止)

WinUI 3 + MVVM Toolkit では同時実行禁止は簡単で、以下のようにします。

MainPage.xaml
<Page
    x:Class="Test.Views.MainWindows.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    mc:Ignorable="d">
  <StackPanel >
    <Button Content="テスト" Command="{x:Bind ViewModel.ButtonTestClickedCommand, Mode=OneWay}" />
  </StackPanel>
</Page>
MainPage.xaml.cs
internal sealed partial class MainPage : Page
{
 public MainPageViewModel ViewModel
 {
  get;
 }

 public MainPage()
 {
  ViewModel = new MainPageViewModel();
  InitializeComponent();
 }
}
MainPageViewModel.cs
internal partial class MainPageViewModel : ObservableRecipient
{
  public MainPageViewModel()
  {
    ButtonTestClickedCommand = new(ButtonTestClicked);
  }

  public AsyncRelayCommand ButtonTestClickedCommand
  {
    get;
  }

  private async Task ButtonTestClicked()
  {
    Debug.WriteLine($"時間のかかる処理 開始 {Environment.TickCount}");
    await Task.Delay(3000);
    Debug.WriteLine($"時間のかかる処理 終了 {Environment.TickCount}");
  }
}

バックグラウンドで実行している時間のかかる処理(ここでは単に Task.Delay() ですが)を await で待機しています。

明示的なボタン無効化処理を記述しなくても、自動的にボタンクリック後にボタンが無効化され、処理完了後に再度ボタンが有効化されます。

ポイントは RelayCommand ではなく AsyncRelayCommand を使用していることで、これにより期待通りにボタンの有効無効が切り替わります。

簡略化(同時実行禁止)

上記のコードと同じ内容(自動的なボタン無効化)を [RelayCommand] 属性を使うことでシンプルに記述できます。MainPage.xaml MainPage.xaml.cs は上記と変わらず、MainPageViewModel.cs を以下のようにします。先頭の internal partial class MainPageViewModel 部分は省略しています(以降同様)。

MainPageViewModel.cs
[RelayCommand]
private async Task ButtonTestClicked()
{
  Debug.WriteLine($"時間のかかる処理 開始 {Environment.TickCount}");
  await Task.Delay(3000);
  Debug.WriteLine($"時間のかかる処理 終了 {Environment.TickCount}");
}

[RelayCommand] 属性が「メソッド名+Command」、つまり ButtonTestClickedCommand を自動生成してくれます。なお、属性の場合は [AsyncRelayCommand] ではなく [RelayCommand] です。

同時実行許可

基本(同時実行許可)

処理中もボタンを無効化せず、何度もクリックできるようにしたい場合、MainPageViewModel.cs を以下のようにします。

AsyncRelayCommand のコンストラクターに AsyncRelayCommandOptions.AllowConcurrentExecutions オプションを付けるだけの違いで、他は変わりません。

MainPageViewModel.cs
public MainPageViewModel()
{
  ButtonTestClickedCommand = new(ButtonTestClicked, AsyncRelayCommandOptions.AllowConcurrentExecutions);
}

public AsyncRelayCommand ButtonTestClickedCommand
{
  get;
}

private async Task ButtonTestClicked()
{
  Debug.WriteLine($"時間のかかる処理 開始 {Environment.TickCount}");
  await Task.Delay(3000);
  Debug.WriteLine($"時間のかかる処理 終了 {Environment.TickCount}");
}

もしくは、AsyncRelayCommand の代わりに RelayCommand を用いてもボタンは無効化されません。

MainPageViewModel.cs
public MainPageViewModel()
{
  ButtonTestClickedCommand = new(ButtonTestClicked);
}

public RelayCommand ButtonTestClickedCommand
{
  get;
}

private async Task ButtonTestClicked()
{
  Debug.WriteLine($"時間のかかる処理 開始 {Environment.TickCount}");
  await Task.Delay(3000);
  Debug.WriteLine($"時間のかかる処理 終了 {Environment.TickCount}");
}

簡略化(同時実行許可)

[RelayCommand] 属性もボタンを無効化しないことができます。

AllowConcurrentExecutions オプションを付けるだけです。

MainPageViewModel.cs
[RelayCommand(AllowConcurrentExecutions = true)]
private async Task ButtonTestClicked()
{
  Debug.WriteLine($"時間のかかる処理 開始 {Environment.TickCount}");
  await Task.Delay(3000);
  Debug.WriteLine($"時間のかかる処理 終了 {Environment.TickCount}");
}

途中で有効無効を切り替える

レアケースとは思いますが、前半は同時実行禁止(ボタン無効化)、後半は同時実行許可(ボタン有効化)といったことも可能です。

MainPageViewModel.cs
[RelayCommand(AllowConcurrentExecutions = true, CanExecute = nameof(CanExecuteButtonTestClickedCommand))]
private async Task ButtonTestClicked()
{
  CanExecuteButtonTestClickedCommand = false;
  Debug.WriteLine($"時間のかかる処理 前半 {Environment.TickCount}");
  await Task.Delay(3000);

  CanExecuteButtonTestClickedCommand = true;
  Debug.WriteLine($"時間のかかる処理 後半 {Environment.TickCount}");
  await Task.Delay(3000);
}

[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(ButtonTestClickedCommand))]
public partial Boolean CanExecuteButtonTestClickedCommand
{
  get;
  set;
} = true;

CanExecuteButtonTestClickedCommand プロパティーを作成し、ButtonTestClickedCommandCanExecuteButtonTestClickedCommand が相互参照するように属性の設定を行います。

CanExecuteButtonTestClickedCommand を false にすればボタンが無効となり、CanExecuteButtonTestClickedCommand を true にすればボタンが有効となります。

本番コードでは、finally 節でも CanExecuteButtonTestClickedCommand を true にするほうが良いかと思います。

なお、上記の形式で [ObservableProperty] を使用するためには、csproj の設定に <LangVersion> の設定が必要となります(恐らく次回の MVVM Toolkit リリースで不要になるのではないかと思います)。

<PropertyGroup>
  <LangVersion>preview</LangVersion>
</PropertyGroup>

まとめ

[RelayCommand] 属性の AllowConcurrentExecutions オプションにより、ボタン処理中にボタンを無効化するか否かを簡単に切り替えられます。

  • AllowConcurrentExecutions オプションを付けない ➡ 処理中はボタン無効化(同時実行禁止)
  • AllowConcurrentExecutions を true にする ➡ 処理中もボタンは有効のまま(同時実行許可)
[RelayCommand(AllowConcurrentExecutions = true)]

補足

本稿はあくまでもボタンの有効無効切り替えによる同時実行制御についてで、言うなれば「入口による排他制御」です。

ボタン以外からも同じ処理を呼ぶなど、処理本体(入口ではなく中身)に排他制御が必要な場合は lock などが必要となります。

確認環境

項目 環境
OS Windows 11 Pro 25H2
Visual Studio 2026 18.2.1
.NET 10.0
Template Studio for WinUI 5.5
WinUIEx 2.9.0
Windows App SDK 1.8.260101001 (1.8.4)
CommunityToolkit.Mvvm 8.4.0

参考リンク

Discussion