Можно ли использовать другой шаблон для выбранного элемента в WPF ComboBox, чем для элементов в раскрывающемся списке?
У меня есть Combibox WPF, который заполнен, скажем, объектами Customer. У меня есть DataTemplate:
<DataTemplate DataType="{x:Type MyAssembly:Customer}">
<StackPanel>
<TextBlock Text="{Binding Name}" />
<TextBlock Text="{Binding Address}" />
</StackPanel>
</DataTemplate>
Таким образом, когда я открываю свой ComboBox, я могу видеть разных Клиентов с их именем и, ниже этого, Адрес.
Но когда я выбираю Клиента, я хочу показать только имя в 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:
<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, not True).
Ответы
Ответ 1
Проблема с использованием решения DataTrigger/Binding, упомянутого выше, имеет две стороны. Во-первых, вы фактически получаете связующее предупреждение о том, что не можете найти относительный источник для выбранного элемента. Однако большая проблема заключается в том, что вы запутали свои шаблоны данных и сделали их специфичными для ComboBox.
Представленное мною решение лучше следует проектам WPF в том DataTemplateSelector
что в нем используется 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 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 != null && !(itemToCheck is ComboBoxItem) && !(itemToCheck is ComboBox))
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);
}
}
Примечание. Для простоты в моем примере кода используется новый символ "?". особенность С# 6 (VS 2015). Если вы используете старую версию, просто удалите "?" и явно проверять нулевое значение перед вызовом 'SelectTemplate' выше и возвращать нулевое значение, иначе так:
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
};
}
}
А вот как ты это используешь. Красиво, чисто и понятно, а ваши шаблоны остаются "чистыми"
Примечание: '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} />
Наслаждайтесь!
Ответ 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
)
Если вы хотите отключить весь шаблон, вы можете сделать это, используя триггер, например. примените другой 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.
Ответ 3
Я собирался предложить использовать комбинацию ItemTemplate для комбо-элементов с параметром Text в качестве выбора заголовка, но я вижу, что ComboBox не уважает параметр Text.
Я столкнулся с чем-то подобным, переопределив ComboBox ControlTemplate. Здесь MSDN веб-сайт с образцом для .NET 4.0.
В моем решении я изменяю ContentPresenter в шаблоне ComboBox, чтобы привязать его к тексту, а его 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"/>
С этой ссылкой связывания я могу управлять отображением выбора Combo непосредственно через параметр Text на элементе управления (который я привязываю к соответствующему значению на моем ViewModel).
Ответ 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;
}
}
работал как шарм. Не нравится довольно загруженное событие здесь, но вы можете исправить его, если хотите
Ответ 5
Да. Вы используете Выбор шаблона, чтобы определить, какой шаблон привязать во время выполнения. Таким образом, если IsSelected = False, то используйте этот шаблон, если IsSelected = True, используйте этот другой шаблон.
Примечание:
После того как вы реализуете селектор шаблонов, вам нужно будет предоставить имена шаблонов.
Ответ 6
В дополнение к тому, что говорит ответ HB, с помощью конвертера можно избежать ошибки привязки. Следующий пример основан на решении, отредактированном самим OP.
Идея очень проста: привязка к чему-то, что существует (Control
), и выполнить соответствующую проверку внутри конвертера. Соответствующей частью модифицированного XAML является следующее. Обратите внимание, что Path=IsSelected
никогда не требовался, и ComboBoxItem
заменяется Path=IsSelected
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>
Код конвертера С# следующий:
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();
}
}