読者です 読者をやめる 読者になる 読者になる

SourceChord

C#とXAML好きなプログラマの備忘録。最近はWPF系の話題が中心です。

イベントのマークアップ拡張~コレクション走査パスへの対応~

C# WPF WPF4.5


この間作成したイベントのマークアップ拡張を使ったInvokeCommandExtensionクラスですが、以下のような問題点がありました。

コレクション走査パスでのバインディング

以前書いたコードでは、ParsePropertyPathというメソッドで、マークアップ拡張で指定されたコマンドへのパスを自力で解析していました。(.演算子でのメンバー指定のみですが)
そのため、コレクションにバインドしているときに、選択項目を表す「/」を用いたバインディングには対応できていませんでした。

対処方法

ちゃんとバインディングの仕様に従うように、自力で解析するのは現実的ではありません。
ということで、今までコマンドへのパスをstring型で扱っていた所を、PropertyPath型で受け内部でバインディングを用いて目的のコマンドを取得するよう修正にしました。


バインディングのターゲットとするためには、DependencyPropertyを作らなければいけません。
しかし、DependencyPropertyをメンバに持てるのは、DependencyObject派生のクラスだけです。
そこで、DependencyObjectから派生したTargetObjectというクラスを作成し、マークアップ拡張のクラスにメンバとして持たせます。
そして、このTargetObjectのTargetValueプロパティを、バインディングのターゲットとして利用します。
↓の記事でやってる方法と似た感じの方法ですね。
[WPF]自作ValidationRuleのプロパティにバインディング | OITA: Oika's Information Technological Activities

対処したコード

InvokeCommandExtension.cs
    [MarkupExtensionReturnType(typeof(EventHandler))]
    public sealed class InvokeCommandExtension : MarkupExtension
    {
        /// <summary>
        /// イベント発生時に呼び出すコマンドのパスを取得または設定します。
        /// </summary>
        public PropertyPath Path { get; set; }

        private TargetObject _targetObject;

        public InvokeCommandExtension(PropertyPath bindingCommandPath)
        {
            this.Path = bindingCommandPath;
        }

        public override object ProvideValue(IServiceProvider serviceProvider)
        {
            var pvt = serviceProvider.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget;

            if (pvt != null)
            {
                var ei = pvt.TargetProperty as EventInfo;
                var mi = pvt.TargetProperty as MethodInfo;
                var type = (ei != null) ? ei.EventHandlerType :
                                          (mi != null) ? mi.GetParameters()[1].ParameterType :
                                                         null;

                if (type != null)
                {
                    var target = pvt.TargetObject as FrameworkElement;

                    _targetObject = new TargetObject();
                    var binding = new Binding()
                    {
                        Source = target.DataContext,
                        Path = this.Path,
                    };
                    BindingOperations.SetBinding(_targetObject, TargetObject.TargetValueProperty, binding);


                    // ここで、イベントハンドラを作成し、マークアップ拡張の結果として返す
                    var nonGenericMethod = GetType().GetMethod("PrivateHandlerGeneric", BindingFlags.NonPublic | BindingFlags.Instance);
                    var argType = type.GetMethod("Invoke").GetParameters()[1].ParameterType;
                    var genericMethod = nonGenericMethod.MakeGenericMethod(argType);

                    return Delegate.CreateDelegate(type, this, genericMethod);
                }
            }

            return null;
        }

        private void PrivateHandlerGeneric<T>(object sender, T e)
        {
            // コマンドの取得
            var command = _targetObject.TargetValue as ICommand;

            // コマンドを呼び出す
            if (command != null && command.CanExecute(e))
            {
                command.Execute(e);
            }
        }
    }

    /// <summary>
    /// 実行対象のコマンドをバインディングターゲットとして保持するために使用するクラス
    /// InvokeCommandExtensionクラス内で使用します
    /// </summary>
    internal class TargetObject : DependencyObject
    {
        public object TargetValue
        {
            get { return (object)GetValue(TargetValueProperty); }
            set { SetValue(TargetValueProperty, value); }
        }

        // Using a DependencyProperty as the backing store for TargetValue.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty TargetValueProperty =
            DependencyProperty.Register("TargetValue", typeof(object), typeof(TargetObject), new PropertyMetadata(null));
    }

サンプル

f:id:minami_SC:20150118120803p:plain:w300
MainWindowViewModelで持っているItemViewModelのリストをMainWindow.xamlのListBoxで表示しています。
(選択項目の同期を取るため、IsSynchronizedWithCurrentItem="True"を設定しています。)
画面下部のShowボタンのClickイベントで、このマークアップ拡張を使ったコマンド呼び出しを設定しています。
ここでは、以下のように「/」を用いてリストで選択された項目のコマンド呼び出しを設定しています。

Click="{local:InvokeCommand List/ShowValueCommand}"

※補足
クリック時のコマンド実行動作なので、本来ならばButtonのCommandプロパティを使えばよいです。
ここでは動作確認のため、Clickイベントでのマークアップ拡張を用いています。

MainWindow.xaml
<Window x:Class="WpfBaseTemplate1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:WpfBaseTemplate1"
        Title="MainWindow" Height="300" Width="400">
    <Window.DataContext>
        <local:MainWindowViewModel />
    </Window.DataContext>
    <Grid>
        <TextBlock Margin="5"
                       VerticalAlignment="Top"
                       Text="Showボタンを押すと、リストで選択された子要素のVMのコマンドを実行します。"
                       TextWrapping="Wrap" />
        <ListBox Margin="10,40,10,60"
                     DisplayMemberPath="Name"
                     IsSynchronizedWithCurrentItem="True"
                     ItemsSource="{Binding List}" />
        <Slider Margin="10,30"
                    VerticalAlignment="Bottom"
                    LargeChange="10"
                    Maximum="100"
                    SmallChange="1"
                    Value="{Binding List/Value}" />
        <Button Width="75"
                    Margin="5"
                    HorizontalAlignment="Right"
                    VerticalAlignment="Bottom"
                    Click="{local:InvokeCommand List/ShowValueCommand}"
                    Content="Show" />
    </Grid>
</Window>
MainWindowViewModel.cs
    /// <summary>
    /// MainWindow.xamlに使用するViewModel
    /// </summary>
    class MainWindowViewModel : BindableBase
    {
        private ObservableCollection<ItemViewModel> list;
        public ObservableCollection<ItemViewModel> List
        {
            get { return list; }
            set { this.SetProperty(ref this.list, value); }
        }

        public MainWindowViewModel()
        {
            this.List = new ObservableCollection<ItemViewModel>()
            {
                new ItemViewModel(){Name="hoge1", Value=10},
                new ItemViewModel(){Name="hoge2", Value=20},
                new ItemViewModel(){Name="hoge3", Value=30},
                new ItemViewModel(){Name="hoge4", Value=40},
                new ItemViewModel(){Name="hoge5", Value=50},
            };
        }
    }


    class ItemViewModel : BindableBase
    {
        private string name;
        public string Name
        {
            get { return name; }
            set { this.SetProperty(ref this.name, value); }
        }

        private int value;
        public int Value
        {
            get { return value; }
            set { this.SetProperty(ref this.value, value); }
        }

        #region コマンドの実装
        private RelayCommand showValueCommand;
        public RelayCommand ShowValueCommand
        {
            get { return showValueCommand = showValueCommand ?? new RelayCommand(ShowValue); }
        }
        private void ShowValue()
        {
            var msg = string.Format("{0}: {1}", this.Name, this.Value);
            System.Windows.MessageBox.Show(msg);
        }
        #endregion
    }


あとは、DataContext変更への追従ですね。
次回に続きます。