我可以为WPF ComboBox中选定的项目使用不同的模板吗?与下拉部分中的项目不同?

76

我有一个 WPF Combobox ,其中填充有一些 Customer 对象。我有一个 DataTemplate:

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

这种方式,当我打开下拉菜单时,可以看到不同的客户及其名称,并在名称下方显示地址。

但是当我选择一个客户时,我只想在下拉菜单中显示名称。例如:

<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)。


2
你的解决方案是可行的,但我在输出窗口中收到错误信息。System.Windows.Data Error: 4 : 找不到与引用'RelativeSource FindAncestor, AncestorType='System.Windows.Controls.ComboBoxItem', AncestorLevel='1''绑定的源。BindingExpression:Path=IsSelected; DataItem=null; target element is 'ContentPresenter' (Name=''); target property is 'NoTarget' (type 'Object') - user11909
1
我记得我也看到过这些错误。但是我已经不在这个项目上了(甚至不在公司里),所以我无法检查,抱歉。 - Peter
在DataTrigger中提及Binding Path是不必要的。当ComboBoxItem被选中时,将应用不同的模板到控件上,DataTrigger绑定将无法在其元素树中找到类型为ComboBoxItem的祖先。因此,与null的比较将始终成功。这种方法之所以有效,是因为ComboBoxItem的可视树取决于它是选中还是显示在弹出窗口中。 - Dennis Kassel
7个回答

82

使用上述数据触发器/绑定解决方案存在两个问题。第一个是你实际上会出现绑定警告,无法找到所选项目的相对源。然而更大的问题是,你已经使你的数据模板混乱,并且使它们特定于ComboBox。

我提出的解决方案更符合WPF设计,它使用了DataTemplateSelector,你可以在其中使用SelectedItemTemplateDropDownItemsTemplate属性以及两者的‘selector’变体来指定不同的模板。

注意:已为启用nullability的C#9更新,并在搜索期间使用模式匹配。

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 itemToCheck = container;

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

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

        return inDropDown
            ? DropdownItemsTemplate ?? DropdownItemsTemplateSelector?.SelectTemplate(item, container)
            : SelectedItemTemplate  ?? SelectedItemTemplateSelector?.SelectTemplate(item, container); 
    }
}
为了在XAML中更容易使用,我也包括了一个标记扩展,它只是在其ProvideValue函数中创建并返回上述类。
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)
        => new ComboBoxTemplateSelector(){
            SelectedItemTemplate          = SelectedItemTemplate,
            SelectedItemTemplateSelector  = SelectedItemTemplateSelector,
            DropdownItemsTemplate         = DropdownItemsTemplate,
            DropdownItemsTemplateSelector = DropdownItemsTemplateSelector
        };
}

这是如何使用它的方法。简单易懂,模板仍保持“纯净”。

注意:'is:'这里是我在代码中放置类别的xmlns映射。请确保导入您自己的名称空间并根据需要更改'is:'。

<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}}" />

此外,如果您没有为所选项或下拉列表项指定模板或模板选择器,则它将简单地回退到基于数据类型解析数据模板的常规方式,与您预期的一样。因此,在下面的情况中,所选项目已经显式设置了其模板,但下拉列表将继承适用于数据上下文中对象的数据类型的任何数据模板。

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

享受吧!


您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - Mark A. Donohoe
5
我向你脱帽致敬,因为你提供了一种真正可重复使用的解决方案! - henon
听起来你的XAML没有正确设置。尝试使用自己的TemplateSelector。如果这不起作用,请确保你认为正在使用的XAML实际上正在被使用,可以更改其他属性(如颜色、字体等)来验证。 - Mark A. Donohoe
也许值得一提的是,设置 DisplayMemberPath 会使 ComboBox 忽略 ItemTemplateSelectorSelectTemplate 函数将不会被调用。 - M C
@MarkA.Donohoe 请看一下我的回答:https://dev59.com/92445IYBdhLWcg3w1tmi#69470062 - Orace
显示剩余5条评论

35
简单的解决方案:
<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>

(Note that the element that is selected and displayed in the box and not the list is not inside a ComboBoxItem hence the trigger on Null)
如果您想要完全更换模板,也可以使用触发器来将不同的ContentTemplate应用于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的答案

4
IsSelected 不可为空,因此它从未真正为 NULL。您不需要使用 Path=IsSelected,因为对周围的 ComboBoxItem 进行空值检查已完全足够。 - springy76
有时候短文本框对我来说无法显示,即使ShortName属性已经设置并且OnPropertyChanged等。你应该会得到一个绑定错误吗?每当短名称字段从空(未正确显示)变为填充时,以及在启动时弹出以下内容:"System.Windows.Data Error: 4 : Cannot find source for binding with reference 'RelativeSource FindAncestor, AncestorType='System.Windows.Controls.ComboBoxItem', AncestorLevel='1''. BindingExpression:(no path); DataItem=null; target element is 'ContentControl' (Name=''); target property is 'NoTarget' (type 'Object')" - Simon F
@SimonF:我不知道你的具体情况,所以无法给你任何建议。我在这方面没有遇到任何问题,绑定是绝对标准的。你难道不是使用Artiom的方法吗?(因为你提到了“ShortName”)。 - H.B.
@H.B. 我正在按照您的方式进行,并使用springy76的建议。如果你没有遇到这个绑定错误,那么这可能是我开始寻找的好地方。 - Simon F
@Peter,看一下我的补充。(是的,我知道这条信息很旧了,但是将来你可能还会喜欢我的答案。) - Mark A. Donohoe
显示剩余3条评论

3
除了H.B.答案中提到的内容外,绑定错误可以通过转换器避免。以下示例基于OP自己编辑的解决方案
思路非常简单:绑定到始终存在的东西(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#转换器代码如下:

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();
    }
}

1

我本来想建议使用 ItemTemplate 的组合项,将 Text 参数作为标题选择,但是我发现 ComboBox 不尊重 Text 参数。

我曾经通过覆盖 ComboBox ControlTemplate 来处理类似的问题。这里是 MSDN website .NET 4.0 的示例。

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

<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"/>

通过这个绑定链接,我能够直接通过控件上的文本参数(我将其绑定到ViewModel上的适当值)来控制组合框的选择显示。

不太确定这是否符合我的要求。我想要一个ComboBox的外观,它不是“活动的”(即用户没有单击它,它没有“打开”),只显示一段文本。但是,当用户单击它时,它应该打开/下拉,并且每个项目都应该显示两个文本(因此,使用不同的模板)。 - Peter
如果您尝试以上代码,我认为您会达到想要的效果。通过设置控件模板,您可以通过其Text属性(或其他任何属性)控制组合框的折叠文本,从而使您能够显示简单的未选中文本。当创建组合框时,您可以通过指定ItemTemplate来修改各个项目文本。(ItemTemplate可能包含一个StackPanel和两个TextBlock,或者您喜欢的任何格式。) - cunningdave

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;
    }
}

非常好用。这里的加载事件可能不太好,但如果您想要修复它,可以这样做。


0

我提出了一种解决方案,不需要使用 DataTemplateSelectorTriggerbinding 或者 behavior

第一步是将所选元素的 ItemTemplate 放入 ComboBox 资源中,并将下拉菜单中项目的 ItemTemplate 放入 ComboBox.ItemsPanel 资源中,并给这两个资源相同的键

第二步是通过在实际的 ComboBox.ItemTemplate 实现中同时使用 ContentPresenterDynamicResource 来推迟 ItemTemplate 的运行时解析。

<ComboBox ItemsSource="{Binding Items, Mode=OneWay}">

    <ComboBox.Resources>
        <!-- Define ItemTemplate resource -->
        <DataTemplate x:Key="ItemTemplate" DataType="viewModel:ItemType">
            <TextBlock Text="{Binding FieldOne, Mode=OneWay}" />
        </DataTemplate>
    </ComboBox.Resources>

    <ComboBox.ItemsPanel>
        <ItemsPanelTemplate>
            <StackPanel Grid.IsSharedSizeScope="True"
                        IsItemsHost="True">
                <StackPanel.Resources>
                    <!-- Redefine ItemTemplate resource -->
                    <DataTemplate x:Key="ItemTemplate" DataType="viewModel:ItemType">
                        <Grid>
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="Auto" SharedSizeGroup="GroupOne" />
                                <ColumnDefinition Width="10" SharedSizeGroup="GroupSpace" />
                                <ColumnDefinition Width="Auto" SharedSizeGroup="GroupTwo" />
                            </Grid.ColumnDefinitions>
                
                            <TextBlock Grid.Column="0" Text="{Binding FieldOne, Mode=OneWay}" />
                            <TextBlock Grid.Column="2" Text="{Binding FieldTwo, Mode=OneWay}" />
                        </Grid>
                    </DataTemplate>
                </StackPanel.Resources>
            </StackPanel>
        </ItemsPanelTemplate>
    </ComboBox.ItemsPanel>

    <ComboBox.ItemTemplate>
        <DataTemplate>
            <ContentPresenter ContentTemplate="{DynamicResource ItemTemplate}" />
        </DataTemplate>
    </ComboBox.ItemTemplate>
</ComboBox>

这是一种相当新颖的方法,使用相同的DataTemplateKey在两个不同的范围中。然而,这种方法不允许你为选定的项目设置模板,并默认为下拉菜单中的常规分辨率,因为它会“捡起”外部控件中的模板。另一个问题是现在需要你明确定义面板,这可能并不总是能够做到,或者可能需要大量的额外设置。尽管如此,对于快速临时解决方案来说,这似乎非常不错。我的建议是,你所解决的问题正是ItemTemplateSelector被创建的原因。 - Mark A. Donohoe

0

是的。您可以使用模板选择器在运行时确定要绑定哪个模板。因此,如果IsSelected = False,则使用此模板,如果IsSelected = True,则使用另一个模板。

需要注意的是: 一旦您实现了模板选择器,您将需要为模板命名关键字。


我尝试过使用我在这里找到的示例(http://www.developingfor.net/net/dynamically-switch-wpf-datatemplate.html),但发现它不太可重用,我想仅在XAML中解决此问题。 - Peter

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接