0

I have a ComboBox which should have a text when nothing is selected. This seems like a straight forward problem, and has many answers on the net, but unfortunately, it is not working for me. I think, the reason is, that I don't want to show a static text, but rather a bound text.

My minimal not working example looks like this:

public class Model
{
    public string Name { get; set; }

    public SubModel SelectedItem { get; set; }

    public List<SubModel> Items { get; set; }
}

public class SubModel
{
    public string Description { get; set; }
}

and the MainWindow:

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        var selectedSubModel = new SubModel { Description = "SubModel5" };
        var model1 = new Model
        {
            Name = "Model1",
            Items = new List<SubModel>
                {
                    new SubModel { Description = "SubModel1" },
                    new SubModel { Description = "SubModel2" },
                    new SubModel { Description = "SubModel3" }
                }
        };
        var model2 = new Model
        {
            Name = "Model2",
            SelectedItem = selectedSubModel,
            Items = new List<SubModel>
                {
                    new SubModel { Description = "SubModel4" },
                    selectedSubModel,
                    new SubModel { Description = "SubModel6" }
                }
        };
        var model3 = new Model
        {
            Name = "Model3",
            Items = new List<SubModel>
                {
                    new SubModel { Description = "SubModel7" },
                    new SubModel { Description = "SubModel8" },
                    new SubModel { Description = "SubModel9" }
                }
        };

        _itemsControl.Items.Add(model1);
        _itemsControl.Items.Add(model2);
        _itemsControl.Items.Add(model3);
    }
}

with xaml:

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:WpfApplication1="clr-namespace:WpfApplication1"
        Title="MainWindow" Height="350" Width="525">
    <ItemsControl x:Name="_itemsControl">
        <ItemsControl.ItemTemplate>
            <DataTemplate DataType="WpfApplication1:Model">
                <ComboBox ItemsSource="{Binding Items}"
                        SelectedItem="{Binding SelectedItem}">
                    <ComboBox.ItemTemplate>
                        <DataTemplate>
                            <TextBlock Text="{Binding Description}"></TextBlock>
                        </DataTemplate>
                    </ComboBox.ItemTemplate>
                    <ComboBox.Style>
                        <Style TargetType="ComboBox">
                            <Style.Triggers>
                                <DataTrigger Binding="{Binding SelectedItem}" Value="{x:Null}">
                                    <Setter Property="Background">
                                        <Setter.Value>
                                            <VisualBrush>
                                                <VisualBrush.Visual>
                                                    <TextBlock Text="{Binding Name}"/>
                                                </VisualBrush.Visual>
                                            </VisualBrush>
                                        </Setter.Value>
                                    </Setter>
                                </DataTrigger>
                            </Style.Triggers>
                        </Style>
                    </ComboBox.Style>
                </ComboBox>
            </DataTemplate>
        </ItemsControl.ItemTemplate>
    </ItemsControl>
</Window>

This gives the follwing: Not expected result

But it should look similar to this: enter image description here

1
  • As far as I can tell the default ControlTemplate for ComboBox doesn't use the Background property which would explain why changing it has no visual effect. Commented Feb 3, 2016 at 15:11

3 Answers 3

1

please first of all take in your mind facts provided in the next sentence - you can only select items provided by ComboBox ItemsSource. Thus since the Name property values (Model1, Model2, Model3 etc.) are not in your collection they can't be selected you will see the empty selection Instead. I can suggest you the next solution the combination of data context proxy and wpf behavior.

Xaml code

<Window x:Class="ComboBoxWhenNoAnySelectedHelpAttempt.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:comboBoxWhenNoAnySelectedHelpAttempt="clr-namespace:ComboBoxWhenNoAnySelectedHelpAttempt"
    xmlns:system="clr-namespace:System;assembly=mscorlib"
    xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
    Title="MainWindow" Height="350" Width="525">
<Grid>
    <ItemsControl x:Name="_itemsControl">
        <ItemsControl.ItemTemplate>
            <DataTemplate DataType="comboBoxWhenNoAnySelectedHelpAttempt:Model">
                <ComboBox x:Name="ComboBox"
                    SelectedItem="{Binding SelectedItem, UpdateSourceTrigger=PropertyChanged}">
                    <ComboBox.Resources>
                        <!--the next object is a proxy that able to provide combo data context each time it requested-->
                        <comboBoxWhenNoAnySelectedHelpAttempt:FreezableProxyClass x:Key="FreezableProxyClass" ProxiedDataContext="{Binding ElementName=ComboBox, Path=DataContext }"></comboBoxWhenNoAnySelectedHelpAttempt:FreezableProxyClass>
                    </ComboBox.Resources>
                    <ComboBox.ItemsSource>
                        <CompositeCollection>
                            <!--the next object is a collapsed combo box that can be selected in code-->
                            <!--keep im mind, since this object is not a SubModel we get the binding expression in output window-->
                            <ComboBoxItem IsEnabled="False" Visibility="Collapsed" Foreground="Black" Content="{Binding Source={StaticResource FreezableProxyClass}, 
                Path=ProxiedDataContext.Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"></ComboBoxItem>
                            <CollectionContainer Collection="{Binding Source={StaticResource FreezableProxyClass}, 
                Path=ProxiedDataContext.Items, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
                        </CompositeCollection>
                    </ComboBox.ItemsSource>
                    <ComboBox.ItemTemplate>
                        <DataTemplate>
                            <TextBlock Text="{Binding Description}"></TextBlock>
                        </DataTemplate>
                    </ComboBox.ItemTemplate>
                    <i:Interaction.Behaviors>
                        <!--next behavior helps to select a zero index (Model.Name collapsed) item from source when selected item is not SubModel-->
                        <comboBoxWhenNoAnySelectedHelpAttempt:ComboBoxLoadingBehavior/>
                    </i:Interaction.Behaviors>
                </ComboBox>
            </DataTemplate>
        </ItemsControl.ItemTemplate>
    </ItemsControl>
</Grid>

Code Behind with Proxy Code

    public class FreezableProxyClass : Freezable
{
    protected override Freezable CreateInstanceCore()
    {
        return new FreezableProxyClass();
    }


    public static readonly DependencyProperty ProxiedDataContextProperty = DependencyProperty.Register(
        "ProxiedDataContext", typeof(object), typeof(FreezableProxyClass), new PropertyMetadata(default(object)));

    public object ProxiedDataContext
    {
        get { return (object)GetValue(ProxiedDataContextProperty); }
        set { SetValue(ProxiedDataContextProperty, value); }
    }
}

/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{

    public MainWindow()
    {
        InitializeComponent();
        var selectedSubModel = new SubModel { Description = "SubModel5" };
        var model1 = new Model
        {
            Name = "Model1",
            Items = new ObservableCollection<SubModel>
            {
                new SubModel { Description = "SubModel1" },
                new SubModel { Description = "SubModel2" },
                new SubModel { Description = "SubModel3" }
            }
        };
        var model2 = new Model
        {
            Name = "Model2",
            SelectedItem = selectedSubModel,
            Items = new ObservableCollection<SubModel>
            {
                new SubModel { Description = "SubModel4" },
                selectedSubModel,
                new SubModel { Description = "SubModel6" }
            }
        };
        var model3 = new Model
        {
            Name = "Model3",
            Items = new ObservableCollection<SubModel>
            {
                new SubModel { Description = "SubModel7" },
                new SubModel { Description = "SubModel8" },
                new SubModel { Description = "SubModel9" }
            }
        };

        _itemsControl.Items.Add(model1);
        _itemsControl.Items.Add(model2);
        _itemsControl.Items.Add(model3);
    }
}

public class Model:BaseObservableObject
{
    private string _name;
    private SubModel _selectedItem;
    private ObservableCollection<SubModel> _items;

    public string Name
    {
        get { return _name; }
        set
        {
            _name = value;
            OnPropertyChanged();
        }
    }

    public SubModel SelectedItem
    {
        get { return _selectedItem; }
        set
        {
            _selectedItem = value;
            OnPropertyChanged();
        }
    }

    public ObservableCollection<SubModel> Items
    {
        get { return _items; }
        set
        {
            _items = value;
            OnPropertyChanged();
        }
    }
}

public class SubModel:BaseObservableObject
{
    private string _description;

    public string Description
    {
        get { return _description; }
        set
        {
            _description = value;
            OnPropertyChanged();
        }
    }
}

BaseObservableObject code (simple implementation for INotifyPropertyChanged )

    /// <summary>
/// implements the INotifyPropertyChanged (.net 4.5)
/// </summary>
public class BaseObservableObject : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        var handler = PropertyChanged;
        if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
    }

    protected virtual void OnPropertyChanged<T>(Expression<Func<T>> raiser)
    {
        var propName = ((MemberExpression)raiser.Body).Member.Name;
        OnPropertyChanged(propName);
    }

    protected bool Set<T>(ref T field, T value, [CallerMemberName] string name = null)
    {
        if (!EqualityComparer<T>.Default.Equals(field, value))
        {
            field = value;
            OnPropertyChanged(name);
            return true;
        }
        return false;
    }
}

WPF Behavior Code

public class ComboBoxLoadingBehavior:Behavior<ComboBox>
{
    private bool _unLoaded;

    protected override void OnAttached()
    {
        base.OnAttached();
        AssociatedObject.Loaded += AssociatedObjectOnLoaded;
        AssociatedObject.LayoutUpdated += AssociatedObjectOnLayoutUpdated;
        AssociatedObject.Unloaded += AssociatedObjectOnUnloaded;
    }

    private void AssociatedObjectOnUnloaded(object sender, RoutedEventArgs routedEventArgs)
    {
        _unLoaded = true;
        UnsubscribeAll();
    }

    private void UnsubscribeAll()
    {
        AssociatedObject.Loaded -= AssociatedObjectOnLoaded;
        AssociatedObject.LayoutUpdated -= AssociatedObjectOnLayoutUpdated;
        AssociatedObject.Unloaded -= AssociatedObjectOnUnloaded;
    }

    private void AssociatedObjectOnLayoutUpdated(object sender, EventArgs eventArgs)
    {
        UpdateSelectionState(sender);
    }

    private static void UpdateSelectionState(object sender)
    {
        var combo = sender as ComboBox;
        if (combo == null) return;
        var selectedItem = combo.SelectedItem as SubModel;
        if (selectedItem == null)
        {
            combo.SelectedIndex = 0;
        }
    }

    private void AssociatedObjectOnLoaded(object sender, RoutedEventArgs routedEventArgs)
    {
        _unLoaded = false;
        UpdateSelectionState(sender);

    }

    protected override void OnDetaching()
    {
        base.OnDetaching();
        if(_unLoaded) return;
        UnsubscribeAll();
    }
}

This is a working complete solution for you problem, just copy/past and use it as a starting point for your farther research. I'll glad to help if you will have any problems with the code.

Regards.

Sign up to request clarification or add additional context in comments.

Comments

1

I've found 2 possible solutions:

Change ComboBox template

enter image description here

Edit standard combobox template by right button click on combobox in designer and select Edit Template -> Edit a Copy... After that change ContentPresenter with a custom converter:

XAML

<ContentPresenter ContentTemplate="{TemplateBinding SelectionBoxItemTemplate}" ContentTemplateSelector="{TemplateBinding ItemTemplateSelector}" ContentStringFormat="{TemplateBinding SelectionBoxItemStringFormat}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" IsHitTestVisible="false" Margin="{TemplateBinding Padding}" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}">
    <ContentPresenter.Content>
        <MultiBinding Converter="{local:ComboboxEmptyValueConverter}">
            <Binding Path="SelectionBoxItem" RelativeSource="{RelativeSource Mode=TemplatedParent}" />
            <Binding Mode="OneWay" Path="DataContext" RelativeSource="{RelativeSource Mode=TemplatedParent}" />
        </MultiBinding>
    </ContentPresenter.Content>
</ContentPresenter>

C#

class ComboboxEmptyValueConverterExtension : MarkupExtension, IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        string stringValue = values[0] as string;
        var dataContext = values[1] as Model;

        return (stringValue != null && String.IsNullOrEmpty(stringValue)) ? dataContext?.Name : values[0];
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        return new object[] { value, null };
    }


    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        return this;
    }
}

Set ComboBox to IsEditable & IsReadOnly and change Text enter image description here

<ComboBox ItemsSource="{Binding Items}" SelectedItem="{Binding SelectedItem}">
    <ComboBox.ItemTemplate>
        <DataTemplate>
            <TextBlock x:Name="textBlock" Text="{Binding Description}"/>
        </DataTemplate>
    </ComboBox.ItemTemplate>
    <ComboBox.Style>
        <Style TargetType="ComboBox">
            <Style.Triggers>
                <DataTrigger Binding="{Binding SelectedItem}" Value="{x:Null}">
                    <Setter Property="IsEditable" Value="True" />
                    <Setter Property="IsReadOnly" Value="True" />
                    <Setter Property="Text" Value="{Binding Name}" />
                </DataTrigger>
            </Style.Triggers>
        </Style>
    </ComboBox.Style>
</ComboBox>

Comments

0

The answer is, to put the visual brush in the resources of the combobox:

<DataTemplate DataType="WpfApplication1:Model">
    <ComboBox ItemsSource="{Binding Items}"
            SelectedItem="{Binding SelectedItem}">
        <ComboBox.ItemTemplate>
            <DataTemplate>
                <TextBlock Text="{Binding Description}"></TextBlock>
            </DataTemplate>
        </ComboBox.ItemTemplate>
        <ComboBox.Resources>
            <VisualBrush x:Key="_myBrush">
                <VisualBrush.Visual>
                    <TextBlock Text="{Binding Name}"/>
                </VisualBrush.Visual>
            </VisualBrush>
        </ComboBox.Resources>
        <ComboBox.Style>
            <Style TargetType="ComboBox">
                <Style.Triggers>
                    <DataTrigger Binding="{Binding SelectedItem}" Value="{x:Null}">
                        <Setter Property="Background" Value="{StaticResource _myBrush}"/>
                    </DataTrigger>
                </Style.Triggers>
            </Style>
        </ComboBox.Style>
    </ComboBox>
</DataTemplate>

Then, together with the rest of the code, it works like expected.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.