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

69

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 der Adresse sehen.

Wenn ich jedoch einen Kunden auswähle, möchte ich nur den Namen in der ComboBox anzeigen. 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 wichtige Teil, um es zum Laufen zu bringen, war Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ComboBoxItem}}, Path=IsSelected}" Value="{x:Null}"(der Teil, in dem der Wert x: Null sein sollte, nicht True).

Peter
quelle
2
Ihre Lösung funktioniert, aber im Ausgabefenster werden Fehler angezeigt. 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')
user11909
1
Ich erinnere mich, dass ich diese Fehler auch gesehen habe. Aber ich bin nicht mehr im Projekt (oder sogar in der Firma), also kann ich das leider nicht überprüfen.
Peter
Die Erwähnung des Bindungspfads im DataTrigger ist nicht erforderlich. Wenn das ComboBoxItem ausgewählt wird, wird eine andere Vorlage auf das Steuerelement angewendet, und die DataTrigger-Bindung kann in ihrem Elementbaum keinen Vorfahren vom Typ ComboBoxItem mehr finden. Somit ist der Vergleich mit null immer erfolgreich. Dieser Ansatz funktioniert, da der visuelle Baum des ComboBoxItem unterschiedlich ist, je nachdem, ob er ausgewählt oder im Popup angezeigt wird.
Dennis Kassel

Antworten:

66

Es gibt zwei Probleme bei der Verwendung der oben genannten DataTrigger / Binding-Lösung. Das erste ist, dass Sie tatsächlich eine verbindliche Warnung erhalten, 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 überfüllt und für eine ComboBox spezifisch gemacht haben.

Die Lösung, die ich vorstelle, folgt besser den WPF-Entwürfen, indem sie eine verwendet, DataTemplateSelectorfür die Sie separate Vorlagen mit ihren Eigenschaften SelectedItemTemplateund DropDownItemsTemplateEigenschaften sowie Auswahlvarianten 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 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); 
    }
}

Hinweis: Der Einfachheit halber verwendet mein Beispielcode hier das neue '?' Merkmal von C # 6 (VS 2015). Wenn Sie eine ältere Version verwenden, entfernen Sie einfach das '?' und prüfen Sie explizit auf null, bevor Sie oben 'SelectTemplate' aufrufen, und geben Sie andernfalls null zurück, wie folgt:

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

Ich habe auch eine Markup-Erweiterung eingefügt, die die obige Klasse einfach 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"

Hinweis: 'is:' Hier ist meine XML-Zuordnung, wo ich die Klasse in Code eingefügt habe. 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 auch DataTemplateSelectors verwenden, wenn Sie ...

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

Oder mischen und anpassen! 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 keinen TemplateSelector angeben, wird einfach wie erwartet wieder auf die reguläre Auflösung von Datenvorlagen basierend auf Datentypen zurückgegriffen. So wird beispielsweise im folgenden Fall die Vorlage des ausgewählten Elements explizit festgelegt, aber die Dropdown-Liste erbt die Datenvorlage, die für den Datentyp des Objekts im Datenkontext gilt.

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

Genießen!

Mark A. Donohoe
quelle
Sehr cool. Und ich habe tatsächlich diese verbindlichen Warnungen (habe nie herausgefunden, woher sie kommen, habe aber auch nicht wirklich nachgesehen). Ich kann es jetzt wirklich ausprobieren, aber ich könnte es in Zukunft tun.
Peter
Ich bin froh, Ihnen behilflich zu sein. Wenn Sie dies in Ihrem Code verwenden, verwendet die return-Anweisung ( return inDropDownoben) das neue C # 6? Wenn Sie VS 2015 nicht verwenden, entfernen Sie einfach das '?' und vor dem Aufruf explizit auf Nullen prüfen SelectTemplate. Ich werde das dem Code hinzufügen.
Mark A. Donohoe
3
Ich nehme Ihnen den Hut ab, um eine wirklich wiederverwendbare Lösung zu finden!
Henon
Vielen Dank! Ich weis das zu schätzen. Wenn du kannst, stimme bitte ab!
Mark A. Donohoe
Aus irgendeinem Grund, wenn ich diese Lösung implementiere, wird der ComboBoxTemplateSelector-Code nie ausgeführt, es gibt auch keine Bindungsfehler.
rollt
34

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 im Feld ausgewählt und angezeigt wird, und nicht die Liste nicht in einem befindet, ComboBoxItemdaher ist der Auslöser aktiviert. Null)

Wenn Sie die gesamte Vorlage austauschen möchten, können Sie dies auch tun, indem Sie den Auslöser verwenden, um z. B. eine andere ContentTemplateauf a anzuwendenContentControl . Auf diese Weise können Sie auch eine standardbasierte DataTypeVorlagenauswahl beibehalten, wenn Sie nur die Vorlage für diesen ausgewählten 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 die Antwort von MarqueIV .

HB
quelle
Ich wollte zwei Vorlagen verwenden, um sie getrennt zu halten. Ich habe Code aus einem Beispielprojekt von dieser Site verwendet: developmentfor.net/net/dynamically-switch-wpf-datatemplate.html . Aber während es für eine ListBox funktionierte, funktionierte es nicht für eine ComboBox. Dein letzter Satz hat es oder mich gelöst. Das ausgewählte Element in einer ComboBox hat nicht IsSelected = True, aber es ist Null. In meinem obigen Abschnitt finden Sie den vollständigen Code, wie ich ihn gelöst habe. Vielen Dank!
Peter
Ich bin froh, dass es nützlich war, obwohl es nicht genau das war, wonach Sie gefragt haben. Ich wusste nichts über das Null-Ding, bevor ich versuchte, Ihre Frage zu beantworten. Ich experimentierte und fand es auf diese Weise heraus.
HB
4
IsSelectedist nicht nullbar und kann daher niemals wirklich NULL sein. Sie brauchen nicht Path=IsSelected, weil die NULL-Prüfung für ein umgebendes ComboBoxItem völlig ausreichend ist.
springy76
Manchmal wird der Kurztext für mich nicht angezeigt, obwohl die ShortName-Eigenschaft festgelegt und OnPropertyChanged usw. festgelegt sind. Sollten Sie einen Bindungsfehler erhalten? Dies wird immer dann angezeigt, wenn das Kurznamenfeld von leer (nicht richtig angezeigt) in ausgefüllt sowie beim Start "System.Windows.Data-Fehler: 4: Quelle für Bindung mit Referenz 'RelativeSource FindAncestor, AncestorType =' System kann nicht gefunden werden. Windows.Controls.ComboBoxItem ', AncestorLevel =' 1 ''. BindingExpression: (kein Pfad); DataItem = null; Zielelement ist 'ContentControl' (Name = ''); Zieleigenschaft ist 'NoTarget' (Typ 'Object') "
Simon F
@ SimonF: Ich habe keine Ahnung, wie Ihre konkreten Umstände sind, daher kann ich Ihnen keinen Rat geben. Ich hatte keine Probleme damit, die Bindungen sind absolut Standard. Verwenden Sie nicht den ArtiomAnsatz? (Wie Sie erwähnen ShortName.)
HB
1

Ich wollte vorschlagen, die Kombination einer ItemTemplate für die Kombinationselemente mit dem Textparameter als Titelauswahl zu verwenden, aber ich sehe, dass ComboBox den Textparameter nicht berücksichtigt.

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 die ContentTemplate an eine einfache DataTemplate gebunden ist, die einen TextBlock wie folgt 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 Anzeige der Kombinationsauswahl direkt über den Parameter Text auf dem Steuerelement steuern (den ich an einen geeigneten Wert in meinem ViewModel binde).

Listdave
quelle
Ich bin mir nicht ganz sicher, wonach ich suche. Ich möchte, dass das Aussehen einer ComboBox, die nicht 'aktiv' ist (dh der Benutzer hat nicht darauf geklickt, es ist nicht 'offen'), nur einen Text zeigt. Wenn der Benutzer darauf klickt, sollte es geöffnet / geöffnet werden und jedes Element sollte zwei Textteile enthalten (also eine andere Vorlage).
Peter
Wenn Sie mit dem obigen Code experimentieren, werden Sie wahrscheinlich dahin gelangen, wo Sie hin möchten. Durch Festlegen dieser Steuerungsvorlage können Sie den reduzierten Text der Kombination über die Eigenschaft Text (oder eine beliebige Eigenschaft, die Sie möchten) steuern und so Ihren einfachen, nicht ausgewählten Text anzeigen. Sie können die einzelnen Elementtexte ändern, indem Sie beim Erstellen Ihrer Combobox die ItemTemplate angeben. (Die ItemTemplate würde vermutlich ein Stackpanel und zwei TextBlocks haben, oder was auch immer Sie formatieren möchten.)
Cunningdave
1

Ich habe den nächsten Ansatz gewählt

 <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 das geladene Ereignis hier nicht so sehr, aber Sie können es beheben, wenn Sie möchten

Artiom
quelle
1

Zusätzlich zu den Antworten von HB kann der Bindungsfehler mit einem Konverter vermieden werden. Das folgende Beispiel basiert auf der vom OP selbst bearbeiteten Lösung .

Die Idee ist sehr einfach: Binden Sie an etwas, das immer existiert ( Control), und führen Sie die entsprechende Überprüfung im Konverter durch. Der relevante Teil der modifizierten XAML ist der folgende. Bitte beachten Sie, dass dies Path=IsSelectednie wirklich benötigt wurde und ComboBoxItemdurch ersetzt wird Control, 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 # -Konvertercode 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();
    }
}
raf
quelle
0

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

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

CodeWarrior
quelle
Ich habe das anhand von Beispielen versucht, die ich auch hier gefunden habe ( developmentfor.net/net/dynamically-switch-wpf-datatemplate.html ), fand es aber nicht so wiederverwendbar, und ich wollte dies nur in XAML lösen.
Peter