Mark Johnson Mark Johnson - 2 months ago 62
C# Question

Combobox SelectedItem Becomes Null

I'm attempting to make a ComboBox that switches between objects. The general gist is that an object has a Key that appears in the ComboBox and a Data component that can theoretically be anything. The Data component is complex while the Key is just a string. For the example below, Data is just a Uri, it actually turns out the type of the Data doesn't matter.

The basic intent is to bind the SelectedItem of the ComboBox to the Model so that the Data of the SelectedItem can be modified through other interactions.

The code is setup such that a couple items are added to the ComboBox, and then the SelectedItem is choose as the first element. That works just fine. When I then click the button, the SelectedItem is assigned null where I throw the exception.

Why does the SelectedItem get assigned null?

Here is complete working code; my target is .NET 4.0, but I'm guessing it doesn't matter too much. Xaml follows:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.ComponentModel;
using System.Collections.ObjectModel;
using System.Collections.Specialized;

namespace Sandbox
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public Model Model { get; set; }

public MainWindow()
{
InitializeComponent();
this.DataContext = this;

Model = new Model();

this.Model.Items.Add(
new ObservableKeyValuePair<string, Uri>()
{
Key = "Apple",
Value = new Uri("http://apple.com")
});

this.Model.Items.Add(
new ObservableKeyValuePair<string, Uri>()
{
Key = "Banana",
Value = new Uri("http://Banana.net")
});

this.Model.SelectedItem = this.Model.Items.First();
}

private void Button_Click(object sender, RoutedEventArgs e)
{
this.Model.SelectedItem.Value = new Uri("http://cranberry.com");
}
}

public class TrulyObservableCollection<T> : ObservableCollection<T> where T : INotifyPropertyChanged
{
public TrulyObservableCollection()
{
CollectionChanged += FullObservableCollectionCollectionChanged;
}

public TrulyObservableCollection(IEnumerable<T> pItems)
: this()
{
foreach (var item in pItems)
{
this.Add(item);
}
}

private void FullObservableCollectionCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.NewItems != null)
{
foreach (Object item in e.NewItems)
{
((INotifyPropertyChanged)item).PropertyChanged += ItemPropertyChanged;
}
}
if (e.OldItems != null)
{
foreach (Object item in e.OldItems)
{
((INotifyPropertyChanged)item).PropertyChanged -= ItemPropertyChanged;
}
}
}

private void ItemPropertyChanged(object sender, PropertyChangedEventArgs e)
{
NotifyCollectionChangedEventArgs args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, sender, sender, IndexOf((T)sender));
OnCollectionChanged(args);
}
}

public class ObservableKeyValuePair<TKey, TValue> :
INotifyPropertyChanged,
IEquatable<ObservableKeyValuePair<TKey, TValue>>
{
public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;

protected virtual void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName));
}
}

public override bool Equals(object rhs)
{
var obj = rhs as ObservableKeyValuePair<TKey, TValue>;

if (obj != null)
{
return this.Key.Equals(obj.Key);
}

return false;
}

public bool Equals(ObservableKeyValuePair<TKey, TValue> other)
{
return this.Key.Equals(other.Key);
}

public override int GetHashCode()
{
return this.Key.GetHashCode();
}

protected TKey _Key;
public TKey Key
{
get
{
return _Key;
}
set
{
if (value is INotifyPropertyChanged)
{
(value as INotifyPropertyChanged).PropertyChanged += new PropertyChangedEventHandler(KeyChanged);
}

_Key = value;

OnPropertyChanged("Key");
}
}
void KeyChanged(object sender, PropertyChangedEventArgs e)
{
OnPropertyChanged("Key");
}

protected TValue _Value;
public TValue Value
{
get
{
return _Value;
}
set
{
if (value is INotifyPropertyChanged)
{
(value as INotifyPropertyChanged).PropertyChanged += new PropertyChangedEventHandler(ValueChanged);
}

_Value = value;

OnPropertyChanged("Value");
}
}
void ValueChanged(object sender, PropertyChangedEventArgs e)
{
OnPropertyChanged("Value");
}
}

public class Model : INotifyPropertyChanged
{
public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;

public Model()
{
Items = new TrulyObservableCollection<ObservableKeyValuePair<string, Uri>>();
}

protected virtual void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName));
}
}

public TrulyObservableCollection<ObservableKeyValuePair<string, Uri>> Items { get; set; }
public ObservableKeyValuePair<string, Uri> _SelectedItem = null;
public ObservableKeyValuePair<string, Uri> SelectedItem
{
get
{
return Items.FirstOrDefault(x => _SelectedItem != null && x.Key == _SelectedItem.Key);
}
set
{
if (value == null)
{
throw new Exception("This is the problem");
}

if (_SelectedItem != value)
{
_SelectedItem = value;
OnPropertyChanged("SelectedItem");
}
}
}
}
}


Xaml:

<Window x:Class="Sandbox.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<StackPanel>
<Button Click="Button_Click" Content="Clsick Me"/>
<ComboBox IsEditable="False" ItemsSource="{Binding Model.Items}" SelectedItem="{Binding Model.SelectedItem, Mode=TwoWay}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Key}">
</TextBlock>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</StackPanel>
</Window>


I'm at a total lose trying to explain why "value" is null.

Answer

The underlying problem is in the implementation of Selector.OnItemsChanged. Towards the end of the method, the currently SelectedItem is nulled.

I worked around this by deriving a new ComboBox class that overrides OnItemsChanged, saves the current SelectedItem, calls base.OnItemsChanged and then resets the SelectedItem. This may require the propagation of an "InhibitEvents" flag into the model if the SelectedItem transition from valid=>null=>valid is not desired.