views:

157

answers:

3

I've been working with two different combo boxes within my solution ( Department and Name) these combo boxes are currently bound to a list collection that is being returned from an external service reference. Where I have a question is in that I need to insert other elements into the combo box other than what is being provided by the API.

For example in addition to showing all department names I need the first element visible within the combo box to be "Select department... " In doing some research (both here and online) it looks as if I may be able to do this using a composite collection.

http://zamjad.wordpress.com/2010/05/18/using-composite-collection-in-c/

My question is for instances where I just have one item that needs to be presented as the first item within a combo box is this the best approach? Or is there some other approach I need to look at?

Thanks in advance,

A: 

I use a composite class to include the embedded assistance ("Select department... ") in a ComboBox and I think it works well.

Once the user chooses a department, I recommend removing the embedded assistance.

Zamboni
Using the composite class is the approach I took. Worked great however the one thing I did notice was when using a Combined collection you need to ensure that the type names within each collection are similar in the event you need to render them out in a Listbox control or combobox or where you need to call DisplayMemeberPath.
randyc
+1  A: 

It sounds as though what you want is some element to be visible when nothing is selected, and appear to be part of the ComboBox, but be invisible when a selection is made.

Most simply, you could just design a UserControl which had your ComboBox (if you're adding your items in code rather than some static XAML markup) and also a TextBlock containing your prompt. 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.

Naturally, all of this is also possible to do by creating a MyComboBox Custom Control, inherited from ComboBox, with an added "UnselectedPromptProperty" as a Dependency Property. Then the logic to show or hide the "UnselectedPromptProperty" would come from the validation callback on the DP. That's more advanced but it would allow you to propagate non-default style templates down into your control, permitting others to reskin it.

Rob Perkins
+2  A: 

Rather than making a mess of your data just to show a temporary value in the UI, you can do this directly in XAML by changing the ComboBox ControlTemplate. You can set up a trigger in the template to swap in a value when nothing is selected.

<MultiTrigger>
    <MultiTrigger.Conditions>
        <Condition Property="SelectedIndex" Value="-1"/>
        <Condition Property="IsDropDownOpen" Value="false"/>
        <Condition Property="HasItems" Value="True"/>
    </MultiTrigger.Conditions>
    <Setter Property="Content" TargetName="Presenter" Value="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Tag}"/>
</MultiTrigger>

This version gives you a further advantage in that it doesn't allow selecting the empty value after a selection is made. It can be used by setting the Tag to your default message.

<ComboBox Tag="Select department..." Template="{StaticResource ComboBoxSelectTemplate}" ItemsSource="{Binding Departments}"/>

Here's a complete version based on the default Aero template, which requires adding a dll reference and xmlns:Microsoft_Windows_Themes="clr-namespace:Microsoft.Windows.Themes;assembly=PresentationFramework.Aero" (the ButtonChrome and SystemDropShadowChrome can be replaced with Border and DropShadow effect to avoid the added references):

<Geometry x:Key="DownArrowGeometry">M 0 0 L 3.5 4 L 7 0 Z</Geometry>
<Style x:Key="ComboBoxReadonlyToggleButton" TargetType="{x:Type ToggleButton}">
    <Setter Property="OverridesDefaultStyle" Value="true"/>
    <Setter Property="IsTabStop" Value="false"/>
    <Setter Property="Focusable" Value="false"/>
    <Setter Property="ClickMode" Value="Press"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type ToggleButton}">
                <Microsoft_Windows_Themes:ButtonChrome x:Name="Chrome" SnapsToDevicePixels="true" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" RenderMouseOver="{TemplateBinding IsMouseOver}" RenderPressed="{TemplateBinding IsPressed}">
                    <Grid HorizontalAlignment="Right" Width="{DynamicResource {x:Static SystemParameters.VerticalScrollBarWidthKey}}">
                        <Path x:Name="Arrow" Fill="Black" HorizontalAlignment="Center" Margin="3,1,0,0" VerticalAlignment="Center" Data="{StaticResource DownArrowGeometry}"/>
                    </Grid>
                </Microsoft_Windows_Themes:ButtonChrome>
                <ControlTemplate.Triggers>
                    <Trigger Property="IsChecked" Value="true">
                        <Setter Property="RenderPressed" TargetName="Chrome" Value="true"/>
                    </Trigger>
                    <Trigger Property="IsEnabled" Value="false">
                        <Setter Property="Fill" TargetName="Arrow" Value="#AFAFAF"/>
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

<ControlTemplate x:Key="ComboBoxSelectTemplate" TargetType="{x:Type ComboBox}">
    <Grid x:Name="MainGrid" SnapsToDevicePixels="true">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition MinWidth="{DynamicResource {x:Static SystemParameters.VerticalScrollBarWidthKey}}" Width="0"/>
        </Grid.ColumnDefinitions>
        <Popup x:Name="PART_Popup" Margin="1" AllowsTransparency="true" IsOpen="{Binding IsDropDownOpen, RelativeSource={RelativeSource TemplatedParent}}" Placement="Bottom" PopupAnimation="{DynamicResource {x:Static SystemParameters.ComboBoxPopupAnimationKey}}" Grid.ColumnSpan="2">
            <Microsoft_Windows_Themes:SystemDropShadowChrome x:Name="Shdw" MaxHeight="{TemplateBinding MaxDropDownHeight}" MinWidth="{Binding ActualWidth, ElementName=MainGrid}" Color="Transparent">
                <Border x:Name="DropDownBorder" Background="{DynamicResource {x:Static SystemColors.WindowBrushKey}}" BorderBrush="{DynamicResource {x:Static SystemColors.WindowFrameBrushKey}}" BorderThickness="1">
                    <ScrollViewer CanContentScroll="true">
                        <ItemsPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" KeyboardNavigation.DirectionalNavigation="Contained"/>
                    </ScrollViewer>
                </Border>
            </Microsoft_Windows_Themes:SystemDropShadowChrome>
        </Popup>
        <ToggleButton Style="{StaticResource ComboBoxReadonlyToggleButton}" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" Grid.ColumnSpan="2" IsChecked="{Binding IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}"/>
        <ContentPresenter x:Name="Presenter" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" Margin="{TemplateBinding Padding}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}" IsHitTestVisible="false" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" Content="{TemplateBinding SelectionBoxItem}" ContentStringFormat="{TemplateBinding SelectionBoxItemStringFormat}" ContentTemplate="{TemplateBinding SelectionBoxItemTemplate}" ContentTemplateSelector="{TemplateBinding ItemTemplateSelector}"/>
    </Grid>
    <ControlTemplate.Triggers>
        <MultiTrigger>
            <MultiTrigger.Conditions>
                <Condition Property="SelectedIndex" Value="-1"/>
                <Condition Property="IsDropDownOpen" Value="false"/>
                <Condition Property="HasItems" Value="True"/>
            </MultiTrigger.Conditions>
            <Setter Property="Content" TargetName="Presenter" Value="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Tag}"/>
        </MultiTrigger>
        <Trigger Property="HasDropShadow" SourceName="PART_Popup" Value="true">
            <Setter Property="Margin" TargetName="Shdw" Value="0,0,5,5"/>
            <Setter Property="Color" TargetName="Shdw" Value="#71000000"/>
        </Trigger>
        <Trigger Property="HasItems" Value="false">
            <Setter Property="Height" TargetName="DropDownBorder" Value="95"/>
        </Trigger>
        <Trigger Property="IsEnabled" Value="false">
            <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
            <Setter Property="Background" Value="#FFF4F4F4"/>
        </Trigger>
        <Trigger Property="IsGrouping" Value="true">
            <Setter Property="ScrollViewer.CanContentScroll" Value="false"/>
        </Trigger>
    </ControlTemplate.Triggers>
</ControlTemplate>
John Bowen
I spent about an hour trying to do that for this question, before falling back on writing the UserControl. @randyc, I would call this the "ultimate" answer to your question, which is why I've added an up-vote. It most closely follows the patterns Microsoft recommends for adapting a control, and permits Blend users to modify it further. But, man, what a lot of extra XAML that is.
Rob Perkins
Yeah, it's so annoying that it takes this much XAML to just make a small change in one section for these complex templates (Slider is even worse). This is even stripped down to remove all of the ComboBox editable stuff. Fortunately it's all generated for you from Blend and you can hide it off in a ResourceDictionary somewhere once you create it.
John Bowen
@Rob Great reply and thanks so much for taking the time to show a full sample in xaml. Your're spot on in that it does muck up the data using a combined collectnion. What I found in workign with this further is in my situaion I am required to use the combined collection. The reason behind this is the application requires events to be keyed off of non databound elements (i.e. the second colleciton) So I have written logic to evaluate these selected items in addtion to those databound. Thanks again for the insight into modifying the control this will come in handy in future projects!
randyc