tips - 独自の依存関係プロパティを作成する

 データバインド機能やアニメーション機能などを提供できるようにするには、 通常の CLR プロパティではなく、依存関係プロパティを定義する必要があります。 ここでは、ユーザコントロールに対して依存関係プロパティを独自に作成し、 そのプロパティに対してデータバインドをおこなう例を紹介します。

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

ページ内リンク

概要

 例えば次のような画面を作ることを考えます。

Fig.1 : よくある設定画面風の配置
項目名が左にあって、それに対する設定値が右側にあるという、 設定画面なんかでよく見るレイアウトです。 これを XAML で書こうとすると次のようになります。
<Window x:Class="CustomDependencyProperty.Views.MainView"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainView" Height="300" Width="300">
    <Grid HorizontalAlignment="Center" VerticalAlignment="Center">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="100" />
        </Grid.ColumnDefinitions>

        <TextBlock Grid.Row="0" Grid.Column="0" Text="設定 1 : " TextAlignment="Right" VerticalAlignment="Center" />
        <TextBlock Grid.Row="1" Grid.Column="0" Text="詳細設定 : " TextAlignment="Right" VerticalAlignment="Center" />
        <TextBlock Grid.Row="2" Grid.Column="0" Text="オプション : " TextAlignment="Right" VerticalAlignment="Center" />

        <TextBox Grid.Row="0" Grid.Column="1" Margin="0,0,0,6" />
        <TextBox Grid.Row="1" Grid.Column="1" Margin="0,0,0,6" />
        <TextBox Grid.Row="2" Grid.Column="1" Margin="0,0,0,6" />
    </Grid>
</Window>
Code 1 : ユーザコントロールを使わずに実現してみた

 ただ縦並びにするだけなら StackPanel でもいいと思われますが、 左側に表示している設定項目名のテキストを ":" で綺麗に揃えたい場合は上記のように Grid で綺麗に並べる必要があります。

 しかし、このような方法を取ると、 設定項目数に応じて RowDefinitions の設定を変更しなくてはいけません。 しかも、TextBlock や TextBox などにいちいち Grid の添付プロパティを設定しなければならないため、 XAML に記述する量が多くなり、 単純な UI を実現しているのにコードが長い&複雑になってしまいます。

 これを解消するために、次のようなユーザコントロールを使用することを考えます。

対象とするユーザコントロール

 ユーザコントロール名を ConfigurationItem として次のようなコントロールを作成します。

<UserControl x:Class="CustomDependencyProperty.Views.ConfigurationItem"
             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="300" d:DesignWidth="300">
    <Grid Margin="0,0,0,6">
        <Grid.ColumnDefinitions>
            <ColumnDefinition />
            <ColumnDefinition Width="Auto" />
        </Grid.ColumnDefinitions>

        <TextBlock Grid.Column="0" Text="項目名 : " TextAlignment="Right" VerticalAlignment="Center" />
        <TextBox Grid.Column="1" Width="100" />
    </Grid>
</UserControl>
Code 2 : 対象とするユーザコントロ−ル

設定項目名を左側に、設定値を入力するための TextBox を右側に配置したユーザコントロールです。 これを MainView で並べてみます。

<Window x:Class="CustomDependencyProperty.Views.MainView"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:CustomDependencyProperty.Views"
        Title="MainView" Height="300" Width="300">
    <Grid>
        <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
            <local:ConfigurationItem />
            <local:ConfigurationItem />
            <local:ConfigurationItem />
        </StackPanel>
    </Grid>
</Window>
Code 3 : ユーザコントロールを StackPanel で並べる
Fig.2 : 並べられたユーザコントロール

 XAML コードのほうは、設定項目の分だけ StackPanel で並べるだけなので、非常に簡潔になりました。

 しかし、このままでは設定項目名はすべて "項目名" で、設定する値も受け渡すことができません。 そこで、項目名や設定値をデータバインド機能を用いて指定するようにするために、 依存関係プロパティを追加します。

独自の依存関係プロパティ

 まず、項目名を外部から指定できるようにユーザコントロールに対して "Text" という名前の依存関係プロパティを追加します。 ConfigurationItem のコードビハインドに次のように追加します。

namespace CustomDependencyProperty.Views
{
    using System.Windows;
    using System.Windows.Controls;

    /// <summary>
    /// ConfigurationItem.xaml の相互作用ロジック
    /// </summary>
    public partial class ConfigurationItem : UserControl
    {
        public ConfigurationItem()
        {
            InitializeComponent();
        }

        public static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string), typeof(ConfigurationItem), new FrameworkPropertyMetadata("設定項目"));
        public string Text
        {
            get { return (string)GetValue(TextProperty); }
            set { SetValue(TextProperty, value); }
        }
    }
}
Code 4 : Text 依存関係プロパティを追加する
DependencyProperty クラスの Register() 静的メソッドによって DependencyProerty クラスを生成します。 このとき、依存関係プロパティの名前、その型、どの型に属するのか、その他オプションを引数として与えます。 ここでは、名前は "Text"、型は typeof(string)、Text プロパティは ConfigurationItem クラスに属しているので typeof(ConfigurationItem)、 オプションとしてメタデータを指定し、このプロパティの既定値を "項目名" として与えています。

 続いて、依存関係プロパティを XAML 上から扱えるように、通常の CLR プロパティで GetValue() や SetValue() をラップしています。

 この状態で一度ビルドすると、Text という依存関係プロパティが生成されたので、 XAML 上でコードを記述するときに、Intellisense 機能で Text というプロパティがヒットします。

Fig.3 : Intellisense 機能で Text プロパティが選択できるようになっている

次に、設定値もデータバインドできるように "Value" という名前の依存関係プロパティを追加します。

public static readonly DependencyProperty ValueProperty = DependencyProperty.Register("Value", typeof(string), typeof(ConfigurationItem), new FrameworkPropertyMetadata(string.Empty, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
public string Value
{
    get { return (string)GetValue(ValueProperty); }
    set { SetValue(ValueProperty, value); }
}
Code 5 : Value 依存関係プロパティを追加する
先ほどの Text プロパティを追加したときとほとんど同じですが、 ここで注意してほしいのは、 メタデータの中で FrameworkPropertyMetadataOptions.BindsTwoWayByDefault を指定しているところです。 依存関係プロパティを追加した場合、デフォルトではデータバインド機能は OneWay となります。 しかし、この Value プロパティは UI からの入力によって値が変更され、 この変更をデータバインド機能によって外部に通知しようとしています。 したがって、データバインドは TwoWay でバインドされるべきです。 このことから、データバインド機能を TwoWay として依存関係プロパティを定義する必要がありますが、 これをメタデータから変更しています。

 Text プロパティと Value プロパティを定義したので、 定義したプロパティを参照してそれぞれを表示するようにユーザコントロールの XAML を書き換えます。

<UserControl x:Class="CustomDependencyProperty.Views.ConfigurationItem"
             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="300" d:DesignWidth="300"
             x:Name="root">
    <Grid Margin="0,0,0,6">
        <Grid.ColumnDefinitions>
            <ColumnDefinition />
            <ColumnDefinition Width="Auto" />
        </Grid.ColumnDefinitions>

        <TextBlock Grid.Column="0" Text="{Binding Text, ElementName=root, StringFormat='{}{0} : '}" TextAlignment="Right" VerticalAlignment="Center" />
        <TextBox Grid.Column="1" Text="{Binding Value, ElementName=root}" Width="100" />
    </Grid>
</UserControl>
Code 6 : 作成した依存関係プロパティを参照するように変更した XAML
TextBlock の Text プロパティに追加した Text プロパティ、 TextBox の Text プロパティに追加した Value プロパティを参照するようにしています。 ただし、参照先は DataContext ではなく、ユーザコントロール自身であるため、 ElementName を指定しなければならないことに注意して下さい。 ElementName を指定しないと、ユーザコントロールの DataContext が参照先になりますが、 DataContext はユーザコントロールを配置する View の DataContext となってしまい、 そちらの Text プロパティや Value プロパティが参照されることになってしまいます。 ここではユーザコントロール自身に作成した Text プロパティと Value プロパティを参照します。

 StringFormat は表示するテキストの様式を指定できるオプションで、 ここでは設定項目名の後ろに " : " を付けるようにしています。

 それではこのユーザコントロールを使用する MainView に戻って、 それぞれのプロパティを設定してみましょう。

<Window x:Class="CustomDependencyProperty.Views.MainView"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:CustomDependencyProperty.Views"
        Title="MainView" Height="300" Width="300">
    <Grid>
        <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
            <local:ConfigurationItem Text="設定 1" Value="{Binding Text1}" />
            <local:ConfigurationItem Text="詳細設定" Value="{Binding Text2}" />
            <local:ConfigurationItem Text="オプション" Value="{Binding Text3}" />
            <TextBlock Text="{Binding WholeText}" />
        </StackPanel>
    </Grid>
</Window>
Code 7 : ユーザコントロールにプロパティを設定する
namespace CustomDependencyProperty.ViewModels
{
    public class MainViewModel : NotificationObject
    {
        private string text1;
        public string Text1
        {
            get { return text1; }
            set { SetProperty(ref text1, value); }
        }

        private string text2;
        public string Text2
        {
            get { return text2; }
            set { SetProperty(ref text2, value); }
        }

        private string text3;
        public string Text3
        {
            get { return text3; }
            set { SetProperty(ref text3, value); }
        }

        public string WholeText
        {
            get { return Text1 + Text2 + Text3; }
        }
    }
}
Code 8 : ViewModel 側にプロパティを用意する
Code 1 が Code 7 のように StackPanel を用いてすっきり書けるようになりました。

 また、ここではプロパティ値が変更されたことを確認するために WholeText プロパティを用意し、 View 側ではそれを TextBlock で見えるようにしています。

 実行結果は次のようになります。

Fig.4 : 設定値を変更すると ViewModel 側にもその変更が反映されている
TextBox の値を変更すると WholeText プロパティまでその変更が伝わっていることがわかります。

Designed by CSS.Design Sample