web-dev-qa-db-fra.com

Puis-je utiliser un modèle pour l'élément sélectionné dans une ComboBox WPF que pour les éléments de la partie déroulante?

J'ai une liste déroulante WPF qui contient, par exemple, des objets client. J'ai un DataTemplate:

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

Ainsi, lorsque j'ouvre ma ComboBox, je peux voir les différents clients avec leur nom et, en dessous, leur adresse.

Mais lorsque je sélectionne un client, je souhaite uniquement afficher le nom dans la zone de liste déroulante. Quelque chose comme:

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

Puis-je sélectionner un autre modèle pour l'élément sélectionné dans une ComboBox?

Solution

Avec l'aide des réponses, je l'ai résolu comme ceci:

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

Ensuite, ma ComboBox:

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

La partie importante pour que cela fonctionne est Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ComboBoxItem}}, Path=IsSelected}" Value="{x:Null}" (la partie où la valeur doit être x: Null, pas True).

56
Peter

Le problème lié à l’utilisation de la solution DataTrigger/Binding mentionnée ci-dessus est double. La première est que vous vous retrouvez avec un avertissement contraignant indiquant que vous ne pouvez pas trouver la source relative pour l'élément sélectionné. Le plus gros problème, cependant, est que vous avez encombré vos modèles de données et les avez rendus spécifiques à une ComboBox.

La solution que je présente suit mieux les conceptions WPF en ce qu’elle utilise un DataTemplateSelector sur lequel vous pouvez spécifier des modèles distincts à l’aide des propriétés SelectedItemTemplate et DropDownItemsTemplate, ainsi que des sélecteurs pour les deux.

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

Remarque: pour des raisons de simplicité, mon exemple de code utilise ici le nouveau "?". caractéristique de C # 6 (VS 2015). Si vous utilisez une version plus ancienne, supprimez simplement le "?" et vérifie explicitement la valeur null avant d'appeler 'SelectTemplate' ci-dessus et renvoie null sinon comme ceci:

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

J'ai également inclus une extension de balisage qui crée et renvoie simplement la classe ci-dessus pour plus de commodité en 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
        };
    }
}

Et voici comment vous l'utilisez. Nice, propre et clair et vos modèles restent «purs»

Note: 'is:' voici mon mapping xmlns pour où je mets la classe en code. Assurez-vous d'importer votre propre espace de noms et modifiez «est:» comme il convient.

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

Vous pouvez également utiliser DataTemplateSelectors si vous préférez ...

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

Ou mélanger et assortir! Ici, j'utilise un modèle pour l'élément sélectionné, mais un sélecteur de modèle pour les éléments DropDown.

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

En outre, si vous ne spécifiez pas de modèle ou de sélecteur de modèle pour les éléments sélectionnés ou les listes déroulantes, la résolution normale des modèles de données en fonction des types de données est rétablie, comme vous le souhaitiez. Ainsi, par exemple, dans le cas ci-dessous, le modèle de l'élément sélectionné a été défini explicitement, mais la liste déroulante héritera du modèle de données qui s'applique au type de données de l'objet dans le contexte de données.

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

Prendre plaisir!

46
MarqueIV

Solution simple:

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

(Notez que l'élément sélectionné et affiché dans la zone et non dans la liste ne se trouve pas dans une ComboBoxItem d'où le déclencheur sur Null)

Si vous souhaitez remplacer tout le modèle, vous pouvez également le faire en utilisant le déclencheur, par exemple. applique une ContentTemplate différente à une ContentControl . Cela vous permet également de conserver une sélection de modèle basée sur DataType par défaut si vous ne modifiez que le modèle pour ce cas sélectif, par exemple:

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

Notez que cette méthode provoquera des erreurs de liaison car la source relative n’est pas trouvée pour l’élément sélectionné. Pour une autre approche, voir Réponse de MarqueIV .

30
H.B.

J'ai utilisé l'approche suivante

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

Et le comportement

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

travaillé comme un charme. Je n'aime pas trop l'événement Loaded ici mais vous pouvez le réparer si vous voulez

1
Artiom

J'allais suggérer d'utiliser la combinaison d'un ItemTemplate pour les éléments combo, avec le paramètre Text comme sélection de titre, mais je constate que ComboBox ne respecte pas le paramètre Text.

J'ai traité avec quelque chose de similaire en remplaçant le ControlTemplate ComboBox. Voici le MSDN website avec un exemple pour .NET 4.0.

Dans ma solution, je modifie ContentPresenter dans le modèle ComboBox pour le lier à Text, son ContentTemplate étant lié à un simple DataTemplate contenant un TextBlock, comme suit:

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

avec ceci dans le ControlTemplate:

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

Grâce à ce lien, je peux contrôler l’affichage de la sélection combinée directement via le paramètre Text du contrôle (que je lie à une valeur appropriée sur mon ViewModel).

1
cunningdave

En plus de ce qui est dit par H.B. answer , l’erreur de reliure peut être évitée avec un convertisseur. L'exemple suivant est basé sur la solution éditée par le OP lui-même .

L'idée est très simple: lier à quelque chose qui existe toujours (Control) et faire la vérification correspondante à l'intérieur du convertisseur . Veuillez noter que Path=IsSelected n'a jamais été réellement nécessaire et que ComboBoxItem est remplacé par Control pour éviter les erreurs de liaison.

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

Le code du convertisseur C # est le suivant:

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

Oui. Vous utilisez un sélecteur de modèle pour déterminer le modèle à lier au moment de l'exécution. Ainsi, si IsSelected = False, utilisez ce modèle. Si IsSelected = True, utilisez cet autre modèle.

À noter: Une fois que vous avez implémenté votre sélecteur de modèles, vous devrez donner les noms clés des modèles.

0
CodeWarrior