かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

T4 TemplateでViewModelの生成をするアイテムテンプレートを作りました

以前にも何度かT4 Templateを使ってViewModelの記述を簡略化する方法を書いてきました。

これまでの方法の課題

ただ、この方法も少しばかり不満がありました。

  1. 全体的にDSLチックな構文になりとっつきにくい
  2. プロパティに属性を指定するのが文字列で指定するためインテリセンスが効かない
  3. ViewModelの基本クラスが固定化されてしまう

今回のチャレンジ

ということで今回作ったものは以下のことを目標にして作りました。

  1. なるべくDSLチックになる箇所を最小限に。
  2. プロパティに属性を指定する方法は、インテリセンスが効くように。
  3. ViewModelの基本クラスは割と好きに変えれるようにしたい
  4. なるべくMVVMフレームワーク非依存に作る(基本はPrism準拠)

インストール方法

このページの一番下にあるダウンロードからダウンロードしたViewModel.zipを以下のフォルダにそのままコピーします。

使い方

ここでは、使い方を説明します。

プロジェクトの作成

まず、WPFアプリケーションを新規作成します。何らかのMVVMフレームワークが必要になるので適当なものを参照に追加します。ここでは、PrismをNugetから参照に追加しました。

ViewModelBaseクラスの作成

PrismにはINotifyPropertyChangedを実装しただけのNotificationObjectはありますが、WPFのViewModelの基本クラスとして使用するにはちょっと貧弱なのでIDataErrorInfoを実装した簡易的なViewModelBaseクラスを作ろうと思います。ここら辺は、各自のViewModelの基本クラスが持つべき要件に応じて作ってください。

namespace MVVMSample.ViewModels
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.ComponentModel.DataAnnotations;
    using System.Linq;
    using Microsoft.Practices.Prism.ViewModel;

    /// <summary>
    /// ViewModelの基本クラス
    /// </summary>
    public class ViewModelBase : NotificationObject, IDataErrorInfo
    {
        // エラー情報の入れ物
        private ErrorsContainer<string> errors;

        public ViewModelBase()
        {
            // エラーに変化があったらHasErrorsの変更通知を行う
            this.errors = new ErrorsContainer<string>(s => this.RaisePropertyChanged("HasErrors"));
        }

        protected override void RaisePropertyChanged(string propertyName)
        {
            base.RaisePropertyChanged(propertyName);
            // プロパティに変更があったタイミングでプロパティの妥当性検証を行う
            var value = this.GetType().GetProperty(propertyName).GetValue(this, null);
            List<ValidationResult> results = new List<ValidationResult>();
            if (Validator.TryValidateProperty(
                    value,
                    new ValidationContext(this, null, null)
                    {
                        MemberName = propertyName
                    },
                    results))
            {
                this.errors.ClearErrors(propertyName);
            }
            else
            {
                this.errors.SetErrors(
                    propertyName,
                    results.Select(r => r.ErrorMessage));
            }
        }

        // Errorプロパティは使わないので明示的な実装で外から隠す
        string IDataErrorInfo.Error
        {
            get { throw new NotSupportedException(); }
        }

        public string this[string columnName]
        {
            get 
            {
                // errorsからエラー情報を返す
                return this.errors.GetErrors(columnName).FirstOrDefault();
            }
        }

        /// <summary>
        /// エラーがある場合はtrueを返す
        /// </summary>
        public bool HasErrors
        {
            get
            {
                return this.errors.HasErrors;
            }
        }
    }
}

ここでは、IDataErrorInfo + DataAnnotationsの合わせ技で妥当性の検証を行うViewModelBaseクラスを作りました。

ViewModelクラスの作成

ついに、先ほどインストールしたViewModelのテンプレートを使います。ViewModelを格納するフォルダで[追加]→[新しい項目]を選択してください。
T4 Base ViewModel Classという項目があるのでそれを選択してViewModelのクラス名を入力してください。ここではMainWindowViewModelとしました。

作成をすると、以下の3つのファイルが作成されます。

  • MainWindowViewModel.cs
    • カスタムロジックを記述するファイル
  • MainWindowViewModel.tt
    • プロパティやコマンドの定義を記述するファイル
  • MVVMParts.ttinclude
    • 共通で使用するメソッドが定義されているファイル
    • 二回目のViewModelの作成から、このファイルを上書きするか聞かれるので上書きする場合は上書きを、カスタマイズしてある場合はキャンセルを選択してください

1つずつファイルを見ていきます。

MainWindowViewModel.cs

非常にシンプルです。ViewModelBaseクラスを継承するように作られています。そして、DataAnnotasionsを使ってバリデーションのルールを定義するためのMetadataクラスも定義されています。ViewModelBaseは固定なので、もしほかのクラスを継承する場合は手動で書き換えてください。
このファイルに、コマンドの処理のメソッドやカスタムロジックを記述していきます。

namespace MVVMSample.ViewModels
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Windows;
    using System.ComponentModel.DataAnnotations;

    [MetadataType(typeof(Metadata))]
    public partial class MainWindowViewModel : ViewModelBase
    {
        class Metadata
        {
        }
    }
}

MainWindowViewModel.tt

コマンドの定義やプロパティの定義を行うファイルです。デフォルトでは以下のようになっています。

<#@ template debug="false" hostspecific="false" language="C#" #>
<#@ output extension=".generated.cs" #>
// <generated />
namespace MVVMSample.ViewModels
{
	[System.ComponentModel.TypeDescriptionProvider(typeof(MainWindowViewModelAssociatedMetadataTypeTypeDescriptionProvider))]
	public partial class MainWindowViewModel
	{
		/* usage
		// 引数なしのAlertCommandの定義
		<# Command("Alert"); #>
		// string型のMessageCommandの定義
		<# Command("Message", "string"); #>
		// string型のNameプロパティの定義
		<# Property("string", "Name"); #>
		// modelフィールドのAddressプロパティのProxyプロパティの定義
		<# ProxyProperty("model", "string", "Address"); #>
		*/
		<# AssociatedMetadataTypeTypeDescriptionProvider("MainWindowViewModel"); #>
	}
		
}
<#@ include file="MVVMParts.ttinclude" #>

なるべく普通のクラスの定義に近い形になるように作成してあります。MainWindowViewModel内に以下のような記述をすることで、コマンドやプロパティを定義できます。

<#
// AlertCommand(パラメータ無し)を定義します
Command("Alert");
// GreetCommand(string型のパラメータ有り)を定義します
Command("Greet", "string");
// string型のNameプロパティを定義します
Property("string", "Name");
// フィールドのプロパティをViewModelのプロパティとして公開します
ProxyProperty("field", "string", "Foo");
#>

ファイル全体は以下のようになります。

<#@ template debug="false" hostspecific="false" language="C#" #>
<#@ output extension=".generated.cs" #>
// <generated />
namespace MVVMSample.ViewModels
{
    [System.ComponentModel.TypeDescriptionProvider(typeof(MainWindowViewModelAssociatedMetadataTypeTypeDescriptionProvider))]
    public partial class MainWindowViewModel
    {
        <#
        // AlertCommand(パラメータ無し)を定義します
        Command("Alert");
        // GreetCommand(string型のパラメータ有り)を定義します
        Command("Greet", "string");
        // string型のNameプロパティを定義します
        Property("string", "Name");
        // フィールドのプロパティをViewModelのプロパティとして公開します
        ProxyProperty("field", "string", "Foo");
        #>        
        <# AssociatedMetadataTypeTypeDescriptionProvider("MainWindowViewModel"); #>
    }
        
}
<#@ include file="MVVMParts.ttinclude" #>

最後のAssociatedMetadataTypeTypeDescriptionProviderの行は重要なので消さないように注意してください。上記の定義をすることで、以下のようなViewModelのコードがMainWindowViewModel.generated.csに作成されます。

// <generated />
namespace MVVMSample.ViewModels
{
    [System.ComponentModel.TypeDescriptionProvider(typeof(MainWindowViewModelAssociatedMetadataTypeTypeDescriptionProvider))]
    public partial class MainWindowViewModel
    {
        
        private Microsoft.Practices.Prism.Commands.DelegateCommand _AlertCommand;
        
        partial void AlertExecute();
        
        partial void CanAlertExecute(ref bool result);
    
        public Microsoft.Practices.Prism.Commands.DelegateCommand AlertCommand
        {
            get
            {
                return this._AlertCommand = this._AlertCommand ??
                    new Microsoft.Practices.Prism.Commands.DelegateCommand(
                        () => this.AlertExecute(),
                        () =>
                        {
                            bool ret = true;
                            this.CanAlertExecute(ref ret);
                            return ret;
                        });
            }
        }

        private Microsoft.Practices.Prism.Commands.DelegateCommand<string> _GreetCommand;
        
        partial void GreetExecute(string parameter);
        
        partial void CanGreetExecute(string parameter, ref bool result);
    
        public Microsoft.Practices.Prism.Commands.DelegateCommand<string> GreetCommand
        {
            get
            {
                return this._GreetCommand = this._GreetCommand ??
                    new Microsoft.Practices.Prism.Commands.DelegateCommand<string>(
                        p => this.GreetExecute(p),
                        p =>
                        {
                            bool ret = true;
                            this.CanGreetExecute(p, ref ret);
                            return ret;
                        });
            }
        }

        private string _Name;
    
        partial void NameChanging(string newValue, ref bool canSetValue);
        partial void NameChanged();
        
        public string Name
        {
            get
            {
                return this._Name;
            }
            
            set
            {
                if (this._Name == value)
                {
                    return;
                }
                
                bool canSetValue = true;
                this.NameChanging(value, ref canSetValue);
                if (!canSetValue)
                {
                    return;
                }
                
                this._Name = value;
                this.RaisePropertyChanged("Name");
                this.NameChanged();
            }
        }

        partial void FooChanging(string newValue, ref bool canSetValue);
        partial void FooChanged();
        
        public string Foo
        {
            get
            {
                return this.field.Foo;
            }
            
            set
            {
                bool canSetValue = true;
                this.FooChanging(value, ref canSetValue);
                if (!canSetValue)
                {
                    return;
                }
                
                this.field.Foo = value;
                this.FooChanged();
            }
        }
        
        
        class MainWindowViewModelAssociatedMetadataTypeTypeDescriptionProvider : System.ComponentModel.DataAnnotations.AssociatedMetadataTypeTypeDescriptionProvider
        {
            public MainWindowViewModelAssociatedMetadataTypeTypeDescriptionProvider()
                : base(typeof(MainWindowViewModel))
            {
            }
        }
    }
        
}

結構な量が作成されました。基本的にこのファイルはいじることはありません。たったあれだけのコードでこれだけのViewModelのコードが生成されます。

MVVMParts.ttinclude

これは、先ほどMainWindowViewModel.ttファイルで使ったCommandメソッドやPropertyメソッドが定義されています。ここをカスタマイズすることで任意のコードを吐きださせるようにすることが出来ますが、多分必要ないような気がします。
カスタマイズの可能性がある箇所は先頭に定義されている以下の2つの定数です。

const string RaisePropertyChanged = "RaisePropertyChanged";
const string DelegateCommand = "Microsoft.Practices.Prism.Commands.DelegateCommand";

上がプロパティ変更イベントを発行するメソッド名で下がDelegateCommandのクラス名です。デフォルト値はPrismのものになっていますが、別のMVVMフレームワークを使用する場合は適時書き換えることで対応可能です。

MainWindowViewModel.csでのカスタマイズ

MainWindowViewModel.ttで作成されたコードはpartialメソッドが定義されているので、それをMainWindowViewModel.csで上書きすることで処理を記述できます。基本的に以下のpartialメソッドが定義されています。

コマンド
  • コマンド名Execute(引数)メソッド
    • コマンドのExecuteが呼ばれた時に呼ばれるメソッド
  • Canコマンド名Execute(引数, ref bool result)メソッド
    • コマンドの実行可否を判断するメソッド。引数のresultに実行可否を表すbool型の値を代入するように実装します
プロパティ
  • プロパティ名Changing(新しいプロパティの値, ref bool canSetValue)メソッド
    • プロパティの値が変更される前に呼び出されます。canSetValueにfalseを設定するとプロパティの値は更新されません
  • プロパティ名Changedメソッド
    • プロパティが変更された後に呼び出されます。
実際にカスタマイズしてみる

MainWindowViewModel.ttのコマンドとプロパティの定義を以下のように変更します。

<#
// AlertCommand(パラメータ無し)を定義します
Command("Alert");
// string型のNameプロパティを定義します
Property("string", "Name");
// string型のMessageプロパティを定義します
Property("string", "Message");
#>				

これをベースにカスタマイズします。

  • Nameプロパティは必須入力項目
  • Alertコマンドは入力エラーが無くなったら実行可能
    • Alertコマンドが実行されるとMessageプロパティに[こんにちはNameさん]と表示する

それでは、MainWindowViewModel.csのクラス定義でpartialと入力しましょう。そうするとT4テンプレートで定義されたpartialメソッドが表示されます。

AlertExecuteとCanAlertExecuteを実装します。また、プロパティに変更があったときにAlertCommandの状態変更通知も行うようにします。

namespace MVVMSample.ViewModels
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Windows;
    using System.ComponentModel.DataAnnotations;

    [MetadataType(typeof(Metadata))]
    public partial class MainWindowViewModel : ViewModelBase
    {
        partial void AlertExecute()
        {
            this.Message = string.Format("こんにちは{0}さん", this.Name);
        }

        partial void CanAlertExecute(ref bool result)
        {
            // エラーが無いときに実行できる
            result = !this.HasErrors;
        }

        protected override void RaisePropertyChanged(string propertyName)
        {
            base.RaisePropertyChanged(propertyName);
            // プロパティに変更があったらコマンドの状態変更通知をおこなう
            this.AlertCommand.RaiseCanExecuteChanged();
        }

        class Metadata
        {
        }
    }
}

次に、DataAnnotationを使って入力値の検証を行います。これはMetadataクラスに指定していきます。WCF RIA Servicesでお馴染みですね。以下のように定義します。

namespace MVVMSample.ViewModels
{
    using System.ComponentModel.DataAnnotations;

    [MetadataType(typeof(Metadata))]
    public partial class MainWindowViewModel : ViewModelBase
    {
        public MainWindowViewModel()
        {
            // 初期状態でエラー値を入れておく
            this.Name = string.Empty;
        }

        partial void AlertExecute()
        {
            this.Message = string.Format("こんにちは{0}さん", this.Name);
        }

        partial void CanAlertExecute(ref bool result)
        {
            // エラーが無いときに実行できる
            result = !this.HasErrors;
        }

        protected override void RaisePropertyChanged(string propertyName)
        {
            base.RaisePropertyChanged(propertyName);
            // プロパティに変更があったらコマンドの状態変更通知をおこなう
            this.AlertCommand.RaiseCanExecuteChanged();
        }

        // このクラスのメタデータ
        class Metadata
        {
            // Nameは必須入力
            [Required]
            public string Name { get; set; }
        }
    }
}

これで、インテリセンスが効いてる状態でプロパティに属性がつけれます!素晴らしい。

Viewの実装

ViewはTextBoxとButtonとTextBlockを置いて、それぞれNameプロパティ、AlertCommandプロパティ、Messageプロパティとバインドしています。IDataErrorInfoの検証が有効になるようにValidatesOnDataErrorsをTrueに設定しています。

<Window x:Class="MVVMSample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:viewModels="clr-namespace:MVVMSample.ViewModels"
        Title="MainWindow" Height="350" Width="525">
    <Window.DataContext>
        <viewModels:MainWindowViewModel />
    </Window.DataContext>
    <Grid>
        <TextBox Height="24" HorizontalAlignment="Left" Margin="12,12,0,0" Name="textBox1" VerticalAlignment="Top" Width="209" Text="{Binding Path=Name, ValidatesOnDataErrors=True, UpdateSourceTrigger=PropertyChanged}" />
        <Button Content="Alert" Height="23" HorizontalAlignment="Left" Margin="12,42,0,0" Name="button1" VerticalAlignment="Top" Width="75" Command="{Binding Path=AlertCommand}" />
        <TextBlock HorizontalAlignment="Left" Margin="12,71,0,0" Name="textBlock1" Text="{Binding Path=Message}" VerticalAlignment="Top" />
    </Grid>
</Window>

実行

初期状態で入力エラーが表示され、ボタンが押せない状態になっています。

何かを入力したらエラーが消えてボタンが押せるようになります。

ボタンを押すといい感じにメッセージが表示されます。

ダウンロード

アイテムテンプレートと、ここで説明したサンプルプロジェクトはCodePlex上に公開しています。
MVVM Support Tools(Prism Based)