簡體   English   中英

WPF 雙向綁定在修改控件之前不起作用

[英]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更改選擇時, SelectedWidgetsDataGrid的內部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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM