susieloo_
susieloo_

Reputation: 1519

xaml - how to show display value that is different from its property value without using KeyValuePair

I have a ComboBox that grabs its data from a List of strings.

Styles.Add("bold"); 
Styles.Add("italic");
Styles.Add("underline");
Styles.Add("");

When shown in the ComboBox, I want it to actually display: { "bold", "italic", "underline", "[Discard style]" }, respectively. It's important that I'm able to change that empty string value to display "[Discard style]" but also retain the empty string value when the SelectedChanged event fires.

Is there some way to do this (without changing the underlying data structure to support a KeyValuePair)?

This is what I originally started with:

<ComboBox ItemsSource="{Binding Styles, Mode=OneWay}" SelectedItem="{Binding SelectedStyle}" SelectionChanged="StyleComboBox_SelectionChanged" />

Since then, I've tried adding and doing many variations/combinations of

<ComboBox ItemsSource="{Binding Style, Mode=OneWay}" SelectedItem="{Binding SelectedStyle}" DisplayMemberPath="{Binding SelectedStyle, Converter={StaticResource StyleConverter}}" SelectionChanged="StyleComboBox_SelectionChanged" Height="22" Width="175" />

with SelectedValuePath={Binding SelectedStyle}, where StyleConverter is as follows:

public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
    string selectedStyle = value as string;

    if (string.IsNullOrEmpty(selectedStyle))
        return "[Discard style]";

    return value;
}

I've mostly encountered the scenario where the display value is correct, but the underlying value is not - it's somehow always changed to "[Discard style]" as well. Do I maybe need two converters somehow? One just for displaying in the ComboBox and another just for selecting the item?

Another important note is that "[Discard style]" will be translated in the UI based on the operating system. So I can't simply just compare the string to see if it matches and turn it back into an empty string.

Upvotes: 1

Views: 1432

Answers (1)

Mark Feldman
Mark Feldman

Reputation: 16119

There are actually quite a few different ways of achieving this, but for something this simple I'd probably just use a DataTemplate with a DataTrigger to change the text when it's an empty string:

xmlns:sys="clr-namespace:System;assembly=mscorlib"

<ComboBox ItemsSource="{Binding Styles, Mode=OneWay}" SelectedItem="{Binding SelectedStyle}">
    <ComboBox.Resources>
        <DataTemplate DataType="{x:Type sys:String}">
            <TextBlock>
                <TextBlock.Style>
                    <Style TargetType="{x:Type TextBlock}">
                        <Setter Property="Text" Value="{Binding}" />
                        <Style.Triggers>
                            <DataTrigger Binding="{Binding}" Value="">
                                <Setter Property="Text" Value="[Discard style]" />
                            </DataTrigger>
                        </Style.Triggers>
                    </Style>
                </TextBlock.Style>
            </TextBlock>
        </DataTemplate>
    </ComboBox.Resources>
</ComboBox>

You will, however, need to use a converter if you want to handle null in your string list. In that case ignore the code above and apply the converter to the ItemsSource binding:

<Window.Resources>
    <conv:StringConverter x:Key="StringConverter" />
</Window.Resources>

<StackPanel Orientation="Vertical">
    <ComboBox ItemsSource="{Binding Styles, Mode=OneWay, Converter={StaticResource StringConverter}}" SelectedItem="{Binding SelectedStyle}" />
</StackPanel>

And then your converter would just look something like this:

public class StringConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return (value as IEnumerable<string>).Select(s => String.IsNullOrEmpty(s) ? "[Discard style]" : s);
    }

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

UPDATE: Oh ok, I see what you're trying to do. In that case yes, you need to use two converters....one to filter the list of strings that the view model is passing to the view, and then a second to change the value being propagated back again.

Before I go any further I should point out that what you're trying to do is a really, really bad idea. The whole point of MVVM is that logic is done in the view model. What you're effectively trying to do is implement logic in the view layer, and that's completely against the whole MVVM/data-binding paradigm. The whole purpose of the view model is to prepare data in a format that the view can readily consume with as few changes as possible; the minute you find yourself putting logic in the view (including converters, which are also part of that layer) it's a very strong sign that your view model is not doing its job properly.

To answer your question though, you would need a converter in your SelectedStyleBinding in addition to the one I posted above. You would now also be forced to make that binding one-way-to-source, which means you lose the ability to programatically control the currently selected item:

<ComboBox ItemsSource="{Binding Styles, Mode=OneWay, Converter={StaticResource StringConverter}}" SelectedItem="{Binding SelectedStyle, Converter={StaticResource SingleStringConverter}, Mode=OneWayToSource}" />

And the second converter:

public class SingleStringConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return Binding.DoNothing;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return (value?.ToString() == "[Discard style]") ? "" : value?.ToString();

    }
}

If you need to retain programatic item selection then your only other option is to bind to SelectedIndex instead of SelectedItem, your view model would then have to be responsible for looking the string up in the source list.

But again, all of this is very bad code. The correct thing to do here is to modify your source list to be a custom view model class or something else your view can more readily work with.

Upvotes: 1

Related Questions