Auslösen eines Doppelklickereignisses aus einem WPF ListView-Element mit MVVM

102

In einer WPF-Anwendung, die MVVM verwendet, habe ich eine Benutzersteuerung mit einem Listenansichtselement. Zur Laufzeit wird die Datenansicht verwendet, um die Listenansicht mit einer Sammlung von Objekten zu füllen.

Wie kann ein Doppelklickereignis korrekt an die Elemente in der Listenansicht angehängt werden, sodass beim Doppelklicken auf ein Element in der Listenansicht ein entsprechendes Ereignis im Ansichtsmodell ausgelöst wird und ein Verweis auf das angeklickte Element vorhanden ist?

Wie kann es auf saubere MVVM-Weise gemacht werden, dh ohne Code in der Ansicht?

Emad Gabriel
quelle

Antworten:

76

Bitte, Code dahinter ist überhaupt keine schlechte Sache. Leider haben ziemlich viele Leute in der WPF-Community das falsch verstanden.

MVVM ist kein Muster, um den Code dahinter zu entfernen. Es dient dazu, den Ansichtsteil (Erscheinungsbild, Animationen usw.) vom Logikteil (Workflow) zu trennen. Darüber hinaus können Sie den logischen Teil einem Unit-Test unterziehen.

Ich kenne genug Szenarien, in denen Sie Code dahinter schreiben müssen, weil die Datenbindung nicht für alles eine Lösung ist. In Ihrem Szenario würde ich das DoubleClick-Ereignis im Code hinter der Datei behandeln und diesen Aufruf an das ViewModel delegieren.

Beispielanwendungen, die Code verwenden und dennoch die MVVM-Trennung erfüllen, finden Sie hier:

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

jbe
quelle
5
Gut gesagt, ich weigere mich, all diesen Code und eine zusätzliche DLL zu verwenden, nur um einen Doppelklick zu machen!
Eduardo Molteni
4
Diese einzige verbindliche Sache bereitet mir echte Kopfschmerzen. Es ist, als würde man gebeten, mit einem Arm, einem Auge auf einer Augenklappe und einem Bein zu codieren. Ein Doppelklick sollte einfach sein, und ich sehe nicht, wie sich all dieser zusätzliche Code lohnt.
Echiban
1
Ich fürchte, ich stimme Ihnen nicht ganz zu. Wenn Sie sagen, dass Code dahinter nicht schlecht ist, habe ich eine Frage dazu: Warum delegieren wir nicht das Klickereignis für die Schaltfläche, sondern verwenden stattdessen häufig die Bindung (mithilfe der Befehlseigenschaft)?
Nam G VU
21
@Nam Gi VU: Ich würde immer eine Befehlsbindung bevorzugen, wenn sie vom WPF-Steuerelement unterstützt wird. Eine Befehlsbindung ist mehr als nur das Weiterleiten des 'Click'-Ereignisses an das ViewModel (z. B. CanExecute). Befehle sind jedoch nur für die gängigsten Szenarien verfügbar. Für andere Szenarien können wir die CodeBehind-Datei verwenden und dort nicht mit der Benutzeroberfläche zusammenhängende Bedenken an das ViewModel oder das Modell delegieren.
18.
2
Jetzt verstehe ich dich mehr! Nettes Gespräch mit dir!
Nam G VU
73

Ich kann dies mit .NET 4.5 zum Laufen bringen. Scheint einfach und es wird kein Dritter oder Code benötigt.

<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
quelle
2
Scheint nicht für den gesamten Bereich zu funktionieren, z. B. mache ich dies auf einem Dock-Panel und es funktioniert nur, wenn sich etwas im Dock-Panel befindet (z. B. Textblock, Bild), aber nicht die Leerstelle.
Stephen Drew
3
OK - diese alte Kastanie wieder ... muss den Hintergrund auf transparent setzen, um Mausereignisse zu empfangen, gemäß stackoverflow.com/questions/7991314/…
Stephen Drew
6
Ich kratzte mir am Kopf und versuchte herauszufinden, warum es für euch alle und nicht für mich funktionierte. Plötzlich wurde mir klar, dass der Datenkontext im Kontext der Elementvorlage das aktuelle Element aus der Elementquelle und nicht das Ansichtsmodell des Hauptfensters ist. Also habe ich Folgendes verwendet, um es zum Laufen zu bringen. <MouseBinding MouseAction = "LeftDoubleClick" Command = "{Bindungspfad = DataContext.EditBandCommand, RelativeSource = {RelativeSource AncestorType = {x: Typfenster}}}" /> In meinem Fall ist der EditBandCommand Der Befehl im Ansichtsmodell der Seite nicht für die gebundene Entität.
Naskew
naskew hatte die geheime Sauce, die ich mit MVVM Light benötigte, wobei ein Befehlsparameter als Modellobjekt im doppelt angeklickten Listenfeldelement abgerufen wurde und der Datenkontext des Fensters auf das Ansichtsmodell festgelegt wurde, das den Befehl verfügbar macht: <MouseBinding Gesture = "LeftDoubleClick "Command =" {Binding Path = DataContext.OpenSnapshotCommand, RelativeSource = {RelativeSource AncestorType = {x: Typfenster}}} "CommandParameter =" {Binding} "/>
MC5
Ich möchte nur hinzufügen, dass diese InputBindingsin .NET 3.0 verfügbar und in Silverlight nicht verfügbar sind.
Martin
44

Ich verwende gerne angehängte Befehlsverhalten und Befehle. Marlon Grech hat eine sehr gute Implementierung des Attached Command Behaviors. Mit diesen können wir dann der ItemContainerStyle- Eigenschaft von ListView einen Stil zuweisen, der den Befehl für jedes ListViewItem festlegt.

Hier legen wir den Befehl fest, der für das MouseDoubleClick-Ereignis ausgelöst werden soll, und der CommandParameter ist das Datenobjekt, auf das wir klicken. Hier gehe ich den visuellen Baum entlang, um den Befehl zu erhalten, den ich verwende, aber Sie können genauso gut anwendungsweite Befehle erstellen.

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

Für die Befehle können Sie entweder einen ICommand direkt implementieren oder einige der Hilfsprogramme verwenden , wie sie im MVVM Toolkit enthalten sind .

rmoore
quelle
1
+1 Ich habe festgestellt, dass dies meine bevorzugte Lösung ist, wenn ich mit Composite Application Guidance für WPF (Prism) arbeite.
Travis Heseman
1
Wofür steht der Namespace 'acb:' in Ihrem Code-Sampleabove?
Nam G VU
@NamGiVU acb:= AttachedCommandBehavior. Der Code kann im ersten Link in der Antwort gefunden werden
Rachel
Ich habe genau das versucht und eine Nullzeigerausnahme von der Klasse CommandBehaviorBinding Zeile 99 erhalten. Die Variable "Strategie" ist null. Was ist los mit dir?
etwas77
13

Ich habe einen sehr einfachen und sauberen Weg gefunden, dies mit den Blend SDK Event-Triggern zu tun. Sauberes MVVM, wiederverwendbar und ohne Code-Behind.

Sie haben wahrscheinlich schon so etwas:

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

Fügen Sie jetzt eine ControlTemplate für das ListViewItem wie folgt hinzu, falls Sie noch keine verwenden:

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

Der GridViewRowPresenter ist die visuelle Wurzel aller Elemente "innerhalb", aus denen ein Listenzeilenelement besteht. Jetzt könnten wir dort einen Trigger einfügen, um nach gerouteten MouseDoubleClick-Ereignissen zu suchen und einen Befehl über InvokeCommandAction wie folgt aufzurufen:

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

Wenn Sie visuelle Elemente "über" dem GridRowPresenter haben (wahrscheinlich beginnend mit einem Raster), können Sie den Trigger auch dort platzieren.

Leider werden MouseDoubleClick-Ereignisse nicht aus jedem visuellen Element generiert (sie stammen aus Steuerelementen, aber nicht beispielsweise aus FrameworkElements). Eine Problemumgehung besteht darin, eine Klasse von EventTrigger abzuleiten und nach MouseButtonEventArgs mit einem ClickCount von 2 zu suchen. Dadurch werden alle Nicht-MouseButtonEvents und alle MoseButtonEvents mit einem ClickCount! = 2 effektiv herausgefiltert.

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

Jetzt können wir dies schreiben ('h' ist der Namespace der obigen Hilfsklasse):

<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
quelle
Wie ich herausgefunden habe, könnte es ein Problem geben, wenn Sie den Trigger direkt auf dem GridViewRowPresenter platzieren. Die leeren Räume zwischen den Spalten erhalten wahrscheinlich überhaupt keine Mausereignisse (wahrscheinlich besteht eine Problemumgehung darin, sie mit Ausrichtungsdehnung zu formatieren).
Gunter
In diesem Fall ist es wahrscheinlich besser, ein leeres Raster um den GridViewRowPresenter zu legen und den Trigger dort zu platzieren. Das scheint zu funktionieren.
Gunter
1
Beachten Sie, dass Sie den Standardstil für ListViewItem verlieren, wenn Sie die Vorlage wie folgt ersetzen. Es war egal für die Anwendung, an der ich arbeitete, da sie sowieso ein stark angepasstes Styling verwendete.
Gunter
6

Mir ist klar, dass diese Diskussion ein Jahr alt ist, aber gibt es bei .NET 4 irgendwelche Gedanken zu dieser Lösung? Ich stimme absolut zu, dass der Sinn von MVVM NICHT darin besteht, einen Code hinter der Datei zu entfernen. Ich habe auch das starke Gefühl, dass nur weil etwas kompliziert ist, es nicht besser ist. Folgendes habe ich in den Code dahinter eingefügt:

    private void ButtonClick(object sender, RoutedEventArgs e)
    {
        dynamic viewModel = DataContext;
        viewModel.ButtonClick(sender, e);
    }
Aaron
quelle
12
Sie sollten das Ansichtsmodell mit Namen versehen, die die Aktionen darstellen, die Sie in Ihrer Domäne ausführen können. Was ist eine "ButtonClick" -Aktion in Ihrer Domain? ViewModel repräsentiert die Logik der Domäne in einem ansichtsfreundlichen Kontext und ist nicht nur ein Helfer für die Ansicht. Also: ButtonClick sollte sich niemals im Ansichtsmodell befinden. Verwenden Sie stattdessen viewModel.DeleteSelectedCustomer oder was auch immer diese Aktion tatsächlich darstellt.
Marius
4

Mit der Aktionsfunktion von Caliburn können Sie Ereignisse Methoden in Ihrem ViewModel zuordnen . Angenommen, Sie haben eine ItemActivatedMethode auf Ihrem ViewModel, dann würde die entsprechende XAML wie folgt aussehen:

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

Weitere Informationen finden Sie in der Dokumentation und den Mustern von Caliburn.

idursun
quelle
4

Ich finde es einfacher, den Befehl zu verknüpfen, wenn die Ansicht erstellt wird:

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

In meinem Fall BindAndShowsieht das so aus (updatecontrols + avalondock):

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

Der Ansatz sollte jedoch mit jeder Methode zum Öffnen neuer Ansichten funktionieren.

Timothy Pratley
quelle
Es scheint mir, dass dies die einfachste Lösung ist, anstatt zu versuchen, sie nur in XAML zum Laufen zu bringen.
Mas
1

Ich habe die Lösung von Rushui mit den InuptBindings gesehen, konnte aber immer noch nicht den Bereich des ListViewItem erreichen, in dem kein Text vorhanden war - selbst nachdem der Hintergrund auf transparent gesetzt wurde. Daher habe ich ihn mithilfe verschiedener Vorlagen gelöst.

Diese Vorlage ist für den Fall vorgesehen, dass das ListViewItem ausgewählt wurde und aktiv ist:

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

Diese Vorlage ist für den Fall vorgesehen, dass das ListViewItem ausgewählt wurde und inaktiv ist:

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

Dies ist der Standardstil, der für das ListViewItem verwendet wird:

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

Was ich nicht mag, ist die Wiederholung des TextBlocks und seiner Textbindung. Ich weiß nicht, dass ich es umgehen kann, wenn ich das nur an einer Stelle deklariere.

Ich hoffe das hilft jemandem!

user3235445
quelle
Dies ist eine großartige Lösung, und ich verwende eine ähnliche, aber Sie benötigen wirklich nur eine Steuerungsvorlage. Wenn ein Benutzer auf a doppelklickt listviewitem, ist es ihm wahrscheinlich egal, ob es bereits ausgewählt ist oder nicht. Es ist auch wichtig zu beachten, dass der Hervorhebungseffekt möglicherweise auch an den listviewStil angepasst werden muss . Up-Voted.
David Bentley
1

Es gelingt mir, diese Funktionalität mit dem .NET 4.7-Framework mithilfe der Interaktivitätsbibliothek zu erstellen. Stellen Sie zunächst sicher, dass Sie den Namespace in der XAML-Datei deklarieren

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

Setzen Sie dann den Ereignisauslöser mit seiner jeweiligen InvokeCommandAction in der ListView wie unten beschrieben.

Aussicht:

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

Das Anpassen des obigen Codes sollte ausreichen, damit das Doppelklick-Ereignis auf Ihrem ViewModel funktioniert. Ich habe Ihnen jedoch die Klassen Model und View Model aus meinem Beispiel hinzugefügt, damit Sie die vollständige Idee haben.

Modell:

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

    public string DevelopedBy { get; set; }
}

Modell anzeigen:

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

Falls Sie die Implementierung der DelegateCommand- Klasse benötigen .

luis_laurent
quelle
0

Hier ist ein Verhalten, das sowohl bei ListBoxals auch bei ausgeführt wird 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
}

Hier ist die Erweiterungsklasse, mit der das übergeordnete Element gefunden wird.

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

Verwendung:

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>
Prinz Owen
quelle