Весь код написан на 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 для обхода потерянного выбора. Однако мне все еще интересно, почему существует такое поведение.