[英]WPF two way binding doesn't work until control is modified
我正在尝试使用我的数据中的集合来同步 DataGrid 中的选择。 我有这个主要工作,有一个小怪癖。
当我在 DataGrid 中更改选择时,更改将写入我的数据集合,到目前为止一切顺利。 然后,如果数据集合更改了我的 DataGrid 中的选择,则按预期更新。 但是,如果我在修改 DataGrid 之前修改我的数据,则 DataGrid 选择不会更新。
代码
using System.Collections;
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Controls;
namespace Testbed
{
public class Widget
{
public string Name { get; set; }
}
public class Data
{
public static Data Instance { get; } = new Data();
public ObservableCollection<Widget> Widgets { get; set; } = new ObservableCollection<Widget>();
public IList SelectedWidgets { get; set; } = new ObservableCollection<Widget>();
Data()
{
Widgets.Add(new Widget() { Name = "Widget 1" });
Widgets.Add(new Widget() { Name = "Widget 2" });
Widgets.Add(new Widget() { Name = "Widget 3" });
}
};
public class BindableDataGrid : DataGrid
{
public static readonly DependencyProperty SelectedItemsProperty = DependencyProperty.Register(
"SelectedItems",
typeof(IList),
typeof(BindableDataGrid),
new PropertyMetadata(default(IList)));
public new IList SelectedItems
{
get { return (IList) GetValue(SelectedItemsProperty); }
set { SetValue(SelectedItemsProperty, value); }
}
protected override void OnSelectionChanged(SelectionChangedEventArgs e)
{
base.OnSelectionChanged(e);
SetCurrentValue(SelectedItemsProperty, base.SelectedItems);
}
}
public partial class MainWindow : Window
{
public MainWindow ()
{
InitializeComponent();
}
private void Button1_Click(object sender, RoutedEventArgs e) { Button_Clicked(0); }
private void Button2_Click(object sender, RoutedEventArgs e) { Button_Clicked(1); }
private void Button3_Click(object sender, RoutedEventArgs e) { Button_Clicked(2); }
private void Button_Clicked(int index)
{
Data data = Data.Instance;
Widget widget = data.Widgets[index];
if (data.SelectedWidgets.Contains(widget))
{
data.SelectedWidgets.Remove(widget);
}
else
{
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 class 获取 SelectedItems 集合,并从 OnSelectionChanged 方法调用将其传递给视图 model。 基本 DataGrid 处理此集合本身的通知。
但是,如果您首先单击该按钮,SelectedItems 属性将获得一个新集合,而基础 DataGrid 对此一无所知。 我认为您需要处理 propertyChangedCallback,并处理提供的 collections 的通知以手动更新网格中的选择。 请参阅以下演示该概念的代码。 请注意,为简单起见,我已重命名该属性,但仍未对其进行调试。
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
,这仍然会中断。 如果您可以将其设为只读,那就太好了,但这会阻止它在数据绑定中使用。 因此,请确保您的绑定使用OneWayToSource
而不是TwoWay
:
<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>
如果您想确保这永远不会中断,您可以添加CoerceValueCallback
以确保新的SelectedItems
永远不会设置为base.SelectedItems
以外的其他内容:
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;
}
@Drreamer 的回答为我指明了正确的方向。 但是,它归结为接受源数据集合被 DataGrid.SelectedItems 集合替换的事实。 在第一次修改后它最终绕过了OnPropertyChanged
,因为绑定的两端实际上是相同的 object。
我不想替换源集合,所以我找到了另一个同步 collections内容的解决方案。 它还具有更直接的好处。
当 DependencyProperty 初始化 SelectedItems 时,我存储对源和目标 collections 的引用。 我还在源上注册 CollectionChanged 并在目标上覆盖 OnSelectionChanged。 每当一个集合更改时,我都会清除另一个集合并复制内容。 作为另一个好处,我不再需要将我的源集合公开为 IList 以允许 DependencyProperty 工作,因为在缓存源之后我没有使用它。
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();
}
}
我敢肯定有一种更优雅的方法可以解决这个问题,但这是我现在最好的方法。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.