102 votes

Déclenchement d'un événement de double-clic à partir d'un élément de ListView WPF en utilisant MVVM

Dans une application WPF utilisant MVVM, j'ai un usercontrol avec un élément listview. Au moment de l'exécution, il utilisera la liaison de données pour remplir la liste avec une collection d'objets.

Quelle est la bonne façon d'attacher un événement de double-clic aux éléments de la vue de liste de sorte que lorsqu'un élément de la vue de liste est double-cliqué, un événement correspondant dans le modèle de vue est déclenché et a une référence à l'élément cliqué ?

Comment le faire d'une manière propre à MVVM, c'est-à-dire sans code derrière dans la vue ?

79voto

jbe Points 4629

S'il vous plaît, le code derrière n'est pas une mauvaise chose du tout. Malheureusement, de nombreuses personnes de la communauté WPF se sont trompées à ce sujet.

MVVM n'est pas un modèle permettant d'éliminer le code derrière. Il s'agit de séparer la partie vue (apparence, animations, etc.) de la partie logique (flux de travail). En outre, vous êtes en mesure de tester unitairement la partie logique.

Je connais suffisamment de scénarios où vous devez écrire du code derrière parce que la liaison de données n'est pas une solution à tout. Dans votre scénario, je traiterais l'événement DoubleClick dans le fichier de code arrière et je déléguerais cet appel au ViewModel.

Des exemples d'applications qui utilisent le code derrière et qui respectent toujours la séparation MVVM peuvent être trouvés ici :

Win Application Framework (WAF) - https://github.com/jbe2277/waf

5 votes

Bien dit, je refuse d'utiliser tout ce code et une DLL supplémentaire juste pour faire un double-clic !

4 votes

Cette histoire de reliure à usage unique me donne un vrai mal de tête. C'est comme si on me demandait de coder avec un bras, un œil sur un cache-œil et en se tenant sur une jambe. Le double clic devrait être simple, et je ne vois pas en quoi tout ce code supplémentaire en vaut la peine.

1 votes

Je crains de ne pas être totalement d'accord avec vous. Si vous dites "le code derrière n'est pas mauvais", alors j'ai une question à ce sujet : Pourquoi ne pas déléguer l'événement de clic pour le bouton, mais souvent utiliser la liaison (en utilisant la propriété Command) à la place ?

77voto

rushui Points 682

J'ai réussi à le faire fonctionner avec .NET 4.5. Cela semble simple et ne nécessite aucune tierce partie ou code derrière.

<ListView ItemsSource="{Binding Data}">
        <ListView.ItemsPanel>
            <ItemsPanelTemplate>
                <StackPanel Orientation="Horizontal"/>
            </ItemsPanelTemplate>
        </ListView.ItemsPanel>
        <ListView.ItemTemplate>
            <DataTemplate>
                <Grid Margin="2">
                    <Grid.InputBindings>
                        <MouseBinding Gesture="LeftDoubleClick" Command="{Binding ShowDetailCommand}"/>
                    </Grid.InputBindings>
                    <Grid.RowDefinitions>
                        <RowDefinition/>
                        <RowDefinition/>
                    </Grid.RowDefinitions>
                    <Image Source="..\images\48.png" Width="48" Height="48"/>
                    <TextBlock Grid.Row="1" Text="{Binding Name}" />
                </Grid>
            </DataTemplate>
        </ListView.ItemTemplate>
    </ListView>

2 votes

Cela ne semble pas fonctionner pour toute la zone, par exemple, je le fais sur un panneau de dock et cela ne fonctionne que lorsqu'il y a quelque chose à l'intérieur du panneau de dock (par exemple, un bloc de texte, une image) mais pas l'espace vide.

3 votes

OK - encore cette vieille histoire... il faut que l'arrière-plan soit transparent pour recevoir les événements liés à la souris, comme indiqué dans le tableau ci-dessous. stackoverflow.com/questions/7991314/

7 votes

Je me grattais la tête en essayant de comprendre pourquoi ça marchait pour vous tous et pas pour moi. J'ai soudain réalisé que dans le contexte du modèle d'élément, le contexte de données est l'élément actuel de l'itemssource et non le modèle de vue de la fenêtre principale. J'ai donc utilisé ce qui suit pour que cela fonctionne <MouseBinding MouseAction="LeftDoubleClick" Command="{Binding Path=DataContext.EditBandCommand, RelativeSource={RelativeSource AncestorType={x:Type Window}}"/> Dans mon cas, l'EditBandCommand est la commande sur le modèle de vue de la page et non sur l'entité liée.

44voto

rmoore Points 9561

J'aime utiliser Comportements des commandes attachées et Commandes. Marlon Grech a une très bonne implémentation des comportements de commande attachés. À l'aide de ces derniers, nous pourrions alors attribuer un style à l'élément ListView ItemContainerStyle qui définira la commande pour chaque ListViewItem.

Ici, nous définissons la commande à lancer lors de l'événement MouseDoubleClick, et le CommandParameter sera l'objet de données sur lequel nous cliquerons. Ici, je remonte l'arbre visuel pour obtenir la commande que j'utilise, mais vous pouvez tout aussi bien créer des commandes à l'échelle de l'application.

<Style x:Key="Local_OpenEntityStyle"
       TargetType="{x:Type ListViewItem}">
    <Setter Property="acb:CommandBehavior.Event"
            Value="MouseDoubleClick" />
    <Setter Property="acb:CommandBehavior.Command"
            Value="{Binding ElementName=uiEntityListDisplay, Path=DataContext.OpenEntityCommand}" />
    <Setter Property="acb:CommandBehavior.CommandParameter"
            Value="{Binding}" />
</Style>

Pour les commandes, vous pouvez soit implémenter une fonction ICommand directement, ou utiliser certains des assistants comme ceux qui viennent dans le Boîte à outils MVVM .

1 votes

+1 C'est la solution que je préfère lorsque je travaille avec Composite Application Guidance for WPF (Prism).

1 votes

A quoi correspond l'espace de nom 'acb:' dans votre exemple de code ci-dessus ?

0 votes

@NamGiVU acb: = AttachedCommandBehavior. Le code se trouve dans le premier lien de la réponse

14voto

Gunter Points 186

J'ai trouvé un moyen très simple et propre de le faire avec les déclencheurs d'événements du SDK Blend. Propre MVVM, réutilisable et sans code-behind.

Vous avez probablement déjà quelque chose comme ça :

<Style x:Key="MyListStyle" TargetType="{x:Type ListViewItem}">

Incluez maintenant un ControlTemplate pour le ListViewItem comme ceci si vous n'en utilisez pas déjà un :

<Setter Property="Template">
  <Setter.Value>
    <ControlTemplate TargetType="{x:Type ListViewItem}">
      <GridViewRowPresenter Content="{TemplateBinding Content}"
                            Columns="{TemplateBinding GridView.ColumnCollection}" />
    </ControlTemplate>
  </Setter.Value>
 </Setter>

Le GridViewRowPresenter sera la racine visuelle de tous les éléments "intérieurs" constituant un élément de ligne de liste. Nous pourrions maintenant y insérer un déclencheur pour rechercher les événements routés MouseDoubleClick et appeler une commande via InvokeCommandAction comme ceci :

<Setter Property="Template">
  <Setter.Value>
    <ControlTemplate TargetType="{x:Type ListViewItem}">
      <GridViewRowPresenter Content="{TemplateBinding Content}"
                            Columns="{TemplateBinding GridView.ColumnCollection}">
        <i:Interaction.Triggers>
          <i:EventTrigger EventName="MouseDoubleClick">
            <i:InvokeCommandAction Command="{Binding DoubleClickCommand}" />
          </i:EventTrigger>
        </i:Interaction.Triggers>
      </GridViewRowPresenter>
    </ControlTemplate>
  </Setter.Value>
 </Setter>

Si vous avez des éléments visuels "au-dessus" du GridRowPresenter (probablement en commençant par une grille), vous pouvez également y placer le déclencheur.

Malheureusement, les événements MouseDoubleClick ne sont pas générés par tous les éléments visuels (ils le sont par les contrôles, mais pas par les FrameworkElements, par exemple). Une solution de contournement consiste à dériver une classe de EventTrigger et à rechercher les MouseButtonEventArgs dont le nombre de clics est égal à 2. Cela permet de filtrer efficacement tous les événements qui ne sont pas des boutons de souris et tous les événements MouseButtonEvents dont le nombre de clics != 2.

class DoubleClickEventTrigger : EventTrigger
{
    protected override void OnEvent(EventArgs eventArgs)
    {
        var e = eventArgs as MouseButtonEventArgs;
        if (e == null)
        {
            return;
        }
        if (e.ClickCount == 2)
        {
            base.OnEvent(eventArgs);
        }
    }
}

Maintenant nous pouvons écrire ceci ('h' est le Namespace de la classe d'aide ci-dessus) :

<Setter Property="Template">
  <Setter.Value>
    <ControlTemplate TargetType="{x:Type ListViewItem}">
      <GridViewRowPresenter Content="{TemplateBinding Content}"
                            Columns="{TemplateBinding GridView.ColumnCollection}">
        <i:Interaction.Triggers>
          <h:DoubleClickEventTrigger EventName="MouseDown">
            <i:InvokeCommandAction Command="{Binding DoubleClickCommand}" />
          </h:DoubleClickEventTrigger>
        </i:Interaction.Triggers>
      </GridViewRowPresenter>
    </ControlTemplate>
  </Setter.Value>
 </Setter>

0 votes

Comme je l'ai découvert, si vous placez le déclencheur directement sur le GridViewRowPresenter, il peut y avoir un problème. Les espaces vides entre les colonnes ne reçoivent probablement pas du tout les événements de la souris (une solution de contournement serait probablement de leur donner un style avec un alignement extensible).

0 votes

Dans ce cas, il est probablement préférable de placer une grille vide autour du GridViewRowPresenter et d'y placer le déclencheur. Cela semble fonctionner.

1 votes

Notez que vous perdez le style par défaut pour le ListViewItem si vous remplacez le modèle comme ceci. Cela n'avait pas d'importance pour l'application sur laquelle je travaillais, car elle utilisait de toute façon un style fortement personnalisé.

6voto

Aaron Points 51

Je réalise que cette discussion date d'un an, mais avec .NET 4, y a-t-il des réflexions sur cette solution ? Je suis tout à fait d'accord que le but de MVVM n'est PAS d'éliminer le code derrière le fichier. Je suis également convaincu que ce n'est pas parce que quelque chose est compliqué que c'est mieux. Voici ce que j'ai mis dans le code derrière :

    private void ButtonClick(object sender, RoutedEventArgs e)
    {
        dynamic viewModel = DataContext;
        viewModel.ButtonClick(sender, e);
    }

12 votes

Le modèle de vue doit avoir des noms représentant les actions que vous pouvez effectuer dans votre domaine. Qu'est-ce qu'une action "ButtonClick" dans votre domaine ? Le ViewModel représente la logique du domaine dans un contexte convivial pour la vue, il n'est pas seulement une aide pour la vue. Ainsi : ButtonClick ne devrait jamais être sur le viewmodel, utilisez plutôt viewModel.DeleteSelectedCustomer ou ce que cette action représente réellement.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X