Почему SelectedItem очищается при изменении источника элементов с помощью DataTrigger, когда элемент существует в обоих источниках? - PullRequest
0 голосов
/ 25 октября 2019

Весь код написан на C # и XAML с использованием .NET Framework 4.6.1.

Минимальный воспроизводимый пример

Приложение. xaml

<Application x:Class="ComboBoxSwapTest.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Application.Resources />
</Application>

App.xaml.cs

using System.Windows;

namespace ComboBoxSwapTest
{
    public partial class App : Application
    {
        protected override void OnStartup(StartupEventArgs e)
        {
            var vm = new ViewModel();
            var window = new MainWindow() { DataContext = vm };
            window.Show();

            base.OnStartup(e);
        }
    }
}

Option.cs

namespace ComboBoxSwapTest
{
    public class Option
    {
        public int Id { get; set; }

        public string Number { get; set; }

        public string Name { get; set; }

        public string NumberName { get { return string.Format("{0} - {1}", Number, Name); } }

        public string NameNumber { get { return string.Format("{1} - {0}", Number, Name); } }
    }
}

ViewModel.cs

using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;

namespace ComboBoxSwapTest
{
    public class ViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        private void RaisePropertyChanged(string propertyName)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }

        private bool _nameFirst = true;
        public bool NameFirst
        {
            get { return _nameFirst; }
            set
            {
                if (_nameFirst != value)
                {
                    _nameFirst = value;
                    RaisePropertyChanged(nameof(NameFirst));
                }
            }
        }

        public List<Option> Options { get; }
        public List<Option> NameNumberOptions
        {
            get { return Options.OrderBy(s => s.Name).ThenBy(s => s.Number).ToList(); }
        }
        public List<Option> NumberNameOptions
        {
            get { return Options.OrderBy(s => s.Number).ThenBy(s => s.Name).ToList(); }
        }

        private Option _selectedOption;
        public Option SelectedOption
        {
            get { return _selectedOption; }
            set
            {
                if (_selectedOption != value)
                {
                    _selectedOption = value;
                    RaisePropertyChanged(nameof(SelectedOption));
                }
            }
        }

        public ViewModel()
        {
            Options = new List<Option>();
            Options.Add(new Option() { Id = 1, Name = "Foo", Number = "111" });
            Options.Add(new Option() { Id = 2, Name = "Bar", Number = "222" });
            Options.Add(new Option() { Id = 3, Name = "Baz", Number = "333" });
        }
    }
}

InvertBoolConverter.cs

using System;
using System.Globalization;
using System.Windows.Data;

namespace ComboBoxSwapTest
{
    public class InvertBoolConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            return !(bool)value;
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            return !(bool)value;
        }
    }
}

MainWindow.xaml

<Window x:Class="ComboBoxSwapTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:ComboBoxSwapTest"
        Title="Main">
    <Window.Resources>
        <local:InvertBoolConverter x:Key="InvertBoolConverter" />
    </Window.Resources>
    <StackPanel Orientation="Vertical">
        <StackPanel Orientation="Horizontal">
            <RadioButton GroupName="Toggle"
                         Content="Name First"
                         IsChecked="{Binding Path=NameFirst}" />
            <RadioButton GroupName="Toggle"
                         Content="Number First"
                         IsChecked="{Binding Path=NameFirst, Converter={StaticResource InvertBoolConverter}}" />
        </StackPanel>
        <ComboBox IsEditable="False" IsReadOnly="False"
                  SelectedItem="{Binding Path=SelectedOption}">
            <ComboBox.Style>
                <Style TargetType="{x:Type ComboBox}" BasedOn="{StaticResource {x:Type ComboBox}}">
                    <Style.Triggers>
                        <DataTrigger Binding="{Binding Path=NameFirst}" Value="True">
                            <Setter Property="DisplayMemberPath" Value="NameNumber" />
                            <Setter Property="ItemsSource" Value="{Binding Path=NameNumberOptions}" />
                        </DataTrigger>
                        <DataTrigger Binding="{Binding Path=NameFirst}" Value="False">
                            <Setter Property="DisplayMemberPath" Value="NumberName" />
                            <Setter Property="ItemsSource" Value="{Binding Path=NumberNameOptions}" />
                        </DataTrigger>
                    </Style.Triggers>
                </Style>
            </ComboBox.Style>
        </ComboBox>
    </StackPanel>
</Window>

MainWindow.xaml.cs

using System.Windows;

namespace ComboBoxSwapTest
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }
    }
}

Шаги для воспроизведения:
1) Запустите приложение.
2) Выберите элемент из выпадающего списка.
3) Измените переключатель на Number First.

На этом этапе элемент все еще выбран, но он находится в другом формате и использует экземпляр списка с другим видом.

4) Измените переключатель обратно на Name First.

Теперь элемент больше не выбран, пока мы возвращаемся к экземпляру списка с исходной сортировкой.


Это подводит нас к вопросу о названии: почему SelectedItem очищается при измененииItemsSource, используя DataTrigger, когда элемент существует в обоих источниках?

Обратите внимание, что я не ищу философскую причину, а именно почему среда разработана таким образом. Я ищу место в структуре фреймворка, которое вызывает такое поведение.

Я пытался отлаживать, используя декомпилированные источники, но многие значения переменных оптимизированы, поэтому за ними довольно трудно следить.

Вот несколько интересных вещей, которые можно попробовать начать с базового примера в каждом сценарии:


Изменить порядок триггеров данных. Теперь SelectedItem очищается при переключении на Number First, но он сохраняется при переключении на Name First.


Добавление этой строки в стиль ComboBox вместе с DataTriggers приводит к сохранению SelectedItem в обоих направлениях. :

<Setter Property="ItemsSource" Value="{Binding Path=Options}" />

Предоставить список, подобный следующему:

public List<Option> SortedOptions
{
    get { return NameFirst ? NameNumberOptions : NumberNameOptions; }
}

Сделайте так, чтобы NameFirst также указывал, что это свойство изменилось, а затем привязывайте его вместо использования DataTriggers. Опять же, SelectedItem сохраняется в обоих направлениях.


Перейдите к использованию CollectionViewSources, которые связаны с базовым списком опций, установите для IsSynchronizedWithCurrentItem в ComboBox значение False, а затем поменяйте местами эти источники с помощью DataTriggers. Вы получаете то же поведение, когда очищает SelectedItem.


Обратите внимание, что мое реальное решение использует CollectionViewSources для сортировки списков и включает в себя «базовый» ItemsSource для обхода потерянного выбора. Однако мне все еще интересно, почему существует такое поведение.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...