“Windows 8.1 におけるストア ビジネスアプリの開発”(MVP Community Camp 2014)DEMO解説 #5:ユーザー入力の検証

皆様、こんにちは!

明日からは、いよいよ Build 2014 @ San Francisco なので、その前に片づけておかないと…ということでw、MVP Community Camp 2014 デモ解説の最後、ユーザー入力の検証です。まず冒頭は、スライドシェアのリンクです。

<Windows 8.1 におけるストア ビジネスアプリの設計と開発>

Windows 8.1 におけるストア ビジネスアプリの設計と開発 from Shotaro Suzuki

ユーザー入力の検証に関しては、このトピックになりますので、ご覧ください。

https://msdn.microsoft.com/ja-jp/library/windows/apps/xx13659.aspx

おそらく業務でストアビジネスアプリ、を開発する際には、もっとも重要な機能となってくるでしょう。主要な要素としては、下記のスライドの通り、ValidatableBindableBaseクラス、System.ComponentModel.DataAnnotations 名前空間、 そして ValidateProperties メソッド、になります。

image

デモアプリの概要

当日デモでご紹介したものは、こんなアプリでした。まずアプリを起動すると、このような画面になります。

screenshot_03302014_110549

契約業務ボタンをタッチすると、入力用の画面に遷移します(ナビゲーション)。

screenshot_03302014_110553

ドロップダウンリストから製品を選んで、数量を決めて、

screenshot_03302014_110632

注文に追加します。

screenshot_03302014_110650 

このまま注文実行すると、氏名欄は必須なので検証エラーが出ます。

screenshot_03302014_110658

ここを適当に入力しても、電話番号に文字列を入れると、こちらは正規表現で10ケタのチェックディジットを入れてあるので、検証エラーが出ます。

screenshot_03302014_110708

アプリ全体の方は、こちらにこの後あげますので、参考までに見てみてください。元々のサンプルがWindows 8のテンプレートで作成されたもので、それを、日本語化しつつ、8.1 用にターゲットし直していますので、Common フォルダ等も残っていますが、構成はわかり易いかと思います。

ソリューションの作成

データを入力する

このようなフィールドを持つ画面を作成します。XAML は次に示す通りです。

screenshot_03302014_110553

    1: <prism:VisualStateAwarePage x:Name="pageRoot"
    2:                             x:Class="PrismAppValidation.Views.AddSalePage"
    3:                             xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
    4:                             xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
    5:                             xmlns:local="using:PrismAppValidation.Views"
    6:                             xmlns:common="using:PrismAppValidation.Common"
    7:                             xmlns:behv="using:PrismAppValidation.Behaviors"
    8:                             xmlns:conv="using:PrismAppValidation.Converters"
    9:                             xmlns:prism="using:Microsoft.Practices.Prism.StoreApps"
   10:                             xmlns:d="https://schemas.microsoft.com/expression/blend/2008"
   11:                             xmlns:mc="https://schemas.openxmlformats.org/markup-compatibility/2006"
   12:                             mc:Ignorable="d"
   13:                             prism:ViewModelLocator.AutoWireViewModel="True">
   14:  
   15:     <Page.Resources>
   16:         <Style x:Key="ErrorStyle"
   17:                TargetType="TextBlock">
   18:             <Setter Property="FontSize"
   19:                     Value="20" />
   20:             <Setter Property="Foreground"
   21:                     Value="Red" />
   22:         </Style>
   23:         <conv:FirstErrorConverter x:Key="FirstErrorConverter" />
   24:     </Page.Resources>
   25:  
   26:  
   27:     <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
   28:         <Grid.RowDefinitions>
   29:             <RowDefinition Height="140" />
   30:             <RowDefinition Height="Auto" />
   31:             <RowDefinition Height="*" />
   32:             <RowDefinition Height="Auto" />
   33:         </Grid.RowDefinitions>
   34:  
   35:         <!-- Back button and page title -->
   36:         <Grid>
   37:             <Grid.ColumnDefinitions>
   38:                 <ColumnDefinition Width="Auto" />
   39:                 <ColumnDefinition Width="*" />
   40:             </Grid.ColumnDefinitions>
   41:             <Button x:Name="backButton"
   42:                     Command="{Binding GoBackCommand}"
   43:                     Style="{StaticResource NavigationBackButtonNormalStyle}" />
   44:             <TextBlock x:Name="pageTitle"
   45:                        Grid.Column="1"
   46:                        Text="{StaticResource AppName}"
   47:                        Style="{StaticResource HeaderTextBlockStyle}" />
   48:         </Grid>
   49:         <Grid Grid.Row="1">
   50:             <ComboBox ItemsSource="{Binding Products}"
   51:                       SelectedValue="{Binding SelectedProductId, Mode=TwoWay}"
   52:                       DisplayMemberPath="ProductName"
   53:                       SelectedValuePath="ProductId"
   54:                       HorizontalAlignment="Left"
   55:                       Margin="120,60,0,0"
   56:                       VerticalAlignment="Top"
   57:                       Width="434" FontSize="24" d:IsHidden="True" />
   58:             <TextBox Text="{Binding Quantity, Mode=TwoWay}"
   59:                      HorizontalAlignment="Left"
   60:                      Margin="580,60,0,0"
   61:                      TextWrapping="Wrap"
   62:                      VerticalAlignment="Top"
   63:                      Width="99" FontSize="24" />
   64:             <Button Content="注文に追加"
   65:                     Command="{Binding AddToOrderCommand}"
   66:                     HorizontalAlignment="Left"
   67:                     Margin="750,55,0,0"
   68:                     VerticalAlignment="Top"
   69:                     FontSize="24" />
   70:             <TextBlock Style="{StaticResource BaseTextBlockStyle}"
   71:                        HorizontalAlignment="Left"
   72:                        Margin="120,15,0,0"
   73:                        TextWrapping="Wrap"
   74:                        Text="製品名"
   75:                        VerticalAlignment="Top"
   76:                        Height="25"
   77:                        Width="215" />
   78:             <TextBlock Style="{StaticResource BaseTextBlockStyle}"
   79:                        HorizontalAlignment="Left"
   80:                        Margin="580,15,0,0"
   81:                        TextWrapping="Wrap"
   82:                        Text="数量"
   83:                        VerticalAlignment="Top"
   84:                        Height="25"
   85:                        Width="105" />
   86:         </Grid>
   87:  
   88:         <Grid HorizontalAlignment="Left"
   89:               Margin="120,5,0,0"
   90:               Grid.Row="2"
   91:               VerticalAlignment="Top"
   92:               Width="868">
   93:             <Grid.RowDefinitions>
   94:                 <RowDefinition Height="50" />
   95:                 <RowDefinition Height="*" />
   96:             </Grid.RowDefinitions>
   97:             <Grid Grid.Row="0"
   98:                   Margin="5">
   99:                 <Grid.ColumnDefinitions>
  100:                     <ColumnDefinition Width="300" />
  101:                     <ColumnDefinition Width="150" />
  102:                     <ColumnDefinition Width="150" />
  103:                     <ColumnDefinition Width="300" />
  104:                 </Grid.ColumnDefinitions>
  105:                 <TextBlock Grid.Column="0"
  106:                            Text="製品名"
  107:                            Style="{StaticResource BaseTextBlockStyle}" />
  108:                 <TextBlock Grid.Column="1"
  109:                            Text="価格"
  110:                            Style="{StaticResource BaseTextBlockStyle}" />
  111:                 <TextBlock Grid.Column="2"
  112:                            Text="数量"
  113:                            Style="{StaticResource BaseTextBlockStyle}" />
  114:                 <TextBlock Grid.Column="3"
  115:                            Text="カテゴリー"
  116:                            Style="{StaticResource BaseTextBlockStyle}" />
  117:             </Grid>
  118:             <ListView x:Name="OrderItemsList"
  119:                       Grid.Row="1"
  120:                       ItemsSource="{Binding CurrentOrderItems}">
  121:                 <ListView.ItemTemplate>
  122:                     <DataTemplate>
  123:                         <Grid>
  124:                             <Grid.ColumnDefinitions>
  125:                                 <ColumnDefinition Width="300" />
  126:                                 <ColumnDefinition Width="150" />
  127:                                 <ColumnDefinition Width="150" />
  128:                                 <ColumnDefinition Width="300" />
  129:                             </Grid.ColumnDefinitions>
  130:                             <TextBlock Grid.Column="0"
  131:                                        Text="{Binding Product.ProductName}" />
  132:                             <TextBlock Grid.Column="1"
  133:                                        Text="{Binding Product.UnitPrice}" />
  134:                             <TextBlock Grid.Column="2"
  135:                                        Text="{Binding Quantity}" />
  136:                             <TextBlock Grid.Column="3"
  137:                                        Text="{Binding Product.Category}" />
  138:                         </Grid>
  139:                     </DataTemplate>
  140:                 </ListView.ItemTemplate>
  141:  
  142:             </ListView>
  143:         </Grid>
  144:         <Grid Grid.Row="3">
  145:             <Grid.ColumnDefinitions>
  146:                 <ColumnDefinition Width="Auto" />
  147:                 <ColumnDefinition Width="Auto" />
  148:                 <ColumnDefinition Width="*" />
  149:             </Grid.ColumnDefinitions>
  150:             <Grid.RowDefinitions>
  151:                 <RowDefinition Height="Auto" />
  152:                 <RowDefinition Height="Auto" />
  153:                 <RowDefinition Height="Auto" />
  154:                 <RowDefinition Height="Auto" />
  155:                 <RowDefinition Height="Auto" />
  156:                 <RowDefinition Height="Auto" />
  157:                 <RowDefinition Height="Auto" />
  158:             </Grid.RowDefinitions>
  159:             <TextBlock Grid.Row="0"
  160:                        Grid.Column="0"
  161:                        Text="氏名"
  162:                        Style="{StaticResource BaseTextBlockStyle}"
  163:                        Margin="5" />
  164:             <TextBox Grid.Row="0"
  165:                      Grid.Column="1"
  166:                      Text="{Binding Customer.Name, Mode=TwoWay}"
  167:                      Margin="5"
  168:                      Width="500"
  169:                      HorizontalAlignment="Left"
  170:                      behv:HighlightOnErrors.PropertyErrors="{Binding Customer.Errors[Name]}" FontSize="24" />
  171:             <TextBlock Grid.Row="0"
  172:                        Grid.Column="2"
  173:                        Style="{StaticResource ErrorStyle}"
  174:                        Text="{Binding Customer.Errors[Name], 
  175:                 Converter={StaticResource FirstErrorConverter}}" />
  176:             <TextBlock Grid.Row="1"
  177:                        Grid.Column="0"
  178:                        Text="電話番号"
  179:                        Style="{StaticResource BaseTextBlockStyle}"
  180:                        Margin="5" />
  181:             <TextBox Grid.Row="1"
  182:                      Grid.Column="1"
  183:                      Text="{Binding Customer.Phone, Mode=TwoWay}"
  184:                      Margin="5"
  185:                      Width="500"
  186:                      HorizontalAlignment="Left"
  187:                      behv:HighlightOnErrors.PropertyErrors="{Binding Customer.Errors[Phone]}" FontSize="24" />
  188:             <TextBlock Grid.Row="1"
  189:                        Grid.Column="2"
  190:                        Style="{StaticResource ErrorStyle}"
  191:                        Text="{Binding Customer.Errors[Phone], Converter={StaticResource FirstErrorConverter}}" />
  192:             <TextBlock Grid.Row="2"
  193:                        Grid.Column="0"
  194:                        Text="住所"
  195:                        Style="{StaticResource BaseTextBlockStyle}"
  196:                        Margin="5" />
  197:             <TextBox Grid.Row="2"
  198:                      Grid.Column="1"
  199:                      Text="{Binding Customer.Address, Mode=TwoWay}"
  200:                      Margin="5"
  201:                      Width="500"
  202:                      HorizontalAlignment="Left" FontSize="24" />
  203:             <TextBlock Grid.Row="3"
  204:                        Grid.Column="0"
  205:                        Text="市区町村"
  206:                        Style="{StaticResource BaseTextBlockStyle}"
  207:                        Margin="5" />
  208:             <TextBox Grid.Row="3"
  209:                      Grid.Column="1"
  210:                      Text="{Binding Customer.City, Mode=TwoWay}"
  211:                      Margin="5"
  212:                      Width="500"
  213:                      HorizontalAlignment="Left" FontSize="24" />
  214:             <TextBlock Grid.Row="4"
  215:                        Grid.Column="0"
  216:                        Text="都道府県"
  217:                        Style="{StaticResource BaseTextBlockStyle}"
  218:                        Margin="5" />
  219:             <TextBox Grid.Row="4"
  220:                      Grid.Column="1"
  221:                      Text="{Binding Customer.State, Mode=TwoWay}"
  222:                      Margin="5"
  223:                      Width="500"
  224:                      HorizontalAlignment="Left" behv:HighlightOnErrors.PropertyErrors="{Binding Customer.Errors[State]}" FontSize="24" />
  225:             <TextBlock Grid.Row="4"
  226:                        Grid.Column="2"
  227:                        Style="{StaticResource ErrorStyle}"
  228:                        Text="{Binding Customer.Errors[State], Converter={StaticResource FirstErrorConverter}}" />
  229:             <TextBlock Grid.Row="5"
  230:                        Grid.Column="0"
  231:                        Text="郵便番号"
  232:                        Style="{StaticResource BaseTextBlockStyle}"
  233:                        Margin="5" />
  234:             <TextBox Grid.Row="5"
  235:                      Grid.Column="1"
  236:                      Text="{Binding Customer.Zip, Mode=TwoWay}"
  237:                      Margin="5"
  238:                      Width="500"
  239:                      HorizontalAlignment="Left" FontSize="24" />
  240:             <Button Grid.Row="6"
  241:                     Grid.Column="1"
  242:                     Content="注文実行"
  243:                     Command="{Binding SubmitOrderCommand}"
  244:                     FontSize="24" />
  245:         </Grid>
  246:  
  247:         <VisualStateManager.VisualStateGroups>
  248:  
  249:             <!-- Visual states reflect the application's view state -->
  250:             <VisualStateGroup x:Name="ApplicationViewStates">
  251:                 <VisualState x:Name="FullScreenLandscape" />
  252:                 <VisualState x:Name="Filled" />
  253:  
  254:                 <!-- The entire page respects the narrower 100-pixel margin convention for portrait -->
  255:                 <VisualState x:Name="FullScreenPortrait">
  256:                     <Storyboard>
  257:                         <ObjectAnimationUsingKeyFrames Storyboard.TargetName="backButton"
  258:                                                        Storyboard.TargetProperty="Style">
  259:                             <DiscreteObjectKeyFrame KeyTime="0"
  260:                                                     Value="{StaticResource NavigationBackButtonNormalStyle}" />
  261:                         </ObjectAnimationUsingKeyFrames>
  262:                     </Storyboard>
  263:                 </VisualState>
  264:  
  265:                 <!-- The back button and title have different styles when snapped -->
  266:                 <VisualState x:Name="Snapped">
  267:                     <Storyboard>
  268:                         <ObjectAnimationUsingKeyFrames Storyboard.TargetName="backButton"
  269:                                                        Storyboard.TargetProperty="Style">
  270:                             <DiscreteObjectKeyFrame KeyTime="0"
  271:                                                     Value="{StaticResource NavigationBackButtonNormalStyle}" />
  272:                         </ObjectAnimationUsingKeyFrames>
  273:                         <ObjectAnimationUsingKeyFrames Storyboard.TargetName="pageTitle"
  274:                                                        Storyboard.TargetProperty="Style">
  275:                             <DiscreteObjectKeyFrame KeyTime="0"
  276:                                                     Value="{StaticResource NavigationBackButtonNormalStyle}" />
  277:                         </ObjectAnimationUsingKeyFrames>
  278:                     </Storyboard>
  279:                 </VisualState>
  280:             </VisualStateGroup>
  281:         </VisualStateManager.VisualStateGroups>
  282:     </Grid>
  283: </prism:VisualStateAwarePage>
Customer Model オブジェクトを作成

Model クラスのオブジェクトを作成し、上記の XAML の各フィールドにバインドする必要があります。そこで、入力の妥当性検証のルール=バリデーション ルールを作ります。Prism では、Entity Framework 、WCF RIA Services などで使われてきて、大変有名な検証ルール用オブジェクトである System.ComponentModel.DataAnnotations を使用します。System.ComponentModel.DataAnnotations 名前空間内の属性のセットを宣言し、それらを評価するいくつかのサポートクラスの Model オブジェクトのプロパティに、検証ルールを付与します。

WinRT では、自動的にこれらの属性に基づくルールは評価されないため、いくつかのインフラストラクチャーとしてのコードを作って、これを行わせる必要があります。また、いったんルールが評価されたら、一度発生したエラーをユーザーに表示するために簡単に使用できる方法で格納する必要があります。そこで、Customer クラスのベースクラスとして PrismValidatableBindableBase クラスを継承して実装します。Modelクラスである Customer.cs の内容は、こうなります。

    1: using System;
    2: using System.Collections.Generic;
    3: using System.ComponentModel.DataAnnotations;
    4: using System.Linq;
    5: using Microsoft.Practices.Prism.StoreApps;
    6:  
    7: namespace PrismAppValidation.Models
    8: {
    9:     public class Customer : ValidatableBindableBase
   10:     {
   11:         private string _Name;
   12:         private string _Phone;
   13:         private string _Address;
   14:         private string _City;
   15:         private string _State;
   16:         private string _Zip;
   17:         
   18:         [Required]
   19:         public string Name
   20:         {
   21:             get { return _Name; }
   22:             set { SetProperty(ref _Name, value); }
   23:         }
   24:  
   25:         [RegularExpression(@"^\D?(\d{3})\D?\D?(\d{3})\D?(\d{4})$",
   26:             ErrorMessage="10桁の電話番号を数値で入力してください!")]
   27:         public string Phone
   28:         {
   29:             get { return _Phone; }
   30:             set { SetProperty(ref _Phone, value); }
   31:         }
   32:  
   33:         public string Address
   34:         {
   35:             get { return _Address; }
   36:             set { SetProperty(ref _Address, value); }
   37:         }
   38:  
   39:         public string City
   40:         {
   41:             get { return _City; }
   42:             set { SetProperty(ref _City, value); }
   43:         }
   44:  
   45:         [StringLength(5)]
   46:         public string State
   47:         {
   48:             get { return _State; }
   49:             set { SetProperty(ref _State, value); }
   50:         }
   51:  
   52:         public string Zip
   53:         {
   54:             get { return _Zip; }
   55:             set { SetProperty(ref _Zip, value); }
   56:         }
   57:     }
   58: }

必須、正規表現、文字列の長さ、等の アトリビュート(属性)を、名前、電話、および県等のプロパティにつき、検証する方法がここに示されています。加えて、数値範囲属性、その他、あらゆる種類の複雑なカスタム検証を実行できる独自のメソッドがあります。

Prism にある ValidatableBindableBase クラスは、これを継承して作成されたクラスに、いくつかの機能を提供します。まず、このクラスは BindableBase から継承されています。BindableBase は、その背後にあるプロパティの変更時に、当該オブジェクトが正しくデータ バインディングできるように、INotifyPropertyChanged の実装をカプセル化します。BindableBase は、いくつのヘルパーメソッドを提供します。これにより Customer クラスに示すようなコンパクトなプロパティセッターを記述することができます。ブロックが SetProperty() メンバー変数を設定し、プロパティに設定されている新しい値への参照を渡すようにコールする必要があります。このメソッドは、値が以前と同じかどうかのチェックをカプセル化し、値が無い場合または値が異なる場合には、メンバー変数を設定し、プロパティ変更イベントを発生させます。

ValidatableBindableBase はまた、BindableValidator というクラスのインスタンスをカプセル化します。これは、妥当性検証システムのブレーンともいうべき存在で、その点で、ValidateProperty という名前のメソッドを持ち、あるオブジェクトのために呼ばれると、当該プロパティに対して反映されます。そして当該 DataAnnotation 属性により、それらのプロパティを検証し、そのプロパティのためにエラーを集めてエラーのディクショナリにいれます。このディクショナリは、プロパティごとのエラーのリストを保持しています(プロパティ名がディクショナリ中でキーとなり、エラー値はエラー文字列のリストになっています)。ディクショナリはまた、インデクサーを公開しています。これにより、プロパティ名で BindableValidator をインデックス化でき、そのプロパティに紐づけられたエラー文字列を取り出すことができます。

ValidatableBindableBase は、BindableValidator から基になるインデクサーを公開し、特定のオブジェクトののエラーのためユーザーに検証の概要を表示する場合、GetAllErrors メソッドと共にデータバインディングに使用することができます。また ValidatableBindableBase は、BindableBaseSetProperty メソッドよりも優先されます。これは、データ バインディングにおいてプロパティが変更されるときに、ValidateProperty を呼び出すために必要なトリガーポイントとして使用できるようにするためです。そして、WinRT (というよりは XAML )におけるデータバインディングで周知の通り、この状況は、編集されているフィールドにフォーカスの変更があるとき起こります。

Customer クラスの公開とデータバインディング

Customer プロパティを、AddSalesPageViewModel に追加します。しかし、この ViewModel は、OrderRepository によって管理される現在の注文に関連付けられているため、当該 ViewModel は、リポジトリから Customer を取得します。

    1: public Customer Customer
    2: {
    3:     get { return _OrderRepository.Customer; }
    4: }

そして、当該リポジトリは、現在の注文に関連付けられている Customer だけを初期化します。実際のアプリでは、注文が完了するか、他の注文を配置する許可を取り消されたときに、注文に関連付けられている現在のデータをクリアするための方法がかならず必要となりますが、このサンプルではそのケースまではカバーされていません。

IOrderRepository.cs

    1: using System;
    2: using System.Collections.Generic;
    3: using System.Collections.ObjectModel;
    4: using System.Linq;
    5: using PrismAppValidation.Models;
    6:  
    7: namespace PrismAppValidation.Services
    8: {
    9:     public interface IOrderRepository
   10:     {
   11:         void AddToOrder(Product product, int quantity);
   12:         ObservableCollection<OrderItem> CurrentOrderItems { get; }
   13:         Customer Customer { get; set; }
   14:     }
   15: }

OrderRepository.cs

    1: using System;
    2: using System.Collections.Generic;
    3: using System.Collections.ObjectModel;
    4: using System.Linq;
    5: using PrismAppValidation.Models;
    6:  
    7: namespace PrismAppValidation.Services
    8: {
    9:     public interface IOrderRepository
   10:     {
   11:         void AddToOrder(Product product, int quantity);
   12:         ObservableCollection<OrderItem> CurrentOrderItems { get; }
   13:         Customer Customer { get; set; }
   14:     }
   15: }
入力検証のエラーをユーザーに表示

エラーが関連付けられているルールがある入力フィールド各々の横に、バインディングされたプロパティのエラーを表示する TextBlock を追加しました。この TextBlock は、そのプロパティに関連付けられたエラーのコレクションにバインドする必要があります。バインディングされたオブジェクトが Customer であり、またそれらのエラーを取得する ValidatableBindableBase から公開されているインデクサーを持っていることから、XAML の記述は下記のようになります。

    1: <TextBlock Grid.Row="0"
    2:            Grid.Column="2"
    3:            Style="{StaticResource ErrorStyle}"
    4:            Text="{Binding Customer.Errors[Name], 
    5:     Converter={StaticResource FirstErrorConverter}}" />

Text プロパティは、Customer.Errors[Name] にバインディングされます。この Customer.Errors[Name] は、名前プロパティに関連付けられたルールの評価に起因するエラー文字列のコレクションを返します。ただし、この文字列のコレクションを、テキストプロパティに設定するために、単純な文字列に変えるコンバーターが必要となります。そこで、Prism の AdventureWorks Shopper サンプルから、エラーが存在する場合にコレクションのうち最初の部分を取得するコンバーターをコピーして、ここに実装します。

    1: using System;
    2: using System.Collections.Generic;
    3: using System.Linq;
    4: using Windows.UI.Xaml.Data;
    5:  
    6: namespace PrismAppValidation.Converters
    7: {
    8:     /// <summary>
    9:     /// Value converter that retrieves the first error of the collection, or null if empty.
   10:     /// </summary>
   11:     public sealed class FirstErrorConverter : IValueConverter
   12:     {
   13:         public object Convert(object value, Type targetType, object parameter, string language)
   14:         {
   15:             ICollection<string> errors = value as ICollection<string>;
   16:             return errors != null && errors.Count > 0 ? errors.ElementAt(0) : null;
   17:         }
   18:  
   19:         public object ConvertBack(object value, Type targetType, object parameter, string language)
   20:         {
   21:             throw new NotImplementedException();
   22:         }
   23:     }
   24: }

ErrorStyle セットは、前景色を赤、フォント サイズを 20 に設定します。

妥当性検証エラーがある場合にコントロールをハイライトする

もう一つ、検証メカニズムでサポートする必要があるのは、検証エラーがあるときには、何らかの方法でコントロール自体を強調することです。しかし、WinRT の場合、そのために最初から用意されたものはないので、少し作業を行う必要があります。一番良いのは、既存のコントロールのビヘイビアに不十分な個所を補うために、コードを書くことです Win RT では、(アタッチドプロパティに基づく) アタッチドビヘイビアを使うことは、(Win RT の中で既に Blend ビヘイビアでサポートされている第1級のサポートがなされるまでは)極めて有効な方法です。

Prism のサンプル コードは、このために実装されているものです。ここでは、Validation QuickStart サンプルから、HighlightOnErrors クラスをコピーして実装します。これは、TextBox とともに動作し、それの外観を変更するエラーがあるときに、スタイルを適用するように設計されているクラスです。

    1: using System.Collections.ObjectModel;
    2: using System.Linq;
    3: using Microsoft.Practices.Prism.StoreApps;
    4: using Windows.UI.Xaml;
    5: using Windows.UI.Xaml.Controls;
    6:  
    7: namespace PrismAppValidation.Behaviors
    8: {
    9:     public static class HighlightOnErrors
   10:     {
   11:         public static DependencyProperty PropertyErrorsProperty =
   12:             DependencyProperty.RegisterAttached("PropertyErrors", typeof(ReadOnlyCollection<string>), typeof(HighlightOnErrors),
   13:                                                                    new PropertyMetadata(BindableValidator.EmptyErrorsCollection, OnPropertyErrorsChanged));
   14:  
   15:         public static ReadOnlyCollection<string> GetPropertyErrors(DependencyObject obj)
   16:         {
   17:             if (obj == null)
   18:             {
   19:                 return null;
   20:             }
   21:  
   22:             return (ReadOnlyCollection<string>)obj.GetValue(PropertyErrorsProperty);
   23:         }
   24:  
   25:         public static void SetPropertyErrors(DependencyObject obj, ReadOnlyCollection<string> value)
   26:         {
   27:             if (obj == null)
   28:             {
   29:                 return;
   30:             }
   31:  
   32:             obj.SetValue(PropertyErrorsProperty, value);
   33:         }
   34:  
   35:         private static void OnPropertyErrorsChanged(DependencyObject d, DependencyPropertyChangedEventArgs args)
   36:         {
   37:             if (args == null || args.NewValue == null)
   38:             {
   39:                 return;
   40:             }
   41:  
   42:             TextBox textBox = (TextBox)d;
   43:             var propertyErrors = (ReadOnlyCollection<string>)args.NewValue;
   44:  
   45:             Style textBoxStyle = (propertyErrors.Count() > 0) ? (Style)Application.Current.Resources["HighlightTextStyle"] : null;
   46:  
   47:             textBox.Style = textBoxStyle;
   48:         }
   49:     }
   50: }

PropertyErrors と呼ばれるアタッチドプロパティが宣言されています。これは、プロパティの Errors コレクションへのバインディングを介して、そのプロパティを設定できるように設計されています。Errors コレクションが変更されたとき、そのロジックを再評価し、Errors コレクションにエラーがある場合には、コントロールにスタイルを設定します。そして、TextBox にそれを設定します。

    1: <TextBox Grid.Row="0"
    2:          Grid.Column="1"
    3:          Text="{Binding Customer.Name, Mode=TwoWay}"
    4:          Margin="5"
    5:          Width="500"
    6:          HorizontalAlignment="Left"
    7:          behv:HighlightOnErrors.PropertyErrors="{Binding Customer.Errors[Name]}" FontSize="24" />

名前、電話、および県のフィールドに(エラーが起こるように)適当な値を入れて、それらのフィールドで出るエラーを表示します。

image

注文送信ボタンを押した時にすべてのプロパティを検証する
最終的にやりたいことは、ユーザーが送信しようとしたときに、すべてのエラーがチェックされることです。なぜなら、ユーザーがデザインモードでフィールドを変更しようとしているときには検証エラーは表示されないためです。またいくつかの他の必要なフィールドがある場合があります。加えて、それらの妥当性検証が有効な場合、通常は、バックエンド側の呼び出しもせず、変更も保存しません。
そこで、SubmitOrder ボタンにバインディングされた DelegateCommand を下記のコマンドハンドラーに記述します。
    1: private void OnSubmit()
    2: {
    3:  
    4:     Customer.ValidateProperties();
    5:  
    6:     var allErrors = Customer.GetAllErrors().Values.SelectMany(c => c).ToList();
    7:  
    8:     if (allErrors.Count == 0)
    9:     {
   10:  
   11:     }
   12: }

ValidateProperties() メソッドは、オブジェクト上のすべてのプロパティ間の検証をトリガーできます。これが完了すると、各プロパティのエラーコレクションが設定されます。GetAllErrors メソッドは、すべてのプロパティの間ですべてのエラーを収集でき、LINQ でリストにそのディクショナリ―を平坦化することができます。

まとめ

今回は、Prism の検証機能を使用して、最小限の労力で、豊富な入力検証サポート機能を実装する方法をご紹介しました。最初にやるべきなのは、ValidatableBindableBase を継承した Model オブジェクトを作ることと、プロパティセットのブロックから、ベースクラスにある SetProperty をコールすることです。

以上です。もう少しコンパクトなサンプルと解説については、検証のクイック スタートのページをご参照ください。

それでは、また!今度は Build 後かなw?(^^;)ゞ

鈴木章太郎