我是否可以为 WPF 组合框中的选定项目使用不同的模板,而不是为下拉列表部分中的项目使用不同的模板?

我有一个 WPF 组合框,里面充满了,比如说,Customer 对象。我有一个 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>

我可以为组合框中的选定项目选择另一个模板吗?

解决方案

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

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

40611 次浏览

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

注意: 一旦实现了模板选择器,就需要给模板命名。

简单的解决办法:

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

如果你想切换掉整个模板,你也可以通过使用触发器来做到这一点,例如 对 ABC1应用不同的 ContentTemplate。这也允许你保留一个默认的基于 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>

请注意,此方法将导致绑定错误,因为未找到所选项的相对源。有关替代方法,请参见 马奎尔的回答

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

我通过覆盖 ComboBoBoxControlTemplate 来处理类似的问题。下面是 MSDN 网站的示例。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 上的适当值)。

我用了下一种方法

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

工作像一个魅力。不喜欢在这里几乎加载事件,但你可以修复它,如果你想要的

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

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

注意: 为 C # 9更新,启用了 nullability,并在搜索过程中使用了模式匹配

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

此外,如果您没有为所选项目或下拉列表项目指定 Template 或 TemplateSelector,那么它只是返回到基于数据类型的常规数据模板解析,这也是您所期望的。因此,例如,在下面的示例中,选定的项显式设置了它的模板,但是下拉列表将继承数据上下文中应用于对象的 DataType 的任何数据模板。

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

好好享受吧!

除了 H.B. 回答所说的,绑定错误可以通过转换器避免。下面的示例基于由 自己动手编辑的解决方案。

这个想法非常简单: 绑定到某个始终存在的东西(Control) ,并在转换器内进行相关检查。 修改后的 XAML 的相关部分如下。请注意,从来没有真正需要 Path=IsSelectedComboBoxItem被替换为 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();
}
}

我建议这个解决方案没有 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>