Двухсторонняя привязка WPF не работает до тех пор, пока элемент управления не будет изменен - PullRequest
1 голос
/ 01 мая 2020

Я пытаюсь синхронизировать выбор в DataGrid, используя коллекцию в моих данных. У меня это в основном работает, с одной маленькой причудой.

Когда я меняю выбор в DataGrid, изменения записываются в мою коллекцию данных, пока все хорошо. Затем, если сбор данных изменится, выбор в моей DataGrid будет обновлен, как и ожидалось. Однако, если я изменяю свои данные перед изменением DataGrid, то выбор DataGrid не обновляет.

Пример первого рабочего случая Working

Пример второго, нерабочего дела Not Working

Код

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>

Ответы [ 3 ]

1 голос
/ 01 мая 2020

Это происходит потому, что ваше новое свойство 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, чтобы убедиться, что new 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;
}
1 голос
/ 01 мая 2020

Проблема возникает из-за того, что вы не обрабатываете уведомления коллекции BindableDataGrid.SelectedItems. В первом случае вам не нужно обрабатывать их вручную, поскольку вы на самом деле получаете коллекцию SelectedItems из базового класса DataGrid и передаете ее в модель представления из вызова метода 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);         
  }
0 голосов
/ 02 мая 2020

@ Ответ Drreamer указал мне правильное направление. Однако все сводилось к тому, что исходный набор данных заменялся на коллекцию DataGrid.SelectedItems. В итоге он обходит OnPropertyChanged после первой модификации, поскольку оба конца привязки на самом деле являются одним и тем же объектом.

Я не хотел заменять исходную коллекцию, поэтому нашел другое решение, синхронизирующее содержание коллекций. Преимущество этого также в том, что он более прямой.

Когда SelectedItems инициализируется DependencyProperty, я оставляю sh ссылку на исходную и целевую коллекции. Я также регистрируюсь для 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();
        }
    }

Я уверен, что есть много более элегантный способ решить эту проблему, но это лучшее, что у меня есть сейчас.

...