I have a ComboBox, whose ItemsSource is bound to a new (not default) ListCollectionView, which is linked to an ObservableCollection. The ComboBox SelectedItem property is bound to a public SelectedHat property.
Step 1: Select the 2nd item in the ComboBox. SelectedHat is now the 2nd Hat in the list, as expected. Step 2: (Click the button to) Set the 2nd spot in the list to a new Hat. SelectedHat is first set to null, then set to the new Hat.
Why is SelectedHat set to null before the new Hat?
I want to be able to vm.Collection[index] = new Hat() and
(1) if the ComboBox has that index selected, keep it selected instead of going blank
(2) only set SelectedHat once to the new Hat, instead of null and THEN the new Hat
C#:
public partial class MainWindow : Window
{
private readonly ViewModel vm;
public MainWindow()
{
InitializeComponent();
vm = new ViewModel();
DataContext = vm;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
Hat item = new Hat { Name = "hat 2", Color = "Red"};
vm.Collection[1] = item;
}
}
public class ViewModel : BaseNotifyPropertyChanged
{
public ObservableCollection<Hat> Collection { get; set; }
public ListCollectionView View { get; set; }
private Hat selectedHat;
public Hat SelectedHat
{
get { return selectedHat; }
set
{
selectedHat = value;
Console.WriteLine(string.Format("SelectedHat set to [{0}]", value));
NotifyPropertyChanged("SelectedHat");
}
}
public ViewModel()
{
Collection = new ObservableCollection<Hat>()
{
new Hat { Name = "hat 1", Color = "Black" },
new Hat { Name = "hat 2", Color = "Black" },
new Hat { Name = "hat 3", Color = "Black" },
};
View = new ListCollectionView(Collection);
View.SortDescriptions.Add(new SortDescription("Name", ListSortDirection.Ascending));
}
}
public class Hat
{
public string Name { get; set; }
public string Color { get; set; }
public override string ToString()
{
return string.Format("{0} ({1})", Name, Color);
}
}
public abstract class BaseNotifyPropertyChanged : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void NotifyPropertyChanged(String propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
XAML:
<StackPanel>
<TextBlock Text="{Binding Path=SelectedHat, Mode=OneWay}" />
<ComboBox ItemsSource="{Binding Path=View}" SelectedItem="{Binding Path=SelectedHat, UpdateSourceTrigger=PropertyChanged}" />
<Button Content="click me" Click="Button_Click" />
</StackPanel>
This is the implementation of ObservableCollection.SetItem
protected override void SetItem(int index, T item)
{
this.CheckReentrancy();
T obj = this[index];
base.SetItem(index, item);
this.OnPropertyChanged("Item[]");
this.OnCollectionChanged(NotifyCollectionChangedAction.Replace, (object) obj, (object) item, index);
}
as you can see it raises OnPropertyChanged("Item[]")
and then OnCollectionChanged(NotifyCollectionChangedAction.Replace, (object) obj, (object) item, index)
.
OnCollectionChanged has parameters 'oldItem' and 'newItem'. I expect if we traced the code through to the combo box implementation we would see that the old item was removed and replaced with null, and then the new item was inserted, which is why you get the behaviour your experiencing (I see it as well).
My work around is instead of replacing the item, add the new item, change the currently selected item and then remove the old item.
private void ButtonClick(object sender, System.Windows.RoutedEventArgs e)
{
Hat newHat = new Hat { Name = "hat 2", Color = "Red" };
var viewModel = (ViewModel)DataContext;
var oldHat = viewModel.Collection[1];
if (viewModel.SelectedHat == oldHat)
{
viewModel.Collection.Add(newHat);
viewModel.SelectedHat = newHat;
viewModel.Collection.Remove(oldHat);
}
else
{
viewModel.Collection[1] = newHat;
}
}