it-swarm.com.de

Kann ich für das ausgewählte Element in einer WPF-ComboBox eine andere Vorlage als für die Elemente im Dropdown-Teil verwenden?

Ich habe eine WPF-Combobox, die beispielsweise mit Kundenobjekten gefüllt ist. Ich habe eine DataTemplate:

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

Auf diese Weise kann ich beim Öffnen meiner ComboBox die verschiedenen Kunden mit ihrem Namen und darunter die Adresse sehen.

Wenn ich jedoch einen Kunden auswähle, möchte ich nur den Namen in der ComboBox anzeigen. So etwas wie:

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

Kann ich eine andere Vorlage für das ausgewählte Element in einer ComboBox auswählen?

Lösung

Mit Hilfe der Antworten habe ich es so gelöst:

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

Dann meine ComboBox:

<ComboBox ItemsSource="{Binding Customers}" 
                SelectedItem="{Binding SelectedCustomer}"
                ItemTemplate="{StaticResource CustomerTemplate}" />

Der wichtigste Teil, damit es funktioniert, war Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ComboBoxItem}}, Path=IsSelected}" Value="{x:Null}" (der Teil, dessen Wert x: Null sein sollte, nicht True).

56
Peter

Das Problem bei der Verwendung der oben genannten DataTrigger/Binding-Lösung ist zweifach. Zum einen erhalten Sie tatsächlich eine verbindliche Warnung, dass Sie die relative Quelle für das ausgewählte Element nicht finden können. Das größere Problem ist jedoch, dass Sie Ihre Datenvorlagen unübersichtlich gemacht und für eine ComboBox spezifisch gemacht haben.

Die von mir präsentierte Lösung folgt den WPF-Entwürfen besser, da sie einen DataTemplateSelector verwendet, für den Sie separate Vorlagen mithilfe der SelectedItemTemplate- und DropDownItemsTemplate-Eigenschaften sowie der Selektoren für beide angeben können.

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

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

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

        return inDropDown
            ? DropdownItemsTemplate ?? DropdownItemsTemplateSelector?.SelectTemplate(item, container)
            : SelectedItemTemplate  ?? SelectedItemTemplateSelector?.SelectTemplate(item, container); 
    }
}

Hinweis: Zur Vereinfachung verwendet mein Beispielcode hier das neue '?.' Merkmal von C # 6 (VS 2015). Wenn Sie eine ältere Version verwenden, entfernen Sie einfach das '?' und vor dem Aufruf von 'SelectTemplate' explizit nach null suchen und ansonsten null zurückgeben:

return inDropDown
    ? DropdownItemsTemplate ??
        ((DropdownItemsTemplateSelector != null)
            ? DropdownItemsTemplateSelector.SelectTemplate(item, container)
            : null)
    : SelectedItemTemplate ??
        ((SelectedItemTemplateSelector != null)
            ? SelectedItemTemplateSelector.SelectTemplate(item, container)
            : null)

Ich habe auch eine Markup-Erweiterung hinzugefügt, die einfach die obige Klasse erstellt und zur Vereinfachung in XAML zurückgibt.

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

Und so verwenden Sie es. Schön, sauber und klar und Ihre Vorlagen bleiben "rein"

Anmerkung: 'ist:' Hier ist meine Xmlns-Zuordnung, wo ich die Klasse in Code stecke. Stellen Sie sicher, dass Sie Ihren eigenen Namespace importieren und "is:" entsprechend ändern.

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

Sie können DataTemplateSelectors auch verwenden, wenn Sie ...

<ComboBox x:Name="MyComboBox"
    ItemsSource="{Binding Items}"
    ItemTemplateSelector="{is:ComboBoxTemplateSelector
        SelectedItemTemplateSelector={StaticResource MySelectedItemTemplateSelector},
        DropdownItemsTemplateSelector={StaticResource MyDropDownItemTemplateSelector}}" />

Oder mischen und zusammenpassen! Hier verwende ich eine Vorlage für das ausgewählte Element, aber eine Vorlagenauswahl für die DropDown-Elemente.

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

Wenn Sie für die ausgewählten Elemente oder Dropdown-Elemente keine Vorlage oder einen TemplateSelector angeben, wird außerdem wie üblich auf die reguläre Auflösung von Datenvorlagen basierend auf Datentypen zurückgegriffen. Im unteren Fall wird die Vorlage beispielsweise für das ausgewählte Element explizit festgelegt. Die Dropdown-Liste übernimmt jedoch die jeweils für den Datentyp des Objekts im Datenkontext zutreffende Datenvorlage.

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

Genießen!

46
MarqueIV

Einfache Lösung:

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

(Beachten Sie, dass sich das Element, das in der Box und nicht in der Liste ausgewählt und angezeigt wird, nicht in einem ComboBoxItem befindet, daher der Auslöser für Null).

Wenn Sie die gesamte Vorlage austauschen möchten, können Sie dies auch tun, indem Sie den Auslöser verwenden, um z. einen anderen ContentTemplate auf einen ContentControl anwenden. Auf diese Weise können Sie auch eine standardmäßige DataType-basierte Vorlagenauswahl beibehalten, wenn Sie nur die Vorlage für diesen selektiven Fall ändern, z.

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

Beachten Sie, dass diese Methode Bindungsfehler verursacht, da die relative Quelle für das ausgewählte Element nicht gefunden wird. Für einen alternativen Ansatz siehe MarqueIVs Antwort .

30
H.B.

Ich habe den nächsten Ansatz verwendet

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

Und das Verhalten

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

lief wie am Schnürchen. Ich mag nicht sehr viel geladenes Ereignis hier, aber Sie können es beheben, wenn Sie möchten

1
Artiom

Ich wollte vorschlagen, die Kombination eines ItemTemplate für die Combo-Elemente mit dem Text-Parameter als Titelauswahl zu verwenden, aber ich sehe, dass die ComboBox den Text-Parameter nicht beachtet.

Ich habe mich mit etwas Ähnlichem befasst, indem ich die ComboBox ControlTemplate überschrieben habe. Hier ist die MSDN Website mit einem Beispiel für .NET 4.0.

In meiner Lösung ändere ich den ContentPresenter in der ComboBox-Vorlage so, dass er an Text gebunden wird, wobei sein ContentTemplate an ein einfaches DataTemplate gebunden ist, das einen TextBlock enthält:

<DataTemplate x:Uid="DataTemplate_1" x:Key="ComboSelectionBoxTemplate">
    <TextBlock x:Uid="TextBlock_1" Text="{Binding}" />
</DataTemplate>

damit in der ControlTemplate:

<ContentPresenter Name="ContentSite" IsHitTestVisible="False" Content="{TemplateBinding Text}" ContentTemplate="{StaticResource ComboSelectionBoxTemplate}" Margin="3,3,23,3" VerticalAlignment="Center" HorizontalAlignment="Left"/>

Mit diesem Bindungslink kann ich die Combo-Auswahlanzeige direkt über den Text-Parameter des Steuerelements steuern (der an einen entsprechenden Wert in meinem ViewModel gebunden wird).

1
cunningdave

Zusätzlich zu dem, was von H.B. Antwort , der Bindungsfehler kann mit einem Konverter vermieden werden. Das folgende Beispiel basiert auf der Lösung, die vom OP selbst bearbeitet wurde.

Die Idee ist sehr einfach: Binden Sie an etwas, das schon immer existiert (Control), und prüfen Sie die entsprechende Überprüfung im Konverter. Der relevante Teil der modifizierten XAML ist folgender. Bitte beachten Sie, dass Path=IsSelected nie wirklich benötigt wurde und ComboBoxItem durch Control ersetzt wird, um Bindungsfehler zu vermeiden.

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

Der C # Converter-Code lautet wie folgt:

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

Ja. Mit einer Template Selector bestimmen Sie, welche Vorlage zur Laufzeit gebunden werden soll. Wenn also IsSelected = False, dann Verwenden Sie diese Vorlage. Wenn IsSelected = True ist, verwenden Sie diese andere Vorlage.

Hinweis: Sobald Sie den Vorlagen-Selektor implementiert haben, müssen Sie den Vorlagen Schlüsselnamen geben.

0
CodeWarrior