Могу ли я использовать другой шаблон для выбранного элемента в WPF ComboBox, чем для элементов в раскрывающейся части?

У меня есть Combobox WPF, который заполнен, скажем, объектами Customer. У меня есть DataTemplate:

<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:

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


person Peter    schedule 12.01.2011    source источник
comment
Ваше решение работает, но в окне вывода появляются ошибки. 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')   -  person user11909    schedule 06.04.2017
comment
Я тоже помню, что видел эти ошибки. Но я больше не в проекте (или даже в компании), поэтому я не могу это проверить, извините.   -  person Peter    schedule 06.04.2017
comment
Упоминание Binding Path в DataTrigger необязательно. Когда ComboBoxItem станет выбранным, к элементу управления будет применен другой шаблон, и привязка DataTrigger больше не сможет найти предка типа ComboBoxItem в своем дереве элементов. Таким образом, сравнение с null всегда будет успешным. Этот подход работает, потому что визуальное дерево ComboBoxItem различается в зависимости от того, выбрано оно или отображается во всплывающем окне.   -  person Dennis Kassel    schedule 24.10.2018


Ответы (6)


Проблема с использованием решения DataTrigger / Binding, упомянутого выше, имеет два аспекта. Во-первых, вы фактически получаете обязательное предупреждение о том, что вы не можете найти относительный источник для выбранного элемента. Однако более серьезная проблема заключается в том, что вы загромождали свои шаблоны данных и сделали их специфичными для ComboBox.

Предлагаемое мной решение лучше соответствует дизайну WPF, поскольку в нем используется DataTemplateSelector, на котором вы можете указать отдельные шаблоны, используя его свойства SelectedItemTemplate и DropDownItemsTemplate, а также варианты «селектора» для обоих.

Примечание. Обновлено для 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}}" />

Кроме того, если вы не укажете Template или TemplateSelector для выбранных или раскрывающихся элементов, он просто вернется к обычному разрешению шаблонов данных на основе типов данных, опять же, как и следовало ожидать. Так, например, в приведенном ниже случае для выбранного элемента явно установлен шаблон, но раскрывающийся список унаследует тот шаблон данных, который применяется для DataType объекта в контексте данных.

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

Наслаждаться!

person Mark A. Donohoe    schedule 29.10.2015
comment
Очень круто. И у меня действительно есть эти обязательные предупреждения (я так и не узнал, откуда они, но и не посмотрел). Я действительно могу это проверить прямо сейчас, но, возможно, в будущем. - person Peter; 30.10.2015
comment
Рад помочь. Просто знайте, если вы используете это в своем коде, оператор return (return inDropDown выше) использует новый C # 6?. синтаксис, поэтому, если вы не используете VS 2015, просто удалите '?' и явно проверять наличие нулей перед вызовом SelectTemplate. Я добавлю это в код. - person Mark A. Donohoe; 30.10.2015
comment
Снимаю шляпу за действительно многоразовое решение! - person henon; 18.07.2016
comment
Спасибо! Я ценю это. Если можете, проголосуйте, пожалуйста! - person Mark A. Donohoe; 18.07.2016
comment
По какой-то причине, когда я реализую это решение, код ComboBoxTemplateSelector никогда не выполняется, ошибок привязки тоже нет. - person rolls; 01.04.2019
comment
Похоже, ваш XAML настроен неправильно. Попробуйте использовать свой собственный TemplateSelector. Если это не сработает, убедитесь, что XAML, который, по вашему мнению, используется, на самом деле используется, изменив другие свойства, такие как цвет, шрифт и т. Д. - person Mark A. Donohoe; 02.04.2019

Простое решение:

<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.

person H.B.    schedule 12.01.2011
comment
Я хотел использовать два шаблона, чтобы они были разделены. Я использовал код из образца проекта с этого сайта: developmentfor.net/net /dynamically-switch-wpf-datatemplate.html. Но хотя это сработало для ListBox, это не сработало для ComboBox. Твоя последняя фраза решила это или я. Выбранный элемент в ComboBox не имеет IsSelected = True, но имеет значение NULL. Смотрите мою правку выше, чтобы узнать, как я ее решил. Большое спасибо! - person Peter; 14.01.2011
comment
Рад, что это было полезно, хотя это было не совсем то, о чем вы просили. Я не знал о нулевой вещи и до того, как попытался ответить на ваш вопрос, я экспериментировал и узнал об этом таким образом. - person H.B.; 14.01.2011
comment
IsSelected не может иметь значение NULL и поэтому никогда не может быть действительно NULL. Вам не нужен Path=IsSelected, потому что проверки NULL для окружающего ComboBoxItem вполне достаточно. - person springy76; 04.09.2014
comment
Иногда короткий текст не отображается для меня, даже если установлено свойство ShortName и OnPropertyChanged и т. Д. Вы должны получить ошибку привязки? Это появляется всякий раз, когда поле короткого имени переходит из пустого (не отображается должным образом) в заполненное, а также при запуске System.Windows.Data Ошибка: 4: не удается найти источник для привязки со ссылкой 'RelativeSource FindAncestor, AncestorType =' System.Windows .Controls.ComboBoxItem ', AncestorLevel =' 1 ''. BindingExpression: (нет пути); DataItem = null; целевой элемент - ContentControl (Name = ''); целевое свойство - 'NoTarget' (тип 'Object') - person Simon F; 27.04.2015
comment
@SimonF: Я понятия не имею, каковы ваши конкретные обстоятельства, поэтому я не могу дать вам никаких советов. У меня с этим проблем не было, привязки абсолютно стандартные. Разве вы не используете подход Artiom? (Как вы упомянули ShortName.) - person H.B.; 27.04.2015
comment
@ H.B. Я делаю это по-вашему + используя предложение springy76. Если у вас нет такой же ошибки привязки, то это может быть хорошим местом для меня, чтобы начать поиск. - person Simon F; 28.04.2015
comment
@ Питер, смотри мое дополнение. (Да, я знаю, что это устарело, но в будущем вам может понравиться мой ответ.) - person Mark A. Donohoe; 29.10.2015
comment
Я действительно предпочитаю делать так, потому что в целом это намного меньше кода. И это просто увидеть в шаблоне xaml. Спасибо, что поделились этим HB! - person James McDuffie; 15.08.2017

Я собирался предложить использовать комбинацию ItemTemplate для элементов со списком с параметром Text в качестве выбора заголовка, но я вижу, что ComboBox не учитывает параметр Text.

Я имел дело с чем-то похожим, переопределив ComboBox ControlTemplate. Вот веб-сайт MSDN с образцом для .NET 4.0.

В своем решении я изменяю ContentPresenter в шаблоне ComboBox, чтобы он привязался к 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"/>

С помощью этой привязки я могу управлять отображением выбора Combo напрямую через параметр Text в элементе управления (который я привязываю к соответствующему значению в моей ViewModel).

person cunningdave    schedule 12.01.2011
comment
Не совсем уверен, что это то, что я ищу. Мне нужен вид ComboBox, который не является «активным» (т.е. пользователь не нажимал на него, он не «открыт»), чтобы отображался только один фрагмент текста. Но затем, когда пользователь нажимает на него, он должен открываться / раскрывающийся список, и каждый элемент должен отображать два фрагмента текста (таким образом, другой шаблон). - person Peter; 14.01.2011
comment
Если вы поэкспериментируете с приведенным выше кодом, я думаю, вы добьетесь того, чего хотите. Установив этот шаблон элемента управления, вы можете управлять свернутым текстом комбо с помощью его свойства Text (или любого другого свойства, которое вам нравится), что позволяет отображать простой невыделенный текст. Вы можете изменить тексты отдельных элементов, указав ItemTemplate при создании поля со списком. (ItemTemplate предположительно будет иметь стековую панель и два TextBlocks или любой другой формат, который вам нравится.) - person cunningdave; 18.01.2011

Я использовал следующий подход

 <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, но вы можете исправить его, если хотите

person Artiom    schedule 30.04.2013

В дополнение к тому, что сказал H.B. answer, ошибки привязки можно избежать с помощью конвертера. Следующий пример основан на решении, отредактированном самим 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 # 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();
    }
}
person raf    schedule 06.07.2018