2009-06-23 17 views
92

In einer WPF-Anwendung, die MVVM verwendet, habe ich eine Benutzersteuerung mit einem Listenansichtselement. In der Laufzeit wird eine Datenbindung verwendet, um die Listenansicht mit einer Sammlung von Objekten zu füllen.Ausführen eines Doppelklickereignisses aus einem WPF-ListView-Element mit MVVM

Was ist die korrekte Methode zum Anhängen eines Doppelklickereignisses an die Elemente in der Listenansicht, wenn ein Element in der Listenansicht doppelgeklickt wird. Ein entsprechendes Ereignis im Ansichtsmodell wird ausgelöst und hat einen Verweis auf das angeklickte Element ?

Wie kann es auf eine saubere MVVM-Weise gemacht werden, d. H. Kein Code in der Ansicht?

Antwort

44

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

Hier legen wir den Befehl fest, der auf das MouseDoubleClick-Ereignis ausgelöst werden soll, und das CommandParameter wird das Datenobjekt sein, auf das wir klicken. Hier fahre ich den visuellen Baum hinauf, um den Befehl zu erhalten, den ich verwende, aber Sie könnten genauso einfach 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 eine ICommand direkt implementieren, oder einige der Helfer wie die benutzen, die in der MVVM Toolkit kommen.

+1

+1 Ich habe festgestellt, dass dies meine bevorzugte Lösung bei der Arbeit mit Composite Application Guidance für WPF (Prism) ist. –

+1

Wofür steht der Namespace 'acb:' in Ihrem Codebeispiel? –

+0

@NamGiVU 'acb:' = AttachedCommandBehavior. Der Code kann in der ersten Verbindung in der Antwort gefunden werden – Rachel

4

Sie können die Action-Funktion Caliburn verwenden, um Ereignisse Methoden auf Ihrem ViewModel zuzuordnen. Angenommen, Sie haben eine ItemActivated Methode auf Ihrem ViewModel, dann XAML entsprechenden aussehen würde:

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

Für weitere Informationen können Sie Caliburn die Unterlagen und Proben untersuchen.

73

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

MVVM ist kein Muster, um den Code zu beseitigen. Es trennt den Ansichtsteil (Aussehen, Animationen usw.) vom Logikteil (Workflow). Darüber hinaus können Sie den Logikteil testen.

Ich kenne genug Szenarien, in denen Sie Code hinterschreiben müssen, weil Datenbindung keine Lösung für alles ist. In Ihrem Szenario würde ich das DoubleClick-Ereignis im Code hinter der Datei behandeln und diesen Aufruf an das ViewModel delegieren. http://waf.codeplex.com

+4

Gut gesagt, ich weigere mich, den ganzen Code und eine extra DLL zu benutzen, nur um einen Doppelklick zu machen! –

+4

Diese einzige Verwendung Binding-Sache gibt mir echte Kopfschmerzen. Es ist, als ob man aufgefordert wird, mit 1 Arm, 1 Auge auf einer Augenklappe und 1 Bein zu kodieren. Doppelklick sollte einfach sein, und ich sehe nicht, wie all dieser zusätzliche Code es wert ist. – Echiban

+1

Ich fürchte, ich stimme dir nicht völlig zu. Wenn Sie sagen, dass Code hinten nicht schlecht ist, dann habe ich eine Frage dazu: Warum delegieren wir das click -Ereignis nicht für die Schaltfläche, sondern verwenden stattdessen die Bindung (mit der Command -Eigenschaft)? –

5

Mir ist klar, dass diese Diskussion ein Jahr alt ist, aber mit -

WPF Application Framework (WAF):

Beispielanwendungen, den Code hinter und immer noch die MVVM Trennung erfüllen können hier gefunden werden. NET 4, gibt es Überlegungen zu dieser Lösung? Ich stimme absolut zu, dass der Sinn von MVVM NICHT darin besteht, einen Code hinter einer Datei zu eliminieren. Ich fühle auch sehr stark, dass nur weil etwas kompliziert ist, es nicht besser ist.Hier ist, was ich in den Code setzen hinter:

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

Sie sollten viewmodel sollten Namen haben, die die Aktionen darstellen, die Sie in Ihrer Domäne ausführen können. Was ist eine "ButtonClick" -Aktion in Ihrer Domain? ViewModel stellt die Logik der Domäne in einem sichtfreundlichen Kontext dar und ist nicht nur ein Helfer für die Ansicht. Also: ButtonClick sollte nie auf dem Viewmodel sein, stattdessen viewModel.DeleteSelectedCustomer oder was auch immer diese Aktion eigentlich darstellt. – Marius

4

Ich finde es einfacher, den Befehl zu verbinden, wenn die Ansicht erstellt wird:

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

In meinem Fall BindAndShow sieht wie folgt aus (Updatecontrols + avalondock):

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

Obwohl der Ansatz mit jeder Methode funktionieren sollte, die Sie haben, neue Ansichten zu öffnen.

+0

Es scheint mir, dass dies die einfachste Lösung ist, anstatt zu versuchen, es nur in XAML funktionieren zu lassen. – Mas

12

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

Sie haben wahrscheinlich schon so etwas wie dieses:

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

Jetzt gehören ein Control für die ListViewItem wie diese, wenn Sie nicht bereits über ein verwenden:

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

Die GridViewRowPresenter wird die visuelle sein Wurzel aller Elemente "innen", die ein Listenzeilenelement bilden. Jetzt konnten wir einen Trigger dort einsetzen suchen Mousedoubleclick-Ereignisse geleitet und rufen Sie einen Befehl über InvokeCommandAction wie folgt aus:

<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“ die GridRowPresenter haben (via Modem mit einem Raster ausgehend) können Sie auch setzen der Auslöser dort.

Leider werden MouseDoubleClick-Ereignisse nicht von jedem visuellen Element generiert (sie stammen aus Steuerelementen, aber nicht aus beispielsweise FrameworkElements). Eine Abhilfe ist, eine Klasse von Eventtrigger abzuleiten und suchen Sie nach MouseButtonEventArgs mit einem Clickcount 2. Diese effektiv alle Nicht-MouseButtonEvents herausfiltert und alle MoseButtonEvents mit einem 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); 
     } 
    } 
} 

Jetzt können wir das schreiben ('h' ist der Namespace der Hilfsklasse oben):

<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

Da ich herausgefunden habe, wenn Sie den Trigger direkt auf den GridViewRowPresenter setzen, könnte es ein Problem geben. Die leeren Bereiche zwischen den Spalten erhalten wahrscheinlich überhaupt keine Mausereignisse (wahrscheinlich wäre ein Workaround, sie mit Ausrichtungsstretch zu gestalten). – Gunter

+0

In diesem Fall ist es wahrscheinlich besser, ein leeres Gitter um den GridViewRowPresenter zu legen und den Trigger dort zu platzieren. Dies scheint zu funktionieren. – Gunter

+1

Beachten Sie, dass Sie den Standardstil für das ListViewItem verlieren, wenn Sie die Vorlage wie folgt ersetzen. Es war egal für die Anwendung, an der ich arbeitete, da es sowieso ein stark angepasstes Styling verwendete. – Gunter

53

Ich bin in der Lage, dies mit .NET 4.5 zu arbeiten. Scheint einfach und keine dritte Partei oder Code hinter 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> 
+4

+1 für InputBindings. –

+1

scheint nicht für das gesamte Gebiet, z. Ich mache das auf einem Dock-Panel und es funktioniert nur dort, wo sich etwas im Dock-Panel befindet (z. B. Textblock, Bild), aber nicht die Leerstelle. –

+3

OK - diese alte Kastanie wieder ... muss den Hintergrund zu transparent setzen, um Mausereignisse zu erhalten, laut http://stackoverflow.com/questions/7991314/mouse-event-on-transparent-background –

1

sah ich die Lösung von Rushui mit dem InuptBindings aber ich war immer noch nicht den Bereich des ListViewItem zu treffen, wo es kein Text war - auch nach dem Hintergrund transparent einstellen, so dass ich es gelöst durch die Verwendung verschiedene Vorlagen

Diese Vorlage ist für, wenn der 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 für ist, wenn der ListViewItem ausgewählt wurde und inaktiv:

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

Dies ist der Standard Stil für das ListViewItem verwendet:

<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 dessen Textbindung, ich weiß nicht, ich kann das nur an der einen Stelle erklären.

Ich hoffe, das hilft jemandem!

+0

Das ist eine großartige Lösung und ich benutze eine ähnliche, aber Sie brauchen wirklich nur eine Kontrollschablone. Wenn ein Benutzer auf ein 'listviewitem' doppelklickt, interessiert es wahrscheinlich nicht, ob es bereits ausgewählt ist oder nicht. Es ist auch wichtig zu beachten, dass der Hervorhebungseffekt auch angepasst werden muss, um dem "Listview" -Stil zu entsprechen. Gewählt –

Verwandte Themen