かずきのBlog@hatena

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

MVVMとコマンドはどこへ向かっているのか

ということで、最近MVVMパターンでコマンドのプロパティをいちいち実装するのがめんどくさいと思っている今日このごろです。
ということで、ちょっと試行錯誤してみました。

コマンドのプロパティは要はバインドから取得できればいいだけで実在しなくてもいいんじゃないんだろうか?という観点です。
そうTypeDescriptorで、ありもしないICommand型のプロパティをあるかのように振る舞わせたら・・・?

ということで早速実装です。
まずは、ICommand型のプロパティを表すためのCommandPropertyDescriptorを作ります。

namespace AttrCommandTest
{
    using System;
    using System.ComponentModel;
    using System.Reflection;
    using System.Windows.Input;
    using Microsoft.Practices.Prism.Commands;

    public class CommandPropertyDescriptor<T> : PropertyDescriptor
    {
        // 呼び出すメソッド
        private MethodInfo targetMethod;

        // プロパティ名はメソッド名Command
        public CommandPropertyDescriptor(MethodInfo targetMethod)
            : base(targetMethod.Name + "Command", new Attribute[0])
        {
            this.targetMethod = targetMethod;
        }

        // リセットは出来ない
        public override bool CanResetValue(object component)
        {
            return false;
        }

        public override Type ComponentType
        {
            get { return typeof(T); }
        }

        public override object GetValue(object component)
        {
            // メソッドを呼び出す処理を
            return new DelegateCommand(
                () => this.targetMethod.Invoke(component, null));
        }

        // とりあえず読み取り専用
        public override bool IsReadOnly
        {
            get { return true; }
        }

        // コマンド型のプロパティ
        public override Type PropertyType
        {
            get { return typeof(ICommand); }
        }

        public override void ResetValue(object component)
        {
        }

        public override void SetValue(object component, object value)
        {
        }

        // シリアライズはしない
        public override bool ShouldSerializeValue(object component)
        {
            return false;
        }
    }
}

ちょっと長いですが、肝はGetValueです。ここでDelegateCommandを返しています。
後は、細工してやるためのTypeDescriptorとTypeDescriptionProviderを作ります。

// TypeDescriptor
namespace AttrCommandTest
{
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Linq;

    public class VMTypeDescriptor<T> : CustomTypeDescriptor
    {
        public VMTypeDescriptor(ICustomTypeDescriptor parent)
            : base(parent)
        {
        }

        // プロパティに細工をいれる
        public override PropertyDescriptorCollection GetProperties()
        {
            var props = new List<PropertyDescriptor>(base.GetProperties().OfType<PropertyDescriptor>());
            // CommandAttributeのついてるメソッドを抽出
            var methods = typeof(T).GetMethods()
                .Where(m => m.GetCustomAttributes(typeof(CommandAttribute), true).Any());
            foreach (var m in methods)
            {
                // コマンドのプロパティがあるように細工
                props.Add(new CommandPropertyDescriptor<T>(m));
            }
            return new PropertyDescriptorCollection(props.ToArray());
        }
    }
}

こいつで、あとで作るCommandAttributeという属性がついてるメソッドに対応するコマンドのプロパティがあるかのように細工しています。
CommandAttributeは、特に何もないただのAttributeです。

namespace AttrCommandTest
{
    using System;

    [AttributeUsage(AttributeTargets.Method)]
    public class CommandAttribute : Attribute
    {
    }
}

そして、最後にTypeDescriptionProviderを作成します。

namespace AttrCommandTest
{
    using System;
    using System.ComponentModel;

    public class VMTypeProvider<T> : TypeDescriptionProvider
    {
        public VMTypeProvider()
            : base(TypeDescriptor.GetProvider(typeof(T)))
        {
        }
        // TypeDescriptorの差し替え
        public override ICustomTypeDescriptor GetTypeDescriptor(Type objectType, object instance)
        {
            return new VMTypeDescriptor<T>(base.GetTypeDescriptor(objectType, instance));
        }
    }
}

これで何が出来るかというと・・・コマンドを定義しなくてもメソッドに属性をつけるだけでコマンドがあるかのように見せることが出来ます。
ViewModelは、以下のようになります。コマンドがなくてすっきり!!

namespace AttrCommandTest
{
    using System;
    using System.ComponentModel;
    using Microsoft.Practices.Prism.ViewModel;

    // TypeDescriptionProviderをさっき作ったやつに対応させる
    [TypeDescriptionProvider(typeof(VMTypeProvider<MainWindowViewModel>))]
    public class MainWindowViewModel : NotificationObject
    {
        private string message;

        // Command属性をつけて、このメソッドを呼ぶコマンドのプロパティが
        // あるかのように見せる
        [Command]
        public void Execute()
        {
            // メッセージを更新する処理
            this.Message = "こんにちはボタン " + DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss.fff");
        }

        // 画面に表示するメッセージ
        public string Message
        {
            get
            {
                return this.message;
            }

            set
            {
                this.message = value;
                base.RaisePropertyChanged(() => Message);
            }
        }
    }
}

こいつを紐づけるView側は、以下のようになります。

<Window x:Class="AttrCommandTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:l="clr-namespace:AttrCommandTest"
        Title="MainWindow" Height="350" Width="525">
    <Window.DataContext>
        <!-- ViewModelを設定 -->
        <l:MainWindowViewModel />
    </Window.DataContext>
    <Grid>
        <!-- ViewModelに存在しないコマンドにバインドしてるけど動く -->
        <Button 
            Content="Button" 
            HorizontalAlignment="Left" 
            Margin="12,12,0,0" 
            VerticalAlignment="Top" 
            Command="{Binding ExecuteCommand}" />
        
        <TextBlock 
            HorizontalAlignment="Left" 
            Margin="12,41,0,0" 
            Text="{Binding Path=Message}" 
            VerticalAlignment="Top" />
    </Grid>
</Window>

肝は、Executeメソッドしか定義してないのにExecuteCommandという名前でコマンドをバインドしてるところです。
実際に実行してみると・・・


ちゃんとExecuteメソッドが実行されてるのがわかります。Command属性つけるだけでCommandのプロパティがあるかのように動く。なんだか動きとしては理想形に近づいてきたような気がします。
まぁ、とりあえず作ってみただけなんでCanExecuteへの対応やパラメータへの対応や性能面とか色々課題はありそうですが・・・。


個人的に一番の課題だと思ってるのは、Visual Studio 2010のデータバインディングビルダーにTypeDescriptorで細工して入れ込んだプロパティが表示されないってことです。

これさえうまくいけば・・・!!!と思う今日この頃です。