DependencyProperty定義の記述量削減

独自のDependencyPropertyを定義する際、コールバックも含めると下記のような記述になります。

public static readonly DependencyProperty ValueProperty =
    DependencyProperty.Register("Value", typeof(int), typeof(Counter), new PropertyMetadata(0, (obj, args) =>
    {
        var counter = (Counter)obj;
        // GUI更新など
    }));
public int Value
{
    get { return (int)GetValue(ValueProperty); }
    set { SetValue(ValueProperty, value); }
}

エディタ上で"propdp"と打ち込むとスニペットが利用できるのでタイピング量自体は少ないですが、コード量自体は多くなります。

今回はDependencyPropertyのコード量を減らしてみたいと思います。

なお、プリプロセス(T4など)や、ポストプロセス(PostSharpやMono.Cecilなど)は利用しないで実装してみます。(T4を利用すれば下記サイトのように最低限の記述で済みますので、実用上はT4のほうが良いと思います。)
Declarative Dependency Property Definition with T4 + DTE

DependencyPropertyManager

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

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

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

public class DependencyPropertyManager<TClass>
{
    public DependencyProperty Register<TValue>(TValue defaultValue = default(TValue), [CallerMemberName]string name = "")
    {
        name = name.Substring(0, name.Length - "Property".Length);
        var callback = typeof(TClass).GetMethod($"On{name}Changed", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
        var metadata = callback == null
            ? new PropertyMetadata(defaultValue)
            : new PropertyMetadata(defaultValue, (obj, args) => { callback.Invoke(obj, null); });
        return DependencyProperty.Register(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 + "Property").GetValue(null);
}


Counterコントロール

ボタンでカウントアップ/ダウンが行えるコントロールを作成してみます。

カウンタの値(Value)と、カウントアップ/ダウン(Command)をDependencyPropertyとして定義しています。

CounterにGet/Setメンバを定義すれば、さらにプロパティ内の記述量を減らすことが可能です。
f:id:any-programming:20170515181632p:plain
C#

public partial class Counter : UserControl
{
    static DependencyPropertyManager<Counter> _dp = new DependencyPropertyManager<Counter>();

    public static readonly DependencyProperty ValueProperty = _dp.Register<int>();
    public static readonly DependencyProperty CommandProperty = _dp.Register<Command<int>>();

    public int Value { get => (int)_dp.Get(this); set => _dp.Set(this, value); }
    public Command<int> Command { get => (Command<int>)_dp.Get(this); set => _dp.Set(this, value); }

    void OnValueChanged() => textbox.Text = Value.ToString();

    public Counter()
    {
        InitializeComponent();

        textbox.Text = Value.ToString();
        buttonUp.Click += (s, e) => Command?.Execute(1);
        buttonDown.Click += (s, e) => Command?.Execute(-1);
    }
}

Xaml

<UserControl x:Class="WpfApp.Counter"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" 
             d:DesignHeight="23" d:DesignWidth="200">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="23"/>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="23"/>
        </Grid.ColumnDefinitions>
        <Button x:Name="buttonDown" Content="◀" Grid.Column="0" />
        <TextBox x:Name="textbox" Grid.Column="1" IsReadOnly="True" TextAlignment="Center" VerticalContentAlignment="Center" />
        <Button x:Name="buttonUp" Content="▶" Grid.Column="2" />
    </Grid>
</UserControl>