かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

ReactivePropertyをSilverlightのLabelやDescriptionViewerに対応させる

さて、最近お気に入りのid:neueccさんのReactivePropertyですがSilverlightで画面作りを楽にしてくれるLabelやDescriptionViewerに対応させてみたいと思います。因みに、下記のようなViewModelを想定しています。

namespace RxPropLabelSample
{
    using System.ComponentModel.DataAnnotations;
    using Codeplex.Reactive;

    public class MainPageViewModel
    {
        [Display(Name = "なまえ", Description = "人間の名前")]
        [StringLength(10, ErrorMessage = "10文字以内で入力してね")]
        public ReactiveProperty<string> Name { get; private set; }

        public MainPageViewModel()
        {
            // 属性による検証を有効にする
            this.Name = new ReactiveProperty<string>()
                .SetValidateAttribute(() => Name);
        }
    }
}

属性指定でReactivePropertyが提供してくれているIDataErrorInfoによるエラーの通知の仕組みにのっかってやります。では、これを使った画面を定義します。普通LabelやDescriptionViewerは、ViewModelのプロパティにバインドされたTextBoxなどをTargetプロパティに指定することで、以下のような機能を提供してくれます。

  • Labelの場合
    • 検証エラーがあると赤色になる
    • 必須入力項目の場合は太字になる
    • プロパティのDisplay属性のNameで指定された値を表示する
  • DescriptionViewerの場合
    • プロパティのDisplay属性のDescriptionで指定された値をツールチップで表示する

今回は、Name.ValueをTextBoxのTextプロパティにバインドしてLabelとDescriptionViewerのTargetにTextBoxをバインドするので、下記のようなXAMLになります。

<UserControl 
    x:Class="RxPropLabelSample.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:l="clr-namespace:RxPropLabelSample"
    mc:Ignorable="d"
    d:DesignHeight="300" d:DesignWidth="400" 
    xmlns:sdk="http://schemas.microsoft.com/winfx/2006/xaml/presentation/sdk">
    <UserControl.DataContext>
        <l:MainPageViewModel />
    </UserControl.DataContext>
    <StackPanel x:Name="LayoutRoot" Background="White">
        <!-- TextBoxをTargetにバインド -->
        <sdk:Label Target="{Binding ElementName=textBoxName}" />
        <!-- バリデーションエラーを表示するための余白のマージンを設定しています -->
        <TextBox Name="textBoxName" Margin="5,5,250,5" Text="{Binding Path=Name.Value, Mode=TwoWay, ValidatesOnDataErrors=True, NotifyOnValidationError=True}" />
        <!-- TextBoxをTargetにバインド -->
        <sdk:DescriptionViewer Target="{Binding ElementName=textBoxName}" />
    </StackPanel>
</UserControl>

ただ、これを実行すると以下のように悲しい結果に終わります。

何故かというとTextBoxにバインドされているのはNameプロパティではなくてNameプロパティのValueプロパティだからです。ValueプロパティはReactivePropertyのプロパティで、こいつには何も属性がついてないのでLabelやDescriptionViewerは何も表示することが出来ません。辛うじてLabelのデフォルトの動作がプロパティ名を表示するという動作なので、Valueを表示されていますが、あまり嬉しくありません。


この問題を解決するにはLabelとDescriptionViewerのPropertyPathプロパティに値を設定します。PropertyPathプロパティは、MSDNのLabelコントロールの説明にあるように「コントロールに複数のBindingがあるときに、どのBinding からメタデータを取得するかを指定するもの」です。これだけだとイマイチ何を言っているのかわからないので、PropertyPathプロパティの説明を読むと「この Label が関連付けられている Target コントロールの DataContext の依存関係プロパティへのパス」を指定すると書いてあります。つまり、今回の場合はMainPageViewModelのNameプロパティからメタデータを取ってきてほしいのでNameと指定すればOKです。修正後のXAMLは以下のようになります。

<UserControl 
    x:Class="RxPropLabelSample.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:l="clr-namespace:RxPropLabelSample"
    mc:Ignorable="d"
    d:DesignHeight="300" d:DesignWidth="400" 
    xmlns:sdk="http://schemas.microsoft.com/winfx/2006/xaml/presentation/sdk">
    <UserControl.DataContext>
        <l:MainPageViewModel />
    </UserControl.DataContext>
    <StackPanel x:Name="LayoutRoot" Background="White">
        <!-- TextBoxをTargetにバインド -->
        <sdk:Label Target="{Binding ElementName=textBoxName}" PropertyPath="Name" />
        <!-- バリデーションエラーを表示するための余白のマージンを設定しています -->
        <TextBox Name="textBoxName" Margin="5,5,250,5" Text="{Binding Path=Name.Value, Mode=TwoWay, ValidatesOnDataErrors=True, NotifyOnValidationError=True}" />
        <!-- TextBoxをTargetにバインド -->
        <sdk:DescriptionViewer Target="{Binding ElementName=textBoxName}" PropertyPath="Name" />
    </StackPanel>
</UserControl>

PropertyPathプロパティの説明では依存関係プロパティへのパスと書いてありますが、動作を見る限り依存関係プロパティではなくてもOKのようです。このようにXAMLを書き換えて実行すると下記のようになります。

入力エラーでLabelが赤くなったり、DescriptionViewerに概要が表示されています。これでPOCOのプロパティを使わないで、ReactivePropertyを使う時の障害が個人的に1つ減りました。次はValidationSummaryですが、こちらはまだ回避方法がイマイチひらめきません・・・。

コードのダウンロード

今回のコードは下記からダウンロードできます。