web-dev-qa-db-fra.com

WPF: liaison d'un ContextMenu à une commande MVVM

Disons que j'ai une fenêtre avec une propriété renvoyant une commande (en fait, c'est un UserControl avec une commande dans une classe ViewModel, mais gardons les choses aussi simples que possible pour reproduire le problème).

Les oeuvres suivantes:

<Window x:Class="Window1" ... x:Name="myWindow">
    <Menu>
        <MenuItem Command="{Binding MyCommand, ElementName=myWindow}" Header="Test" />
    </Menu>
</Window>

Mais ce qui suit ne fonctionne pas.

<Window x:Class="Window1" ... x:Name="myWindow">
    <Grid>
        <Grid.ContextMenu>
            <ContextMenu>
                <MenuItem Command="{Binding MyCommand, ElementName=myWindow}" Header="Test" />
            </ContextMenu>            
        </Grid.ContextMenu>
    </Grid>
</Window>

Le message d'erreur que je reçois est

Erreur System.Windows.Data: 4: impossible de trouver la source de liaison avec la référence "ElementName = myWindow". BindingExpression: Path = MyCommand; DataItem = null; l'élément cible est 'MenuItem' (Name = ''); la propriété cible est 'Command' (type 'ICommand')

Pourquoi? Et comment puis-je résoudre ce problème? L'utilisation de DataContext n'est pas une option, car ce problème se produit en bas de l'arborescence visuelle où le DataContext contient déjà les données réelles affichées. J'ai déjà essayé d'utiliser {RelativeSource FindAncestor, ...} à la place, mais cela génère un message d'erreur similaire.

29
Heinzi

Le problème est que le ContextMenu n'est pas dans l'arborescence visuelle, vous devez donc essentiellement dire au menu Context quel contexte de données utiliser.

Découvrez ce blog avec une très belle solution de Thomas Levesque.

Il crée un proxy de classe qui hérite de Freezable et déclare une propriété de dépendance de données.

public class BindingProxy : Freezable
{
    protected override Freezable CreateInstanceCore()
    {
        return new BindingProxy();
    }

    public object Data
    {
        get { return (object)GetValue(DataProperty); }
        set { SetValue(DataProperty, value); }
    }

    public static readonly DependencyProperty DataProperty =
        DependencyProperty.Register("Data", typeof(object), typeof(BindingProxy), new UIPropertyMetadata(null));
}

Ensuite, il peut être déclaré dans le XAML (à un endroit de l'arborescence visuelle où le DataContext correct est connu):

<Grid.Resources>
    <local:BindingProxy x:Key="Proxy" Data="{Binding}" />
</Grid.Resources>

Et utilisé dans le menu contextuel en dehors de l'arborescence visuelle:

<ContextMenu>
    <MenuItem Header="Test" Command="{Binding Source={StaticResource Proxy}, Path=Data.MyCommand}"/>
</ContextMenu>
42
Daniel

Vive web.archive.org ! Voici le blog manquant :

Liaison à un MenuItem dans un menu contextuel WPF

Mercredi 29 octobre 2008 - jtango18

Parce qu'un ContextMenu dans WPF n'existe pas dans l'arborescence visuelle de votre page/fenêtre/contrôle en soi, la liaison de données peut être un peu délicate. J'ai cherché haut et bas sur le Web pour cela, et la réponse la plus courante semble être "faites-le simplement dans le code derrière". FAUX! Je ne suis pas venu dans le monde merveilleux de XAML pour recommencer à faire des choses dans le code derrière.

Voici mon exemple qui vous permettra de vous lier à une chaîne qui existe en tant que propriété de votre fenêtre.

public partial class Window1 : Window
{
    public Window1()
    {
        MyString = "Here is my string";
    }

    public string MyString
    {
        get;
        set;

    }
}

    <Button Content="Test Button" Tag="{Binding RelativeSource={RelativeSource AncestorType={x:Type Window}}}">
        <Button.ContextMenu>
            <ContextMenu DataContext="{Binding Path=PlacementTarget.Tag, RelativeSource={RelativeSource Self}}" >
                <MenuItem Header="{Binding MyString}"/>
            </ContextMenu>
        </Button.ContextMenu>
    </Button>

La partie importante est la balise sur le bouton (bien que vous puissiez tout aussi facilement définir le DataContext du bouton). Cela stocke une référence à la fenêtre parent. Le ContextMenu est capable d'y accéder via sa propriété PlacementTarget. Vous pouvez ensuite passer ce contexte à travers vos éléments de menu.

J'admets que ce n'est pas la solution la plus élégante au monde. Cependant, il bat la mise en place de choses dans le code derrière. Si quelqu'un a une meilleure façon de le faire, j'aimerais l'entendre.

16
mydogisbox

J'ai découvert que cela ne fonctionnait pas pour moi en raison de l'imbrication de l'élément de menu, ce qui signifie que je devais parcourir un "Parent" supplémentaire pour trouver le PlacementTarget.

Une meilleure façon est de trouver le ContextMenu lui-même en tant que RelativeSource, puis de simplement le lier à la cible de placement de cela. De plus, comme la balise est la fenêtre elle-même et que votre commande se trouve dans le modèle d'affichage, vous devez également définir le DataContext.

Je me suis retrouvé avec quelque chose comme ça

<Window x:Class="Window1" ... x:Name="myWindow">
...
    <Grid Tag="{Binding ElementName=myWindow}">
        <Grid.ContextMenu>
            <ContextMenu>
                <MenuItem Command="{Binding PlacementTarget.Tag.DataContext.MyCommand, 
                                            RelativeSource={RelativeSource Mode=FindAncestor,                                                                                         
                                                                           AncestorType=ContextMenu}}"
                          Header="Test" />
            </ContextMenu>
        </Grid.ContextMenu>
    </Grid>
</Window>

Cela signifie que si vous vous retrouvez avec un menu contextuel compliqué avec des sous-menus, etc., vous n'avez pas besoin de continuer à ajouter "Parent" à chaque niveau de commandes.

-- ÉDITER --

Nous avons également proposé cette alternative pour définir une balise sur chaque ListBoxItem qui se lie à Window/Usercontrol. J'ai fini par le faire parce que chaque ListBoxItem était représenté par leur propre ViewModel mais j'avais besoin des commandes de menu pour exécuter via le ViewModel de niveau supérieur pour le contrôle, mais passez leur liste ViewModel en tant que paramètre.

<ContextMenu x:Key="BookItemContextMenu" 
             Style="{StaticResource ContextMenuStyle1}">

    <MenuItem Command="{Binding Parent.PlacementTarget.Tag.DataContext.DoSomethingWithBookCommand,
                        RelativeSource={RelativeSource Mode=FindAncestor,
                        AncestorType=ContextMenu}}"
              CommandParameter="{Binding}"
              Header="Do Something With Book" />
    </MenuItem>>
</ContextMenu>

...

<ListView.ItemContainerStyle>
    <Style TargetType="{x:Type ListBoxItem}">
        <Setter Property="ContextMenu" Value="{StaticResource BookItemContextMenu}" />
        <Setter Property="Tag" Value="{Binding ElementName=thisUserControl}" />
    </Style>
</ListView.ItemContainerStyle>
8
nrjohnstone

Voir cet article de Justin Taylor pour une solution de contournement.

Mise à jour
Malheureusement, le blog référencé n'est plus disponible. J'ai essayé d'expliquer la procédure dans une autre réponse SO. Il peut être trouvé ici .

6
HCL

Basé sur réponse HCL , voici ce que j'ai fini par utiliser:

<Window x:Class="Window1" ... x:Name="myWindow">
    ...
    <Grid Tag="{Binding ElementName=myWindow}">
        <Grid.ContextMenu>
            <ContextMenu>
                <MenuItem Command="{Binding Parent.PlacementTarget.Tag.MyCommand, 
                                            RelativeSource={RelativeSource Self}}"
                          Header="Test" />
            </ContextMenu>
        </Grid.ContextMenu>
    </Grid>
</Window>
4
Heinzi

Si (comme moi) vous avez une aversion pour les expressions de liaison laides et complexes, voici une solution simple à ce code. Cette approche vous permet toujours de conserver des déclarations de commandes propres dans votre XAML.

XAML:

<ContextMenu ContextMenuOpening="ContextMenu_ContextMenuOpening">
    <MenuItem Command="Save"/>
    <Separator></Separator>
    <MenuItem Command="Close"/>
    ...

Code derrière:

private void ContextMenu_ContextMenuOpening(object sender, ContextMenuEventArgs e)
{
    foreach (var item in (sender as ContextMenu).Items)
    {
        if(item is MenuItem)
        {
           //set the command target to whatever you like here
           (item as MenuItem).CommandTarget = this;
        } 
    }
}
2
Tom Makin