AttachedProperty定義の記述量削減

下記記事にて、依存プロパティの定義の記述量を削減しました。
DependencyProperty定義の記述量削減 - 何でもプログラミング

今回は同様に、添付プロパティの記述量を削減してみたいと思います。

素のままでは下記のような記述になり、エディタ上で"propa"とタイプするとスニペットが利用できます。

public static readonly DependencyProperty ValueProperty =
    DependencyProperty.RegisterAttached("Value", typeof(int), typeof(OwnerClass), new PropertyMetadata(0, (obj, args) =>
    {
        // GUIの更新など
    }));
public static int GetValue(DependencyObject obj)
{
    return (int)obj.GetValue(ValueProperty);
}
public static void SetValue(DependencyObject obj, int value)
{
    obj.SetValue(ValueProperty, value);
}


AttachedPropertyManager

Register、GetValue、SetValueを担うクラスになります。

基本的にCallerMemberNameを活用して記述量を減らします。

また値の変化は、On***Changedが定義されていれば呼び出す仕組みにしています。

さらに初回のOn***Changedの時には、On***ChangedFirstTimeも呼び出すようにしています。(対象コントロールのeventにhandlerを追加する時などに利用)

初回かどうかの判定は、ConditionalWeakTableに状態を保存することにより実現しています。

public class AttachedPropertyManager<TClass>
{
    class Dummy { }
    public DependencyProperty Register<TValue>(TValue defaultValue = default(TValue), [CallerMemberName]string name = "")
    {
        name = name.Substring(0, name.Length - "Property".Length);
        var bindingFlag = BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic;
        var callback = typeof(TClass).GetMethod($"On{name}Changed", bindingFlag);
        var callbackFirstTime = typeof(TClass).GetMethod($"On{name}ChangedFirstTime", bindingFlag);
        var metadata = new PropertyMetadata(defaultValue);
        if (callback != null || callbackFirstTime != null)
        {
            var called = new ConditionalWeakTable<DependencyObject, Dummy>();
            metadata.PropertyChangedCallback = (obj, args) =>
            {
                if (called.TryGetValue(obj, out Dummy dummy) == false)
                {
                    callbackFirstTime?.Invoke(null, new object[] { obj });
                    called.GetOrCreateValue(obj);
                }
                callback?.Invoke(null, new object[] { obj });
            };
        }
        return DependencyProperty.RegisterAttached(name, typeof(TValue), typeof(TClass), metadata);
    }
    public object Get(DependencyObject obj, [CallerMemberName]string name = "") =>
        obj.GetValue(GetProperty(name));
    public void Set(DependencyObject obj, object value, [CallerMemberName]string name = "") =>
        obj.SetValue(GetProperty(name), value);
    DependencyProperty GetProperty(string name) =>
        (DependencyProperty)typeof(TClass).GetField(name.Substring(3) + "Property").GetValue(null);
}


RectangleにMouseLeftButtonDownコマンドを追加してみる

Rectangle上でマウスをDownすると、コンソール出力されるアプリケーションを作成してみます。

ICommand型の添付プロパティを定義し、OnChangeFirstTimeにて、ICommand.Executeを実行するhandlerをFrameworkElement.MouseLeftButtonDownに登録しています。

※添付プロパティでeventにhandlerを登録する際はリークに注意してください。(今回の利用方法では問題ありません。)
f:id:any-programming:20170517233730p:plain

Xaml

<Window x:Class="WpfApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:WpfApp"
        Title="MainWindow" Height="200" Width="200">
    <Grid>
        <Rectangle Fill="White" local:Commands.MouseLeftButtonDown="{Binding Down}" />
    </Grid>
</Window>

C#

public class ViewModel
{
    public Command<object> Down { get; } = new Command<object>(x => Console.WriteLine("down"));
}
public class Commands
{
    static AttachedPropertyManager<Commands> _ap = new AttachedPropertyManager<Commands>();

    public static readonly DependencyProperty MouseLeftButtonDownProperty = _ap.Register<ICommand>();
    public static ICommand GetMouseLeftButtonDown(FrameworkElement obj) => (ICommand)_ap.Get(obj);
    public static void SetMouseLeftButtonDown(FrameworkElement obj, ICommand value) => _ap.Set(obj, value);

    static void OnMouseLeftButtonDownChangedFirstTime(FrameworkElement obj) =>
        obj.MouseLeftButtonDown += (s, e) => GetMouseLeftButtonDown(obj)?.Execute(null);
}