[英]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.