sh1’s diary

プログラミング、読んだ本、資格試験、ゲームとか私を記録するところ

WPF Prism サンプルコードの学習4 (Commands)

f:id:shikaku_sh:20211013180838p:plain:w400

WPF + .NET Core (5以降は Core は省略される) で Prism を使ってみよう。

使用している Prism のバージョンは次のとおり:

Prism Full App (.NET Core) テンプレートを体験する」の記事も参考になると思います。

関連記事は以下:

11. DelegateCommands

ボタンのクリックイベントを Command に置き換えるための機能です。Command は自分でもわりと単純に自作することが可能なので、車輪の再開発をして動きを確認しておくと理解しやすい。

f:id:shikaku_sh:20211021174111g:plain:w500

<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
    <CheckBox IsChecked="{Binding IsEnabled}" Content="Can Execute Command" Margin="10"/>
    <Button Command="{Binding ExecuteDelegateCommand}" Content="DelegateCommand" Margin="10"/>
    <Button Command="{Binding DelegateCommandObservesProperty}" Content="DelegateCommand ObservesProperty" Margin="10"/>
    <Button Command="{Binding DelegateCommandObservesCanExecute}" Content="DelegateCommand ObservesCanExecute" Margin="10"/>
    <Button Command="{Binding ExecuteGenericDelegateCommand}" CommandParameter="Passed Parameter" Content="DelegateCommand Generic" Margin="10"/>
    <TextBlock Text="{Binding UpdateText}" Margin="10" FontSize="22"/>
</StackPanel>
public class MainWindowViewModel : BindableBase
{
    private bool _isEnabled;
    public bool IsEnabled
    {
        get { return _isEnabled; }
        set
        {
            SetProperty(ref _isEnabled, value);
            ExecuteDelegateCommand.RaiseCanExecuteChanged();
        }
    }

    private string _updateText;
    public string UpdateText
    {
        get { return _updateText; }
        set { SetProperty(ref _updateText, value); }
    }


    public DelegateCommand ExecuteDelegateCommand { get; private set; }

    public DelegateCommand<string> ExecuteGenericDelegateCommand { get; private set; }

    public DelegateCommand DelegateCommandObservesProperty { get; private set; }

    public DelegateCommand DelegateCommandObservesCanExecute { get; private set; }


    public MainWindowViewModel()
    {
        ExecuteDelegateCommand = new DelegateCommand(Execute, CanExecute);

        DelegateCommandObservesProperty = new DelegateCommand(Execute, CanExecute).ObservesProperty(() => IsEnabled);

        DelegateCommandObservesCanExecute = new DelegateCommand(Execute).ObservesCanExecute(() => IsEnabled);

        ExecuteGenericDelegateCommand = new DelegateCommand<string>(ExecuteGeneric).ObservesCanExecute(() => IsEnabled);
    }

    private void Execute()
    {
        UpdateText = $"Updated: {DateTime.Now}";
    }

    private void ExecuteGeneric(string parameter)
    {
        UpdateText = parameter;
    }

    private bool CanExecute()
    {
        return IsEnabled;
    }
}

IsEnabled の動きは、Xaml のコード上からだとわかりづらいが、C# のコード上からだと DelegateCommand の引数で制御していることがわかる。ここは好みがわかれると思う。

補足しておくべきコマンドは ExecuteGenericDelegateCommand。CommandProperty で Passed Parameter を返却し、C# コード側でジェネリック とすることで、データを受け取っている。イベントの感覚だと、どこでイベントが発生したのか引数 owner のようなもので、親コントロールを受け取ることができた。しかし、疎結合を意識するつくりのため、デフォルトだと引数が存在していない。

たとえば、ポップアップウィンドウを表示したいときなどは、親ウィンドウのハンドルが必要になる可能性が高い。そうした際は、ジェネリック型で受け取る、または、あらかじめクラス変数として取得するなど、工夫する必要があると思う。

どの程度疎結合にする必要があるのか、によって View と ViewModel の結合度を調整するのがよいと思います。(第何回か忘れましたが、わんくま勉強会でこのような意見があったのを覚えています)

12. UsingCompositeCommands

このプロジェクトの構成は、Prism Full App テンプレートの構成についての理解がないと、わかりづらい者だと思います。これについては以下のとおり:

実際のうごきをまず確認します:

ボタンを押すと日付が各タブに入力されます。親ボタンを押すと3つすべてのタブの日付が更新されます。

プロジェクトがどのようになっているのか整理します。

エントリーポイントは、いつもどおり App.xaml.cs からスタート。モジュールを追加しているので、View と ViewModel が存在することがわかります。また、RegisterTypes で IApplicationCommands というものを DI に登録しているのがわかります。

public partial class App : PrismApplication
{
    protected override Window CreateShell()
    {
        return Container.Resolve<MainWindow>();
    }

    protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog)
    {
        moduleCatalog.AddModule<ModuleA.ModuleAModule>();
    }

    protected override void RegisterTypes(IContainerRegistry containerRegistry)
    {
        containerRegistry.RegisterSingleton<IApplicationCommands, ApplicationCommands>();
    }
}

IApplicationCommands は、別のプロジェクトに分けて設計されており具体的な内容はつぎのとおり:

public interface IApplicationCommands
{
    CompositeCommand SaveCommand { get; }
}

public class ApplicationCommands : IApplicationCommands
{
    private CompositeCommand _saveCommand = new CompositeCommand();
    public CompositeCommand SaveCommand
    {
        get { return _saveCommand; }
    }
}

この部分は、Full App を見ておくとよい部分。

つぎの流れに進みます。ModuleAModule が呼び出されて View と ViewModel の準備が始まります。ここでは、同じ View 3つが同時に生成されて、同じ ViewModel クラスがそれぞれに設定されます。(インスタンスは別々)

public class ModuleAModule : IModule
{
    public void OnInitialized(IContainerProvider containerProvider)
    {
        var regionManager = containerProvider.Resolve<IRegionManager>();
        IRegion region = regionManager.Regions["ContentRegion"];

        var tabA = containerProvider.Resolve<TabView>();
        SetTitle(tabA, "Tab A");
        region.Add(tabA);

        var tabB = containerProvider.Resolve<TabView>();
        SetTitle(tabB, "Tab B");
        region.Add(tabB);

        var tabC = containerProvider.Resolve<TabView>();
        SetTitle(tabC, "Tab C");
        region.Add(tabC);
    }

    void SetTitle(TabView tab, string title)
    {
        (tab.DataContext as TabViewModel).Title = title;
    }
}

この時点で、Prism 独特のコーディング手法になっているのがわかると思います。

View に紐づいた ViewModel を初期化する際に DI を通じて IApplicationCommands を受け取っています。各 Tab 内にあるボタンは UpdateCommand を実行している一方で、UpdateCommand コマンドは IApplicationCommands に登録されます。

View に設定された ViewModel はこれ:

public class TabViewModel : BindableBase
{
    IApplicationCommands _applicationCommands;

    private string _title;
    public string Title
    {
        get { return _title; }
        set { SetProperty(ref _title, value); }
    }

    private bool _canUpdate = true;
    public bool CanUpdate
    {
        get { return _canUpdate; }
        set { SetProperty(ref _canUpdate, value); }
    }

    private string _updatedText;
    public string UpdateText
    {
        get { return _updatedText; }
        set { SetProperty(ref _updatedText, value); }
    }

    public DelegateCommand UpdateCommand { get; private set; }

    public TabViewModel(IApplicationCommands applicationCommands)
    {
        _applicationCommands = applicationCommands;

        UpdateCommand = new DelegateCommand(Update).ObservesCanExecute(() => CanUpdate);

        _applicationCommands.SaveCommand.RegisterCommand(UpdateCommand);
    }

    private void Update()
    {
        UpdateText = $"Updated: {DateTime.Now}";
    }
}

ここで ApplicationCommands はどうなっているのか、という話になります。MainWindow の XAML を確認すると、このコマンドが利用されていることがわかります。

<Window x:Class="UsingCompositeCommands.Views.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:prism="http://prismlibrary.com/"
        prism:ViewModelLocator.AutoWireViewModel="True"
        Title="{Binding Title}" Height="350" Width="525">

    <Window.Resources>
        <Style TargetType="TabItem">
            <Setter Property="Header" Value="{Binding DataContext.Title}" />
        </Style>
    </Window.Resources>
    
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>

        <Button Content="Save" Margin="10" Command="{Binding ApplicationCommands.SaveCommand}"/>

        <TabControl Grid.Row="1" Margin="10" prism:RegionManager.RegionName="ContentRegion" />
    </Grid>
</Window>

MainWindow の ViewModel はこれ:

public class MainWindowViewModel : BindableBase
{
    private string _title = "Prism Unity Application";
    public string Title
    {
        get { return _title; }
        set { SetProperty(ref _title, value); }
    }

    private IApplicationCommands _applicationCommands;
    public IApplicationCommands ApplicationCommands
    {
        get { return _applicationCommands; }
        set { SetProperty(ref _applicationCommands, value); }
    }

    public MainWindowViewModel(IApplicationCommands applicationCommands)
    {
        ApplicationCommands = applicationCommands;
    }
}

DI で受け取った ApplicationCommands をコマンドのプロパティとして公開しています。つまり、各タブ3つの UpdateCommand をまとめたコマンドが ApplicationCommands であり、これのことが Composite Commands(複合コマンド)というわけです。

今までのサンプルでやってきたことを組み合わせて使うサンプルなので、複雑なものだと思います。なので、ひとつひとつ理解を進める必要があり、理解に不足のある機能は振り返りましょう。

補足として、いつもの StackPanel に ContentRegion を適用しているのではなくて、TabControl に ContentRegion を適用して、TabView を3つ追加した、というところも Prism の機能を使いこなす上ではポイントになると思います。

機能としても便利なので理解しておくほうがよいです。

13. IActiveAwareCommands

UsingCompositeCommands とほぼ同じプロパティ構成になっているので、12 を先に理解していないとどうにもなりません。

かなりわかりづらいですが、 IApplicationCommands に違いがあります。

public interface IApplicationCommands
{
    CompositeCommand SaveCommand { get; }
}

public class ApplicationCommands : IApplicationCommands
{
    private CompositeCommand _saveCommand = new CompositeCommand(true);
    public CompositeCommand SaveCommand
    {
        get { return _saveCommand; }
    }
}

変更点はここ:

  • new CompositeCommand(true);

この true の箇所は、monitorCommandActivity 引数です。

Indicates when the command activity is going to be monitored.

コマンドアクティビティを監視し、コマンドを実行するかどうかの判定が入るようになります。

判定には IActiveAware を利用しているので、定義を確認します:

public interface IActiveAware
{
    // 概要:
    //     Gets or sets a value indicating whether the object is active.
    // 値:
    //     true if the object is active; otherwise false.
    bool IsActive { get; set; }

    // 概要:
    //     Notifies that the value for Prism.IActiveAware.IsActive property has changed.
    event EventHandler IsActiveChanged;
}

ViewModel の実装箇所はこのとおり:

public class TabViewModel : BindableBase, IActiveAware
{
    bool _isActive;
    public bool IsActive
    {
        get { return _isActive; }
        set
        {
            _isActive = value;
            OnIsActiveChanged();
        }
    }
    private void OnIsActiveChanged()
    {
        UpdateCommand.IsActive = IsActive;

        IsActiveChanged?.Invoke(this, new EventArgs());
    }

    public event EventHandler IsActiveChanged;
}

タブを切り替えたときに OnIsActiveChanged が呼び出され、UpdateCommand.IsActive の値が更新されます。非表示のときはコマンド自体の IsActive プロパティが false になっているため、実行されないという流れです。

monitorCommandActivity 引数は前提条件です。設定しなくても IsActive に関するプロパティやメソッドは実行されますが、IActiveAwareCommands と見なさず、CompositeCommands として動作します。

まとめ

コマンドの使い方についてまとめた。

  • 基本となるコマンドは DelegateCommand
    • DelegateCommand にはジェネリック型があり、引数をとることができる
  • コマンドは CompositeCommands を使うことでコマンドをまとめることができる
    • まとめ方には DI が利用されている
    • 実行可否を細かく設定するときは IActiveAwareCommands を利用する

参考