ささいなことですが。

Windowsアプリテスト自動化ライブラリFriendly開発者の日記です。

ViewModelをシンプルに書きたい! - ④Delegate→Method接続編 -

ViewModelからViewの操作を呼び出したいときあります。メッセージボックスとかメッセージボックスとか。
Messengerは良いものだとは思いますが、個人的にはあんまり好きくない。
以下のように書けるようにしてみました。

こんなアプリで
f:id:ishikawa-tatsuya:20160611175347p:plain

ViewModelはこんな感じ

using Reactive.Bindings;
using System;

namespace WpfApp
{
    public class MainWindowVM
    {
        //これがViewに対する要求。メッセージボックスではなく問い合わせという形に抽象化している。
        public Func<string, bool?> Ask { get; set; }

        public ReactiveProperty<string> Reply { get; } = new ReactiveProperty<string>(string.Empty);
        
        public void Communication()
        {
            var ret = Ask("楽しんでますか?");
            Reply.Value = (ret == true) ? "よかったね" : "そうですか";
        }
    }
}

Xamlです。ここでViewModelの抽象化された要求を具体的なViewのメソッドと接続します。

<Window x:Class="WpfApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:l="clr-namespace:WpfApp"
        xmlns:c="clr-namespace:VVMConnection;assembly=VVMConnection"
        Title="MainWindow" Height="350" Width="525">
    <Window.DataContext>
        <l:MainWindowVM/>
    </Window.DataContext>

    <c:Connection.Methods>
        <c:MethodCollection>
            <c:Method Name ="ShowMessageBox" Invoker="Ask"/>
        </c:MethodCollection>
    </c:Connection.Methods>

    <StackPanel>
        <Button Content="会話" Command="{c:Command Communication }"/>
        <TextBlock Text="{Binding Reply.Value}"/>
    </StackPanel>
</Window>

一旦コードビハインド

using System.Windows;

namespace WpfApp
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        public bool? ShowMessageBox(string text)
        {
            return MessageBox.Show(this, text, "", MessageBoxButton.YesNo) == MessageBoxResult.Yes;
        }
    }
}

MethodCollectionに登録しておけば、つないでくれるという仕様にしてみました。上の書き方ならViewのShowMessageBoxにつないでくれるわけですね。ViewModelからの呼び出しはDelegateになって、直でメッセージボックス呼び出すより抽象化されてまろやかになっております。戻り値もMessageBoxResultでもいいんですけど、bool?に抽象化した方がいい感じですよね。

View以外のメソッドにもつないでみます。

はい、コードビハインド嫌なんでしょ?それにメッセージボックスとかよく使うから部品化したいですよね。こんな感じの書き方でどうでしょうか?メソッドを持っているインスタンスをTargetで指定できるようにしてみました。指定なければViewを見に行きます。これなら、部品化できますよね。これで書き直してみます。ViewModelはそのままでいきましょう。View層だけ付け替え可能なところがViewModelのメリットですね。
Xaml

<Window x:Class="WpfApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:l="clr-namespace:WpfApp"
        xmlns:c="clr-namespace:VVMConnection;assembly=VVMConnection"
        Title="MainWindow" Height="350" Width="525">
    <Window.DataContext>
        <l:MainWindowVM/>
    </Window.DataContext>

    <c:Connection.Methods>
        <c:MethodCollection>
            <c:Method Name ="Show" Invoker="Ask">
                <c:Method.Target>
                    <c:MessageBox Button="YesNo" Caption="質問"/>
                </c:Method.Target>
            </c:Method>
        </c:MethodCollection>
    </c:Connection.Methods>

    <StackPanel>
        <Button Content="会話" Command="{c:Command Communication }"/>
        <TextBlock Text="{Binding Reply.Value}"/>
    </StackPanel>
</Window>

Viewからコードビハインドはなくなりました。

using System.Windows;

namespace WpfApp
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }
    }
}

MessageBoxはこんな感じで部品化します。特にインターフェイスを継承したりしなくてもいいようにしてみました。

using System.Windows;

namespace VVMConnection
{
    public class MessageBox
    {
        public string MessageBoxText { get; set; } = string.Empty;
        public string Caption { get; set; } = string.Empty;
        public MessageBoxButton Button { get; set; } = MessageBoxButton.OK;
        public MessageBoxImage Icon { get; set; } = MessageBoxImage.None;
        public MessageBoxResult DefaultResult { get; set; } = MessageBoxResult.None;
        public MessageBoxOptions Options { get; set; } = MessageBoxOptions.None;

        public bool? Show()
        {
            return Show(MessageBoxText);
        }

        public bool? Show(string messageBoxText)
        {
            switch (System.Windows.MessageBox.Show(messageBoxText, Caption, Button, Icon, DefaultResult, Options))
            {
                case MessageBoxResult.OK:
                case MessageBoxResult.Yes:
                    return true;
                case MessageBoxResult.No:
                case MessageBoxResult.Cancel:
                    return false;
                default:
                    return null;
            }
        }
    }
}

おー、いい感じじゃないか!
長くなったので、c:Connection.Methodsの解説は次回に持ち越します。

連載で使っているMarkup拡張のコードはこちらにおいてます。

github.com