3

So I'm trying to create a user control for an application I'm working on. It's basically a ToggleButton next to a ComboBox. I was able to pretty much mock the ComboBox portion of the user control up in VS2015 the way the designers want it, but I feel like the way I'm going about it is not exactly the best way.

First, here is a link to a screenshot of what it looks like: https://www.dropbox.com/s/019f4xqgu8r4i0e/DropDown.png

To do this, I ended up creating 3 different ComboBoxItem styles. The first puts together a CheckBox, a TextBlock with the ContentPresenter, and a Rectangle. The second just has a Separator, and the last just has the TextBlock with the ContentPresenter. Here is my XAML, which is declared in the UserControl.Resources section:

<Style x:Key="cbTestStyle" TargetType="{x:Type ComboBoxItem}">
    <Setter Property="SnapsToDevicePixels" Value="True"/>
    <Setter Property="HorizontalAlignment" Value="Stretch"/>
    <Setter Property="VerticalAlignment" Value="Stretch"/>
    <Setter Property="OverridesDefaultStyle" Value="True"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="ComboBoxItem">
                <Border Name="Border"
                        Padding="5"
                        Margin="2"
                        BorderThickness="2"
                        CornerRadius="0"
                        BorderBrush="Transparent">
                    <Grid>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="20"/>
                            <ColumnDefinition Width="75"/>
                            <ColumnDefinition Width="15"/>
                        </Grid.ColumnDefinitions>
                        <CheckBox Grid.Column="0"
                                  IsChecked="{Binding Path=IsSelected, RelativeSource={RelativeSource TemplatedParent}}"/>
                        <TextBlock Grid.Column="1"
                                   TextAlignment="Left"
                                   Foreground="Black">
                            <ContentPresenter/>
                        </TextBlock>
                        <Rectangle Grid.Column="2"
                                   Stroke="Black"
                                   Width="15"
                                   Height="15"
                                   Fill="{TemplateBinding Foreground}"/>
                    </Grid>
                </Border>
                <ControlTemplate.Triggers>
                    <Trigger Property="IsHighlighted" Value="True">
                        <Setter TargetName="Border" Property="BorderBrush" Value="Gray"/>
                        <Setter TargetName="Border" Property="Background" Value="LightGray"/>
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

<Style x:Key="cbSeparatorStyle" TargetType="ComboBoxItem">
    <Setter Property="SnapsToDevicePixels" Value="True"/>
    <Setter Property="HorizontalAlignment" Value="Stretch"/>
    <Setter Property="VerticalAlignment" Value="Stretch"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate>
                <Separator/>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

<Style x:Key="cbResetStyle" TargetType="{x:Type ComboBoxItem}">
    <Setter Property="SnapsToDevicePixels" Value="True"/>
    <Setter Property="HorizontalAlignment" Value="Stretch"/>
    <Setter Property="VerticalAlignment" Value="Stretch"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="ComboBoxItem">
                <Border x:Name="Border"
                        Padding="5"
                        Margin="2"
                        BorderThickness="2"
                        CornerRadius="0"
                        BorderBrush="Transparent">
                    <Grid>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="20"/>
                            <ColumnDefinition Width="*"/>
                        </Grid.ColumnDefinitions>
                        <TextBlock Grid.Column="1">
                            <ContentPresenter/>
                        </TextBlock>
                    </Grid>
                </Border>
                <ControlTemplate.Triggers>
                    <Trigger Property="IsHighlighted" Value="True">
                        <Setter TargetName="Border" Property="BorderBrush" Value="Gray"/>
                        <Setter TargetName="Border" Property="Background" Value="LightGray"/>
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

I guess my first question would be, is this the best way to make my ComboBox look like the screenshot I have presented?

Of course, there are deeper issues that I have yet to address. Firstly, the cbTestStyle of ComboBoxItem I want to be able to populate dynamically. Databinding would be my obvious go-to, but with the separator and "Reset" styles at the end, I'm not sure how to do this. I currently have the ComboBoxItems "hard-coded" in XAML:

    <ComboBox x:Name="cbTestSelect"
              Height="34"
              Width="18" 
              IsEnabled="False">
        <ComboBoxItem Style="{StaticResource cbTestStyle}" Content="Test 1" Foreground="#7FFF0000" Selected="ComboBoxItem_Selected"/>
        <ComboBoxItem Style="{StaticResource cbTestStyle}" Content="Test 2" Foreground="#7F00FF00" Selected="ComboBoxItem_Selected"/>
        <ComboBoxItem Style="{StaticResource cbTestStyle}" Content="Test 3" Foreground="#7F0000FF" Selected="ComboBoxItem_Selected"/>
        <ComboBoxItem Style="{StaticResource cbSeparatorStyle}"/>
        <ComboBoxItem Style="{StaticResource cbResetStyle}" Content="Reset all"/>
    </ComboBox>

In this example, I would ideally like to dynamically create the first three items and have the separator and "reset" items remain static. I'm still relatively new to WPF. I felt like trying to create this control in WinForms (which the application this user control would be used in is) would be a lot more complicated. Plus I'm trying to steer us towards using WPF more anyway.

Any help or links to other questions or tutorials online would be greatly appreciated.

WERUreo
  • 194
  • 2
  • 12
  • Thanks to all of you so far who have provided me with some options. I'm going to try these, see which works best for me, and report back. – WERUreo Aug 17 '15 at 20:32

4 Answers4

5

Solution 1:

Use a CompositeCollection so that you can bring up your data items with DataBinding, and use regular XAML to define the hard-coded items:

<Window x:Class="WpfApplication31.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:WpfApplication31"
        Title="MainWindow" Height="350" Width="525"
        x:Name="view">
    <Window.Resources>
        <DataTemplate DataType="{x:Type local:DataItem}">
            <StackPanel Orientation="Horizontal">
                <CheckBox IsChecked="{Binding IsChecked}"/>
                <TextBlock Text="{Binding Text}"/>
                <Rectangle Stroke="Black" StrokeThickness="1"
                           Fill="{Binding Color}" Width="20"/>
            </StackPanel>
        </DataTemplate>
    </Window.Resources>

    <Grid>
        <ComboBox VerticalAlignment="Center"
                  HorizontalAlignment="Center"
                  Width="100" x:Name="Combo">
            <ComboBox.Resources>
                <CompositeCollection x:Key="ItemsSource">
                    <CollectionContainer Collection="{Binding DataContext,Source={x:Reference view}}"/>
                    <Separator Height="10"/>
                    <Button Content="Clear All"/>
                </CompositeCollection>
            </ComboBox.Resources>

            <ComboBox.ItemsSource>
                <StaticResource ResourceKey="ItemsSource"/>
            </ComboBox.ItemsSource>
        </ComboBox>
    </Grid>
</Window>

Code Behind:

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

        var colors = new[] {"Red", "Green", "Blue", "Brown", "Cyan", "Magenta"};

        this.DataContext =
            Enumerable.Range(0, 5)
                .Select(x => new DataItem
                {
                    Text = "Test" + x.ToString(),
                    Color = colors[x],
                    IsChecked = x%2 == 0
                });

    }
}

Data Item:

public class DataItem
{
    public bool IsChecked { get; set; }

    public string Text { get; set; }

    public string Color { get; set; }
}

Result:

enter image description here

Federico Berasategui
  • 43,562
  • 11
  • 100
  • 154
  • I went with this solution. I tried both, and honestly I think I was getting myself turned in circles jumping from one to the other, so I basically stripped everything out and started out with this solution. Finally got the thing looking like my original screenshot. Still need to work out a few kinks, but as for this question, this solution worked best for me. Thanks again! – WERUreo Aug 20 '15 at 20:42
3

Solution 2:

Using Expression Blend, you can get the XAML for the default Template for the ComboBox control, and modify this XAML to accomodate your extra visuals.

The XAML you get is rather long, and I'm not going to post it here. You will have to put that in a ResourceDictionary and reference that in the XAML where you define this ComboBox.

The relevant part you need to edit is the Popup:

<Popup x:Name="PART_Popup" AllowsTransparency="true" Grid.ColumnSpan="2" IsOpen="{Binding IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" Margin="1" PopupAnimation="{DynamicResource {x:Static SystemParameters.ComboBoxPopupAnimationKey}}" Placement="Bottom">
    <Themes:SystemDropShadowChrome x:Name="shadow" Color="Transparent" MaxHeight="{TemplateBinding MaxDropDownHeight}" MinWidth="{Binding ActualWidth, ElementName=templateRoot}">
        <Border x:Name="dropDownBorder" BorderBrush="{DynamicResource {x:Static SystemColors.WindowFrameBrushKey}}" BorderThickness="1" Background="{DynamicResource {x:Static SystemColors.WindowBrushKey}}">
            <DockPanel>
                <Button Content="Clear All" DockPanel.Dock="Bottom"/>
                <Separator Height="2" DockPanel.Dock="Bottom"/>
                <ScrollViewer x:Name="DropDownScrollViewer">
                    <Grid x:Name="grid" RenderOptions.ClearTypeHint="Enabled">
                        <Canvas x:Name="canvas" HorizontalAlignment="Left" Height="0" VerticalAlignment="Top" Width="0">
                            <Rectangle x:Name="opaqueRect" Fill="{Binding Background, ElementName=dropDownBorder}" Height="{Binding ActualHeight, ElementName=dropDownBorder}" Width="{Binding ActualWidth, ElementName=dropDownBorder}"/>
                        </Canvas>
                        <ItemsPresenter x:Name="ItemsPresenter" KeyboardNavigation.DirectionalNavigation="Contained" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
                    </Grid>
                </ScrollViewer>
            </DockPanel>
        </Border>
    </Themes:SystemDropShadowChrome>
</Popup>

Notice that I added a DockPanel, the Button and a Separator.

Then you can bind your ItemsSource to the DataItem collection normally:

<ComboBox ItemsSource="{Binding}"
          VerticalAlignment="Center"
          HorizontalAlignment="Center"
          Width="100"/>

Result:

enter image description here

Notice that this approach is a lot better than my previous solution, and other answers posted here, because it does not wrap the extra visuals in ComboBoxItems, and therefore you don't get the selection highlight for them, which is rather weird.

Federico Berasategui
  • 43,562
  • 11
  • 100
  • 154
  • I decided I'm going to give this approach a try, but I had a question. I am assuming that some of what you suggested in your first solution carries over to this solution. Obviously the DataItem class and the DataTemplate are used here as well. Is there anything else in the XAML that needs to be carried over as well? And a second question I have is, do I need to keep the entire `ComboBox` template just to modify that one part in the Popup? Or can I rip the majority of that out since none of those default styles is really going to be changing? – WERUreo Aug 18 '15 at 14:15
  • @WERUreo 1: You need the `DataItem` from the other answer, and then obviously populate the list with data (which I did in code behind, but you can do otherwise), and the DataTemplate. 2: Control Templates are not really reusable in parts, either override the entire thing or don't, so yeah you need the full XAML you get from Expression Blend. – Federico Berasategui Aug 18 '15 at 15:16
  • Thanks for confirming my suspicions about the Control Templates. I started to see for myself that trying to piecemeal the template together wasn't working really well. While I'm prototyping this user control, I've got the template in my `UserControl.Resources` but I'll probably create a separate `ResourceDictionary` when I put the code into production. – WERUreo Aug 18 '15 at 16:36
2

You could use a DataTemplateSelector with the DataTemplates defined in the XAML and some item type variable it the data you're binding to.

public class StyleSelector : DataTemplateSelector
{
    public DataTemplate DefaultTemplate
    { get; set; }

    public DataTemplate SeparatorTemplate
    { get; set; }

    public DataTemplate ResetTemplate
    { get; set; }

    public override DataTemplate SelectTemplate(object item, DependencyObject container)
    {
        var type = item as SomeType;
        if (type != null)
        {
            switch (type.SomeItemTypeField)
            {
                case TypeENum.Separator: return SeparatorTemplate;
                case TypeENum.Reset: return ResetTemplate;
                default:
                    return DefaultTemplate;
            }
        }

        return base.SelectTemplate(item, container);
    }
}

Check out this more detailed example.

matt
  • 333
  • 2
  • 10
  • 23
1

I think your best bet is to learn about DataTemplate and DataTemplateSelector.

Here is an blog post that will show you a simple example of using a DataTemplate.

The ComboBox Control

Essentially, you could bind your ComboBox to a collection of objects, and use a DataTemplateSelector to pick which template to use based on the type of object.

keithernet
  • 135
  • 6
  • I appreciate the suggestion. Although I didn't go with this approach, I definitely will look into DataTemplates and DataTemplateSelectors. I still have a lot to learn about WPF, so any new information is greatly appreciated. – WERUreo Aug 20 '15 at 20:45