C# WPF双向绑定不';控件修改后才能工作

C# WPF双向绑定不';控件修改后才能工作,c#,wpf,data-binding,C#,Wpf,Data Binding,我正在尝试使用数据中的集合同步DataGrid中的选择。我有一个小怪癖 当我在DataGrid中更改选择时,更改会写入我的数据集合,到目前为止效果良好。然后,如果数据收集更改,则会按照预期更新我的DataGrid中的选择。但是,如果在修改DataGrid之前修改数据,则DataGrid选择不会更新 第一个工作案例的示例 第二个非工作案例的示例 代码 使用系统集合; 使用System.Collections.ObjectModel; 使用System.Windows; 使用System.Win

我正在尝试使用数据中的集合同步DataGrid中的选择。我有一个小怪癖

当我在DataGrid中更改选择时,更改会写入我的数据集合,到目前为止效果良好。然后,如果数据收集更改,则会按照预期更新我的DataGrid中的选择。但是,如果在修改DataGrid之前修改数据,则DataGrid选择不会更新

第一个工作案例的示例

第二个非工作案例的示例

代码

使用系统集合;
使用System.Collections.ObjectModel;
使用System.Windows;
使用System.Windows.Controls;
命名空间测试床
{
公共类小部件
{
公共字符串名称{get;set;}
}
公共类数据
{
公共静态数据实例{get;}=new Data();
公共ObservableCollection小部件{get;set;}=new ObservableCollection();
公共IList SelectedWidgets{get;set;}=new ObservableCollection();
数据()
{
添加(newwidget(){Name=“Widget 1”});
添加(newwidget(){Name=“Widget 2”});
添加(newwidget(){Name=“Widget 3”});
}
};
公共类BindableDataGrid:DataGrid
{
公共静态只读DependencyProperty SelectedItemsProperty=DependencyProperty.Register(
“SelectedItems”,
类型(IList),
类型(BindableDataGrid),
新属性元数据(默认值(IList));
公共新IList SelectedItems
{
获取{return(IList)GetValue(SelectedItemsProperty);}
set{SetValue(selecteditemsprroperty,value);}
}
选择更改时受保护的覆盖无效(SelectionChangedEventArgs e)
{
基础。选举变更(e);
SetCurrentValue(SelectedItemsProperty,base.SelectedItems);
}
}
公共部分类主窗口:窗口
{
公共主窗口()
{
初始化组件();
}
私有无效按钮1_单击(对象发送者,路由目标){按钮_单击(0);}
私有无效按钮2_单击(对象发送者,路由目标){按钮_单击(1);}
私有无效按钮3_单击(对象发送者,路由目标){按钮_单击(2);}
已单击专用无效按钮(整数索引)
{
Data=Data.Instance;
Widget Widget=data.Widgets[index];
if(data.SelectedWidgets.Contains(widget))
{
data.SelectedWidgets.Remove(widget);
}
其他的
{
data.SelectedWidgets.Add(widget);
}
}
}
}
和标记

<Window
    x:Class="Testbed.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:test="clr-namespace:Testbed"
    Title="MainWindow"
    Height="480" Width="640"
    DataContext="{Binding Source={x:Static test:Data.Instance}}">

    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition MinWidth="210" />
            <ColumnDefinition Width="5" />
            <ColumnDefinition MinWidth="210" />
            <ColumnDefinition Width="5" />
            <ColumnDefinition MinWidth="210" />
        </Grid.ColumnDefinitions>

        <!-- Change selection through data -->
        <StackPanel Grid.Column="0">
            <Button Content="Select Widget 1" Click="Button1_Click"/>
            <Button Content="Select Widget 2" Click="Button2_Click"/>
            <Button Content="Select Widget 3" Click="Button3_Click"/>
        </StackPanel>

        <!-- Current selection in data -->
        <DataGrid Grid.Column="2"
            ItemsSource="{Binding SelectedWidgets}"
            IsReadOnly="true">
        </DataGrid>

        <!-- Change selection through UI -->
        <test:BindableDataGrid Grid.Column="4"
            SelectionMode="Extended"
            ColumnWidth="*"
            ItemsSource="{Binding Widgets}"
            SelectedItems="{Binding SelectedWidgets, Mode=TwoWay}"
            IsReadOnly="true">
        <DataGrid.RowStyle>
            <Style TargetType="{x:Type DataGridRow}">
                <Style.Resources>
                <SolidColorBrush x:Key="{x:Static SystemColors.InactiveSelectionHighlightBrushKey}" Color="CornflowerBlue"/>
                </Style.Resources>
            </Style>
        </DataGrid.RowStyle>
        </test:BindableDataGrid>
    </Grid>

</Window>

发生此问题的原因是您没有处理BindableDataGrid.SelectedItems集合的通知。 在第一种情况下,您不需要手动处理它们,因为您实际上从基本DataGrid类获取SelectedItems集合,并通过OnSelectionChanged方法调用将其传递给视图模型。基本DataGrid本身处理此集合的通知

但是,如果先单击按钮,SelectedItems属性将获得一个新集合,而基本DataGrid对此一无所知。 我认为您需要处理propertyChangedCallback,并处理所提供集合的通知以手动更新网格中的选择。请参阅以下演示该概念的代码。请注意,为了简单起见,我已经重命名了该属性,但仍然没有调试它

public static readonly DependencyProperty SelectedItemsNewProperty = DependencyProperty.Register(
      "SelectedItemsNew",
      typeof(IList),
      typeof(BindableDataGrid), new PropertyMetadata(OnPropertyChanged));
        private static void OnPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
     BindableDataGrid bdg = (BindableDataGrid)d;
     if (e.OldValue as INotifyCollectionChanged != null)
        (e.NewValue as INotifyCollectionChanged).CollectionChanged -= bdg.BindableDataGrid_CollectionChanged;
     if (Object.ReferenceEquals(e.NewValue, bdg.SelectedItems))
        return;
     if( e.NewValue as INotifyCollectionChanged != null )
        (e.NewValue as INotifyCollectionChanged).CollectionChanged += bdg.BindableDataGrid_CollectionChanged;
     bdg.SynchronizeSelection(e.NewValue as IList);
  }
  private void BindableDataGrid_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) {
     SynchronizeSelection((IList)sender);
  }
  private void SynchronizeSelection( IList collection) {         
     SelectedItems.Clear();
     if (collection != null) 
        foreach (var item in collection)
           SelectedItems.Add(item);         
  }

这是因为新的
SelectedItems
属性在设置时从不更新基本
SelectedItems
。当然,问题是,
MultiSelector.SelectedItems
是只读的。它是专门设计为不可设置的,但它也是设计为可更新的

代码之所以有效,是因为当您通过
BindableDataGrid
更改选择时,
SelectedWidgets
会被
DataGrid
的内部
SelectedItemsCollection
替换。在此之后,您将添加和删除该集合,因此它将更新
DataGrid

当然,如果您还没有更改选择,这将不起作用,因为
OnSelectionChanged
在此之前不会运行,因此
SetCurrentValue
从未被调用,因此绑定从未更新
SelectedWidgets
。但这很好,您所要做的就是调用
SetCurrentValue
,作为
BindableDataGrid
初始化的一部分

将此添加到
BindableDataGrid

protected override void OnInitialized(EventArgs e)
{
    base.OnInitialized(e);
    SetCurrentValue(SelectedItemsProperty, base.SelectedItems);
}
不过要小心,因为如果在初始化后的某个时间尝试设置
SelectedItems
,此选项仍然会中断。如果您可以将其设置为只读,那就太好了,但这会阻止它在数据绑定中使用。因此,请确保您的绑定使用单向源,而不是双向源:

<test:BindableDataGrid Grid.Column="4"
    SelectionMode="Extended"
    ColumnWidth="*"
    ItemsSource="{Binding Widgets}"
    SelectedItems="{Binding SelectedWidgets, Mode=OneWayToSource}"
    IsReadOnly="true">
    <DataGrid.RowStyle>
        <Style TargetType="{x:Type DataGridRow}">
            <Style.Resources>
                <SolidColorBrush x:Key="{x:Static SystemColors.InactiveSelectionHighlightBrushKey}" Color="CornflowerBlue"/>
            </Style.Resources>
        </Style>
    </DataGrid.RowStyle>
</test:BindableDataGrid>

@雷默博士的回答为我指明了正确的方向。然而,它归结为接受以下事实:源数据集合正被DataGrid.SelectedItems集合所取代。在第一次修改之后,它会绕过
OnPropertyChanged
,因为绑定的两端实际上是同一个对象

我不想替换源集合,所以我找到了另一个同步集合内容的解决方案。它的好处是更直接

Wh
<test:BindableDataGrid Grid.Column="4"
    SelectionMode="Extended"
    ColumnWidth="*"
    ItemsSource="{Binding Widgets}"
    SelectedItems="{Binding SelectedWidgets, Mode=OneWayToSource}"
    IsReadOnly="true">
    <DataGrid.RowStyle>
        <Style TargetType="{x:Type DataGridRow}">
            <Style.Resources>
                <SolidColorBrush x:Key="{x:Static SystemColors.InactiveSelectionHighlightBrushKey}" Color="CornflowerBlue"/>
            </Style.Resources>
        </Style>
    </DataGrid.RowStyle>
</test:BindableDataGrid>
public static readonly DependencyProperty SelectedItemsProperty = DependencyProperty.Register(
    "SelectedItems",
    typeof(IList),
    typeof(BindableDataGrid),
    new PropertyMetadata(default(IList), null, (o, v) => ((BindableDataGrid)o).CoerceBindableSelectedItems(v)));

protected object CoerceBindableSelectedItems(object baseValue)
{
    return base.SelectedItems;
}
    public class BindableDataGrid : DataGrid
    {
        public static readonly DependencyProperty SelectedItemsProperty = DependencyProperty.Register(
            "SelectedItems",
            typeof(IList),
            typeof(BindableDataGrid),
            new PropertyMetadata(OnPropertyChanged));

        private static void OnPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            BindableDataGrid bdg = (BindableDataGrid) d;
            if (bdg.initialized) return;
            bdg.initialized = true;

            bdg.source = (IList) e.NewValue;
            bdg.target = ((DataGrid) bdg).SelectedItems;
            ((INotifyCollectionChanged) e.NewValue).CollectionChanged += bdg.OnCollectionChanged;
        }

        public new IList SelectedItems
        {
            get { return (IList) GetValue(SelectedItemsProperty); }
            set { SetValue(SelectedItemsProperty, value); }
        }

        IList source;
        IList target;
        bool synchronizing;
        bool initialized;

        private void OnSourceChanged()
        {
            if (synchronizing) return;
            synchronizing = true;
            target.Clear();
            foreach (var item in source)
                target.Add(item);
            synchronizing = false;
        }

        private void OnTargetChanged()
        {
            if (synchronizing) return;
            synchronizing = true;
            source.Clear();
            foreach (var item in target)
                source.Add(item);
            synchronizing = false;
        }

        private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            OnSourceChanged();
        }

        protected override void OnSelectionChanged(SelectionChangedEventArgs e)
        {
            base.OnSelectionChanged(e);
            OnTargetChanged();
        }
    }