I'm implementing a user control in WPF using MVVM pattern. I want the control to contain an ItemsControl
, specially a ComboBox
, that contains a list of People. I want the first menu item to be labelled 'No Person' and bind to null
on the data source, while the remaining items are names of people that bind to Person
objects.
The code for Person
and the view model is as follows:
namespace NoValueItem
{
public class Person : IEquatable<Person>
{
public int Id { get; set; }
public string Name { get; set; }
public static bool Equals(Person a, Person b)
{
if (a == null)
return b == null;
return a.Equals(b);
}
public bool Equals(Person other)
{
if (ReferenceEquals(this, other))
return true;
if (ReferenceEquals(null, other))
return false;
return this.Name.Equals(other.Name);
}
public override bool Equals(object obj)
{
return Equals(obj as Person);
}
public override int GetHashCode()
{
return Name.GetHashCode();
}
}
public class ViewModel : INotifyPropertyChanged
{
public ObservableCollection<Person> ListOfPersons { get; } =
new ObservableCollection<Person> {
null,
new Person() { Id = 1, Name = "Alice" },
new Person() { Id = 2, Name = "Bob" },
new Person() { Id = 3, Name = "Charlie" }
};
private Person _SelectedPerson;
public Person SelectedPerson
{
get
{
return _SelectedPerson;
}
set
{
if (!Person.Equals(value, _SelectedPerson)) {
_SelectedPerson = value;
OnPropertyChanged();
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public class NullSubstituteConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return value ?? parameter;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if (parameter == null)
return value;
return parameter.Equals(value) ? null : value;
}
}
}
And the view:
<Window x:Class="NoValueItem.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:NoValueItem"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Grid d:DataContext="{d:DesignInstance local:ViewModel}" VerticalAlignment="Center">
<Grid.DataContext>
<local:ViewModel/>
</Grid.DataContext>
<ComboBox ItemsSource="{Binding ListOfPersons}"
SelectedItem="{Binding SelectedPerson}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</Grid>
</Window>
At run-time, I get 4 items as I would expect: 1 blank menu item followed by 3 non-blank menu items, one for each Person
in the ViewModel.ListOfPersons
collection, with the item text bound to the Name
property of the Person
.
I would like the first blank item to instead show the text 'No Person'. How can I do this?
One thing I've tried is using the following data converter, that converts a null
reference to the object specified in the converter parameter:
namespace NoValueItem
{
public class NullSubstituteConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return value ?? parameter;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if (parameter == null)
return value;
return parameter.Equals(value) ? null : value;
}
}
}
I then made the following changes to the view:
- Added the
NullSubstituteConverter
from above as a static resource. - Added
Person
object as a static resources to represent the 'No Person' item and gave it the key 'NullPerson'. - Set the
NullSubstituteConverter
resource as the Converter for the binding for theSelectedItem
property of the ComboBox. - Set the
NullSubstituteConverter
resource as the Converter for items in the data template for the ComboBox, so that thenull
item in the items source is converted to an theNullPerson
object.
Here's the updated view:
<Window x:Class="NoValueItem.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:NoValueItem"
mc:Ignorable="d"
Title="MainWindow" Height="300" Width="300">
<Grid d:DataContext="{d:DesignInstance local:ViewModel}" VerticalAlignment="Center">
<Grid.Resources>
<local:Person x:Key="NullPerson">
<local:Person.Id>0</local:Person.Id>
<local:Person.Name>No Person</local:Person.Name>
</local:Person>
<local:NullSubstituteConverter x:Key="NullSubstituteConverter"/>
</Grid.Resources>
<Grid.DataContext>
<local:ViewModel/>
</Grid.DataContext>
<ComboBox ItemsSource="{Binding ListOfPersons}"
SelectedItem="{Binding SelectedPerson,
Converter={StaticResource NullSubstituteConverter},
ConverterParameter={StaticResource NullPerson}}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock DataContext="{Binding Converter={StaticResource NullSubstituteConverter},
ConverterParameter={StaticResource NullPerson}}"
Text="{Binding Name}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</Grid>
</Window>
This is closer to what I want. The blank menu item is now showing 'No Person', but there are still 2 problems:
- When the view is first loaded, the 'No Person' item isn't automatically selected by default.
- It's not possible to select the 'No Person' item.
I welcome any suggestions on how I can get the 'No Person' menu item working. It can be based on my approach above, or completely different approach as long as it works!