简体   繁体   English

WPF 双向绑定在修改控件之前不起作用

[英]WPF two way binding doesn't work until control is modified

I'm trying to synchronize selection in a DataGrid using a collection in my data.我正在尝试使用我的数据中的集合来同步 DataGrid 中的选择。 I have this mostly working, with one little quirk.我有这个主要工作,有一个小怪癖。

When I change selection in the DataGrid changes are written to my data collection, so far so good.当我在 DataGrid 中更改选择时,更改将写入我的数据集合,到目前为止一切顺利。 Then, if the data collection changes selection in my DataGrid is updated, as expected.然后,如果数据集合更改了我的 DataGrid 中的选择,则按预期更新。 However, if I modify my data before modifying the DataGrid then the DataGrid selection does not update.但是,如果我在修改 DataGrid 之前修改我的数据,则 DataGrid 选择不会更新。

An example of the first, working case第一个工作案例的示例在职的

An example of the second, non-working case第二个非工作案例的示例不工作

Code代码

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);
            }
        }
    }
}

And markup和标记

<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>

The problem occurs because you do not handle notifications of the BindableDataGrid.SelectedItems collection.出现此问题的原因是您不处理 BindableDataGrid.SelectedItems 集合的通知。 In the first case you do not need to handle them manually because you actually get the SelectedItems collection from the base DataGrid class and pass it to the view model from the OnSelectionChanged method call.在第一种情况下,您不需要手动处理它们,因为您实际上是从基础 DataGrid class 获取 SelectedItems 集合,并从 OnSelectionChanged 方法调用将其传递给视图 model。 The base DataGrid handle notifications of this collection itself.基本 DataGrid 处理此集合本身的通知。

However, if you click the button first, the SelectedItems property get a new collection and the base DataGrid knows nothing about it.但是,如果您首先单击该按钮,SelectedItems 属性将获得一个新集合,而基础 DataGrid 对此一无所知。 I think that you need to handle the propertyChangedCallback, and handle notifications of provided collections to update selection in the grid manually.我认为您需要处理 propertyChangedCallback,并处理提供的 collections 的通知以手动更新网格中的选择。 Refer to the following code demonstrating the concept.请参阅以下演示该概念的代码。 Note that I have renamed the property for simplicity but still have not debugged it.请注意,为简单起见,我已重命名该属性,但仍未对其进行调试。

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);         
  }

This happens because your new SelectedItems property never updates the base SelectedItems when it is set.发生这种情况是因为您的新SelectedItems属性在设置时永远不会更新基本SelectedItems The problem with that is, of course, that MultiSelector.SelectedItems is readonly.当然,问题在于MultiSelector.SelectedItems是只读的。 It was designed specifically not to be set-able - but it was also designed to be updatable .它被专门设计为不可设置 - 但它也被设计为可更新的。

The reason your code works at all is because when you change the selection via the BindableDataGrid , SelectedWidgets gets replaced with the DataGrid 's internal SelectedItemsCollection .您的代码完全起作用的原因是,当您通过BindableDataGrid更改选择时, SelectedWidgetsDataGrid的内部SelectedItemsCollection替换。 After that point, you are adding and removing from that collection, so it updates the DataGrid .在那之后,您将在该集合中添加和删除,因此它会更新DataGrid

Of course, this doesn't work if you haven't changed the selection yet, because OnSelectionChanged doesn't run until then, so SetCurrentValue is never called, so the binding never updated SelectedWidgets .当然,如果您还没有更改选择,这将不起作用,因为OnSelectionChanged直到那时才运行,因此永远不会调用SetCurrentValue ,因此绑定永远不会更新SelectedWidgets But that's fine, all you have to do is called SetCurrentValue as part of BindableDataGrid 's initialization.但这很好,您所要做的就是调用SetCurrentValue作为BindableDataGrid初始化的一部分。

Add this to BindableDataGrid :将此添加到BindableDataGrid

protected override void OnInitialized(EventArgs e)
{
    base.OnInitialized(e);
    SetCurrentValue(SelectedItemsProperty, base.SelectedItems);
}

Be careful, though, because this will still break if you try to set SelectedItems sometime after initialization.但是要小心,因为如果您在初始化后的某个时间尝试设置SelectedItems ,这仍然会中断。 It would be nice if you could make it readonly, but that prevents it from being used in data binding.如果您可以将其设为只读,那就太好了,但这会阻止它在数据绑定中使用。 So make sure that your binding uses OneWayToSource not TwoWay :因此,请确保您的绑定使用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>

If you want to insure this never breaks, you can add a CoerceValueCallback to make sure the new SelectedItems is never set to something other than base.SelectedItems :如果您想确保这永远不会中断,您可以添加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's answer pointed me in the right direction. @Drreamer 的回答为我指明了正确的方向。 However, it boiled down to embracing the fact that the source data collection was being replaced by the DataGrid.SelectedItems collection.但是,它归结为接受源数据集合被 DataGrid.SelectedItems 集合替换的事实。 It ends up bypassing OnPropertyChanged after the first modification because both ends of the binding are actually the same object.在第一次修改后它最终绕过了OnPropertyChanged ,因为绑定的两端实际上是相同的 object。

I didn't want the source collection to be replaced so I found another solution that synchronizes the contents of the collections.我不想替换源集合,所以我找到了另一个同步 collections内容的解决方案。 It has the benefit of being more direct as well.它还具有更直接的好处。

When SelectedItems is initialized by the DependencyProperty I stash a reference to the source and target collections.当 DependencyProperty 初始化 SelectedItems 时,我存储对源和目标 collections 的引用。 I also register for CollectionChanged on the source and override OnSelectionChanged on the target.我还在源上注册 CollectionChanged 并在目标上覆盖 OnSelectionChanged。 Whenever one collection changes I clear the other collection and copy the contents over.每当一个集合更改时,我都会清除另一个集合并复制内容。 As another bonus I no longer have to expose my source collection as IList to allow the DependencyProperty to work since I'm not using it after caching off the source.作为另一个好处,我不再需要将我的源集合公开为 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();
        }
    }

I'm sure there's is a much more elegant way to solve this, but this is the best I've got right now.我敢肯定有一种更优雅的方法可以解决这个问题,但这是我现在最好的方法。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM