dragan10

The Silverlight Geek - ベイビー、君のせいじゃないんだ…データグリッドのバグならぬバグ

2008-12-16 00:40:22

データグリッドに関して一連のミニ・チュートリアルを書こうと思っていますが、それらは王様のアドバイスに従って、はじめから始めて、終わるまで続いた後に終わらせる[1]つもりです。しかしチュートリアルを始める前に、データグリッドの「理解はできるがプログラマからすればそう動いてほしくない振る舞い集」("Set Of Behaviors That Is Perfectly Understandable But Not At All What The Programmer Expects = SOBTIPUBNAAWTPE)があきらかになってきました(私はこれはバグではないと聞いています-私の反応は皮肉っぽいものでした)

これは大変なフラストレーションと混乱を引き起こすので、あなたが髪をかきむしり出す前に警告しておきたいと思います-そして回避方法がわかり次第、お知らせしたいと思っています。

このバグ(おっと)を理解するには、2wayバインディングのDataBinding Validationに関する二つのプロパティを理解している必要があります。

言葉にするならこういうことです。データソースに書き戻されるようなデータを入力した際に、正しくプロパティが設定されているなら、Silverlightはそのデータを検証して以下の2種類の例外を処理してくれます。

 1. バインディング・エンジンが入力されたデータの方を変換しようとしたときに投げられる例外

 2. バインドされているオブジェクトのsetアクセサから投げられる例外

設定しなければならない2つのプロパティとは、NotifyOnValidationErrorValidatesOnExceptionです。これらは共に、デフォルトではfalseになっています。これらはtrueに設定しておきたいところです。そうすれば、発生した例外はいずれもBindingValidationErrorイベントに変換されますーさらによいことには、これはbubblingイベント(訳注:泡が上っていくように上位に伝達されるイベント)なので、これを処理するイベントハンドラは、外側のコントロールに書いておけば良いわけです。

この仕組みの利用方法として想定されているのは、BindingValidationErrorのイベントハンドラをDataGrid自身に割り当てるやり方で、そうすればどの列でバインドされているデータに問題があったとしても、イベントはそのハンドラに上ってきてくれるので、例外がアプリケーションを立ち往生させてしまうようなことにはなりません。

これを実際にテストしてみるために、サンプルをReid Maker(個人的な手紙でこのSOBTIPUBNAAWTPEをしてくれました)と、Manish Dalal(ベータ2でこの仕組みがどう働くべきかを示してくれました)のblogから拝借して、様々なケース(例えばイベントハンドラをXamlの中や外に移してみたり、例外が発生する場所をイベントの中や外にしてみたり、例外の発生をどちらの方法でもやってみたり、です)で件のバグがバグではないことを証明しようとしてみました。

コードは以下の通りですが、やや単純化しています。

 

データソースのクラス

using System;
using System.ComponentModel;

namespace DataGridBindingValidationTester
{
   public class TestData : INotifyPropertyChanged
   {
      public event PropertyChangedEventHandler PropertyChanged;
      private int id = 0;
      private string name = string.Empty;

      private void NotifyChange( String name )
      {
         if ( PropertyChanged != null )
         {
            PropertyChanged( this, new PropertyChangedEventArgs( name ) );
         }
      }

      public string Name
      {
         get { return name; }
         set
         {
            name = value;
            NotifyChange( "Name" );
         }
      }

      public int Id
      {
         get
         {
            return id;
         }
         set
         {
            if ( value == 9 )
            {
               throw new Exception( "can't have 9" );
            }
            id = value;
            NotifyChange( "Id" );
         }
      }
   }
}

Page Xaml

<UserControl x:Class="DataGridBindingValidationTester.Page"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Width="400" Height="300" xmlns:data="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data">
    <Grid x:Name="LayoutRoot" Background="White">
        <Grid.RowDefinitions>
            <RowDefinition Height="0.15*"/>
            <RowDefinition Height="0.85*"/>
        </Grid.RowDefinitions>
        <TextBlock Margin="18,8,56,8" x:Name="Output" FontSize="14" Text="Error Messages Show Here" TextWrapping="Wrap"/>
        <data:DataGrid x:Name="TestDataGrid" Margin="10,10,10,10" Grid.Row="1"  AutoGenerateColumns="False" >
            <data:DataGrid.Columns>
                <data:DataGridTextColumn Header="Name" Binding="{Binding Name}"  />
                <data:DataGridTemplateColumn Header="ID">
                    <data:DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <TextBlock  Text="{Binding Id}"  />
                        </DataTemplate>
                     </data:DataGridTemplateColumn.CellTemplate>
                    <data:DataGridTemplateColumn.CellEditingTemplate>
                        <DataTemplate>
                            <TextBox Text="{Binding Id,Mode=TwoWay,NotifyOnValidationError=true,ValidatesOnExceptions=true}" />
                        </DataTemplate>
                    </data:DataGridTemplateColumn.CellEditingTemplate>
                </data:DataGridTemplateColumn>
            </data:DataGrid.Columns>
        </data:DataGrid>
    </Grid>
</UserControl>

Page.xaml.cs

using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;

namespace DataGridBindingValidationTester
{
   public partial class Page : UserControl
   {
      public Page()
      {
         InitializeComponent();
         Loaded += new RoutedEventHandler( Page_Loaded );
         TestDataGrid.BindingValidationError +=
            new EventHandler<ValidationErrorEventArgs>( TestDataGrid_BindingValidationError );
      }

      void TestDataGrid_BindingValidationError( object sender, ValidationErrorEventArgs e )
      {
         if ( e.Action == ValidationErrorEventAction.Added )
         {
            ( (Control) e.OriginalSource ).Background = new SolidColorBrush( Colors.Red );
            Output.Text = "Error: " + e.Error.Exception.Message;
         }
         else if ( e.Action == ValidationErrorEventAction.Removed )
         {
            ( (Control) e.OriginalSource ).Background = new SolidColorBrush( Colors.White );
         }

      }

      void Page_Loaded( object sender, RoutedEventArgs e )
      {
         List<TestData> tests = new List<TestData>();

         tests.Add( new TestData { Id = 1, Name = "A" } );
         tests.Add( new TestData { Id = 4, Name = "B" } );
         tests.Add( new TestData { Id = 2, Name = "C" } );
         tests.Add( new TestData { Id = 5, Name = "D" } );
         tests.Add( new TestData { Id = 3, Name = "E" } );
         tests.Add( new TestData { Id = 8, Name = "F" } );
         tests.Add( new TestData { Id = 7, Name = "G" } );
         TestDataGrid.ItemsSource = tests;
         
      }
   }
}

この仕掛けを動かす鍵になるのは、IDをバインドする(ユーザがデータを入力するとき)DataGridのCell Editing Templateが、必要な二つのプロパティが指定されているTextBoxを使ったDataTemplateを持っているところです。

<DataTemplate>
    <TextBox Text="{Binding Id,
                    Mode=TwoWay,
                    NotifyOnValidationError=true,
                    ValidatesOnExceptions=true}" />
</DataTemplate>

加えて、イベントハンドラはクラスのコンストラクタで生成され、コードビハインド中で実装されています。

TestDataGrid.BindingValidationError +=
   new EventHandler<ValidationErrorEventArgs>( TestDataGrid_BindingValidationError );

void TestDataGrid_BindingValidationError( object sender, ValidationErrorEventArgs e )
{
   if ( e.Action == ValidationErrorEventAction.Added )
   {
      ( (Control) e.OriginalSource ).Background = new SolidColorBrush( Colors.Red );
      Output.Text = "Error: " + e.Error.Exception.Message;
   }
   else if ( e.Action == ValidationErrorEventAction.Removed )
   {
      ( (Control) e.OriginalSource ).Background = new SolidColorBrush( Colors.White );
   }

}

 

バグというのは、実はこのやりかたが動かないということなのです。これがバグでなくてSUBTIPUBNAAWTPE であるという理由は以下の通りです:これが動かない実際の理由は、イベントが発生しないということではなく、エラーが発生した時点で、すでにDataGridはTextBoxからTextBlockへの移行を済ませてしまっているから、なのです-つまり、プロパティはその時点ではもう設定されていないというわけです。

(OK、私の皮肉っぽい答はこうです…これはBindingValidationのバグじゃないかも知れませんが、どんな正当な定義に照らし合わせてもSilverlightのバグです。そうでないというなら、それは動かないブレーキのことを、そのブレーキは実際の路上走行時に止まることができるようには設計されていないだけで、リフトの上では完全に動くからバグじゃない、といっているようなものでしょう)

すぐに続きます…よりシステマティックなDataGridのレビューもお送りします。

[1] Alice in Wonderland – Public Domain – Project Gutenberg

(訳注:訳者は「不思議の国のアリス」を読んでいないので、この部分の訳が適切かどうかは判断できません。たぶんもっとしかるべき訳があるんだろうと思います)

(原文はこちら

 

※このエントリは ブロガーにより投稿されたものです。朝日インタラクティブ および ZDNet Japan編集部の見解・意向を示すものではありません。
  • 新着記事
  • 特集
  • ブログ