【问题标题】:Can I use a different Template for the selected item in a WPF ComboBox than for the items in the dropdown part?我可以为 WPF ComboBox 中的选定项目使用与下拉部分中的项目不同的模板吗?
【发布时间】:2011-06-08 01:20:30
【问题描述】:

我有一个 WPF 组合框,里面装满了客户对象。我有一个数据模板:

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

这样,当我打开我的 ComboBox 时,我可以看到不同的客户及其姓名,以及在其下方的地址。

但是当我选择一个客户时,我只想在组合框中显示名称。比如:

<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 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 的部分)。

【问题讨论】:

  • 您的解决方案有效,但在“输出”窗口中出现错误。 System.Windows.Data Error: 4 : Cannot find source for binding with reference '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')
  • 我记得也看到过这些错误。但是我不再参与这个项目(甚至不在公司),所以我无法检查这个,抱歉。
  • DataTrigger 中没有必要提及绑定路径。当 ComboBoxItem 被选中时,将向控件应用不同的模板,并且 DataTrigger 绑定将不再能够在其元素树中找到类型为 ComboBoxItem 的祖先。因此,与 null 的比较将始终成功。这种方法之所以有效,是因为 ComboBoxItem 的可视化树会根据它是被选中还是显示在弹出窗口中而有所不同。

标签: wpf templates combobox


【解决方案1】:

使用上面提到的 DataTrigger/Binding 解决方案有两个问题。第一个是您实际上最终会收到一个绑定警告,即您找不到所选项目的相对来源。然而,更大的问题是您将数据模板弄得乱七八糟,并使它们特定于 ComboBox。

我提出的解决方案更好地遵循 WPF 设计,因为它使用 DataTemplateSelector,您可以在其上使用其 SelectedItemTemplateDropDownItemsTemplate 属性以及两者的“选择器”变体指定单独的模板。

注意:针对 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}}" />

此外,如果您没有为选定项或下拉项指定模板或 TemplateSelector,则它会再次按照您的预期退回到基于数据类型的数据模板的常规解析。因此,例如,在以下情况下,所选项目的模板已显式设置,但下拉菜单将继承适用于数据上下文中对象的 DataType 的任何数据模板。

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

享受吧!

【讨论】:

  • 非常酷。而且我确实有那些具有约束力的警告(从未发现它们来自哪里,但也没有真正看一看)。我现在真的可以检查一下,但将来可能会。
  • 很高兴能提供帮助。只要知道您是否在代码中使用它,return 语句(上面的return inDropDown)使用新的 C#6 ?。语法,所以如果您不使用 VS 2015,只需删除“?”并在调用SelectTemplate 之前明确检查空值。我会把它添加到代码中。
  • 我向您致敬,这是一个真正可重复使用的解决方案!
  • 谢谢!我很感激。如果可以,请投票!
  • 由于某种原因,当我实现此解决方案时,ComboBoxTemplateSelector 代码永远不会执行,也没有绑定错误。
【解决方案2】:

简单的解决方案:

<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

【讨论】:

  • 我想使用两个模板,将其分开。我使用了来自该站点的示例项目的代码:developingfor.net/net/dynamically-switch-wpf-datatemplate.html。但是,虽然它适用于 ListBox,但它不适用于 ComboBox。你的最后一句话解决了它或我。 ComboBox 中的选定项目没有 IsSelected = True,但它是 Null。有关我如何解决它的完整代码,请参阅上面的编辑。非常感谢!
  • 很高兴它很有用,尽管它并不完全符合您的要求。在尝试回答您的问题之前,我也不知道 null-thing,我通过这种方式进行了实验并发现了它。
  • IsSelected 不能为空,因此永远不能真正为 NULL。您不需要Path=IsSelected,因为对周围的 ComboBoxItem 进行 NULL 检查就足够了。
  • 有时,即使设置了 ShortName 属性和 OnPropertyChanged 等,短文本也不会显示给我。你应该得到绑定错误吗?每当短名称字段从空(未正确显示)变为已填充时,以及在启动时“System.Windows.Data Error: 4: Cannot find source for binding with reference 'RelativeSource FindAncestor, AncestorType='System. Windows.Controls.ComboBoxItem',AncestorLevel='1''。BindingExpression:(无路径);DataItem=null;目标元素是'ContentControl'(名称='');目标属性是'NoTarget'(类型'Object') "
  • @SimonF:我不知道你的具体情况,所以我不能给你任何建议。我对此没有任何问题,绑定是绝对标准的。你不是用Artiom的方法吗? (正如你提到的ShortName。)
【解决方案3】:

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

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

在我的解决方案中,我将 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 参数控制组合选择显示(我将其绑定到我的 ViewModel 上的适当值)。

【讨论】:

  • 不太确定这是我要找的。我希望 ComboBox 的外观不是“活动”(即用户没有点击它,它不是“打开”),只显示一段文本。但是,当用户点击它时,它应该打开/下拉,并且每个项目都应该显示两条文本(因此,不同的模板)。
  • 如果你试验一下上面的代码,我想你会到达你想去的地方。通过设置此控件模板,您可以通过其 Text 属性(或您喜欢的任何属性)控制组合的折叠文本,从而允许您显示简单的未选择文本。您可以通过在创建组合框时指定 ItemTemplate 来修改单个项目文本。 (ItemTemplate 可能有一个堆栈面板和两个 TextBlock,或者任何你喜欢的格式。)
【解决方案4】:

我使用了下一个方法

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

工作就像一个魅力。不太喜欢这里的 Loaded 事件,但您可以根据需要修复它

【讨论】:

    【解决方案5】:

    除了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#转换器代码如下:

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

    【讨论】:

      【解决方案6】:

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

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

      【讨论】:

      【解决方案7】:

      我建议这个解决方案没有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 的原因。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-03-13
      • 1970-01-01
      相关资源
      最近更新 更多