かずきのBlog@hatena

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

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で公開しています。まだ、公開しているのはアイテムテンプレートだけですが、ソースコードを覗いて貰うと、各種コードが入っています。何かの参考にして頂ければ幸いです。