かずきのBlog@hatena

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

コントロールのDependencyPropertyじゃないプロパティをViewModelに設定する

上記の記事を読んで、現場でSilverlightを使って開発してる人の声というのはとても参考になると思いました。私はというと、何かしらの課題や問題をどうやって解けるかということを考えるのが好きな人なので、上記の記事の中で取り上げられていたTextBoxのSelectedTextプロパティのようなDependencyPropertyじゃないプロパティをViewModelにセットする方法をコードビハインドを書いたり、カスタムコントロールを作ったりしないで実現してみるということをして遊んでみようと思います。id:wave1008さんは、カスタムコントロールというアプローチで、この問題に対処しています。


まず、SelectionChangedイベントをきっかけにしてViewModelのプロパティにTextBoxのSelectedTextプロパティの値を入れてやればOKと思ってたのでEventTriggerとChangePropertyActionを使ってやろうと思いました。しかし、ChangePropertyActionのValueプロパティにTextBoxのSelectedTextプロパティをバインドしたところまでは良かったのですが、SelectedTextは変更通知をあげてくれるプロパティではなかったので、うまいことChangePropertyActionから最新の値をとることが出来ないという問題が発生して断念しましたorz

無ければ作ればいいじゃない?

ということで、既存の部品が使えないとわかったら自作です。任意のプロパティの値を取得して任意のプロパティへセットするTriggerActionを作りました。

namespace MVVMSelectionText
{
    using System;
    using System.Reflection;
    using System.Windows;
    using System.Windows.Interactivity;

    /// <summary>
    /// SourceからTargetObjectに対して指定したプロパティの値をセットするアクション
    /// </summary>
    public class SetPropertyAction : TargetedTriggerAction<object>
    {
        protected override void OnTargetChanged(object oldTarget, object newTarget)
        {
            base.OnTargetChanged(oldTarget, newTarget);
            // PropertyInfoの更新
            this.TargetPropertyChanged(this.TargetPropertyName);
            this.SourcePropertyChanged(this.SourcePropertyName);
        }

        public static readonly DependencyProperty SourcePropertyNameProperty =
            DependencyProperty.Register(
                "SourcePropertyName",
                typeof(string),
                typeof(SetPropertyAction),
                new PropertyMetadata(
                    (sender, e) =>
                    {
                        ((SetPropertyAction)sender).SourcePropertyChanged(e.NewValue as string);
                    }));

        public static readonly DependencyProperty TargetPropertyNameProperty =
            DependencyProperty.Register(
                "TargetPropertyName",
                typeof(string),
                typeof(SetPropertyAction),
                new PropertyMetadata(
                    (sender, e) =>
                    {
                        ((SetPropertyAction)sender).TargetPropertyChanged(e.NewValue as string);
                    }));

        private PropertyInfo sourceProperty;

        private PropertyInfo targetProperty;

        /// <summary>
        /// ターゲットのプロパティ名
        /// </summary>
        public string TargetPropertyName
        {
            get { return (string)GetValue(TargetPropertyNameProperty); }
            set { SetValue(TargetPropertyNameProperty, value); }
        }

        /// <summary>
        /// ソースのプロパティ名
        /// </summary>
        public string SourcePropertyName
        {
            get { return (string)GetValue(SourcePropertyNameProperty); }
            set { SetValue(SourcePropertyNameProperty, value); }
        }

        protected override void Invoke(object parameter)
        {
            // 必要な設定がされているかチェック
            if (this.sourceProperty == null)
            {
                throw new InvalidOperationException("SourcePropertyName is null");
            }

            if (this.targetProperty == null)
            {
                throw new InvalidOperationException("TargetPropertyName is null");
            }

            // targetにsourceのプロパティの値をコピー
            this.targetProperty.SetValue(
                this.TargetObject,
                this.sourceProperty.GetValue(this.AssociatedObject, null),
                null);
        }

        private void TargetPropertyChanged(string propertyName)
        {
            this.targetProperty = this.GetPropertyInfo(
                this.TargetObject,
                propertyName);
        }

        private void SourcePropertyChanged(string propertyName)
        {
            this.sourceProperty = this.GetPropertyInfo(
                this.AssociatedObject,
                propertyName);
        }

        private PropertyInfo GetPropertyInfo(object obj, string propertyName)
        {
            if (obj == null)
            {
                return null;
            }

            if (string.IsNullOrEmpty(propertyName))
            {
                return null;
            }

            return obj.GetType().GetProperty(propertyName);
        }
    }
}

SetPropertyActionを使ってみよう

目的のTriggerActionが出来たので、使ってみようと思います。Prismの基本クラスを使ってViewModelを作成します。

namespace MVVMSelectionText
{
    using Microsoft.Practices.Prism.ViewModel;

    public class MainPageViewModel : NotificationObject
    {
        private string selectedText;

        public string SelectedText
        {
            get
            {
                return this.selectedText;
            }

            set
            {
                this.selectedText = value;
                base.RaisePropertyChanged(() => SelectedText);
            }
        }
    }
}

Viewはこんな感じで。

<UserControl 
    x:Class="MVVMSelectionText.MainPage"
    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:local="clr-namespace:MVVMSelectionText"
    mc:Ignorable="d"
    d:DesignHeight="300" 
    d:DesignWidth="400" 
    xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" 
    xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions">
    <UserControl.DataContext>
        <!-- ViewModelの設定 -->
        <local:MainPageViewModel />
    </UserControl.DataContext>
    <StackPanel>
        <TextBox>
            <i:Interaction.Triggers>
                <!-- SelectionChangedをきっかけにして -->
                <i:EventTrigger EventName="SelectionChanged">
                    <!-- TextBox.SelectedTextからMainPageViewModel.SelectedTextへセットする -->
                    <local:SetPropertyAction 
                        TargetObject="{Binding}" 
                        TargetPropertyName="SelectedText" 
                        SourcePropertyName="SelectedText" />
                </i:EventTrigger>
            </i:Interaction.Triggers>
        </TextBox>
        <TextBlock Text="{Binding Path=SelectedText}" />
    </StackPanel>
</UserControl>

TriggerとTriggerActionのコードは全部手書きだと辛いので自作のサポートツールを使ってXAMLを生成しました。

このツールを使うと、Visual Studioのデザイナでビヘイビアを設定したコントロールの右クリックメニューからBehaviorやTriggerを設定できるのでBlend持ってない人でもBehaviorを導入する敷居が下がると思います。

実行してみよう

実行してテキストボックスに適当な文字をうって選択すると、ViewModelに値が設定されているのがわかります。

感想

たったこれしきのことやるのに、Behaviorとか使うとえらい長いXAMLになっちゃいますね・・・。カスタムコントロール使うのとどっちがいいのだろうか。

プロジェクトのダウンロード

このサンプルプロジェクトは以下からダウンロードできます。