0

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.

Image showing MainWindow view at run-time

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:

  1. Added the NullSubstituteConverter from above as a static resource.
  2. Added Person object as a static resources to represent the 'No Person' item and gave it the key 'NullPerson'.
  3. Set the NullSubstituteConverter resource as the Converter for the binding for the SelectedItem property of the ComboBox.
  4. Set the NullSubstituteConverter resource as the Converter for items in the data template for the ComboBox, so that the null item in the items source is converted to an the NullPerson 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:

  1. When the view is first loaded, the 'No Person' item isn't automatically selected by default.
  2. It's not possible to select the 'No Person' item.

Image showing MainWindow view at run-time after add data converter

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!

Dan Stevens
  • 6,392
  • 10
  • 49
  • 68
  • maybe, add `NoPerson` item (with id = 0) into `ListOfPersons` instead of `null`? – ASh Sep 06 '16 at 13:59
  • 2
    Possible duplicate of [WPF Combobox DefaultValue (Please Select)](http://stackoverflow.com/questions/23130763/wpf-combobox-defaultvalue-please-select) – decPL Sep 06 '16 at 14:05

0 Answers0