我可以在WPF ComboBox中为所选项目使用不同的模板而不是下拉部分中的项目吗?

时间:2011-01-12 19:02:30

标签: wpf templates combobox

我有一个WPF组合框,例如,客户对象。我有一个DataTemplate:

<DataTemplate DataType="{x:Type MyAssembly:Customer}">
    <StackPanel>
        <TextBlock Text="{Binding Name}" />
        <TextBlock Text="{Binding Address}" />
    </StackPanel>
</DataTemplate>

这样,当我打开我的ComboBox时,我可以看到不同的客户名字,以及地址。

但是当我选择一个Customer时,我只想在ComboBox中显示Name。类似的东西:

<DataTemplate DataType="{x:Type MyAssembly:Customer}">
    <StackPanel>
        <TextBlock Text="{Binding Name}" />
    </StackPanel>
</DataTemplate>

我可以为ComboBox中的所选项目选择另一个模板吗?

解决方案

在答案的帮助下,我解决了这个问题:

<UserControl.Resources>
    <ControlTemplate x:Key="SimpleTemplate">
        <StackPanel>
            <TextBlock Text="{Binding Name}" />
        </StackPanel>
    </ControlTemplate>
    <ControlTemplate x:Key="ExtendedTemplate">
        <StackPanel>
            <TextBlock Text="{Binding Name}" />
            <TextBlock Text="{Binding Address}" />
        </StackPanel>
    </ControlTemplate>
    <DataTemplate x:Key="CustomerTemplate">
        <Control x:Name="theControl" Focusable="False" Template="{StaticResource ExtendedTemplate}" />
        <DataTemplate.Triggers>
            <DataTrigger Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ComboBoxItem}}, Path=IsSelected}" Value="{x:Null}">
                <Setter TargetName="theControl" Property="Template" Value="{StaticResource SimpleTemplate}" />
            </DataTrigger>
        </DataTemplate.Triggers>
    </DataTemplate>
</UserControl.Resources>

然后,我的ComboBox:

<ComboBox ItemsSource="{Binding Customers}" 
                SelectedItem="{Binding SelectedCustomer}"
                ItemTemplate="{StaticResource CustomerTemplate}" />

让它工作的重要部分是Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ComboBoxItem}}, Path=IsSelected}" Value="{x:Null}"(值应为x的部分:Null,而不是True)。

6 个答案:

答案 0 :(得分:48)

使用上面提到的DataTrigger / Binding解决方案的问题是双重的。首先,您实际上最终会得到一个绑定警告,您无法找到所选项目的相对来源。但更大的问题是,您的数据模板混乱并使其特定于ComboBox。

我提出的解决方案更符合WPF设计,因为它使用DataTemplateSelector,您可以使用SelectedItemTemplate和DropDownItemsTemplate属性以及两者的选择器指定单独的模板。

public class ComboBoxTemplateSelector : DataTemplateSelector
{
    public DataTemplate         SelectedItemTemplate          { get; set; }
    public DataTemplateSelector SelectedItemTemplateSelector  { get; set; }
    public DataTemplate         DropdownItemsTemplate         { get; set; }
    public DataTemplateSelector DropdownItemsTemplateSelector { get; set; }

    public override DataTemplate SelectTemplate(object item, DependencyObject container)
    {
        var parent = container;

        // Search up the visual tree, stopping at either a ComboBox or
        // a ComboBoxItem (or null). This will determine which template to use
        while(parent != null && !(parent is ComboBoxItem) && !(parent is ComboBox))
            parent = VisualTreeHelper.GetParent(parent);

        // If you stopped at a ComboBoxItem, you're in the dropdown
        var inDropDown = (parent is ComboBoxItem);

        return inDropDown
            ? DropdownItemsTemplate ?? DropdownItemsTemplateSelector?.SelectTemplate(item, container)
            : SelectedItemTemplate  ?? SelectedItemTemplateSelector?.SelectTemplate(item, container); 
    }
}
  

注意:为简单起见,我的示例代码使用了新的&#39;?。&#39; C#6(VS 2015)的特点。如果您使用的是旧版本,只需删除&#39;?&#39;并在调用&#39; SelectTemplate&#39;之前显式检查null。在上面并返回null,否则就像这样:

return inDropDown
    ? DropdownItemsTemplate ??
        ((DropdownItemsTemplateSelector != null)
            ? DropdownItemsTemplateSelector.SelectTemplate(item, container)
            : null)
    : SelectedItemTemplate ??
        ((SelectedItemTemplateSelector != null)
            ? SelectedItemTemplateSelector.SelectTemplate(item, container)
            : null)

我还包含了一个标记扩展,它只是为了方便XAML而创建并返回上面的类。

public class ComboBoxTemplateSelectorExtension : MarkupExtension
{
    public DataTemplate         SelectedItemTemplate          { get; set; }
    public DataTemplateSelector SelectedItemTemplateSelector  { get; set; }
    public DataTemplate         DropdownItemsTemplate         { get; set; }
    public DataTemplateSelector DropdownItemsTemplateSelector { get; set; }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        return new ComboBoxTemplateSelector(){
            SelectedItemTemplate          = SelectedItemTemplate,
            SelectedItemTemplateSelector  = SelectedItemTemplateSelector,
            DropdownItemsTemplate         = DropdownItemsTemplate,
            DropdownItemsTemplateSelector = DropdownItemsTemplateSelector
        };
    }
}

以下是你如何使用它。漂亮,干净,清晰,模板保持纯净&#39;

  

注意:&#39;是:&#39;这是我的xmlns映射,我把这个类放在代码中。确保导入您自己的命名空间并更改&#39;是:&#39;酌情。

<ComboBox x:Name="MyComboBox"
    ItemsSource="{Binding Items}"
    ItemTemplateSelector="{is:ComboBoxTemplateSelector
        SelectedItemTemplate={StaticResource MySelectedItemTemplate},
        DropdownItemsTemplate={StaticResource MyDropDownItemTemplate}}" />

如果您愿意,也可以使用DataTemplateSelectors ......

<ComboBox x:Name="MyComboBox"
    ItemsSource="{Binding Items}"
    ItemTemplateSelector="{is:ComboBoxTemplateSelector
        SelectedItemTemplateSelector={StaticResource MySelectedItemTemplateSelector},
        DropdownItemsTemplateSelector={StaticResource MyDropDownItemTemplateSelector}}" />

或者混搭!在这里,我使用了所选项目的模板,但是DropDown项目的模板选择器。

<ComboBox x:Name="MyComboBox"
    ItemsSource="{Binding Items}"
    ItemTemplateSelector="{is:ComboBoxTemplateSelector
        SelectedItemTemplate={StaticResource MySelectedItemTemplate},
        DropdownItemsTemplateSelector={StaticResource MyDropDownItemTemplateSelector}}" />

此外,如果您没有为所选项或下拉项指定模板或TemplateSelector,它只会再次回归到基于数据类型的数据模板的常规解析,正如您所期望的那样。因此,例如,在下面的例子中,所选项目的模板显式设置,但下拉列表将继承适用于数据上下文中对象的DataType的数据模板。

<ComboBox x:Name="MyComboBox"
    ItemsSource="{Binding Items}"
    ItemTemplateSelector="{is:ComboBoxTemplateSelector
        SelectedItemTemplate={StaticResource MyTemplate} />

享受!

答案 1 :(得分:30)

简单的解决方案:

<DataTemplate>
    <StackPanel>
        <TextBlock Text="{Binding Name}"/>
        <TextBlock Text="{Binding Address}">
            <TextBlock.Style>
                <Style TargetType="TextBlock">
                    <Style.Triggers>
                        <DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType=ComboBoxItem}}" Value="{x:Null}">
                            <Setter Property="Visibility" Value="Collapsed"/>
                        </DataTrigger>
                    </Style.Triggers>
                </Style>
            </TextBlock.Style>
        </TextBlock>
    </StackPanel>
</DataTemplate>

(请注意,选中并显示在框中而不是列表中的元素不在ComboBoxItem内,因此触发Null

如果您想要切换整个模板,您也可以使用触发器来执行此操作。 apply a different ContentTemplate to a ContentControl。如果您只是更改此选择案例的模板,这也允许您保留基于默认DataType的模板选择,例如:

<ComboBox.ItemTemplate>
    <DataTemplate>
        <ContentControl Content="{Binding}">
            <ContentControl.Style>
                <Style TargetType="ContentControl">
                    <Style.Triggers>
                        <DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType=ComboBoxItem}}"
                                        Value="{x:Null}">
                            <Setter Property="ContentTemplate">
                                <Setter.Value>
                                    <DataTemplate>
                                        <!-- ... -->
                                    </DataTemplate>
                                </Setter.Value>
                            </Setter>
                        </DataTrigger>
                    </Style.Triggers>
                </Style>
            </ContentControl.Style>
        </ContentControl>
    </DataTemplate>
</ComboBox.ItemTemplate>

请注意,此方法将导致绑定错误,因为找不到所选项目的相对来源。有关替代方法,请参阅MarqueIV's answer

答案 2 :(得分:1)

我打算建议对组合项使用ItemTemplate的组合,将Text参数作为标题选择,但我看到ComboBox不尊重Text参数。

我通过覆盖ComboBox ControlTemplate来处理类似的事情。这是带有.NET 4.0示例的MSDN website

在我的解决方案中,我更改了ComboBox模板中的ContentPresenter,使其绑定到Text,其ContentTemplate绑定到一个包含TextBlock的简单DataTemplate,如下所示:

<DataTemplate x:Uid="DataTemplate_1" x:Key="ComboSelectionBoxTemplate">
    <TextBlock x:Uid="TextBlock_1" Text="{Binding}" />
</DataTemplate>

在ControlTemplate中使用它:

<ContentPresenter Name="ContentSite" IsHitTestVisible="False" Content="{TemplateBinding Text}" ContentTemplate="{StaticResource ComboSelectionBoxTemplate}" Margin="3,3,23,3" VerticalAlignment="Center" HorizontalAlignment="Left"/>

使用此绑定链接,我可以通过控件上的Text参数直接控制Combo选择显示(我将其绑定到ViewModel上的适当值)。

答案 3 :(得分:1)

我使用了下一个方法

 <UserControl.Resources>
    <DataTemplate x:Key="SelectedItemTemplate" DataType="{x:Type statusBar:OffsetItem}">
        <TextBlock Text="{Binding Path=ShortName}" />
    </DataTemplate>
</UserControl.Resources>
<StackPanel Orientation="Horizontal">
    <ComboBox DisplayMemberPath="FullName"
              ItemsSource="{Binding Path=Offsets}"
              behaviors:SelectedItemTemplateBehavior.SelectedItemDataTemplate="{StaticResource SelectedItemTemplate}"
              SelectedItem="{Binding Path=Selected}" />
    <TextBlock Text="User Time" />
    <TextBlock Text="" />
</StackPanel>

行为

public static class SelectedItemTemplateBehavior
{
    public static readonly DependencyProperty SelectedItemDataTemplateProperty =
        DependencyProperty.RegisterAttached("SelectedItemDataTemplate", typeof(DataTemplate), typeof(SelectedItemTemplateBehavior), new PropertyMetadata(default(DataTemplate), PropertyChangedCallback));

    public static void SetSelectedItemDataTemplate(this UIElement element, DataTemplate value)
    {
        element.SetValue(SelectedItemDataTemplateProperty, value);
    }

    public static DataTemplate GetSelectedItemDataTemplate(this ComboBox element)
    {
        return (DataTemplate)element.GetValue(SelectedItemDataTemplateProperty);
    }

    private static void PropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var uiElement = d as ComboBox;
        if (e.Property == SelectedItemDataTemplateProperty && uiElement != null)
        {
            uiElement.Loaded -= UiElementLoaded;
            UpdateSelectionTemplate(uiElement);
            uiElement.Loaded += UiElementLoaded;

        }
    }

    static void UiElementLoaded(object sender, RoutedEventArgs e)
    {
        UpdateSelectionTemplate((ComboBox)sender);
    }

    private static void UpdateSelectionTemplate(ComboBox uiElement)
    {
        var contentPresenter = GetChildOfType<ContentPresenter>(uiElement);
        if (contentPresenter == null)
            return;
        var template = uiElement.GetSelectedItemDataTemplate();
        contentPresenter.ContentTemplate = template;
    }


    public static T GetChildOfType<T>(DependencyObject depObj)
        where T : DependencyObject
    {
        if (depObj == null) return null;

        for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
        {
            var child = VisualTreeHelper.GetChild(depObj, i);

            var result = (child as T) ?? GetChildOfType<T>(child);
            if (result != null) return result;
        }
        return null;
    }
}

像魅力一样工作。不喜欢这里的加载事件,但如果你想要

,你可以修复它

答案 4 :(得分:0)

是。您使用Template Selector来确定在运行时绑定哪个模板。因此,如果IsSelected = False,则使用此模板,如果IsSelected = True,则使用此其他模板。

注意: 实现模板选择器后,您需要为模板指定键名。

答案 5 :(得分:0)

除了H.B. answer所说的以外,使用转换器可以避免绑定错误。以下示例基于OP himself编辑的解决方案。

这个想法很简单:绑定到一直存在的东西(Control)并在转换器内部进行相关检查。 修改后的XAML的相关部分如下。请注意,从未真正需要Path=IsSelected,并且将ComboBoxItem替换为Control以避免绑定错误。

<DataTrigger Binding="{Binding 
    RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Control}},
    Converter={StaticResource ComboBoxItemIsSelectedConverter}}"
    Value="{x:Null}">
  <Setter TargetName="theControl" Property="Template" Value="{StaticResource SimpleTemplate}" />
</DataTrigger>

C#Converter代码如下:

public class ComboBoxItemIsSelectedConverter : IValueConverter
{
    private static object _notNull = new object();
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        // value is ComboBox when the item is the one in the closed combo
        if (value is ComboBox) return null; 

        // all the other items inside the dropdown will go here
        return _notNull;
    }

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