none
WPFのデータグリッドのDataTemplateのBindingの挙動について RRS feed

  • 質問


  • こんにちは。
    WPFのデータグリッドのDataTemplateの挙動について質問です。

    問題は、データグリッドの内のカスタムコントール内のテキストボックスの値を変更しても変更の通知がうまくできませんでした。
    (データグリッドの外の通常のコンポーネントとして作ると正しく動作しました)

    データグリッドに以下のようなテンプレートの列を作成しました。

    <!-- TEST -->
    <DataGridTemplateColumn Header="test" Width="Auto" MinWidth="45" IsReadOnly="True">
    	<DataGridTemplateColumn.CellTemplate>
    		<DataTemplate>
    			<local:UserControl1 Value="{Binding Value}" /> <!-- UpdateSourceTrigger=PropertyChanged をつけると奇妙な更新をする -->
    		</DataTemplate>
    	</DataGridTemplateColumn.CellTemplate>
    </DataGridTemplateColumn>


    <UserControl x:Class="_170307__BindingTest01.UserControl1"
                 // 略
                 x:Name="Control"
                 mc:Ignorable="d">
        <StackPanel Orientation="Horizontal">
            <TextBox Text="{Binding ElementName=Control, Path=Value"
                     VerticalContentAlignment="Center"
                     MinWidth="60" />
    		<!-- UpdateSourceTrigger=PropertyChanged をつけると奇妙な更新をする -->
    				 
            <TextBlock FontSize="8" Text=".omake" VerticalAlignment="Bottom" />
        </StackPanel>
    </UserControl>



    一応動かす方法は見つかったのですが、
    ・カスタムコントロールにUpdateSourceTriggerとつけると、テキストボックスをロストフォーカスしたときに変更通知が発生しました。
    ・DataGridのDataTemplateのUserControl1にもUpdateSourceTriggerをつけると、一文字ずつ入力するたびに変更通知が発生しました。

    どうも通常の通知とふるまいが異なっており、同じようなふるまいをするように修正する方法がわからず困っています。

    <local:UserControl1 Value="{Binding Value}" />
    であれば、テキストボックスからロストフォーカスしたときに、通知が発生してほしいし、

    <local:UserControl1 Value="{Binding Value, UpdateSourceTrigger=PropertyChanged}" />
    であれば、一文字ずつ入力するたびに変更通知が発生してほしいです。
    (ふるまいが異なっているとややこしいし、できるだけ通常そうあるようにしたいと思っているのですが、そもそも考えを誤っているのでしょうか?)

    以上です。なにかわかるかたいましたら、お手数おかけしますが、教えていただけないでしょうか。
    よろしくお願いします。

    環境
    Window10 Pro x64
    Visual Studio 2015 Update3

    テストをしたプロジェクトデータ
    https://1drv.ms/u/s!AmGCTVXH7L6MtCTM4TviLsK5j5RG

    2017年3月7日 6:51

回答

  • DataGridではDataGridRow単位でBindingGroupが設定されています。この設定によりDataGridRowの子要素のバインディングがBindingGroupに所属するようになります。

    BindingGroupはそのグループに属しているバインディングを一括検証などのため、ソースへ反映されるタイミングは遅らせることになります。つまり、DatagGridRowでは「行」の編集が終わったらソースに反映する動作をします。
    ですから行内でフォーカスが移動してもソースへは反映されません。
    (ただし、DataGridTemplateColumnではそのような仕組みがあるのを無視して直接ソースにバインドすることで反映ができてしまいます。)

    それと、DataGridTemplateColumnをIsReadOnly=trueにしていると、そのセルに配置したユーザーコントロール内で値を編集しても、DataGridRowのBindingGroupが編集を認識できません。
    そのため、編集が開始されていないので行からフォーカスを移動しても編集を確定するという動作が行われず、ソースに反映がされません。
    正しくはReadOnlylにせずにCellEditingTemplateを表示させて編集が行われたことを認識させないといけません。

    以上のDataGridの挙動ではなく、単純にセル単位で確定できれば良いのであれば、

    public partial class UserControl1 : UserControl
    {
        public static readonly DependencyProperty ValueProperty = DependencyProperty.Register("Value", typeof(double?), typeof(UserControl1), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
    
        public double? Value
        {
            get { return (double?)GetValue(ValueProperty); }
            set { SetValue(ValueProperty, value); }
        }
    
        public UserControl1()
        {
            InitializeComponent();
            this.LostFocus += UserControl1_LostFocus;
        }
    
        void UserControl1_LostFocus(object sender, RoutedEventArgs e)
        {
            if (!this.IsKeyboardFocusWithin)
            {
                var bnd = BindingOperations.GetBinding(this, ValueProperty);
                if (bnd != null && (bnd.UpdateSourceTrigger == UpdateSourceTrigger.LostFocus || bnd.UpdateSourceTrigger == UpdateSourceTrigger.Default))
                {
                    var be = this.GetBindingExpression(ValueProperty);
                    if (be != null)
                    {
                        //if (be.BindingGroup != null && be.BindingGroup.Owner is DataGridRow)
                        //{
                        //    System.Diagnostics.Debug.WriteLine("BindingGroupが"+ be.BindingGroup.Owner.GetType().ToString()+"に設定されています。未反映状態="+ be.BindingGroup.IsDirty);
                        //}
                        be.UpdateSource();
                    }
                }
            }
        }
    }
    のようにBindingExpressionを取得して強制的にUpdateSourceしてやればソースへ反映させることができます。

    個別に明示されていない限りgekkaがフォーラムに投稿したコードにはフォーラム使用条件に基づき「MICROSOFT LIMITED PUBLIC LICENSE」が適用されます。(かなり自由に使ってOK!)

    • 回答としてマーク ichiethel 2017年3月8日 1:54
    2017年3月7日 11:37

すべての返信

  • DataGridではDataGridRow単位でBindingGroupが設定されています。この設定によりDataGridRowの子要素のバインディングがBindingGroupに所属するようになります。

    BindingGroupはそのグループに属しているバインディングを一括検証などのため、ソースへ反映されるタイミングは遅らせることになります。つまり、DatagGridRowでは「行」の編集が終わったらソースに反映する動作をします。
    ですから行内でフォーカスが移動してもソースへは反映されません。
    (ただし、DataGridTemplateColumnではそのような仕組みがあるのを無視して直接ソースにバインドすることで反映ができてしまいます。)

    それと、DataGridTemplateColumnをIsReadOnly=trueにしていると、そのセルに配置したユーザーコントロール内で値を編集しても、DataGridRowのBindingGroupが編集を認識できません。
    そのため、編集が開始されていないので行からフォーカスを移動しても編集を確定するという動作が行われず、ソースに反映がされません。
    正しくはReadOnlylにせずにCellEditingTemplateを表示させて編集が行われたことを認識させないといけません。

    以上のDataGridの挙動ではなく、単純にセル単位で確定できれば良いのであれば、

    public partial class UserControl1 : UserControl
    {
        public static readonly DependencyProperty ValueProperty = DependencyProperty.Register("Value", typeof(double?), typeof(UserControl1), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
    
        public double? Value
        {
            get { return (double?)GetValue(ValueProperty); }
            set { SetValue(ValueProperty, value); }
        }
    
        public UserControl1()
        {
            InitializeComponent();
            this.LostFocus += UserControl1_LostFocus;
        }
    
        void UserControl1_LostFocus(object sender, RoutedEventArgs e)
        {
            if (!this.IsKeyboardFocusWithin)
            {
                var bnd = BindingOperations.GetBinding(this, ValueProperty);
                if (bnd != null && (bnd.UpdateSourceTrigger == UpdateSourceTrigger.LostFocus || bnd.UpdateSourceTrigger == UpdateSourceTrigger.Default))
                {
                    var be = this.GetBindingExpression(ValueProperty);
                    if (be != null)
                    {
                        //if (be.BindingGroup != null && be.BindingGroup.Owner is DataGridRow)
                        //{
                        //    System.Diagnostics.Debug.WriteLine("BindingGroupが"+ be.BindingGroup.Owner.GetType().ToString()+"に設定されています。未反映状態="+ be.BindingGroup.IsDirty);
                        //}
                        be.UpdateSource();
                    }
                }
            }
        }
    }
    のようにBindingExpressionを取得して強制的にUpdateSourceしてやればソースへ反映させることができます。

    個別に明示されていない限りgekkaがフォーラムに投稿したコードにはフォーラム使用条件に基づき「MICROSOFT LIMITED PUBLIC LICENSE」が適用されます。(かなり自由に使ってOK!)

    • 回答としてマーク ichiethel 2017年3月8日 1:54
    2017年3月7日 11:37
  • gekka さん回答ありがとうございました。

    BindingGroup というプロパティの理解がありませんでした。
    なんらかの条件を満たしたときに更新イベントを発行するようにできるのですね。

    BindingGroup のMSDNがあわせて参考になりました。
    サンプルコードで少し理解できた気がしますが、もうすこし調べておきます。

    セル単位の情報については、教えていただいた方法で正しく Binding を更新することができました。
    ありがとうございました。

    BindingGroup
    https://msdn.microsoft.com/ja-jp/library/system.windows.frameworkelement.bindinggroup(v=vs.110).aspx
    2017年3月8日 1:53