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を登録する際はリークに注意してください。(今回の利用方法では問題ありません。)
<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>
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); }