WPF Archive

MAY
3

Southridge Hands-on-Labが公開されています

Published:2009-05-03 12:26:56 UTC
WPF

image WPF Futuresには、WPF Model-View-ViewModel Toolkitの他にも色々な参考になるサンプルが含まれています。Southridge Hands-on-LabWPF Ribbon ControlとDataGridを使ったリッチなアプリケーション開発のチュートリアルとなっており、PDCのセッションでも出てきていたものがチュートリアルの形式としてまとめられた物のようですね。結構大きなアプリケーションですが、手取り足取り細かく解説してくれるので、参考になるところが多いのではないでしょうか。

ただ、M-V-VMパターンには則っていないところがあり、ViewModelに書くべきロジックをViewのコードビハインドに書いていたりするのでM-V-VMパターンのサンプルとするには注意が必要です。WPF Ribbon Controlのメニュー等は、CommandとしてExecuteメソッドやCanExecuteメソッド以外にも、LabelTitleプロパティなどを持ったオブジェクトを必要とするため、View側でCommandとしてセットするオブジェクトを用意する必要があります。その制約に引きずられてViewのコードビハインドにCommandの処理ロジックを記述しているようです。

では、Ribbon Controlを使用する場合はM-V-VMパターンが使えないかというとそういう訳でもなく、SouthridgeをM-V-VMパターンに則って書き直したバージョンも存在しているようです。自分は詳しく調べられていないのですが、先ほどの問題はNonRoutedRibbonCommandDelegatorというRibbonCommandと同じインターフェイスを備えたプロキシクラスをはさむことで解決しているようです。

・・・とここまで書いたところで気づいたのですが、Ribbon V1 Roadmapをみたところ、Ribbon関係のCommandに設計変更が入るみたいですね。LabelTitleとかまでRibbonCommandに纏めるのをやめて、通常のコントロールと同じようなスタイルに戻るようです。これは素直に新しいバージョンがリリースされるのを待ったほうが楽かもしれません。

MAY
3

WPF Model-View-ViewModel Toolkitのテンプレートやサンプルに含まれるDelegateCommand.cs、とりあえずコメントを日本語訳しながら読んでみました。DelegateCommandという名前ですが、Composite Application Guidance for WPFのDelegateCommandとは異なります。MSDN MagazineのWPF のための MODEL-VIEW-VIEWMODEL (MVVM) デザイン パターンという記事で使われているRelayCommandに、CommandManagerのRequerySuggestedイベントへの応答の有効/無効の設定プロパティ(IsAutomaticRequeryDisabled)と、CanExecuteChangedイベントを発生させるパブリックメソッド(RaiseCanExecuteChanged)を追加し、メモリリークを防ぐためにCanExecuteChangedイベントへのハンドラを弱参照で持つようにした?感じですね。

// This source code is licensed by Microsoft under Ms-PL license.
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Input;

namespace WpfModelViewApplication1.Commands
{
    /// <summary>
    /// <para>
    ///     This class allows delegating the commanding logic to methods passed as parameters,
    ///     and enables a View to bind commands to objects that are not part of the element tree.
    /// </para>
    /// <para>
    ///     このクラスはパラメータとして渡されたメソッドへのコマンドのロジックの委譲を実現します。
    ///     また、Viewが要素ツリーに含まれないオブジェクトにコマンドをバインドすることを可能にします。
    /// </para>
    /// </summary>
    public class DelegateCommand : ICommand
    {
        #region Constructors

        /// <summary>
        ///     Constructor
        /// </summary>
        public DelegateCommand(Action executeMethod)
            : this(executeMethod, null, false)
        {
        }

        /// <summary>
        ///     Constructor
        /// </summary>
        public DelegateCommand(Action executeMethod, Func<bool> canExecuteMethod)
            : this(executeMethod, canExecuteMethod, false)
        {
        }

        /// <summary>
        ///     Constructor
        /// </summary>
        public DelegateCommand(Action executeMethod, Func<bool> canExecuteMethod, bool isAutomaticRequeryDisabled)
        {
            if (executeMethod == null)
            {
                throw new ArgumentNullException("executeMethod");
            }

            _executeMethod = executeMethod;
            _canExecuteMethod = canExecuteMethod;
            _isAutomaticRequeryDisabled = isAutomaticRequeryDisabled;
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// <para>
        ///     Method to determine if the command can be executed
        /// </para>
        /// <para>
        ///     コマンドが実行可能かを返す
        /// </para>
        /// </summary>
        public bool CanExecute()
        {
            if (_canExecuteMethod != null)
            {
                return _canExecuteMethod();
            }
            return true;
        }

        /// <summary>
        /// <para>
        ///     Execution of the command
        /// </para>
        /// <para>
        ///     コマンドを実行する
        /// </para>
        /// </summary>
        public void Execute()
        {
            if (_executeMethod != null)
            {
                _executeMethod();
            }
        }

        /// <summary>
        /// <para>
        ///     Property to enable or disable CommandManager's automatic requery on this command
        /// </para>
        /// <para>
        ///     CommandManagerのこのコマンドに対する自動再要求の有効/無効を設定する
        /// </para>
        /// </summary>
        public bool IsAutomaticRequeryDisabled
        {
            get
            {
                return _isAutomaticRequeryDisabled;
            }
            set
            {
                if (_isAutomaticRequeryDisabled != value)
                {
                    if (value)
                    {
                        CommandManagerHelper.RemoveHandlersFromRequerySuggested(_canExecuteChangedHandlers);
                    }
                    else
                    {
                        CommandManagerHelper.AddHandlersToRequerySuggested(_canExecuteChangedHandlers);
                    }
                    _isAutomaticRequeryDisabled = value;
                }
            }
        }

        /// <summary>
        /// <para>
        ///     Raises the CanExecuteChaged event
        /// </para>
        /// <para>
        ///     CanExecuteChangedイベントを発生させる
        /// </para>
        /// </summary>
        public void RaiseCanExecuteChanged()
        {
            OnCanExecuteChanged();
        }

        /// <summary>
        /// <para>
        ///     Protected virtual method to raise CanExecuteChanged event
        /// </para>
        /// <para>
        ///     CanExecuteChangedイベントを発生させるprotected virtualメソッド
        /// </para>
        /// </summary>
        protected virtual void OnCanExecuteChanged()
        {
            CommandManagerHelper.CallWeakReferenceHandlers(_canExecuteChangedHandlers);
        }

        #endregion

        #region ICommand Members

        /// <summary>
        /// <para>
        ///     ICommand.CanExecuteChanged implementation
        /// </para>
        /// <para>
        ///     ICommand.CanExecuteChangedの実装
        /// </para>
        /// </summary>
        public event EventHandler CanExecuteChanged
        {
            add
            {
                if (!_isAutomaticRequeryDisabled)
                {
                    CommandManager.RequerySuggested += value;
                }
                CommandManagerHelper.AddWeakReferenceHandler(ref _canExecuteChangedHandlers, value, 2);
            }
            remove
            {
                if (!_isAutomaticRequeryDisabled)
                {
                    CommandManager.RequerySuggested -= value;
                }
                CommandManagerHelper.RemoveWeakReferenceHandler(_canExecuteChangedHandlers, value);
            }
        }

        bool ICommand.CanExecute(object parameter)
        {
            return CanExecute();
        }

        void ICommand.Execute(object parameter)
        {
            Execute();
        }

        #endregion

        #region Data

        private readonly Action _executeMethod = null;
        private readonly Func<bool> _canExecuteMethod = null;
        private bool _isAutomaticRequeryDisabled = false;
        private List<WeakReference> _canExecuteChangedHandlers;

        #endregion
    }

    /// <summary>
    /// <para>    
    ///     This class allows delegating the commanding logic to methods passed as parameters,
    ///     and enables a View to bind commands to objects that are not part of the element tree.
    /// </para>
    /// <para>    
    ///     このクラスはパラメータとして渡されたメソッドへのコマンドのロジックの委譲を実現します。
    ///     また、Viewが要素ツリーに含まれないオブジェクトにコマンドをバインドすることを可能にします。
    /// </para>
    /// </summary>
    /// <typeparam name="T">Type of the parameter passed to the delegates</typeparam>
    public class DelegateCommand<T> : ICommand
    {
        #region Constructors

        /// <summary>
        ///     Constructor
        /// </summary>
        public DelegateCommand(Action<T> executeMethod)
            : this(executeMethod, null, false)
        {
        }

        /// <summary>
        ///     Constructor
        /// </summary>
        public DelegateCommand(Action<T> executeMethod, Func<T, bool> canExecuteMethod)
            : this(executeMethod, canExecuteMethod, false)
        {
        }

        /// <summary>
        ///     Constructor
        /// </summary>
        public DelegateCommand(Action<T> executeMethod, Func<T, bool> canExecuteMethod, bool isAutomaticRequeryDisabled)
        {
            if (executeMethod == null)
            {
                throw new ArgumentNullException("executeMethod");
            }

            _executeMethod = executeMethod;
            _canExecuteMethod = canExecuteMethod;
            _isAutomaticRequeryDisabled = isAutomaticRequeryDisabled;
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// <para>
        ///     Method to determine if the command can be executed
        /// </para>
        /// <para>
        ///     コマンドが実行可能かを返す
        /// </para>
        /// </summary>
        public bool CanExecute(T parameter)
        {
            if (_canExecuteMethod != null)
            {
                return _canExecuteMethod(parameter);
            }
            return true;
        }

        /// <summary>
        /// <para>
        ///     Execution of the command
        /// </para>
        /// <para>
        ///     コマンドを実行する
        /// </para>
        /// </summary>
        public void Execute(T parameter)
        {
            if (_executeMethod != null)
            {
                _executeMethod(parameter);
            }
        }

        /// <summary>
        /// <para>
        ///     Raises the CanExecuteChaged event
        /// </para>
        /// <para>
        ///     CanExecuteChangedイベントを発生させる
        /// </para>
        /// </summary>
        public void RaiseCanExecuteChanged()
        {
            OnCanExecuteChanged();
        }

        /// <summary>
        /// <para>
        ///     Protected virtual method to raise CanExecuteChanged event
        /// </para>
        /// <para>
        ///     CanExecuteChangedイベントを発生させるprotected virtualメソッド
        /// </para>
        /// </summary>
        protected virtual void OnCanExecuteChanged()
        {
            CommandManagerHelper.CallWeakReferenceHandlers(_canExecuteChangedHandlers);
        }

        /// <summary>
        /// <para>
        ///     Property to enable or disable CommandManager's automatic requery on this command
        /// </para>
        /// <para>
        ///     CommandManagerのこのコマンドに対する自動再要求の有効/無効を設定する
        /// </para>
        /// </summary>
        public bool IsAutomaticRequeryDisabled
        {
            get
            {
                return _isAutomaticRequeryDisabled;
            }
            set
            {
                if (_isAutomaticRequeryDisabled != value)
                {
                    if (value)
                    {
                        CommandManagerHelper.RemoveHandlersFromRequerySuggested(_canExecuteChangedHandlers);
                    }
                    else
                    {
                        CommandManagerHelper.AddHandlersToRequerySuggested(_canExecuteChangedHandlers);
                    }
                    _isAutomaticRequeryDisabled = value;
                }
            }
        }

        #endregion

        #region ICommand Members

        /// <summary>
        /// <para>
        ///     ICommand.CanExecuteChanged implementation
        /// </para>
        /// <para>
        ///     ICommand.CanExecuteChangedの実装
        /// </para>
        /// </summary>
        public event EventHandler CanExecuteChanged
        {
            add
            {
                if (!_isAutomaticRequeryDisabled)
                {
                    CommandManager.RequerySuggested += value;
                }
                CommandManagerHelper.AddWeakReferenceHandler(ref _canExecuteChangedHandlers, value, 2);
            }
            remove
            {
                if (!_isAutomaticRequeryDisabled)
                {
                    CommandManager.RequerySuggested -= value;
                }
                CommandManagerHelper.RemoveWeakReferenceHandler(_canExecuteChangedHandlers, value);
            }
        }

        bool ICommand.CanExecute(object parameter)
        {
            // if T is of value type and the parameter is not
            // set yet, then return false if CanExecute delegate
            // exists, else return true

            // Tが値型でかつ、parameterがセットされていない場合、
            // CanExecuteデリゲートが存在するならfalseを返します。
            // そうでなければtrueを返します。
            if (parameter == null &&
                typeof(T).IsValueType)
            {
                return (_canExecuteMethod == null);
            }
            return CanExecute((T)parameter);
        }

        void ICommand.Execute(object parameter)
        {
            Execute((T)parameter);
        }

        #endregion

        #region Data

        private readonly Action<T> _executeMethod = null;
        private readonly Func<T, bool> _canExecuteMethod = null;
        private bool _isAutomaticRequeryDisabled = false;
        private List<WeakReference> _canExecuteChangedHandlers;

        #endregion
    }

    /// <summary>
    /// <para>
    ///     This class contains methods for the CommandManager that help avoid memory leaks by
    ///     using weak references.
    /// </para>
    /// <para>
    ///     このクラスはCommandManagerに対する、弱参照を使うことでメモリリークを防ぐ助けとなるメソッドを含んでいます。
    /// </para>
    /// </summary>
    internal class CommandManagerHelper
    {
        internal static void CallWeakReferenceHandlers(List<WeakReference> handlers)
        {
            if (handlers != null)
            {
                // Take a snapshot of the handlers before we call out to them since the handlers
                // could cause the array to me modified while we are reading it.

                // 我々が私への配列を読んでいる間にハンドラはそれの変更を引き起こしうるので、
                // それらを呼び出す前にハンドラのスナップショットをとります。

                EventHandler[] callees = new EventHandler[handlers.Count];
                int count = 0;

                for (int i = handlers.Count - 1; i >= 0; i--)
                {
                    WeakReference reference = handlers[i];
                    EventHandler handler = reference.Target as EventHandler;
                    if (handler == null)
                    {
                        // Clean up old handlers that have been collected
                        // 収集されたハンドラを削除します
                        handlers.RemoveAt(i);
                    }
                    else
                    {
                        callees[count] = handler;
                        count++;
                    }
                }

                // Call the handlers that we snapshotted

                // スナップショットをとったハンドラを呼びます
                for (int i = 0; i < count; i++)
                {
                    EventHandler handler = callees[i];
                    handler(null, EventArgs.Empty);
                }
            }
        }

        internal static void AddHandlersToRequerySuggested(List<WeakReference> handlers)
        {
            if (handlers != null)
            {
                foreach (WeakReference handlerRef in handlers)
                {
                    EventHandler handler = handlerRef.Target as EventHandler;
                    if (handler != null)
                    {
                        CommandManager.RequerySuggested += handler;
                    }
                }
            }
        }

        internal static void RemoveHandlersFromRequerySuggested(List<WeakReference> handlers)
        {
            if (handlers != null)
            {
                foreach (WeakReference handlerRef in handlers)
                {
                    EventHandler handler = handlerRef.Target as EventHandler;
                    if (handler != null)
                    {
                        CommandManager.RequerySuggested -= handler;
                    }
                }
            }
        }

        internal static void AddWeakReferenceHandler(ref List<WeakReference> handlers, EventHandler handler)
        {
            AddWeakReferenceHandler(ref handlers, handler, -1);
        }

        internal static void AddWeakReferenceHandler(ref List<WeakReference> handlers, EventHandler handler, int defaultListSize)
        {
            if (handlers == null)
            {
                handlers = (defaultListSize > 0 ? new List<WeakReference>(defaultListSize) : new List<WeakReference>());
            }

            handlers.Add(new WeakReference(handler));
        }

        internal static void RemoveWeakReferenceHandler(List<WeakReference> handlers, EventHandler handler)
        {
            if (handlers != null)
            {
                for (int i = handlers.Count - 1; i >= 0; i--)
                {
                    WeakReference reference = handlers[i];
                    EventHandler existingHandler = reference.Target as EventHandler;
                    if ((existingHandler == null) || (existingHandler == handler))
                    {
                        // Clean up old handlers that have been collected
                        // in addition to the handler that is to be removed.

                        // 削除されるハンドラに加えて、収集された古いハンドラ
                        // を削除します。
                        handlers.RemoveAt(i);
                    }
                }
            }
        }
    }
}

5/1にCodePlexのWPFのサイトでリリースされたWPF Futures、様々なサンプルやテンプレートが含まれていて興味深いのですが、この中には、WPF Model-View-ViewModel Toolkitというものも含まれていました。
内容は、

  • Visual Studio 2008用のテンプレート (Visual C# Express 2008もサポート)
  • 文書 
    • M-V-VMの概略紹介
    • VSテンプレート利用の手引き
  • MVVMパターンの適用されたWPFアプリケーションのデモ

となっています。

Visual Studioテンプレート

規約というほどのものではありませんが、M-V-VMパターンもRuby on RailsやASP.NET MVC、CakePHPなどのMVCパターンのフレームワークのように命名規則やディレクトリ構造の作法が存在します。Model、View、ViewModel毎にディレクトリを作成し、その中にModelやViewといった接尾辞をつけたクラスを配置するといったものです。この作法に則ったプロジェクトを簡単に作成するために、WPF Model-View-ViewModel ToolkitにはVisual Studioのテンプレートが含まれています。

テンプレートのインストール

ダウンロードしてきたToolkitのzipファイルの中には、WPFModelViewTemplate.msiが含まれています。まずはこれを実行してインストールを行います。
image

ただし、Visual Studioの日本語環境では、これだけでは利用可能になりません。ASP.NET MVCのPreview版がそうであったように、Visual Studio のテンプレート が英語環境用(1033)にしかセットアップされませんので、英語環境用にインストールされたファイルを日本語環境用(1041)にコピーしてやる必要があります。

以下の位置に存在するファイルを、
<Visual Studioのインストールディレクトリ>\Common7\IDE\ProjectTemplates\CSharp\Windows\1033\WpfModelViewApplicationProjectTemplateV0.1.cs.zip

こちらのディレクトリにコピーします。
<Visual Studioのインストールディレクトリ>\Common7\IDE\ProjectTemplates\CSharp\Windows\1041\

imageその上で、管理者として実行したVisual Studioコマンドプロンプトから

devenv /InstallVSTemplates 

を実行し、VisualStudioに変更を反映させます。

これで、テンプレートが追加されました。

image

作成されたプロジェクトはこのような構造となります。

image

CommandやViewModelの基本クラスも用意されています。CommandやViewModelの基本クラスは様々な人が公開しているものの中からどれを選択すべきか迷うところがありましたが、これからWPF Model-View-ViewModel Toolkitが標準として育っていけば、迷うことも少なくなりそうですね。

DelegateCommandやViewModelBaseについては、追って解説エントリをあげられればと思います。

M-V-VMパターンとアニメーションをどのようにして組み合わせるのかは、MVVMパターンを使う上で常に頭を悩ませる問題です。この問題に対して、Josh SmithさんがOrchestrating Animated Transitions between View and ViewModel >> Josh Smith on WPFというエントリでひとつの方法を示していたので、今回はこれを訳してみました。

Introduction

Model-View-ViewModel(MVVM)パターンを使う場合、Viewに表示されるデータはほぼ常にデータバインディングを通じてやり取りされます。ViewModelオブジェクトのプロパティに対する変更はすぐにViewのバインディングターゲットに対して伝達されます。これは普通は良いことです。しかしながら、時によっては、伝達が即時に行われることが問題になることがあります。

When working with the Model-View-ViewModel (MVVM) pattern, the data shown in a View is almost always transferred to it via data binding.  Changes made to properties on a ViewModel object immediately propagate to the binding targets in the View.  This is normally a good thing.  Sometimes, however, the immediacy of the propagation can be a problem.

なぜ新しいプロパティの値がUIに即座には現われて欲しくないと思うのでしょうか?では、古い値と新しい値との間でのアニメーション化された遷移を表示したい場合を考えてみてください。新しい値が即座にViewに表示された場合、新しい値を”アニメートイン”する前に古い値を”アニメートアウト”するチャンスがありません。例えば、プロパティの値がリストとして表示される1ページのデータ項目だと考えてみてください。新しい項目のページをフェードする前に最初のページのアイテムをフェードアウトしたい場合、新しい項目のページが表示される前に、フェードアウトアニメーションを始める必要があります。

Why would you want a new property value to not immediately show up in the UI?  Well, suppose you want to display an animated transition between the old value and the new value.  If the new value immediately shows up in the View, you won’t have a chance to animate “out” the old value before animating “in” the new value.   For example, suppose the property’s value is a page of data items to show in a list.  If we want to fade away the first page of items before fading in the new page of items, we need to start the fade-away animations before the new page of items is displayed.

これは、Visual State Manager(VSM)が解決する種類の問題です。VSMは未だ正式なWPFの一部になっておらず(.NET 4.0まではなりません)、我々は未だそれをアプリケーション作成に役立てることはできません。また、VSMはカスタムコントロールの中で使われるために設計されているので、私達がこれから探そうというものとは完全に同じという訳ではありません。

This is the type of problem that the Visual State Manager (VSM) solves.  Since the VSM is not yet part of WPF proper (it won’t be until .NET 4.0), we cannot make use of it yet in production applications.  Also, the VSM is designed to be used inside of custom controls, so it is not exactly equivalent to what we are going to explore below.

The AnimatedTransition Class

私はWPFやSilverlightのアプリケーションに取り組んでいる際に、まさにこの問題に何回も直面しました。プロパティの変化をアニメーション化された遷移と協調させるための様々なアプローチを試したあと、私は核となる問題を、より鮮明に理解し始めました。昨日私はScherzo from Mendelssohn’s “A Midsummer’s Night Dream”のこのとてつもなく素晴らしい演奏を聴き、そしてパッと閃きました。私は二つのプロパティ値間でのアニメーション化された遷移を作成するための、ViewModelとViewをして協調させるシンプルで、タイプセーフで、再利用可能な方法を悟りました。AnimatedTransitionクラスが生まれたのです!

I faced this exact problem many times while working on WPF and Silverlight applications.  After experimenting with several approaches to coordinating property changes with animated transitions, I started to understand the core problem more clearly.   Yesterday I listened to this incredible rendition of the Scherzo from Mendelssohn’s “A Midsummer’s Night Dream” and was hit by a flash of inspiration.  I realized a way to create a simple, type-safe, reusable way of having a ViewModel and View work together to create animated transitions between two property values.  The AnimatedTransition class was born!

image

AnimatedTransitionのインスタンスはどのViewModelクラスにでも埋め込むことが出来ます。遷移を行う必要のあるViewは、遷移を何時始めるべきかを知るために、AnimatedTransitionオブジェクトにアクセスし、そのイベントの一つ以上をフックする必要があります。

An instance of AnimatedTransition can be embedded in any ViewModel class.  The View(s) that need to perform transitions must access the AnimatedTransition object and hook one or more of its events in order to know when transitions should start.

The Demo App

このエントリの最後に載せたデモアプリでは、以下のように「ページ」毎に四つのハローキティを表示するItemsControlがあります。

In the demo app, which is available at the bottom of this post, there is an ItemsControl that displays four Hello Kitty images per “page,” as seen below…

image

ユーザ―が下の四角を通じて別のページに遷移する際に、古いキティがフェードアウトし、新しいのがフェードインします。

When the user navigates to a different page, via the circles below, the old kitties fade away and then the new ones fade in.

この小さいアプリを動かすのに関係している全てのクラスを精査していくことはしません。ViewModelクラスについてしっかりと理解したい場合は、このクラスダイアグラムをクリックしてフルサイズのを見てください。

We are not going to examine all of the classes involved in making this little app work.  If you want to get a high-level understanding of the ViewModel classes, click on this class diagram to view it at full size:

image

ItemsControlに表示されるキティの現在のページはKittyKatTerraceViewModelのSelectedPageプロパティに含まれます。あのプロパティの二つの値の間での遷移を必要とする場合、我々はAnimatedTransitionを埋め込んで使います。KittyKatTerraceViewModelがAnimatedTransitionを使用し、公開する方法はこのようになっています:

The current page of kitties to show in the ItemsControl is contained in SelectedPage property of KittyKatTerraceViewModel.  Since we need to have a transition between the two values of that property, we use an AnimatedTransition to enable that.  Here is how KittyKatTerraceViewModel uses and exposes an AnimatedTransition:

public AnimatedTransition<PageOfKittyKatsViewModel> GetSelectedPageTransition()
{
    if (_selectedPageTransition == null)
        _selectedPageTransition =
            new AnimatedTransition<PageOfKittyKatsViewModel>(page => this.SelectedPage = page);

    return _selectedPageTransition;
}
public PageOfKittyKatsViewModel SelectedPage
{
    get { return _selectedPage; }
    private set
    {
        if (value == _selectedPage)
            return;

        _selectedPage = value;

        foreach (var page in this.Pages)
            page.RefreshSelectionState();

        this.OnPropertyChanged("SelectedPage");
    }
}

ユーザーがページを選択するためにItemsControlの真下の小さい四角をクリックしたとき、PageOfKittyKatsViewModelクラスのこのメソッドが実行されます:

When the user clicks on a little circle beneath the ItemsControl to select a page, this method in the PageOfKittyKatsViewModel class executes:

void Select()
{
	// Tell the KittyKatTerraceViewModel to begin
	// a transition of the SelectedPage property
	// so that it references this page.
    _owner.GetSelectedPageTransition().Start(this);
}

KittyKatTerraceViewコントロールのコードビハインドにある全ての関連するロジックを以下にリストアップします。これはViewがどうやってAnimatedTransitionオブジェクトを使用し、相互作用しているかを示しています。

All of the relevant logic in the KittyKatTerraceView control’s code-behind is listed below.  This shows how a View can consume and interact with an AnimatedTransition object.

void OnItemsHostPanelLoaded(object sender, RoutedEventArgs e)
{
    _itemsHost = sender as Panel;
}

void OnLoaded(object sender, RoutedEventArgs e)
{
    var viewModel = base.DataContext as KittyKatTerraceViewModel;

    viewModel.SetKittyLocations(new List<Point>
    {
        new Point(255, 65),
        new Point(115, 145),
        new Point(280, 225),
        new Point(135, 340)
    });

    _transition = viewModel.GetSelectedPageTransition();
    _transition.BeforeApplyNewValue += this.OnBeforeNewSelectedPage;
    _transition.AfterApplyNewValue += this.OnAfterNewSelectedPage;
}

void OnBeforeNewSelectedPage(object sender, EventArgs e)
{
    Storyboard sb = this.CreateStoryboard("FadeOutAnim");
    sb.Completed += delegate { _transition.Finish(); };
    sb.Begin();
}

void OnAfterNewSelectedPage(object sender, EventArgs e)
{
    // Wait until the panel has new children before animating.
    _itemsHost.LayoutUpdated += this.OnItemsHostLayoutUpdated;
}

void OnItemsHostLayoutUpdated(object sender, EventArgs e)
{
    _itemsHost.LayoutUpdated -= this.OnItemsHostLayoutUpdated;
    this.CreateStoryboard("FadeInAnim").Begin();
}

古いアイテムをフェードアウトさせるStoryboardのCompletedイベントにハンドラをアタッチする必要があることによく気を付けて下さい。そのイベントが発生したとき、新しい値への遷移を終わらせるために、AnimatedTransitionは発生を伝えられる必要があり、それをうけて実際にその値を設定します。それはFinish()メソッドを呼ぶことによって実現されます。一度新しい値が設定されたならば、AfterApplyNewValueイベントが発生し、その際あなたは新しいアイテムにフェードインすることが出来ます。

It’s important to note that you must attach a handler to the Completed event of the Storyboard that fades away the old items.  When the event is raised, the AnimatedTransition must be told to finish transitioning to the new value, and actually apply the value.  That is accomplished by calling the Finish() method.  Once the new value has been applied, the AfterApplyNewValue event is raised, at which time you can fade in the new items.

ソースコードはここからダウンロードすることが出来ます。注意:ファイルの拡張子を.DOCから.ZIPに変更して解凍して下さい。

You can download the source code here.  Note: Be sure to change the file extension from .DOC to .ZIP and then decompress it.

APR
24

※今回のエントリはWPFでの話ですが、Silverlightでも参考になると思います。

Expression Blend 3で追加されたBehavior、元はと言えば、Attached Behavior(添付ビヘイビア)という名前でWPF/Silverlight開発者の間で親しまれていたパターンが、Expression Blend 3でデザイナのサポートを受けれるようになったものです。

添付ビヘイビアとは、内部的には添付プロパティを利用し、添付された要素への参照を得ることでそのイベントを購読し、そのイベントハンドラにおいて、オブジェクトに対する様々な操作を実現する便利なテクニックです。

BehaviorがExpression Blendでサポートされたことにより、要素に対する添付ビヘイビアの追加、削除を、コードを書かずにGUI操作だけでできるようになりました。また、Microsoft.Expression.Interactivity.dll内の抽象クラスを利用することで、実装もより容易になっています。

image

Preview版では現在のところBehaviorはExpression Blendに付属しておらず、別途ダウンロードしてきて参照に加えることが必要ですが(Sample Silverlight 3 Behaviors(Silverlight)やMicrosoft Expression Community Gallery)、自分で書いたBehaviorを使うことももちろん可能です。 今回は試しにBehaviorを自分で書いてみました。お題は以前書いた添付ビヘイビアでTextBoxにCommandを実装してみた – SharpLab.の再実装。

まずは<Expression Blend3 Previewのインストールディレクトリ>\Libraries\WPF以下に存在するMicrosoft.Expression.Interactivity.dllを参照を加えます。このアセンブリに含まれているBehavior<T>や、TriggerAction<T>などが、Behaviorを作る上での基本クラスとなるようです(他にもあるようですが、まだ追えていません)。さて、今回はTextBoxに添付するBehaviorなので、Behavior<TextBox>を継承したクラスを作ります。

using System.Windows;
using System.Windows.Input;
using Microsoft.Expression.Interactivity;
using System.Windows.Controls;

namespace SharpLab.IKnow.ItemBankPane {
	public class ExecuteCommandOnEnterKeyDownBehavior : Behavior<TextBox> {
	}
}

続いて、アタッチ/デタッチされる時に実行されるOnAttach/OnDetachingメソッドを作成します。AssociatedObjectプロパティに添付対象の要素が入っているので、ここからイベントの購読/購読解消を行います。

using System.Windows;
using System.Windows.Input;
using Microsoft.Expression.Interactivity;
using System.Windows.Controls;

namespace SharpLab.IKnow.ItemBankPane {
	public class ExecuteCommandOnEnterKeyDownBehavior : Behavior<TextBox> {

		protected override void OnAttached() {
			base.OnAttached();

			this.AssociatedObject.KeyDown += OnKeyDown;
		}

		protected override void OnDetaching() {
			base.OnDetaching();

			this.AssociatedObject.KeyDown -= OnKeyDown;
		}

		void OnKeyDown(object sender, KeyEventArgs e) {
		
		}
	}
}

さて、今回はCommandをEnterキーが押された時に実行するのが目的なので、まずはCommandを受け取れるように依存関係プロパティを実装します。

using System.Windows;
using System.Windows.Input;
using Microsoft.Expression.Interactivity;
using System.Windows.Controls;

namespace SharpLab.IKnow.ItemBankPane {
	public class ExecuteCommandOnEnterKeyDownBehavior : Behavior<TextBox> {

		public string Command {
			get {
				return (string)GetValue(CommandProperty);
			}
			set {
				SetValue(CommandProperty, value);
			}
		}

		public static readonly DependencyProperty CommandProperty =	
									DependencyProperty.Register("Command",
										typeof(string),
										typeof(ExecuteCommandOnEnterKeyDownBehavior));

		protected override void OnAttached() {
			base.OnAttached();

			this.AssociatedObject.KeyDown += OnKeyDown;
		}

		protected override void OnDetaching() {
			base.OnDetaching();

			this.AssociatedObject.KeyDown -= OnKeyDown;
		}

		void OnKeyDown(object sender, KeyEventArgs e) {
		}
	}
}

Enterキーが押された時に、Commandが実行されるようにします。

using System.Windows;
using System.Windows.Input;
using Microsoft.Expression.Interactivity;
using System.Windows.Controls;

namespace SharpLab.IKnow.ItemBankPane {
	public class ExecuteCommandOnEnterKeyDownBehavior : Behavior<TextBox> {

		public string Command {
			get {
				return (string)GetValue(CommandProperty);
			}
			set {
				SetValue(CommandProperty, value);
			}
		}

		public static readonly DependencyProperty CommandProperty =	
									DependencyProperty.Register("Command",
										typeof(string),
										typeof(ExecuteCommandOnEnterKeyDownBehavior));

		protected override void OnAttached() {
			base.OnAttached();

			this.AssociatedObject.KeyDown += OnKeyDown;
		}

		protected override void OnDetaching() {
			base.OnDetaching();

			this.AssociatedObject.KeyDown -= OnKeyDown;
		}

		void OnKeyDown(object sender, KeyEventArgs e) {
			if (e.Key == Key.Enter) {
				string path = Command;
				object dataContext = AssociatedObject.DataContext;
				ICommand command = dataContext.GetType().GetProperty(path).GetValue(dataContext, null) as ICommand;

				if (command != null && command.CanExecute(AssociatedObject)) {
					command.Execute(AssociatedObject);
				}
			}			
		}
	}
}

これで完成しました!…と言いたいところですが、どうもMicrosoft.Expression.Interactivity.dllのバグか、これだけではコンパイルが通りません。どうもアセンブリ内にMicrosoft.Expression.Interactivity.Layout名前空間がないとコンパイルが通らないようで、適当にdummy.csというファイルを作っておき、

namespace Microsoft.Expression.Interactivity.Layout {}

と一行書いておきましょう。

image

これで、作成したBehaviorを参照に加えれば、AssetLibraryのBehaviorsタブに作ったBehaviorが追加されるようになります。

image

あとは要素にD&Dで添付した上で、プロパティペインから、諸々のプロパティを設定すれば使えるようになります。

APR
21

Expression Blend 3のBehaviorサポート

Published:2009-04-21 16:38:44 UTC
WPF

昨日はMicrosoft新宿本社で開かれたCodeseek勉強会に参加させて頂きました。今回はMicrosoftのエバンジェリストである大西さんによるSilverlight3とExpression Blend 3についての話でした。大変興味深い話ばかりが凝縮された二時間で、大いに勉強になりました。皆様どうも有難うございました。

全体を通して感じたのは、Expression Blendのツールとしての有用性。正直、今まで自分はExpression BlendをVisualStudioよりちょっとデザイナが使いやすいXAMLエディタとしてしか使えておらず、タグは手書きすることが多くて有効活用できているとは言い難かったのですが、これからはBlendの支援機能も使いこなせるようになったほうが得だなと感じました。

さて、紹介された中で、特に自分が気になったのはExpression Blend 3によるBehaviorのサポート。Expression Blend3ではBehaviorがサポートされ、それはどうも添付ビヘイビアのことらしい…、というのはネットでちらちら聞いていたのですが、実際にBehaviorを使った開発のデモを見せて頂いて、動いているのを見ると非常に便利そうな印象を受けたので、早速自分でも試してみました。

実際のExpression Blendの操作については大西さんによるAkira Onishi’s weblog : Expression Blend 3: Behaviorを使ったコーディングレスでのSilverlight 3アプリのインタラクションデザインというエントリに詳しいのですが、一点補足しますと、Expression Blend 3 PreviewでBehaviorを使うに当たっては、Behaviorを定義したアセンブリを自分で参照に加える必要があります。kirupaBlog – If it isn’t broken, take it apart and fix it! » Blog Archive » Using Behaviors : A Quick Walkthroughというエントリにある通り、Sample Silverlight 3 Behaviors辺りから適切なのを探してMicrosoft.Expression.Interactivity.dllと、ビヘイビアのdllを参照に加えて下さい。そうしないと、[Asset Library]の[Behaviors]の項には何も表示されません。製品版ではMSお手製ビヘイビアのアセンブリもFCLに同梱されるか、Silverlight Extentionに同梱されてほしいですね。

この例ではBehaviorである必要ありませんが、一応、テストしてみたときのXAMLソースを載せておきます。もちろんコードビハインドはありません。
ボタンがY軸を中心に回転する例:

<UserControl
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:i="clr-namespace:Microsoft.Expression.Interactivity;assembly=Microsoft.Expression.Interactivity" xmlns:SLPreviewBehaviorsLibrary="clr-namespace:SLPreviewBehaviorsLibrary;assembly=SLPreviewBehaviorsLibrary" x:Class="Silverlight3Testbed.MainPage" 
    Width="400" Height="300" mc:Ignorable="d">
	<UserControl.Resources>
		<Storyboard x:Name="Storyboard1" AutoReverse="True" RepeatBehavior="Forever">
			<DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="button" Storyboard.TargetProperty="(UIElement.Projection).(PlaneProjection.RotationX)">
				<EasingDoubleKeyFrame KeyTime="00:00:00" Value="0"/>
				<EasingDoubleKeyFrame KeyTime="00:00:03" Value="180"/>
			</DoubleAnimationUsingKeyFrames>
			<DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="button" Storyboard.TargetProperty="(UIElement.Projection).(PlaneProjection.RotationY)" Duration="00:00:00.0010000">
				<EasingDoubleKeyFrame KeyTime="00:00:00" Value="0"/>
			</DoubleAnimationUsingKeyFrames>
			<DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="button" Storyboard.TargetProperty="(UIElement.Projection).(PlaneProjection.RotationZ)" Duration="00:00:00.0010000">
				<EasingDoubleKeyFrame KeyTime="00:00:00" Value="0"/>
			</DoubleAnimationUsingKeyFrames>
		</Storyboard>
	</UserControl.Resources>
    <Grid x:Name="LayoutRoot" Background="White">
    	<Button x:Name="button" Height="57" Margin="84,86,196,0" VerticalAlignment="Top" Content="Button">
    		<Button.Projection>
    			<PlaneProjection RotationX="0" RotationY="0" RotationZ="0"/>
    		</Button.Projection>
    		<i:Interaction.Triggers>
    			<i:EventTrigger EventName="Click">
    				<SLPreviewBehaviorsLibrary:PlayStoryboardAction StoryboardName="Storyboard1"/>
    			</i:EventTrigger>
    		</i:Interaction.Triggers>
    	</Button>

    </Grid>
</UserControl>

Behaviorについては、もう少し追ってみようと思います。

APR
20

ItemsControlのItemTemplate

Published:2009-04-20 15:16:28 UTC
WPF

ここ数日System.Windows.Controls.Primitives.Selectorを継承したコントロールを作ろうとしてもがいていたのですが、

ItemTemplateのデータテンプレートはItemsSourceを使ったときだけ有効だと思っていましたが、WPFのListBoxコントロールやComboBoxコントロールはコンテンツに対しても有効なようです。なお、なぜかItemsControlを使うと無効になります。
WPFではコンテンツに対してもデータテンプレートを利用できる – Yuya Yamaki’s blog

で大いに悩まされました。SelectorとかItemsControlではItemTemplateをコンテンツに対して適用できないんですね・・・。不便。

image以前公開したiKnow ItemBankPanel for VisualStudioをM-V-VMパターンを使って書き直しています。まだ途中ですが、一応辞書を引いて結果を表示するとこまでは出来たので、M-V-VMパターンの参考までに公開してみます。WPF自体よく分かっていなかった前のコードと比べると見違えるような綺麗さになりましたw

 

SharpLab.IKnow.ItemBankPanes.zip

 

添付ビヘイビアでTextBoxにCommandを実装してみた – SharpLab.の添付ビヘイビアは上の検索ツールバーで、添付ビヘイビア試してみた – SharpLab.で紹介した添付ビヘイビアは検索結果を表示しているListBoxで実際に使用しています。

M-V-VMパターンの威力

埋め草までに、ViewのXAMLコードを貼っておきます。このアプリではUIロジックに複雑なところがなかったので、実際、コードビハインドにイベントハンドラを書かずに済みました。ボタンクリックやTextBoxでのEnterキー押下時の処理は、全てCommandを使ってViewModelに移譲しています。

<UserControl x:Class="SharpLab.IKnow.ItemBankPane.ItemBankPaneView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:SharpLab.IKnow.ItemBankPane"
	>
    <UserControl.Resources>
    	<ResourceDictionary>
			<local:ItemBankPaneViewModel x:Key="viewModel" />
		</ResourceDictionary>
	</UserControl.Resources>
	<UserControl.DataContext>
		<Binding Mode="OneTime" Source="{StaticResource viewModel}" />
	</UserControl.DataContext>
	<DockPanel>
		<ToolBar DockPanel.Dock="Top" Height="30" VerticalAlignment="Top">
			<TextBox local:TextBoxBehavior.Command="{Binding Mode=OneTime, Path=StartSearchCommand}"  HorizontalAlignment="Left" Text="{Binding Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, Path=Keyword}" VerticalAlignment="Top" Width="184" />
			<Button Command="{Binding Mode=OneTime, Path=StartSearchCommand}" Content="Search"></Button>
		</ToolBar>
		<ContentControl>
				<local:ResultPageView DataContext="{Binding Mode=OneTime, Path=ResultPage}" />
		</ContentControl>
	</DockPanel>
</UserControl>
<UserControl x:Class="SharpLab.IKnow.ItemBankPane.ResultPageView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
	xmlns:local="clr-namespace:SharpLab.IKnow.ItemBankPane"
	xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignWidth="300" d:DesignHeight="300"
    >
	<UserControl.Resources>
		<ResourceDictionary>
			<ResourceDictionary.MergedDictionaries>
				<ResourceDictionary Source="Resources/ResultPageResources.xaml"/>
				<ResourceDictionary Source="Resources/VocabularyViewerStyle.xaml"/>
			</ResourceDictionary.MergedDictionaries>
		</ResourceDictionary>
	</UserControl.Resources>
	<Grid DockPanel.Dock="Bottom">
		<Grid.RowDefinitions>
			<RowDefinition />
			<RowDefinition Height="35" />
		</Grid.RowDefinitions>
		<Grid.ColumnDefinitions>
			<ColumnDefinition />
			<ColumnDefinition Width="40" />
			<ColumnDefinition />
		</Grid.ColumnDefinitions>
		<ListBox Name="VocabularyViewer" Grid.Row="0" Grid.ColumnSpan="3" ItemsSource="{Binding Mode=OneWay, NotifyOnTargetUpdated=true, Path=Items}" Style="{StaticResource VocabularyViewerStyle}" />
		<Button Grid.Row="1"  Grid.Column="0" Margin="6,6,6,6" Command="{Binding Mode=OneTime, Path=NavigatePrevPageCommand}" Content="Prev"/>
		<Button Grid.Row="1" Grid.Column="2" Margin="6,6,6,6" Command="{Binding Mode=OneTime, Path=NavigateNextPageCommand}" Content="Next"/>
		<TextBlock Grid.Row="1" Grid.Column="1" Text="{Binding Mode=OneWay, Path=PageCounter}" HorizontalAlignment="Center" VerticalAlignment="Center" />
	</Grid>
</UserControl>
<UserControl x:Class="SharpLab.IKnow.ItemBankPane.ItemView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:SharpLab.IKnow.ItemBankPane"
	>

	<UserControl.Resources>
		<ResourceDictionary>
			<ResourceDictionary.MergedDictionaries>
				<ResourceDictionary Source="Resources/PlusButton.xaml"/>
				<ResourceDictionary Source="Resources/PlayButton.xaml"/>
				<ResourceDictionary Source="Resources/SentenceListViewerStyle.xaml"/>
			</ResourceDictionary.MergedDictionaries>
		</ResourceDictionary>
	</UserControl.Resources>

	<DockPanel>
		<Expander x:Name="Sentences" DockPanel.Dock="Bottom" TabIndex="3" Visibility="{Binding Mode=OneTime, Path=SentencesVisility}" >
			<Expander.Header>
				<TextBlock Text="Example Sentences:" />
			</Expander.Header>
			<ItemsControl Name="SentenceListViewer" ItemsSource="{Binding Mode=OneTime, Path=Sentences}" Style="{StaticResource SentenceListViewerStyle}"  />
		</Expander>
		<!--DropDown="{DynamicResource VocabularyViewerItemContextMenu}"-->
		<local:DropDownButton DockPanel.Dock="Right" Width="24" Height="24" Style="{StaticResource PlusButtonStyle}"  TabIndex="1" Margin="0,0,8,0" />
		<DockPanel DockPanel.Dock="Top" LastChildFill="true">
			<MediaElement x:Name="mp3Player" LoadedBehavior="Manual" Source="{Binding Mode=OneTime, Path=CueSound}" Width="0"  Height="0"  />
			<Button DockPanel.Dock="Left" Width="18" Height="18" Margin="2,0,4,0" TabIndex="0" Style="{StaticResource PlayButtonStyle}" Command="{Binding Mode=OneTime, Path=PlaySoundCommand}" CommandParameter="{Binding ElementName=mp3Player}" />
			<ContentControl Content="{Binding Mode=OneTime, Path=CueText}" IsTabStop="False" />
		</DockPanel>
		<TextBlock DockPanel.Dock="Top" FontSize="12" Margin="20,0,0,0" Text="{Binding Mode=OneTime, Path=Meaning}" TextWrapping="Wrap" />
	</DockPanel>
</UserControl>
<UserControl x:Class="SharpLab.IKnow.ItemBankPane.SentenceView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
	xmlns:local="clr-namespace:SharpLab.IKnow.ItemBankPane"
	>
	<UserControl.Resources>
		<local:StringToTextBlockConverter x:Key="StringToTextBlockConverter" />
	</UserControl.Resources>

	<UserControl.Content>
		<Binding Path="Text" Mode="OneTime"  Converter="{StaticResource StringToTextBlockConverter}" />
	</UserControl.Content>
</UserControl>
MAR
14

TextBox内でEnterキーが押下された時に、割り当てたCommandが実行されたりすると便利ですよね。ちょっと必要になったので添付ビヘイビアを利用して実装してみました。ちなみにKeyBindingも試しましたが、あれCommandプロパティが依存関係プロパティになっていないのでバインドが出来ないんですね…。不便。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Input;
using System.Windows.Controls;
using System.Windows;

namespace SharpLab.IKnow.ItemBankPane {
	public class TextBoxBehavior {

		#region Command

		public static ICommand GetCommand(TextBox textBox) {
			return (ICommand)textBox.GetValue(CommandProperty);
		}

		public static void SetCommand(
		  TextBox textBox, bool value) {
			textBox.SetValue(CommandProperty, value);
		}

		public static readonly DependencyProperty CommandProperty =
			DependencyProperty.RegisterAttached(
			"Command",
			typeof(ICommand),
			typeof(TextBoxBehavior),
			new UIPropertyMetadata(null, OnCommandPropertyChanged));

		static void OnCommandPropertyChanged(DependencyObject depObj, DependencyPropertyChangedEventArgs e) {
			TextBox textBox = depObj as TextBox;
			if (textBox == null)
				return;

			if (e.NewValue is ICommand == false)
				return;

			ICommand command = (ICommand)e.NewValue;

			textBox.KeyDown += new KeyEventHandler(OnTextBoxKeyDown);
		}

		static void OnTextBoxKeyDown(object sender, KeyEventArgs e) {
			TextBox textBox = (TextBox)e.OriginalSource;
			ICommand command = TextBoxBehavior.GetCommand(textBox);
			if (e.Key == Key.Enter && command != null && command.CanExecute(TextBoxBehavior.GetCommandParameter(textBox))) {
				command.Execute(TextBoxBehavior.GetCommandParameter(textBox));
			}
		}

		#endregion

		#region CommandParameter

		public static object GetCommandParameter(TextBox textBox) {
			return textBox.GetValue(CommandParameterProperty);
		}

		public static void SetCommandParameter(
		  TextBox textBox, object value) {
			textBox.SetValue(CommandParameterProperty, value);
		}

		public static readonly DependencyProperty CommandParameterProperty =
			DependencyProperty.RegisterAttached(
			"CommandParameter",
			typeof(object),
			typeof(TextBoxBehavior),
			new UIPropertyMetadata(null));
		
		#endregion
	}
}

利用する側はこんな感じ。スマート!

<TextBox v:TextBoxBehavior.Command="{Binding Path=StartSearchCommand}" />
MAR
14

添付ビヘイビア試してみた

Published:2009-03-14 00:55:12 UTC
WPF

ひとつ前のエントリJosh Smithさんの添付プロパティの解説記事を訳したのですが、実際に自分でもコードを書いて試してみようということで、少し変えて、添付されたListBoxのItemsSourceが更新された時にListBoxのスクロール位置を自動的にトップに巻き戻す添付ビヘイビアを作成してみました。ItemsSourceが更新されてもスクロール位置が変わらないというListBoxの挙動は、一つづつ項目の追加削除をする場合は便利なのですが、丸ごと全部項目を入れ替える場合、例えば非常に多くの項目をページに分割して表示しているときにページを行き来する時などには、ページを移った先でスクロール位置が保持されているのはかえって困ることになります。
で、書いてみたのはこんな感じ。

using System.Windows;
using System.Windows.Controls;

namespace SharpLab.IKnow.ItemBankPane {
	public class ListBoxBehavior {

		//添付されたListBoxのItemsSourceが更新された時にListBoxのスクロール位置を自動的に巻き戻すかを設定します。
		#region AutoRewind

		public static bool GetAutoRewind(ListBox listBox) {
			return (bool)listBox.GetValue(AutoRewindProperty);
		}

		public static void SetAutoRewind(
		  ListBox listBox, bool value) {
			listBox.SetValue(AutoRewindProperty, value);
		}

		public static readonly DependencyProperty AutoRewindProperty =
			DependencyProperty.RegisterAttached(
			"AutoRewind",
			typeof(bool),
			typeof(ListBoxBehavior),
			new UIPropertyMetadata(false, OnAutoRewindChanged));

		static void OnAutoRewindChanged(DependencyObject depObj, DependencyPropertyChangedEventArgs e) {
			ListBox listBox = depObj as ListBox;
			if (listBox == null)
				return;

			if (e.NewValue is bool == false)
				return;

			bool scrollToTop = (bool)e.NewValue;

			if (scrollToTop) {
				listBox.TargetUpdated += OnListBoxTargetUpdated;
			}
			else {
				listBox.TargetUpdated -= OnListBoxTargetUpdated;
			}
		}

		static void OnListBoxTargetUpdated(object sender, System.Windows.Data.DataTransferEventArgs e) {
			if (e.Property == ListBox.ItemsSourceProperty) {
				ListBox listBox = e.OriginalSource as ListBox;
				if (listBox.Items.Count > 0) {
					listBox.ScrollIntoView(listBox.Items[0]);
				}
			}
		}

		#endregion
	}
}

使う時はこんな感じ。

<ListBox v:ListBoxBehavior.AutoRewind="true" ItemsSource="{Binding NotifyOnTargetUpdated=true, Path=<適当に>}"/>

添付プロパティの値の変更時のイベントハンドラ内で、添付された要素のインスタンスのイベントを購読するのがキモで、後はどのように要素のふるまいを変えるかはお好み次第、といったとこでしょうか。同じ処理をコードビハインド内にイベントハンドラとして書く場合と異なり、即再利用可能ですから便利ですね。

 

ところでこれを書いているときに少しはまりかけたのが、FrameworkElement.SourceUpdated イベント (System.Windows)FrameworkElement.TargetUpdated イベント (System.Windows)の使い分け。バインディングソースが変更された結果、ListBoxが更新されるのを監視したいのだから、SourceUpdatedイベントを購読すればいいのかと思ったらTargetUpdatedイベントだったという罠。Binding.SourceUpdated アタッチされるイベント (System.Windows.Data)Binding.TargetUpdated アタッチされるイベント (System.Windows.Data)のエイリアスだというから、データバインディングの結果、どちらが影響を受けたかで決まっているのでしょうか??よくわかりません。