RunningCSharp

.net中心の開発話。記事は個人の見解であり、所属組織を代表するものてはありません。

WPF:コマンド実行中は非活性となり、連続実行を抑止するボタン

わざわざIsEnabledプロパティをバインドしてコマンドにasync/awaitを書いて、連続クリックをロックで止めて…と書く画面に実装するのが面倒だと思い、作ってみました。

    public class AsyncButton : Button
    {
        /// <summary>
        /// Commandに値を入れられないようスコープを変更
        /// </summary>
        private new ICommand Command
        {
            get { return base.Command; }
            set { base.Command = value; }
        }

        /// <summary>
        /// 非同期実行用コマンド
        /// </summary>
        public ICommand AsyncCommand
        {
            get { return (ICommand)GetValue(AsyncCommandProperty); }
            set { SetValue(AsyncCommandProperty, value); }
        }

        /// <summary>
        /// キー連打による連続実行を止めるためのロックオブジェクト
        /// </summary>
        private object lockObject { get; set; }

        // Using a DependencyProperty as the backing store for AsyncCommand.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty AsyncCommandProperty =
            DependencyProperty.Register("AsyncCommand", typeof(ICommand), typeof(AsyncButton), new PropertyMetadata(null,OnAsyncCommandChanged));

        private static void OnAsyncCommandChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            AsyncButton btn = d as AsyncButton;
            //クリックイベントでEnterキー押下時などもハンドル可能なので、こちらを利用
            btn.Click += Btn_Click;
        }

        private static void Btn_Click(object sender, RoutedEventArgs e)
        {
            AsyncButton btn = sender as AsyncButton;
            ExecAsyncCommand(btn, btn.AsyncCommand, btn.CommandParameter);
        }

        private static async void ExecAsyncCommand(AsyncButton btn, ICommand command , object parameter)
        {
            if (btn.lockObject == null)
            {
                btn.lockObject = new object();
            }
            else
            {
                return;
            }
            await Task.Run(() =>
            {
                lock (btn.lockObject)
                {
                    btn.Dispatcher.Invoke(() => btn.IsEnabled = false);
                    command.Execute(parameter);
                    btn.Dispatcher.Invoke(() => btn.IsEnabled = true);
                }
                btn.lockObject = null;
            });
        }

使うときは、こんな感じで。

View

<local:AsyncButton AsyncCommand="{Binding MyCommand}" HorizontalAlignment="Center" VerticalAlignment="Center" Content="Test" />

ViewModel

    public class MyViewModel
    {
        private RelayCommand _myCommand = null;
        public RelayCommand MyCommand
        {
            get
            {
                return _myCommand ?? (_myCommand = new RelayCommand(() =>
                {
                    //重い処理
                    Thread.Sleep(10000);
                }));
            }
        }
    }