LoginSignup
0
4

C# Maui/Xamarin/Blazorで、CollectionViewに配置したボタンのクリックCommandを1つのクラスで実現する

Last updated at Posted at 2023-05-30

はじめに

皆さんこんにちは。Kazuyaです。

今日は、MauiやXamarin、Blazorで見かける、
動的にViewModelのデータを用いてボタンをコレクション形式で配置する際のお話。

「System.Windows.Input.ICommand」を実装したクラスを使うのだけど、
そのICommandを継承したクラスを、ボタンのアクションごとに個別実装すると、クラスが爆発していやだ!
と。。。

僕はいつも、動的に作られるもののために静的なクラスを用意するのが嫌でして。
かといって、ICommand#Executeの中で、パラメータを用いて、処理を振り分けるなんてもっての外。

ということで、以下のようにします。
<ポリシー>
・コマンドリストを作成するときに、実行したいアクションをコマンドのインスタンスに設定できるようにする
・ボタンクリック時には、Executeを経由して、コマンドに指定したアクションを呼び出す
・パラメータも指定できるようにする

実装

と、仰々しく言いましたが、簡単です。

ButtonClickedCommand.cs

    public class ButtonClickedCommand : ICommand
    {
        private T _baseParam;
        public ButtonClickedCommand(Object baseParam)
        {
            this._baseParam = baseParam;
        }
        public event EventHandler CanExecuteChanged;

        public bool CanExecute(object parameter)
        {
            //TODO: ご自由にどうぞ
            return !isDoing;
        }
        private bool isDoing = false;
        public async void Execute(object parameter)
        {
            //(おすすめは、UIスレッドでやるほうがいいですよ
            this.isDoing = true;

            var act = (Func<Object, Task<bool>>)parameter;
            await act(this._baseParam);

            //(おすすめは、UIスレッドでやるほうがいいですよ
            this.isDoing = false;
        }
    }

ま、こんな感じです。

次いで、CollectionViewにバインドされるモデル側にリストとして定義されるフィールドのクラス

Command.cs

public class Command
{
        public ButtonClickedCommand ButtonClickedCommand { get; set; }
        public Func<Object, Task<bool>> ClickedCallBack { get; set; }

}

であとは、ViewModel側

ViewModel.cs
    public class ChatViewModel : ViewModel, INotifyPropertyChanged
    {
        public ObservableCollection<Command> Commands { get; set; } = new();

        public async void OnActivated(CollectionView cv)
        {
            this.CreateCommandButton();
        }

        private void CreateCommandButton() {
            this.Commands.Add(new Command()
                {
                    //ここがクリックされたときに発動する
                    ClickedCallBack = new Func<Object, Task<bool>>(async (param) =>
                    {
                        var data = (String)param;
                        await mv._helper.RunOnUiThreadAsync(() =>
                        {
                            //UIスレッドで同期でやりたいこと
                            Console.Write(data);
                        });
                        //こっちは非同期でやりたいこと
                        Console.Write(data);
                        return true;
                    }),
                    ButtonClickedCommand = new ButtonClickedCommand("Test1"),//パラメータはここで渡す
                });
        }
    }

で、最後 View側

Test.xaml
<CollectionView ItemsSource="{Binding Commands}">
    <CollectionView.ItemTemplate>
        <DataTemplate>
            <ImageButton Source="{Binding IconName}" 
                         Command="{Binding ButtonClickedCommand}"
                         CommandParameter="{Binding ClickedCallBack}" ></ImageButton>

        </DataTemplate>
    </CollectionView.ItemTemplate>
</CollectionView>

とまぁ、ザックリこんな感じです。
このコマンドを追加する処理を動的に行ったりすることで、
ICommandを継承したクラスを何個も作る必要はなくなります。

ちょっと自分のコードを、公開用に一部書き換えながら書きましたので、動作確認しておらずですので、
もし不明点あれば、コメントくださいませー

終わりに

「ボタンクリック時の処理」という同じ枠組みなのに、何個も何個も「クラス」を作ることに難色を僕は示すので、
Action / Func / Task あたりをうまく使って、関数ポインタを渡すっていうのが一昔前から結構好きです。
とはいえ、あまりに重たい処理とかはこれで渡すとよろしくないのと、
トレーサビリティ下がる!とか始まりそうですけど、そもそも「async」な時点で、
トレーサビリティは低いんですよw

ということで僕は、一貫性のあるまとめてかけるこの書き方、おすすめです。
使い道あれば、参考にしてみてくださいませー

それでは皆様、また会いましょう!

免責事項

本記事は、著者が独自で調査・検証を行った内容で、所属企業やいずれかの企業の公式見解に基づきません。
また、本記事を参考にして行った作業に関して発生するすべての損害、問題、課題について、当方では一切の責任を負えませんのであらかじめご了承ください。

0
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
4