Déclencher un événement de double-clic à partir d'un élément WPF ListView à l'aide de MVVM

102

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

Quelle est la manière correcte 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 comporte une référence à l'élément sur lequel l'utilisateur clique?

Comment cela peut-il être fait d'une manière MVVM propre, c'est-à-dire sans code derrière dans la vue?

Emad Gabriel
la source

Réponses:

76

S'il vous plaît, le code derrière n'est pas du tout une mauvaise chose. Malheureusement, de nombreux membres de la communauté WPF se sont trompés.

MVVM n'est pas un modèle pour éliminer le code derrière. Il s'agit de séparer la partie vue (apparence, animations, etc.) de la partie logique (workflow). De plus, vous pouvez tester unitaire la partie logique.

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

Des exemples d'applications qui utilisent le code derrière tout en respectant la séparation MVVM peuvent être trouvés ici:

Cadre d'application WPF (WAF) - http://waf.codeplex.com

jbe
la source
5
Bien dit, je refuse d'utiliser tout ce code et une DLL supplémentaire juste pour faire un double-clic!
Eduardo Molteni
4
Cette seule utilisation de Binding me donne un vrai mal de tête. C'est comme être invité à coder avec 1 bras, 1 œil sur un cache-œil et se tenir debout sur 1 jambe. Le double-clic devrait être simple et je ne vois pas en quoi tout ce code supplémentaire en vaut la peine.
Echiban
1
J'ai bien peur 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 déléguons-nous pas l'événement de clic pour le bouton mais en utilisant souvent la liaison (en utilisant la propriété Command) à la place?
Nam G VU
21
@Nam Gi VU: Je préférerais toujours une liaison de commande lorsqu'elle est prise en charge par le contrôle WPF. Une liaison de commande fait plus que simplement relayer l'événement 'Click' au ViewModel (par exemple CanExecute). Mais les commandes ne sont disponibles que pour les scénarios les plus courants. Pour d'autres scénarios, nous pouvons utiliser le fichier code-behind et là, nous déléguons les préoccupations non liées à l'interface utilisateur au ViewModel ou au Model.
jbe
2
Maintenant je te comprends plus! Belle discussion avec vous!
Nam G VU
73

Je peux faire fonctionner cela avec .NET 4.5. Semble simple et aucun tiers ou code derrière nécessaire.

<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>
Rushui Guan
la source
2
Cela ne semble pas fonctionner pour toute la zone, par exemple, je fais cela sur un panneau du dock et cela ne fonctionne que là où il y a quelque chose dans le panneau du dock (par exemple, bloc de texte, image) mais pas l'espace vide.
Stephen Drew
3
OK - ce vieux châtaigne à nouveau ... besoin de définir l'arrière-plan transparent pour recevoir les événements de la souris, selon stackoverflow.com/questions/7991314/…
Stephen Drew
6
Je me grattais la tête en essayant de comprendre pourquoi cela fonctionnait pour vous tous et pas pour moi. J'ai soudainement réalisé que dans le contexte du modèle d'élément, le contexte de données est l'élément actuel de la source d'éléments et non le modèle de vue de la fenêtre principale. J'ai donc utilisé ce qui suit pour le faire fonctionner <MouseBinding MouseAction = "LeftDoubleClick" Command = "{Binding Path = DataContext.EditBandCommand, RelativeSource = {RelativeSource AncestorType = {x: Type Window}}}" /> Dans mon cas, EditBandCommand est la commande sur le viewmodel de la page et non sur l'entité liée.
naskew
naskew avait la sauce secrète dont j'avais besoin avec MVVM Light, obtenant un paramètre de commande étant l'objet de modèle dans la zone de liste double-cliquée, et le contexte de données de la fenêtre est défini sur le modèle de vue qui expose la commande: <MouseBinding Gesture = "LeftDoubleClick "Command =" {Binding Path = DataContext.OpenSnapshotCommand, RelativeSource = {RelativeSource AncestorType = {x: Type Window}}} "CommandParameter =" {Binding} "/>
MC5
Je veux juste ajouter qui InputBindingssont disponibles à partir de .NET 3.0 et ne sont pas disponibles dans Silverlight.
Martin
44

J'aime utiliser les comportements et commandes de commande attachés . Marlon Grech a une très bonne implémentation des comportements de commande attachés. En utilisant ceux-ci, nous pourrions alors attribuer un style à la propriété ItemContainerStyle de ListView qui définira la commande pour chaque ListViewItem.

Ici, nous définissons la commande à déclencher sur l'événement MouseDoubleClick, et le CommandParameter, sera l'objet de données sur lequel nous cliquons. Ici, je voyage dans l'arborescence visuelle pour obtenir la commande que j'utilise, mais vous pouvez tout aussi facilement 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 directement une ICommand , soit utiliser certains des helpers tels que ceux fournis dans MVVM Toolkit .

rmoore
la source
1
+1 J'ai trouvé que c'était ma solution préférée lorsque je travaillais avec Composite Application Guidance for WPF (Prism).
Travis Heseman
1
Que signifie l'espace de noms «acb:» dans votre échantillon de code ci-dessus?
Nam G VU
@NamGiVU acb:= AttachedCommandBehavior. Le code peut être trouvé dans le premier lien de la réponse
Rachel
J'ai essayé juste cela et obtenir une exception de pointeur nul de la ligne 99 de classe CommandBehaviorBinding. La variable «stratégie» est nulle. Qu'est-ce qui ne va pas?
etwas77
13

J'ai trouvé un moyen très simple et propre de le faire avec les déclencheurs d'événements du SDK Blend. Clean 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 celui-ci 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 "à l'intérieur" 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 Trigger.

Malheureusement, les événements MouseDoubleClick ne sont pas générés à partir de chaque élément visuel (ils proviennent de Controls, mais pas de FrameworkElements par exemple). Une solution de contournement consiste à dériver une classe de EventTrigger et à rechercher MouseButtonEventArgs avec un ClickCount de 2. Cela filtre efficacement tous les non-MouseButtonEvents et tous les MoseButtonEvents avec un ClickCount! = 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 l'espace de noms de la classe d'assistance 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>
Gunter
la source
Comme j'ai découvert que si vous mettez le déclencheur directement sur le GridViewRowPresenter, il pourrait y avoir un problème. Les espaces vides entre les colonnes ne reçoivent probablement pas du tout les événements de souris (probablement une solution de contournement serait de les styliser avec un étirement d'alignement).
Gunter
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.
Gunter
1
Notez que vous perdez le style par défaut pour ListViewItem si vous remplacez le modèle de cette manière. Cela n'avait pas d'importance pour l'application sur laquelle je travaillais car elle utilisait de toute façon un style fortement personnalisé.
Gunter
6

Je me rends compte 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 un code derrière un fichier. Je suis également très 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);
    }
Aaron
la source
12
Vous devez afficher le modèle 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? ViewModel représente la logique du domaine dans un contexte convivial pour la vue, ce n'est pas seulement une aide à la vue. Donc: ButtonClick ne doit jamais être sur le viewmodel, utilisez viewModel.DeleteSelectedCustomer ou ce que cette action représente réellement à la place.
Marius
4

Vous pouvez utiliser la fonction Action de Caliburn pour mapper des événements à des méthodes sur votre ViewModel. En supposant que vous ayez une ItemActivatedméthode sur votre ViewModel, le XAML correspondant ressemblerait à ceci:

<ListView x:Name="list" 
   Message.Attach="[Event MouseDoubleClick] = [Action ItemActivated(list.SelectedItem)]" >

Pour plus de détails, vous pouvez consulter la documentation et les exemples de Caliburn.

idursun
la source
4

Je trouve plus simple de lier la commande lorsque la vue est créée:

var r = new MyView();
r.MouseDoubleClick += (s, ev) => ViewModel.MyCommand.Execute(null);
BindAndShow(r, ViewModel);

Dans mon cas, cela BindAndShowressemble à ceci (updatecontrols + avalondock):

private void BindAndShow(DockableContent view, object viewModel)
{
    view.DataContext = ForView.Wrap(viewModel);
    view.ShowAsDocument(dockManager);
    view.Focus();
}

Bien que l'approche devrait fonctionner avec la méthode que vous avez pour ouvrir de nouvelles vues.

Timothy Pratley
la source
Il me semble que c'est la solution la plus simple, plutôt que d'essayer de la faire fonctionner uniquement en XAML.
Mas du
1

J'ai vu la solution de rushui avec les InuptBindings, mais je ne pouvais toujours pas atteindre la zone de ListViewItem où il n'y avait pas de texte - même après avoir défini l'arrière-plan sur transparent, je l'ai donc résolu en utilisant différents modèles.

Ce modèle est utilisé lorsque le ListViewItem a été sélectionné et est actif:

<ControlTemplate x:Key="SelectedActiveTemplate" TargetType="{x:Type ListViewItem}">
   <Border Background="LightBlue" HorizontalAlignment="Stretch">
   <!-- Bind the double click to a command in the parent view model -->
      <Border.InputBindings>
         <MouseBinding Gesture="LeftDoubleClick" 
                       Command="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=DataContext.ItemSelectedCommand}"
                       CommandParameter="{Binding}" />
      </Border.InputBindings>
      <TextBlock Text="{Binding TextToShow}" />
   </Border>
</ControlTemplate>

Ce modèle est utilisé lorsque le ListViewItem a été sélectionné et est inactif:

<ControlTemplate x:Key="SelectedInactiveTemplate" TargetType="{x:Type ListViewItem}">
   <Border Background="Lavender" HorizontalAlignment="Stretch">
      <TextBlock Text="{Binding TextToShow}" />
   </Border>
</ControlTemplate>

Il s'agit du style par défaut utilisé pour ListViewItem:

<Style TargetType="{x:Type ListViewItem}">
   <Setter Property="Template">
      <Setter.Value>
         <ControlTemplate>
            <Border HorizontalAlignment="Stretch">
               <TextBlock Text="{Binding TextToShow}" />
            </Border>
         </ControlTemplate>
      </Setter.Value>
   </Setter>
   <Style.Triggers>
      <MultiTrigger>
         <MultiTrigger.Conditions>
            <Condition Property="IsSelected" Value="True" />
            <Condition Property="Selector.IsSelectionActive" Value="True" />
         </MultiTrigger.Conditions>
         <Setter Property="Template" Value="{StaticResource SelectedActiveTemplate}" />
      </MultiTrigger>
      <MultiTrigger>
         <MultiTrigger.Conditions>
            <Condition Property="IsSelected" Value="True" />
            <Condition Property="Selector.IsSelectionActive" Value="False" />
         </MultiTrigger.Conditions>
         <Setter Property="Template" Value="{StaticResource SelectedInactiveTemplate}" />
      </MultiTrigger>
   </Style.Triggers>
</Style>

Ce que je n'aime pas, c'est la répétition du TextBlock et sa liaison de texte, je ne sais pas que je peux me déplacer en déclarant cela dans un seul endroit.

J'espère que ça aidera quelqu'un!

user3235445
la source
C'est une excellente solution et j'utilise une solution similaire, mais vous n'avez vraiment besoin que d'un seul modèle de contrôle. Si un utilisateur doit double-cliquer sur a listviewitem, il ne se soucie probablement pas de savoir s'il est déjà sélectionné ou non. Il est également important de noter que l'effet de surbrillance devra peut-être également être modifié pour correspondre au listviewstyle. Voté.
David Bentley
1

J'ai réussi à faire cette fonctionnalité avec le framework .Net 4.7 en utilisant la bibliothèque d'interactivité, tout d'abord assurez-vous de déclarer l'espace de noms dans le fichier XAML

xmlns: i = "http://schemas.microsoft.com/expression/2010/interactivity"

Ensuite, définissez le déclencheur d'événement avec son InvokeCommandAction respectif dans le ListView comme ci-dessous.

Vue:

<ListView x:Name="lv" IsSynchronizedWithCurrentItem="True" 
          ItemsSource="{Binding Path=AppsSource}"  >
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="MouseDoubleClick">
            <i:InvokeCommandAction CommandParameter="{Binding ElementName=lv, Path=SelectedItem}"
                                   Command="{Binding OnOpenLinkCommand}"/>
        </i:EventTrigger>
    </i:Interaction.Triggers>
    <ListView.View>
        <GridView>
            <GridViewColumn Header="Name" DisplayMemberBinding="{Binding Name}" />
            <GridViewColumn Header="Developed By" DisplayMemberBinding="{Binding DevelopedBy}" />
        </GridView>
    </ListView.View>
</ListView>

L'adaptation du code ci-dessus devrait suffire à faire fonctionner l'événement de double-clic sur votre ViewModel, mais je vous ai ajouté la classe Model and View Model de mon exemple afin que vous puissiez avoir une idée complète.

Modèle:

public class ApplicationModel
{
    public string Name { get; set; }

    public string DevelopedBy { get; set; }
}

Voir le modèle:

public class AppListVM : BaseVM
{
        public AppListVM()
        {
            _onOpenLinkCommand = new DelegateCommand(OnOpenLink);
            _appsSource = new ObservableCollection<ApplicationModel>();
            _appsSource.Add(new ApplicationModel("TEST", "Luis"));
            _appsSource.Add(new ApplicationModel("PROD", "Laurent"));
        }

        private ObservableCollection<ApplicationModel> _appsSource = null;

        public ObservableCollection<ApplicationModel> AppsSource
        {
            get => _appsSource;
            set => SetProperty(ref _appsSource, value, nameof(AppsSource));
        }

        private readonly DelegateCommand _onOpenLinkCommand = null;

        public ICommand OnOpenLinkCommand => _onOpenLinkCommand;

        private void OnOpenLink(object commandParameter)
        {
            ApplicationModel app = commandParameter as ApplicationModel;

            if (app != null)
            {
                //Your code here
            }
        }
}

Au cas où vous auriez besoin de l'implémentation de la classe DelegateCommand .

luis_laurent
la source
0

Voici un comportement qui fait cela à la fois sur ListBoxet ListView.

public class ItemDoubleClickBehavior : Behavior<ListBox>
{
    #region Properties
    MouseButtonEventHandler Handler;
    #endregion

    #region Methods

    protected override void OnAttached()
    {
        base.OnAttached();

        AssociatedObject.PreviewMouseDoubleClick += Handler = (s, e) =>
        {
            e.Handled = true;
            if (!(e.OriginalSource is DependencyObject source)) return;

            ListBoxItem sourceItem = source is ListBoxItem ? (ListBoxItem)source : 
                source.FindParent<ListBoxItem>();

            if (sourceItem == null) return;

            foreach (var binding in AssociatedObject.InputBindings.OfType<MouseBinding>())
            {
                if (binding.MouseAction != MouseAction.LeftDoubleClick) continue;

                ICommand command = binding.Command;
                object parameter = binding.CommandParameter;

                if (command.CanExecute(parameter))
                    command.Execute(parameter);
            }
        };
    }

    protected override void OnDetaching()
    {
        base.OnDetaching();
        AssociatedObject.PreviewMouseDoubleClick -= Handler;
    }

    #endregion
}

Voici la classe d'extension utilisée pour trouver le parent.

public static class UIHelper
{
    public static T FindParent<T>(this DependencyObject child, bool debug = false) where T : DependencyObject
    {
        DependencyObject parentObject = VisualTreeHelper.GetParent(child);

        //we've reached the end of the tree
        if (parentObject == null) return null;

        //check if the parent matches the type we're looking for
        if (parentObject is T parent)
            return parent;
        else
            return FindParent<T>(parentObject);
    }
}

Usage:

xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
xmlns:coreBehaviors="{{Your Behavior Namespace}}"


<ListView AllowDrop="True" ItemsSource="{Binding Data}">
    <i:Interaction.Behaviors>
       <coreBehaviors:ItemDoubleClickBehavior/>
    </i:Interaction.Behaviors>

    <ListBox.InputBindings>
       <MouseBinding MouseAction="LeftDoubleClick" Command="{Binding YourCommand}"/>
    </ListBox.InputBindings>
</ListView>
Prince Owen
la source