08-ViewModelLocator

Descriptionには using the ViewModelLocator と書かれています。

このサンプルでは、アプリケーションのタイトルが変更されます。

Views\MainWindow.xaml

<Window
    x:Class="ViewModelLocator.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/"
    Title="{Binding Title}"
    Width="525"
    Height="350"
    prism:ViewModelLocator.AutoWireViewModel="True">
    <Grid>
        <ContentControl prism:RegionManager.RegionName="ContentRegion" />
    </Grid>
</Window>

ViewModels\MainWindowViewModel.cs

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

        public MainWindowViewModel() { }
    }
}

まあ、これ CommunityToolkit.Mvvm の覚え書き をきちんと読んでくれた人なら「ほとんど同じ」となりますよね?

CommunityToolkit.Mvvm では ObservableObject だったところが BindingBase になっているだけです。

違いは、View…ここでは MainWindow.xaml.cs に当たりますが、 ここで DataContext に設定しなくても動くことでしょうか?

個人的には、裏で勝手にやられすぎていてなんか嫌だな…って印象を受けます、ブラックボックス的じゃないですか。

09-ChangeConvention

Descriptionには Change the ViewModelLocator naming conventions とあります。

動きは 08-ViewModelLocator と同じなんですけど、気持ち悪いフォルダ構成になっています。 なんと ViewModel が Views フォルダに含まれているんですね。

その理由は以下にあります。

App.xaml.cs より抜粋

protected override void ConfigureViewModelLocator()
{
    base.ConfigureViewModelLocator();

    ViewModelLocationProvider.SetDefaultViewTypeToViewModelTypeResolver((viewType) =>
    {
        var viewName = viewType.FullName;
        var viewAssemblyName = viewType.GetTypeInfo().Assembly.FullName;
        var viewModelName = $"{viewName}ViewModel, {viewAssemblyName}";
        return Type.GetType(viewModelName);
    });
}

View のパスに「ViewModel」を追記して、ViewModelLocationProvider に登録しています。

ここから、デフォルトでは「Views/View名」に対して 「ViewModel/View名ViewModel」を割りあてていると推測できます。

…CommunityToolkit.Mvvm に慣れた俺からすると、本当に気持ち悪い挙動ですね。

MainWindow.xaml.cs で DataContext = Ioc.Default.GetService<IMainWindowViewModel>() 等を書くのは確かに面倒ですが、 Prism の動きは「これじゃない」感が酷いです。

10-CustomRegistrations

Descriptionには Manually register ViewModels for specific views とあります。

これはウィンドウタイトルを「Custom ViewModel Application」とします。

App.xaml.cs より抜粋

protected override void ConfigureViewModelLocator()
{
    base.ConfigureViewModelLocator();

    // type / type
    //ViewModelLocationProvider.Register(typeof(MainWindow).ToString(), typeof(CustomViewModel));

    // type / factory
    //ViewModelLocationProvider.Register(typeof(MainWindow).ToString(), () => Container.Resolve<CustomViewModel>());

    // generic factory
    //ViewModelLocationProvider.Register<MainWindow>(() => Container.Resolve<CustomViewModel>());

    // generic type
    ViewModelLocationProvider.Register<MainWindow, CustomViewModel>();
}

「generic type」の行をコメントアウトして、他の行の処理を使っても同じ動作をします。

CustomViewModel.cs

namespace ViewModelLocator.ViewModels
{
    public class CustomViewModel : BindableBase
    {
        private string _title = "Custom ViewModel Application";
        public string Title
        {
            get { return _title; }
            set { SetProperty(ref _title, value); }
        }

        public CustomViewModel() { }
    }
}

そうですよ、この手法の方がよっぽどいい。

むしろ、このような記述も必須にした方がいいのではないかと感じてしまいます。

ViewModelLocationProvider.Register<MainWindow, MainWindowViewModel>();

ViewModelLocator については以上です。

CommunityToolkit.Mvvm を使う場合は、DI にViewModelを注入して、 DataContext にViewModel のインターフェース経由でインスタンスを渡せばいいだけで、裏では基本的に何もしない(V8は色々やりますが)、手間は確かにかかるけど美しい設計ですね。

CommunityToolkit.Mvvm V8 を使う場合、CustomViewModel 自体がとんでもないことになりますね。

namespace ViewModelLocator.ViewModels
{
    public interface ICustomViewModel {}

    public partial class CustomViewModel : ObservableObject, ICustomViewModel
    {
        [ObservesProperty]
        private string _title = "Custom ViewModel Application";

        public CustomViewModel() { }
    }
}

次からは Command にまつわる内容になります。

11-UsingDelegateCommands

Descriptionには Use DelegateCommand and DelegateCommand<T> とあります。

MainWindow.xaml

<Window
    x:Class="UsingDelegateCommands.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/"
    Title="Using DelegateCommand"
    Width="350"
    Height="275"
    prism:ViewModelLocator.AutoWireViewModel="True">
    <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
        <CheckBox
            Margin="10"
            Content="Can Execute Command"
            IsChecked="{Binding IsEnabled}" />
        <Button
            Margin="10"
            Command="{Binding ExecuteDelegateCommand}"
            Content="DelegateCommand" />
        <Button
            Margin="10"
            Command="{Binding DelegateCommandObservesProperty}"
            Content="DelegateCommand ObservesProperty" />
        <Button
            Margin="10"
            Command="{Binding DelegateCommandObservesCanExecute}"
            Content="DelegateCommand ObservesCanExecute" />
        <Button
            Margin="10"
            Command="{Binding ExecuteGenericDelegateCommand}"
            CommandParameter="Passed Parameter"
            Content="DelegateCommand Generic" />
        <TextBlock
            Margin="10"
            FontSize="22"
            Text="{Binding UpdateText}" />
    </StackPanel>
</Window>

MainWindowViewModel.cs

namespace UsingDelegateCommands.ViewModels
{
    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; }

        public DelegateCommand<string> ExecuteGenericDelegateCommand { get; }

        public DelegateCommand DelegateCommandObservesProperty { get; }

        public DelegateCommand DelegateCommandObservesCanExecute { get; }

        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;
        }
    }
}

動作解説

「Can Execute Command」チェックボックスにチェックを付けると、4つのボタンが有効になります。チェックを外すと無効になります。

これは、ボタンの Command にバインディングしているプロパティが、このチェックボックス状態で使えるか否かを決定しているからです。

MainWindowViewModel のコンストラクタにあるうち、以下の三行は同じ動作「Updated: 時刻」というテキストが表示されます。

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

細かいことは分かりません!そして最後の一行だけは異色で「Passed Parameter」と表示されます。

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

このボタンは以下のように定義されています。

<Button
    Margin="10"
    Command="{Binding ExecuteGenericDelegateCommand}"
    CommandParameter="Passed Parameter"
    Content="DelegateCommand Generic" />

そう、CommandParameter を使うサンプルなのです。この CommandParameter の値をそのまま TextBlock に送っているわけです。

で、試しに DelegateCommand を引数二つにして試そうとしたら通らないです。

というわけで、基本的に以下の二つを抑えておけばいいのではないかと…

// CommandParameter がないコマンド
ExecuteDelegateCommand = new DelegateCommand(Execute, CanExecute);
// CommandParameter があるコマンド
ExecuteGenericDelegateCommand = new DelegateCommand<string>(ExecuteGeneric, CanExecute);

と思きや、違いました残念!

System.NotSupportedException: ‘Operation not supported for the given expression type. Only MemberExpression and ConstantExpression are currently supported.’

こう出ます、結局こうするしかありません。

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

これ、CommunityToolkit.Mvvm では極めてシンプルに書けますよ。

[RelayCommand(CanExecute = nameof(CanExecuteGeneric))]
private void ExecuteGeneric(string parameter)
{
    UpdateText = parameter;
}

private bool CanExecuteGeneric() => IsEnabled;

12-UsingCompositeCommands

Descriptionには Learn how to use CompositeCommands to invoke multiple commands as a single command とあります。

なにやらタブが出てきますが、チェックボックスをチェックしたらボタンが押せるという挙動は一見同じようです。

しかし、上部にある「Save」ボタンを押すと、全てのボタンが押されたかのような挙動を示すことが、恐らく本題でしょう。

UsingCompositeCommands.Core 内の ApplicationCommands.cs

namespace UsingCompositeCommands.Core
{
    public interface IApplicationCommands
    {
        CompositeCommand SaveCommand { get; }
    }

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

実際のサンプルよりコードを整理しましたが、現時点これが何物かわかりません。

UsingCompositeCommands 内の App.xaml.cs

using System.Windows;
using UsingCompositeCommands.Core;
using UsingCompositeCommands.Views;

namespace UsingCompositeCommands
{
    /// <summary>
    /// Interaction logic for App.xaml
    /// </summary>
    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>();
        }
    }
}

ConfigureModuleCatalog は文字通り、ModuleA をモジュールカタログに追加して使えるようにしてますね。

RegisterTypes では、先ほどよくわからなかった IApplicationCommandsApplicationCommands をSingletonとして登録してるんですかね?

一見すると、CommunityToolkit.Mvvm の DI 登録コードに酷似していますね。

UsingCompositeCommands の Views\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/"
    Title="{Binding Title}"
    Width="525"
    Height="350"
    prism:ViewModelLocator.AutoWireViewModel="True">

    <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
            Margin="10"
            Command="{Binding ApplicationCommands.SaveCommand}"
            Content="Save" />

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

UsingCompositeCommands の ViewModels\MainWindowViewModel.cs

using UsingCompositeCommands.Core;

namespace UsingCompositeCommands.ViewModels
{
    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;
        }
    }
}

まるで本当にコンストラクタ注入らしきことをしています。

さきほどの CommunityToolkit.Mvvm の DI 登録に酷似していると言ったのは、大きく外してなかったかもしれません。

ModuleA の ModuleAModule.cs

using ModuleA.ViewModels;
using ModuleA.Views;

namespace ModuleA
{
    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);
        }

        public void RegisterTypes(IContainerRegistry containerRegistry)
        {
        }

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

ModuleA の Views\TabView.xaml

<UserControl
    x:Class="ModuleA.Views.TabView"
    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">
    <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
        <TextBlock
            Margin="5"
            FontSize="18"
            Text="{Binding Title}" />
        <CheckBox
            Margin="5"
            Content="Can Execute"
            IsChecked="{Binding CanUpdate}" />
        <Button
            Margin="5"
            Command="{Binding UpdateCommand}"
            Content="Save" />
        <TextBlock Margin="5" Text="{Binding UpdateText}" />
    </StackPanel>
</UserControl>

ModuleA の ViewModels\TabViewModel.cs

using UsingCompositeCommands.Core;

namespace ModuleA.ViewModels
{
    public class TabViewModel : BindableBase
    {
        private readonly 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; }

        public TabViewModel(IApplicationCommands applicationCommands)
        {
            _applicationCommands = applicationCommands;

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

            _applicationCommands.SaveCommand.RegisterCommand(UpdateCommand);
        }

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

やっと IApplicationCommands が何物かわかりましたね、ModuleA はタブの中身、 そこに MainWindow の層から複数の(=コンポジット)タブにコマンドを送るための仕組みのようです。

…いやいやちょっと待って、たかが Command 絡みでなんでこんな複雑になってるの…訳がわからない。

あまりに酷いので、同等の機能を持つ CommunityToolkit.Mvvm 版サンプルを次の記事で書きたいと思います。

13-IActiveAwareCommands

Descriptionには Make your commands IActiveAware to invoke only the active command とあります。

DeepL に翻訳させると「アクティブなコマンドのみを呼び出すように、コマンドを IActiveAware に設定します」だそうです。

動き自体は 12-UsingCompositeCommands と同等に見えますね。

UsingCompositeCommands.Core 内の ApplicationCommands.cs

namespace UsingCompositeCommands.Core
{
    public interface IApplicationCommands
    {
        CompositeCommand SaveCommand { get; }
    }

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

デフォルトで true を渡すように変わっています。

UsingCompositeCommands の App.xaml.cs

前節と同じです、サンプルコードではメソッドの位置が逆転していますが、内容は同一です。

UsingCompositeCommands の Views\MainWindow.xaml

前節と同じです。

UsingCompositeCommands の ViewModels\MainWindowViewModel.cs

using UsingCompositeCommands.Core;

namespace UsingCompositeCommands.ViewModels
{
    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;
        }
    }
}

ModuleA の ModuleAModule.cs と Views\TabView.xaml

前節と同じです。

ModuleA の ViewModel\TabViewModel.cs

using UsingCompositeCommands.Core;

namespace ModuleA.ViewModels
{
    public class TabViewModel : BindableBase, IActiveAware
    {
        private readonly 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; }

        public TabViewModel(IApplicationCommands applicationCommands)
        {
            _applicationCommands = applicationCommands;

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

            _applicationCommands.SaveCommand.RegisterCommand(UpdateCommand);
        }

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

        bool _isActive;
        public bool IsActive
        {
            get { return _isActive; }
            set
            {
                _isActive = value;
                OnIsActiveChanged();
            }
        }
        private void OnIsActiveChanged()
        {
            UpdateCommand.IsActive = IsActive;

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

        public event EventHandler IsActiveChanged;
    }
}

EventArgs.Empty など微修正をしています。

これは何?ちょっとギブアップ

一見すると IsActiveChanged のイベントを受け取ってる場所がありません。

しかし IActiveAware インターフェースで実装を強要されます。

試しに OnIsActiveChanged(); をコメントアウトすると、MainWindow の上部にあるボタンが常時無効になりますね…

おそらく UpdateCommand.IsActive = IsActive; でアクティブか否かを操作しているのでしょうが、複雑怪奇すぎます。

一旦締めます

今回はひたすらダメ出しになってしまいましたね。

View と ViewModel の関係は、別に CommunityToolkit.Mvvm がベストだとは言わないけれど、 プログラマが明示的に指示した時に初めて View と ViewModel が結びつけられる方が、まだ筋がいいと感じます。

まあ、CommunityToolkit.Mvvm の方が後発なので、 CommunityToolkit.Mvvm の方が洗練されているのは、当然と言えば当然かもしれません。

しかし、それにしても Command は複雑怪奇過ぎて手に負えないですね…

本当に細かい所は取り上げてませんが、どこに何が書かれているかは、記事内でほぼ網羅しているはずです。 どうか、本気で Prism の Command を使いたいならば、自力でなんとかしてください。

先ほども書きましたが、次回記事では CommunityToolkit.Mvvm で同等のサンプルを作成します。