かずきのBlog@hatena

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

PrismのDelegateCommandとCommandManagerの連携

Prismは好きなんですがDelegateCommandのCanExecuteイベントハンドラが弱参照でイベントハンドラを持っているのと、CommandManagerのRequerySuggestedイベントも弱参照でイベントハンドラを持っているので、以下のようなコードを書くと、GCが走るとイベントに登録されてるDelegateがGCに回収されちゃって悲しいことになります。

CommandManager.RequerySuggested += (sender, args) => 
{
    HogeCommand.RaiseCanExecuteChanged();
};

以下のフォーラムでも討論されています。
http://compositewpf.codeplex.com/discussions/74364

とりあえず、ここで紹介しようとしている内容を最後につたない英語で書いておいたのですが・・・まぁいいです。英語できないので。日本語でここに書きます。要は弱参照でしか持たれてないなら、どっかで強参照で持ってしまえばいいじゃない?という発想です。1つ1つ手でやるのもめんどくさかったので、ViewModelの基本クラスでサクッとやっちゃいました。

以下のコードがそれです。

namespace Okazuki.MVVM.PrismSupport.ViewModels
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Windows.Input;
    using Microsoft.Practices.Prism.Commands;
    using Microsoft.Practices.Prism.ViewModel;
    using Okazuki.MVVM.PrismSupport.Utils;

    /// <summary>
    /// ViewModelの基本クラス。
    /// InteractionRequestクラスのインスタンスの自動初期化機能と、DelegateCommandのプロパティと
    /// CommandManager.RequerySuggestedの自動接続機能を提供します。
    /// </summary>
    public abstract class ViewModelBase : NotificationObject
    {

        protected ViewModelBase()
        {
            this.InitializeCommandProperties();
            this.InitializeInteractionRequestProperties();
        }

        /// <summary>
        /// AutoInitAttributeのついているIInteractionRequestの派生型のプロパティの値を初期化します。
        /// </summary>
        private void InitializeInteractionRequestProperties()
        {
            ObjectUtils.Init(this);
        }

        /// <summary>
        /// CommandManager.RequerySuggestedへのイベントハンドラの参照を保持します。
        /// </summary>
        private ICollection<EventHandler> eventHandlers = new List<EventHandler>();

        /// <summary>
        /// クラスに定義されたDelegateCommand型のプロパティのCanExecuteChangedイベントの発生と
        /// CommandManager.RequerySuggestedイベントの同期を行います。
        /// </summary>
        private void InitializeCommandProperties()
        {
            var commands = this.GetType().GetProperties()
                .Where(p => typeof(DelegateCommandBase).IsAssignableFrom(p.PropertyType))
                .Select(p => p.GetValue(this, null) as DelegateCommandBase);

            foreach (var command in commands)
            {
                EventHandler eventHandler = this.MakeEventHandler(command);
                CommandManager.RequerySuggested += eventHandler;
                this.eventHandlers.Add(eventHandler);
            }
        }

        private EventHandler MakeEventHandler(DelegateCommandBase command)
        {
            return (sender, e) => command.RaiseCanExecuteChanged();
        }

    }
}

InitializeCommandPropertiesがそれですね、DelegateCommandBaseのプロパティをかきあつめて、RaiseCanExecuteChangedを呼び出すイベントハンドラをCommandManagerに登録します。そのあと、内部でフィールドのListにも登録しておきます。Listに登録することで、GCへイベントハンドラが回収されることを防いでます。

とりあえず、これで問題なく動いています。

Prismに、個人的に欲しいなと思ってるようなクラスを実装しているものを、CodePlexで公開しています。まだ、公開しているのはアイテムテンプレートだけですが、ソースコードを覗いて貰うと、各種コードが入っています。何かの参考にして頂ければ幸いです。