Attivazione di un evento di doppio clic da un elemento ListView di WPF utilizzando MVVM


102

In un'applicazione WPF che utilizza MVVM, ho un controllo utente con un elemento listview. In fase di esecuzione, utilizzerà l'associazione dati per riempire il listview con una raccolta di oggetti.

Qual è il modo corretto per allegare un evento di doppio clic agli elementi nella visualizzazione elenco in modo che quando si fa doppio clic su un elemento nella visualizzazione elenco, viene attivato un evento corrispondente nel modello di visualizzazione e ha un riferimento all'elemento su cui si è fatto clic?

Come può essere fatto in un modo MVVM pulito, cioè senza codice nella vista?

Risposte:


76

Per favore, il codice dietro non è affatto una cosa negativa. Sfortunatamente, molte persone nella comunità WPF hanno sbagliato.

MVVM non è un modello per eliminare il codice sottostante. Serve a separare la parte della vista (aspetto, animazioni, ecc.) Dalla parte logica (flusso di lavoro). Inoltre, puoi eseguire un test unitario della parte logica.

Conosco abbastanza scenari in cui devi scrivere codice dietro perché il data binding non è una soluzione a tutto. Nel tuo scenario, gestirò l'evento DoubleClick nel file code behind e delegherei questa chiamata a ViewModel.

Le applicazioni di esempio che utilizzano il codice sottostante e soddisfano ancora la separazione MVVM possono essere trovate qui:

WPF Application Framework (WAF) - http://waf.codeplex.com


5
Ben detto, mi rifiuto di usare tutto quel codice e una DLL extra solo per fare un doppio clic!
Eduardo Molteni

4
Questo solo uso Binding mi sta dando un vero mal di testa. È come chiedersi di programmare con 1 braccio, 1 occhio su una benda sull'occhio e in piedi su 1 gamba. Il doppio clic dovrebbe essere semplice e non vedo quanto valga la pena di tutto questo codice aggiuntivo.
Echiban

1
Temo di non essere totalmente d'accordo con te. Se dici "il codice dietro non è male", allora ho una domanda al riguardo: perché non deleghiamo l'evento click per il pulsante ma spesso usando invece l'associazione (usando la proprietà Command)?
Nam G VU

21
@ Nam Gi VU: Preferirei sempre un Command Binding quando è supportato dal controllo WPF. Un Command Binding non si limita a trasmettere l'evento "Click" al ViewModel (ad esempio CanExecute). Ma i comandi sono disponibili solo per gli scenari più comuni. Per altri scenari possiamo usare il file code-behind e lì deleghiamo le preoccupazioni non relative all'interfaccia utente al ViewModel o al Model.
jbe

2
Adesso ti capisco di più! Bella discussione con te!
Nam G VU

73

Sono in grado di farlo funzionare con .NET 4.5. Sembra semplice e non è necessario alcun codice o terze parti.

<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
Non sembra funzionare per l'intera area, ad esempio lo faccio su un pannello dock e funziona solo dove c'è qualcosa all'interno del pannello dock (ad esempio blocco di testo, immagine) ma non lo spazio vuoto.
Stephen Drew

3
OK - di nuovo questa vecchia castagna ... è necessario impostare lo sfondo su trasparente per ricevere gli eventi del mouse, come da stackoverflow.com/questions/7991314/…
Stephen Drew

6
Mi stavo grattando la testa cercando di capire perché funzionava per tutti voi e non per me. Improvvisamente mi sono reso conto che nel contesto del modello di elemento il contesto dei dati è l'elemento corrente da itemssource e non il modello di visualizzazione della finestra principale. Quindi ho usato quanto segue per farlo funzionare <MouseBinding MouseAction = "LeftDoubleClick" Command = "{Binding Path = DataContext.EditBandCommand, RelativeSource = {RelativeSource AncestorType = {x: Type Window}}}" /> Nel mio caso EditBandCommand è il comando sul viewmodel della pagina non sull'entità associata.
naskew

naskew aveva la salsa segreta di cui avevo bisogno con MVVM Light, ottenendo un parametro di comando come oggetto modello nell'elemento listbox con doppio clic e il contesto dati della finestra è impostato sul modello di visualizzazione che espone il comando: <MouseBinding Gesture = "LeftDoubleClick "Command =" {Binding Path = DataContext.OpenSnapshotCommand, RelativeSource = {RelativeSource AncestorType = {x: Type Window}}} "CommandParameter =" {Binding} "/>
MC5

Voglio solo aggiungere che InputBindingssono disponibili da .NET 3.0 e non sono disponibili in Silverlight.
Martin

44

Mi piace usare Comandi e Comandi allegati . Marlon Grech ha un'ottima implementazione dei Comportamenti di comando allegati. Usandoli , potremmo quindi assegnare uno stile alla proprietà ItemContainerStyle di ListView che imposterà il comando per ogni ListViewItem.

Qui impostiamo il comando da lanciare sull'evento MouseDoubleClick e CommandParameter, sarà l'oggetto dati su cui faremo clic. Qui sto percorrendo l'albero visivo per ottenere il comando che sto usando, ma potresti creare altrettanto facilmente comandi a livello di applicazione.

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

Per i comandi, puoi implementare direttamente un ICommand o utilizzare alcuni degli helper come quelli forniti in MVVM Toolkit .


1
+1 Ho scoperto che questa è la mia soluzione preferita quando lavoro con Composite Application Guidance for WPF (Prism).
Travis Heseman

1
Che cosa significa lo spazio dei nomi "acb:" nel codice di esempio sopra?
Nam G VU

@NamGiVU acb:= AttachedCommandBehavior. Il codice può essere trovato nel primo collegamento nella risposta
Rachel

ho provato proprio questo e ho ottenuto l'eccezione del puntatore nullo dalla riga 99 della classe CommandBehaviorBinding. la variabile "strategia" è nulla. Cosa c'è che non va?
etwas77

13

Ho trovato un modo molto semplice e pulito per farlo con i trigger di evento Blend SDK. MVVM pulito, riutilizzabile e senza code-behind.

Probabilmente hai già qualcosa di simile:

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

Ora includi un ControlTemplate per ListViewItem come questo se non ne usi già uno:

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

Il GridViewRowPresenter sarà la radice visiva di tutti gli elementi "all'interno" che costituiscono un elemento di riga di elenco. Ora potremmo inserire un trigger lì per cercare gli eventi indirizzati MouseDoubleClick e chiamare un comando tramite InvokeCommandAction come questo:

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

Se hai elementi visivi "sopra" il GridRowPresenter (probabilmente cominciando con una griglia) puoi anche mettere il Trigger lì.

Sfortunatamente gli eventi MouseDoubleClick non vengono generati da ogni elemento visuale (provengono da Controls, ma non da FrameworkElements per esempio). Una soluzione alternativa consiste nel derivare una classe da EventTrigger e cercare MouseButtonEventArgs con un ClickCount di 2. Questo filtra efficacemente tutti gli eventi non MouseButtonEvents e tutti i MoseButtonEvents con 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);
        }
    }
}

Ora possiamo scrivere questo ('h' è lo spazio dei nomi della classe helper sopra):

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

Come ho scoperto, se metti il ​​trigger direttamente su GridViewRowPresenter potrebbe esserci un problema. Gli spazi vuoti tra le colonne probabilmente non ricevono affatto gli eventi del mouse (probabilmente una soluzione alternativa sarebbe modellarli con l'allineamento allungato).
Gunter

In questo caso è probabilmente meglio mettere una griglia vuota attorno a GridViewRowPresenter e mettere lì il trigger. Sembra funzionare.
Gunter

1
Nota che perdi lo stile predefinito per ListViewItem se sostituisci il modello in questo modo. Non importava per l'applicazione su cui stavo lavorando perché utilizzava comunque uno stile fortemente personalizzato.
Gunter

6

Mi rendo conto che questa discussione è vecchia di un anno, ma con .NET 4, ci sono pensieri su questa soluzione? Sono assolutamente d'accordo sul fatto che lo scopo di MVVM NON è eliminare un codice dietro il file. Sento anche molto forte che solo perché qualcosa è complicato, non significa che sia migliore. Ecco cosa ho inserito nel codice dietro:

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

12
Il viewmodel dovrebbe avere nomi che rappresentano le azioni che puoi eseguire nel tuo dominio. Che cos'è un'azione "ButtonClick" nel tuo dominio? ViewModel rappresenta la logica del dominio in un contesto di facile visualizzazione, non è solo un aiuto per la visualizzazione. Quindi: ButtonClick non dovrebbe mai essere sul viewmodel, usa viewModel.DeleteSelectedCustomer o qualunque cosa questa azione rappresenti effettivamente.
Marius

4

È possibile utilizzare la funzione Azione di Caliburn per mappare gli eventi ai metodi sul ViewModel. Supponendo che tu abbia un ItemActivatedmetodo sul tuo ViewModel, allora il corrispondente XAML sarebbe simile a:

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

Per ulteriori dettagli è possibile esaminare la documentazione e gli esempi di Caliburn.


4

Trovo più semplice collegare il comando quando viene creata la vista:

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

Nel mio caso BindAndShowassomiglia a questo (updatecontrols + avalondock):

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

Sebbene l'approccio dovrebbe funzionare con qualsiasi metodo tu abbia per aprire nuove visualizzazioni.


Mi sembra che questa sia la soluzione più semplice, piuttosto che cercare di farlo funzionare solo in XAML.
Mas

1

Ho visto la soluzione da rushui con InuptBindings ma non ero ancora in grado di colpire l'area di ListViewItem in cui non c'era testo, anche dopo aver impostato lo sfondo su trasparente, quindi l'ho risolto utilizzando modelli diversi.

Questo modello è per quando ListViewItem è stato selezionato ed è attivo:

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

Questo modello è per quando ListViewItem è stato selezionato ed è inattivo:

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

Questo è lo stile predefinito utilizzato per 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>

Quello che non mi piace è la ripetizione del TextBlock e la sua associazione di testo, non so che posso andare in giro dichiarandolo in una sola posizione.

Spero che questo aiuti qualcuno!


Questa è un'ottima soluzione e ne uso uno simile, ma in realtà è necessario solo un modello di controllo. Se un utente sta per fare doppio clic su a listviewitem, probabilmente non gli interessa se è già selezionato o meno. Inoltre è importante notare che anche l'effetto di evidenziazione potrebbe dover essere ottimizzato per adattarlo allo listviewstile. Up-votato.
David Bentley

1

Riesco a realizzare questa funzionalità con il framework .Net 4.7 utilizzando la libreria di interattività, prima di tutto mi assicuro di dichiarare il namespace nel file XAML

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

Quindi imposta il trigger di evento con il suo rispettivo InvokeCommandAction all'interno di ListView come di seguito.

Visualizza:

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

Adattare il codice sopra dovrebbe essere sufficiente per far funzionare l'evento di doppio clic sul tuo ViewModel, tuttavia ti ho aggiunto la classe Model e View Model dal mio esempio in modo che tu possa avere un'idea completa.

Modello:

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

    public string DevelopedBy { get; set; }
}

Visualizza modello:

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

Nel caso in cui sia necessaria l'implementazione della classe DelegateCommand .


0

Ecco un comportamento che lo fa su entrambi ListBoxe 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
}

Ecco la classe di estensione utilizzata per trovare il genitore.

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

Uso:

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>
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.