views:

695

answers:

4

G'day!

I want my WPF ComboBox to display some alternative text when its data-bound selection is null.

The view model has the expected properties:

public ThingoSelectionViewModel : INotifyPropertyChanged {
    public ThingoSelectionViewModel(IProvideThingos) {
        this.Thingos = IProvideThingos.GetThingos();
    }

    public ObservableCollection<Thingo> Thingos { get; set; }

    public Thingo SelectedThingo { 
        get { return this.selectedThingo; }
        set { // set this.selectedThingo and raise the property change notification
    }

    // ...

}

The view has XAML binding to the view model in the expected way:

<ComboBox x:Name="ComboboxDrive" SelectedItem="{Binding Path=SelectedThingo}"
          IsEditable="false" HorizontalAlignment="Left" MinWidth="100" 
          IsReadOnly="false" Style="{StaticResource ComboboxStyle}"
          Grid.Column="1" Grid.Row="1" Margin="5" SelectedIndex="0">
    <ComboBox.ItemsSource>
        <CompositeCollection>
        <ComboBoxItem IsEnabled="False">Select a thingo</ComboBoxItem>
        <CollectionContainer 
            Collection="{Binding Source={StaticResource Thingos}}" />
        </CompositeCollection>
    </ComboBox.ItemsSource>
</ComboBox>

The ComboBoxItem wedged into the top is a way to get an extra item at the top. It's pure chrome: the view model stays pure and simple. There's just one problem: the users want "Select a thingo" displayed whenever the ComboBox' selection is null.

The users do not want a thingo selected by default. They want to see a message telling them to select a thingo.

I'd like to avoid having to pollute the viewmodel with a ThingoWrapper class with a ToString method returning "Select a thingo" if its .ActualThingo property is null, wrapping each Thingo as I populate Thingos, and figuring out some way to prevent the user from selecting the nulled Thingo.

Is there a way to display "Select a thingo" within the ComboBox' boundaries using pure XAML, or pure XAML and a few lines of code in the view's code-behind class?

+2  A: 

Edit: Looks like the trigger idea is a no go. I added the following to the control template of a test combo box to no avail:

    <Trigger Property="SelectedItem" Value="{x:Null}">
        <Setter Property="Text" Value="No Item Selected"/>
    </Trigger>

Additionally, when trying to edit the control template in Blend (Edit Current) I am left with a featureless combobox, no colors, just an ugly button (but there is a borderless dropdown). Try someone elses suggestion (Mike Brown perhaps).

Original:

You can use a Trigger in the Control template. Here is an example using a ListBox from an app I am working on.

<ControlTemplate x:Key="SnazzyFormListBoxTemplate" TargetType="{x:Type ListBox}">
    <Microsoft_Windows_Themes:ClassicBorderDecorator x:Name="Bd" SnapsToDevicePixels="True" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderStyle="Sunken" BorderThickness="{TemplateBinding BorderThickness}">
        <ScrollViewer Padding="{TemplateBinding Padding}" Focusable="False" Template="{DynamicResource SnazzyScrollViewerControlTemplate}">
            <Grid>
            <TextBlock x:Name="textBlock" Text="No Items" FontFamily="Arial" FontWeight="Bold" FontSize="13.333" Foreground="#4D000000" RenderTransformOrigin="0.5,0.5" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="0,10"/>
            <ItemsPresenter x:Name="itemsPresenter" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
            </Grid>
        </ScrollViewer>
    </Microsoft_Windows_Themes:ClassicBorderDecorator>
    <ControlTemplate.Triggers>
        <Trigger Property="Selector.IsSelected" Value="True"/>
        <Trigger Property="HasItems" Value="False">
            <Setter Property="Visibility" TargetName="textBlock" Value="Visible"/>
            <Setter Property="Visibility" TargetName="itemsPresenter" Value="Collapsed"/>
        </Trigger>
        <Trigger Property="HasItems" Value="True">
            <Setter Property="Visibility" TargetName="textBlock" Value="Collapsed"/>
            <Setter Property="Visibility" TargetName="itemsPresenter" Value="Visible"/>
        </Trigger>
    </ControlTemplate.Triggers>
</ControlTemplate>

The above ControlTemplate has a Trigger which checks the Property HasItems. If False, a textblock saying "No Items" is displayed in the middle of the ListBox. If there are Items, they are displayed.

In your case change the trigger to check to see if ItemSelected is x:Null and set the Text property to "Nothing Selected".

OffApps Cory
User is asking how to have a "nothing selected" value in the combobox, not to have a different look for an empty combobox.
Mike Brown
I know. They can set a trigger in the control template that checks the state of Items selected and if it is null then set the value of the text of the box to "Nothing Selected". I was hoping the user could infer that from the example given. Updated my answer to better fit.
OffApps Cory
@Mike Brown: I have edited my answer, so I would appreciate it if you could remote the downvote.
OffApps Cory
+1  A: 

The path of least resistance here that I've found is to use the Null Object Pattern For an example of using this pattern in the .NET Framework, consider the static value Double.NaN if you create a Null Object for your Thingo, in your view model you can append it to the front of your list to signify "nothing is selected". Create a DataTemplate for the Thingo class that has a DataTrigger for the Null Object instance that shows "Select a Value".

I could give a code sample but it's past my bed time.

Mike Brown
A: 

You can't use a control template trigger, but you could set up a simple item template for the combobox:

<ComboBox ItemsSource="{Binding}" >
        <ComboBox.ItemTemplate>
            <DataTemplate>
                <TextBlock x:Name="displayText" Text="{Binding}" />
                <DataTemplate.Triggers>
                    <DataTrigger Binding="{Binding}" Value="{x:Null}">
                        <Setter TargetName="displayText" Property="Text" Value="Default Value" />
                    </DataTrigger>
                </DataTemplate.Triggers>
            </DataTemplate>
        </ComboBox.ItemTemplate>
    </ComboBox>
Steve Greatrex
A: 

How strict is your MVVM requirement? Can you have a little code-behind in the view?

Perhaps you could contain the ComboBox in a grid, something like this:

<Grid>
    <ComboBox x:Name="ComboBoxControl"
              SelectionChanged="ComboBoxControl_SelectionChanged"
              HorizontalAlignment="Left" VerticalAlignment="Top" 
              MinWidth="{Binding ElementName=UnselectedText, Path=ActualWidth}">
        <ComboBoxItem>One</ComboBoxItem>
        <ComboBoxItem>Two</ComboBoxItem>
        <ComboBoxItem>Three</ComboBoxItem>
    </ComboBox>
    <TextBlock IsHitTestVisible="False" 
               x:Name="UnselectedText" 
               HorizontalAlignment="Left" 
               Text="Select an option..." 
               VerticalAlignment="Top" Margin="4" 
               Padding="0,0,30,0" />
</Grid>

Then, in the code-behind, insert some logic in an event handler:

Private Sub ComboBoxControl_SelectionChanged(ByVal sender As System.Object, ByVal e As System.Windows.Controls.SelectionChangedEventArgs)
    If ComboBoxControl.SelectedIndex = -1 Then
        UnselectedText.Visibility = Windows.Visibility.Visible
    Else
        UnselectedText.Visibility = Windows.Visibility.Hidden
    End If
End Sub

Setting the IsHitTestVisible="False" DependencyProperty on the TextBlock lets mouse events through so that you can click on the ComboBox, and setting the visibility to Hidden in the code-behind keeps the layout of a default ComboBox's appearance from jumping around when the prompt text is hidden.

Rob Perkins