Dans une application WPF utilisant MVVM, j'ai un contrôle utilisateur avec un élément listview. Au moment de l'exécution, il utilisera la liaison de données pour remplir la listview 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 liste afin que, lorsqu'un élément de la vue liste est double-cliqué, un événement correspondant du modèle de vue est déclenché et qu'une référence à l'élément sur lequel l'utilisateur a cliqué?
Comment peut-on le faire de manière MVVM propre, c’est-à-dire qu’il n’ya pas de code en arrière dans la vue?
S'il vous plaît, le code derrière n'est pas une mauvaise chose du tout. Malheureusement, beaucoup de membres de la communauté WPF se sont trompés.
MVVM n'est pas un modèle pour éliminer le code en retard. Il consiste à séparer la partie vue (apparence, animations, etc.) de la partie logique (flux de travail). En outre, vous êtes en mesure de tester la partie logique.
Je connais assez 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 fichier code derrière et déléguerais cet appel à ViewModel.
Des exemples d’applications qui utilisent du code derrière et qui remplissent toujours la séparation MVVM peuvent être trouvés ici:
Cadre d'application WPF (WAF) - http://waf.codeplex.com
Je peux obtenir ceci pour fonctionner avec .NET 4.5. Cela semble simple et aucun tiers ni code n'est 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>
J'aime utiliser comportements de commandes attachées et commandes. Marlon Grech a une très bonne implémentation des comportements de commande attachés. En utilisant ceux-ci, nous pourrions alors affecter un style à la propriété ItemContainerStyle de ListView qui définira la commande pour chaque ListViewItem.
Ici, nous configurons la commande pour qu'elle soit déclenchée sur l'événement MouseDoubleClick, et le CommandParameter sera l'objet de données sur lequel nous cliquons. Ici, je vais dans l’arbre visuel 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 un ICommand , soit utiliser certains des utilitaires comme ceux fournis dans le MVVM Toolkit .
J'ai trouvé un moyen très simple et propre de le faire avec les déclencheurs d'événement Blend SDK. MVVM propre, 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 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>
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" de GridRowPresenter (probalby 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 à partir de tous les éléments visuels (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 événements autres que 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>
Je me rends compte que cette discussion a déjà un an, mais avec .NET 4, avez-vous des idées sur cette solution? Je conviens absolument que le but de MVVM n'est PAS d'éliminer un code derrière un fichier. Je suis aussi très convaincu que le fait que quelque chose soit compliqué ne signifie pas 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);
}
Vous pouvez utiliser la fonction Action de Caliburn pour mapper les événements aux méthodes de votre ViewModel. En supposant que vous avez une méthode ItemActivated
sur votre ViewModel
, alors le XAML correspondant ressemblera à 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.
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, BindAndShow
ressemble à ceci (updatecontrols + avalondock):
private void BindAndShow(DockableContent view, object viewModel)
{
view.DataContext = ForView.Wrap(viewModel);
view.ShowAsDocument(dockManager);
view.Focus();
}
Bien que l'approche doive fonctionner avec n'importe quelle méthode que vous avez pour ouvrir de nouvelles vues.
J'ai vu la solution de rushui avec InuptBindings mais je ne pouvais toujours pas accéder à 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 pour quand 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 pour quand 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>
C'est le 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 de TextBlock et de sa liaison de texte. Je ne sais pas si je peux me permettre de le déclarer en un seul endroit.
J'espère que ça aidera quelqu'un!
Voici un comportement qui obtient cela fait à la fois sur ListBox
et 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>