かずきのBlog@hatena

日本マイクロソフトに勤めています。XAML + C#の組み合わせをメインに、たまにASP.NETやJavaなどの.NET系以外のことも書いています。掲載内容は個人の見解であり、所属する企業を代表するものではありません。

よりMVVMに、よりライトにPrism 5がリリースされました

10日ほど前の2014年4月19日にPrism 5 for .NET4.5がリリースされてました。

Prismは、MSの中の人たちが作ってるOSSのWPF用(SL用やストアアプリ用などもある)のフレームワークで、複合型アプリケーション(モジュールを組み合わせて1つのアプリケーションに仕立て上げるもの)のサポートと、MVVMパターンのサポート機能が含まれています。

ライブラリの入手

サンプルプログラムなどを見るならMSのサイトからDLしてもいいですが、さくっと始めるだけならNuGetで入手するのが楽です。

Install-Package Prism

もちろんGUIからもOKです。ダウンロードする際に、結構細分化された状態でパッケージが入ってくるのがわかります。

  • Prism.Composition
    • 複合型アプリケーションを作成するための機能が含まれてます。一番複雑怪奇。
  • Prism.Interactivity
    • MVVMパターンでVMから、ユーザと対話するための機能を提供するクラス群が入ってます。簡単にいうと確認ダイアログとかが出せます。
  • Prism.PubSubEvents
    • モジュール間で疎結合にメッセージのやり取りを行うためのクラス群が入ってます。Medatorパターンですね。
  • Prism.Mvvm
    • MVVMをするための基本的なICommandの実装やINotifyPropertyChangedの実装の他にViewとViewModelを自動的に結び付ける機能を提供します。
  • Prism
    • 上記のNuGetパッケージをまとめるためのプレースホルダーです。

複合型アプリケーション

昔とかわりません。ムズイです。

MVVMサポート

結構シンプルに実装してます。そのぶんめんどいです。大体以下のクラスです。

  • INotifyPropertyChangedの実装のBindableBaseクラス。
  • ViewからViewModelの自動引き当てを行うViewModelLocatorProviderクラス。
  • INotifyErrorInfoを実装するときに楽できるErrorsContainer
  • ICommandの実装のDelegateCommand

遊んでみた

DataAnnotationでのエラーチェックをサポートしたViewModel作って遊んでみた。

using Microsoft.Practices.Prism.Mvvm;
using Microsoft.Practices.Prism.ViewModel;
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Runtime.CompilerServices;

namespace WpfApplication7.ViewModels
{
    /// <summary>
    /// PrismのBindableBaseを拡張してデータの検証を追加
    /// </summary>
    public class ViewModelBase : BindableBase, INotifyDataErrorInfo
    {
        // INotifyDataErrorInfo実装時に楽できるクラス
        private readonly ErrorsContainer<string> Errors;

        public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

        public ViewModelBase()
        {
            // ErrorsChangedを発行するメソッドをコンストラクタに渡してインスタンス化すると
            // SetErrorsなどのときに、いい感じにイベント出してくれる
            this.Errors = new ErrorsContainer<string>(this.OnErrorsChanged);
        }

        // INotifyDataErrorInfoの実装。基本委譲するだけ。
        public IEnumerable GetErrors(string propertyName)
        {
            return this.Errors.GetErrors(propertyName);
        }

        public bool HasErrors
        {
            get { return this.Errors.HasErrors; }
        }

        protected virtual void OnErrorsChanged([CallerMemberName]string propertyName = null)
        {
            var h = this.ErrorsChanged;
            if (h != null)
            {
                h(this, new DataErrorsChangedEventArgs(propertyName));
            }
        }

        /// <summary>
        /// 値の設定と値の検証を行う
        /// </summary>
        protected override bool SetProperty<T>(ref T storage, T value, [CallerMemberName]string propertyName = null)
        {
            var changed = base.SetProperty<T>(ref storage, value, propertyName);
            if (changed)
            {
                // DataAnnotationsでプロパティの検証
                var context = new ValidationContext(this)
                {
                    MemberName = propertyName
                };
                List<ValidationResult> validationErrors = new List<ValidationResult>();
                if (!Validator.TryValidateProperty(value, context, validationErrors))
                {
                    // エラーがあったらエラーを設定
                    this.Errors.SetErrors(
                        propertyName,
                        validationErrors.Select(error => error.ErrorMessage).ToArray());
                }
                else
                {
                    // なければクリア
                    this.Errors.ClearErrors(propertyName);
                }
            }
            return changed;
        }

        public bool ValidateObject()
        {
            // オブジェクト全体の検証を行う
            var context = new ValidationContext(this);
            List<ValidationResult> validationErrors = new List<ValidationResult>();
            if (Validator.TryValidateObject(this, context, validationErrors))
            {
                return true;
            }

            var errors = validationErrors
                .Where(error => error.MemberNames.Any())
                .GroupBy(error => error.MemberNames.First());
            foreach (var error in errors)
            {
                this.Errors.SetErrors(
                    error.Key,
                    error.Select(e => e.ErrorMessage).ToArray());
            }

            return false;
        }
    }
}

そして、これを継承したViewModel

using Microsoft.Practices.Prism.Commands;
using Microsoft.Practices.Prism.Interactivity.InteractionRequest;
using System.ComponentModel.DataAnnotations;

namespace WpfApplication7.ViewModels
{
    public class MainWindowViewModel : ViewModelBase
    {

        private string input;

        /// <summary>
        /// 必須入力の項目
        /// </summary>
        [Required(ErrorMessage = "必須!")]
        public string Input
        {
            get { return this.input; }
            // 値が設定されたタイミングでコマンドの実行可否の確認
            set { this.SetProperty(ref this.input, value); this.AlertCommand.RaiseCanExecuteChanged(); }
        }

        private InteractionRequest<Notification> alertRequest = new InteractionRequest<Notification>();

        /// <summary>
        /// Viewへ通知を行うためのInteractionRequest
        /// </summary>
        public InteractionRequest<Notification> AlertRequest
        {
            get { return this.alertRequest; }
            set { this.SetProperty(ref this.alertRequest, value); }
        }

        private DelegateCommand alertCommand;

        /// <summary>
        /// 適当にコマンド
        /// </summary>
        public DelegateCommand AlertCommand
        {
            get
            {
                return this.alertCommand ?? 
                    (this.alertCommand = new DelegateCommand(this.AlertExecute, this.CanAlertExecute));
            }
        }

        public void AlertExecute()
        {
            // エラーがなければ
            if (!this.ValidateObject())
            {
                return;
            }

            // Viewに投げる
            this.AlertRequest.Raise(new Notification
            { 
                Title = "お知らせ", 
                Content = this.HasErrors ? "エラー" : "オッケー" 
            });
        }

        public bool CanAlertExecute()
        {
            return !this.HasErrors;
        }

    }
}

んでViewのコードビハインドです。

using Microsoft.Practices.Prism.Mvvm;
using System.Windows;

namespace WpfApplication7.Views
{
    /// <summary>
    /// IViewを実装してViewModelLocatorProviderでViewModelを自動で設定
    /// </summary>
    public partial class MainWindow : Window, IView
    {
        public MainWindow()
        {
            InitializeComponent();
            ViewModelLocationProvider.AutoWireViewModelChanged(this);
        }
    }
}

ViewからViewModelを設定する方法はXAMLでViewModelLocatorを使う方法があるけど、それだとXAMLエディタがエラーをはくので個人的にはコードビハインドでViewModelLocationProviderを使うほうが好み。

そしてXAML。

<Window
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:mvvm="clr-namespace:Microsoft.Practices.Prism.Mvvm;assembly=Microsoft.Practices.Prism.Mvvm.Desktop"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
    xmlns:ViewModels="clr-namespace:WpfApplication7.ViewModels" 
    xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" xmlns:Custom="http://www.codeplex.com/prism" 
    xmlns:DefaultPopupWindows="clr-namespace:Microsoft.Practices.Prism.Interactivity.DefaultPopupWindows;assembly=Microsoft.Practices.Prism.Interactivity" 
    mc:Ignorable="d" 
    x:Class="WpfApplication7.Views.MainWindow"
    Title="MainView" 
    Height="300" 
    Width="300" 
    d:DataContext="{d:DesignInstance {x:Type ViewModels:MainWindowViewModel}, IsDesignTimeCreatable=True}">
    <i:Interaction.Triggers>
        <!-- AlertRequestからの要求を受けるTrigger -->
        <Custom:InteractionRequestTrigger SourceObject="{Binding AlertRequest}">
            <!-- 地味にPrism 5からWindow表示アクションがある -->
            <Custom:PopupWindowAction />
        </Custom:InteractionRequestTrigger>
    </i:Interaction.Triggers>
    <Grid>
        <!-- VMの入力項目にバインド -->
        <TextBox x:Name="textBox" HorizontalAlignment="Left" Height="23" Margin="10,10,0,0" TextWrapping="Wrap" Text="{Binding Input, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" VerticalAlignment="Top" Width="120"/>
        <!-- TextBoxのエラーの内容を表示する -->
        <TextBlock HorizontalAlignment="Left" Margin="135,18,0,0" TextWrapping="Wrap" 
            Text="{Binding (Validation.Errors)[0].ErrorContent, ElementName=textBox, Mode=OneWay}" VerticalAlignment="Top"/>
        <!-- コマンドと接続するためのボタン -->
        <Button Content="Button" HorizontalAlignment="Left" Margin="10,38,0,0" VerticalAlignment="Top" Width="75" Command="{Binding AlertCommand, Mode=OneWay}"/>

    </Grid>
</Window>

実行するとこんな感じ。

f:id:okazuki:20140427024728j:plain

何もせずボタンお押すとブロックするよ。これはCommandでValidateObjectを呼んでるおかげ。

f:id:okazuki:20140427024830j:plain

何か入力するとボタンが押せるようになる。押したらデフォルトのウィンドウが出てくる。

f:id:okazuki:20140427024933j:plain

所感

軽いMVVMフレームワークとして、PrismのMVVM部分だけ参照して使うのはありかなと思いました。(NuGetでもパッケージが細かくわかれてるので) 複合アプリ作りたい人はフルパッケージでどうぞ!