MVVMフレームワークライブラリ「Livet」を使ってViewModelからファイル選択ダイアログを出す


はじめに

本記事は、MVVMフレームワークライブラリ「Livet」を使って、MVVMを守りながらViewModelからファイル選択ダイアログを出す方法についてまとめます。

Livetとは

WPFのためのMVVMフレームワークライブラリの1つです。
ViewとViewModelの連携を支援する仕組みがいくつも搭載されている点が特徴です。
詳細はこちらを参照ください。

ViewModelからファイル選択ダイアログを出す際の困りごと

ファイル選択ダイアログに限らず、MVVMを守りながらViewModelを起点にしてダイアログを出すためにはMessengerパターンの実装が必要になります。
MessengerパターンはViewとVMで共通のMessengerを持ち、そのMessengerを介してメッセージを送りあうことで、ViewとVMの連携をする設計パターンのことです。
ViewModelからダイアログを出す際には、このMessengerパターンを都度実装しないといけない点が困った点でした。
Livetは、このMessengerパターンを提供してくれており、なおかつファイル選択ダイアログや確認ダイアログなどのよく使うダイアログについては、メッセージをView側で受け取ってダイアログを表示するという実装すらいらず、簡単にダイアログが表示できる仕組みを提供してくれています。

Livetを使ってViewModelからファイル選択ダイアログを出してみる

Livetを使うためには、まずVisual Studioの拡張機能から「Livet」をインストールします。
その後、プロジェクトを新規作成し、Livetが提供するテンプレートを選択してWPFアプリを作成します。

テンプレートから作成後のプロジェクトに存在するMainWindow.xamlに「開くファイルを選択ボタン」と「開いたファイルのパスを表示するテキストブロック」を追加します。
動きとしては、「開くファイルを選択ボタン」を押すとファイル選択ダイアログが開き、ファイル選択ダイアログで選択したファイルのパスが「開いたファイルのパスを表示するテキストブロック」に表示されるイメージです。

<Window
    x:Class="LivetMessengerSample.Views.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:behaviors="http://schemas.microsoft.com/xaml/behaviors"
    xmlns:l="http://schemas.livet-mvvm.net/2011/wpf"
    xmlns:v="clr-namespace:LivetMessengerSample.Views"
    xmlns:vm="clr-namespace:LivetMessengerSample.ViewModels"
    Title="MainWindow"
    Width="500"
    Height="100">

    <Window.DataContext>
        <vm:MainWindowViewModel />
    </Window.DataContext>

    <behaviors:Interaction.Triggers>
        <!-- テンプレートから作成後の状態では、いくつかトリガ設定があるが省略 -->
    </behaviors:Interaction.Triggers>

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        <!-- 開くファイル選択するボタン -->
        <Button Grid.Row="0" Grid.Column="0" Width="100" Height="20"
                Command="{Binding Path=OpenFileCommand}"
                HorizontalAlignment="Left">
            開くファイルを選択
        </Button>
        <!-- 開いたファイルのパスを表示するテキストブロック -->
        <TextBlock Grid.Row="1" Grid.Column="0" Text="開いたファイルのパス:"/>
        <TextBlock Grid.Row="1" Grid.Column="1" Text="{Binding Path=OpenFilePath}"/>
    </Grid>
</Window>

上記のMainWindow.xamlのViewModelとなるMainWindowViewModelでは、MainWindow.xamlの「開くファイルを選択ボタン」押下時に実行するコマンド(OpenFileCommand)と開いたファイルのパスを表現するプロパティ(OpenFilePath)を定義します。
OpenFileCommand実行時の実処理であるOpenFileメソッドで、Livetが提供するMessengerパターンを使ってメッセージを通知しています。Livetが提供しているファイル選択ダイアログを開く専用メッセージ(OpeningFileSelectionMessageクラス)を作成し、作成時にキーを設定します。その後、RaiseAsyncメソッドでメッセージを送信し、メッセージに対しての応答を受け取るまでawaitで待機します。
ファイル選択ダイアログメッセージの応答ではダイアログで選択されたファイルパスを受け取れるので、OpenFilePathプロパティに設定してあげます。このタイミングでMainWindow.xamlの「開いたファイルのパスを表示するテキストブロック」には、ファイル選択ダイアログで選択したファイルパスが表示されます。

using Livet;
using Livet.Commands;
using Livet.Messaging.IO;
using System.Linq;
using System.Windows.Input;

namespace LivetMessengerSample.ViewModels
{
    /// <summary>
    /// MainWindow用のVM
    /// </summary>
    public class MainWindowViewModel : ViewModel
    {
        // ViewModel型を継承時にはこのInitializeメソッドの実装が必須
        // ただ、今回の例では特に初期化処理がないため何もしていない
        public void Initialize()
        {
        }

        /// <summary>
        /// 開いたファイルのパス
        /// </summary>
        private string _OpenFilePath;

        /// <summary>
        /// 開いたファイルのパス
        /// </summary>
        public string OpenFilePath
        {
            get => _OpenFilePath;
            set
            {
                if (value == _OpenFilePath) return;

                _OpenFilePath = value;
                RaisePropertyChanged();
            }
        }

        /// <summary>
        /// ファイルを開くコマンド
        /// </summary>
        public ICommand OpenFileCommand => new ViewModelCommand(OpenFile);

        /// <summary>
        /// ファイルを開く
        /// </summary>
        private async void OpenFile()
        {
            // メッセージを作成
            var message = new OpeningFileSelectionMessage("MessageKey_OpenFile");

            // メッセージを送信
            await Messenger.RaiseAsync(message);

            // メッセージへの応答がない場合は何もしない
            // ファイル選択ダイアログでキャンセルされた場合も応答なしになる
            if(message.Response == null) return;

            // 開いたファイルのパスを更新
            OpenFileName = message.Response.First();
        }
    }
}

ここまでのMainWindow.xamlとMainWindowViewModelの実装だとメッセージを受け取ることができないため、MainWindow.xamlにメッセージを受信するための記載を追記します。MainWindowViewModelでのメッセージ作成時に設定した時と同じキーを設定して、メッセージを受け取るように設定します。なお、メッセージを受け取った際にはファイル選択ダイアログを開くアクションを行うように設定しています(このアクションがOpenFileDialogInteractionMessageActionです)。これでMainWindowViewModelからのメッセージを受け取り、そのアクションとしてファイル選択ダイアログを表示するように設定ができます。

<Window
    x:Class="LivetMessengerSample.Views.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:behaviors="http://schemas.microsoft.com/xaml/behaviors"
    xmlns:l="http://schemas.livet-mvvm.net/2011/wpf"
    xmlns:v="clr-namespace:LivetMessengerSample.Views"
    xmlns:vm="clr-namespace:LivetMessengerSample.ViewModels"
    Title="MainWindow"
    Width="500"
    Height="100">

    <Window.DataContext>
        <vm:MainWindowViewModel />
    </Window.DataContext>

    <behaviors:Interaction.Triggers>
        <!-- テンプレートから作成後の状態では、いくつかトリガ設定があるが省略 -->

        <!-- ここを追加 -->
        <!-- メッセージを受け取ってファイル選択ダイアログを表示するためのトリガ設定 -->
        <l:InteractionMessageTrigger MessageKey="MessageKey_OpenFile" Messenger="{Binding Path=Messenger}">
            <l:OpenFileDialogInteractionMessageAction/>
        </l:InteractionMessageTrigger>
    </behaviors:Interaction.Triggers>

    <Grid>
        <!-- 省略 -->
    </Grid>
</Window>

さいごに

MVVMフレームワークライブラリ「Livet」を使ってViewModelからファイル選択ダイアログを出す方法をまとめました。Messengerパターンを提供してくれるだけでなく、ファイル選択ダイアログを開くための実装も自前で用意する必要がないので、非常に便利に感じました。ファイル選択ダイアログ以外にも確認ダイアログなども同様にほとんど手間なく出せるため、デスクトップアプリ開発が楽になるのではと思いました。