Способ поместить null-элемент в ComboBox

В каких случаях может понадобиться включить в коллекцию null? Ну, например, пользователь должен выбрать в ComboBox какой-то из пунктов и один из них должен означать, что выбор сделан не был. В code behind в таком случае мы ждём возврата значения null, а со стороны WPF пускай выглядит как некий пункт “Пусто” или “Нет значения”.

К слову, встречал разные мнения по правомерности такого подхода. Некоторые считают, что null – это отсутствие значения и не должно использоваться. По первому пункту они правы – да это отсутствие значения, по второму – не согласен, null несёт смысловую информацию как раз этого отсутствия значения. Это видится естественным и повсеместно используется в .NET.

С моральной стороной разобрались, перейдём к практической. В чём же сложность такого подхода?

Для начала, встаёт вопрос как отобразить null в интерфейсе? Давайте состряпаем простенькую ViewModel с коллекцией и свойством, куда будем привязывать выбранный пользователем элемент.

public string SelectedMyValue { get; set; }

public string[] MyValues
{
    get
    {
        return new[]
        {
            null,
            "Пункт 1",
            "Пункт 2",
            "Пункт 3",
            "Пункт 4"
        };
    }
}

Как видно, первым идёт наш null. Что же мы увидим просто привязавшись к такой коллекции?

<ComboBox
	ItemsSource="{Binding MyValues}"
	SelectedValue="{Binding SelectedMyValue}"
	/>

Как и ожидалось, null никак не отображается. Самым простым способ отображения в WPF нулевых значений является добавление в привязку параметра TargetNullValue. В нём можно указать, что отображать в интерфейсе вместо null. Так как применять TargetNullValue нужно для каждого пункта ComboBox‘а, то придётся немного усложнить наш пример и сделать шаблон для каждого элемента.

<ComboBox
	ItemsSource="{Binding MyValues}"
	SelectedValue="{Binding SelectedMyValue}"
	>
	<ComboBox.ItemTemplate>
		<DataTemplate>
			<TextBlock Text="{Binding ., TargetNullValue='(Пусто)'}" />
		</DataTemplate>
	</ComboBox.ItemTemplate>
</ComboBox>

Заменили null на строковой литерал “(Пусто)”. На первый взгляд всё удалось.

Что же не так? Узнать это можно только попытавшись выбрать пункт “(Пусто)”. Он просто не может быть выбран. Никак. Совсем. 🙁

Причиной этому служит то, что для ComboBoxListBox) null является священным особым значением, означающим, что ни один из пунктов списка не был выбран. Такая уж беда, простым способом обойти не удастся, но пожалуй компромисс найти можно.

Почему бы не воспользоваться конвертором? Будем фильтровать коллекцию и заменять null на любое значение переданное в качестве параметра. Таким образом, наш список будет выглядеть как надо и мы даже сможем выбрать пункт “(Пусто)”.

class EnumerableNullReplaceConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        var collection = (IEnumerable)value;

        return
            collection
            .Cast<object>()
            .Select(x => x ?? parameter)
            .ToArray();
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotSupportedException();
    }
}

Но стоит не забывать про вторую привязку к SelectedValue, она обязана возвращать при выборе “(Пусто)” null. В текущем варианте она будет возвращать строковой литерал, что явно неправильно. Что ж, значит без второго конвертора не обойтись, хоть и более простого. Будем просто заменять null на указанное значение в одну сторону и заменять значение на null в обратную.

class NullReplaceConverter : 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)
    {
        return value.Equals(parameter) ? null : value;
    }
}

Теперь при выборе пункта “(Пусто)” в свойство SelectedMyValue нашей ViewModel’и гарантированно попадёт null.
Использование в коде получается довольно лаконичное. Просто передаём в качестве параметра конвертору литерал, которым хотим подменить null.

<ComboBox
	ItemsSource="{Binding MyValues, Converter={StaticResource EnumerableNullReplaceConverter}, ConverterParameter='(Пусто)'}"
	SelectedValue="{Binding SelectedMyValue, Converter={StaticResource NullReplaceConverter}, ConverterParameter='(Пусто)'}"
	/>

Результат устраивает! Можно выбирать все пункты выпадающего списка и все привязки работают правильно с null-значением.

Примечание: С чем же могут возникнуть проблемы? Есть несколько особых случаев.
При использовании ObservableCollection о уведомлениях об изменениях можно забыть, так как она будет конвертироваться в новую коллекцию. Также, не стоит добавлять в коллекцию несколько null‘ов – в этом, впрочем, смыла и так нет. И на последок, если коллекция формируется динамически из неизвестных данных, то надо учесть, что некоторые пункты меню могут совпасть с литералом, которым мы заменяем null, что может привести к неправильному значению в свойстве SelectedMyValue.

Пример на GitHub

Способ поместить null-элемент в ComboBox: 2 комментария

  1. womanblog

    Нашел более правильное, на мой взгляд, решение. Причина – в коллекции не должно быть элемента null, иначе форма начинает работать неадекватно.

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *