web-dev-qa-db-fra.com

WPF MVVM: Les commandes sont faciles. Comment connecter View et ViewModel avec RoutedEvent

Supposons que ma vue soit implémentée en tant que DataTempate dans un dictionnaire de ressources. Et j'ai un ViewModel correspondant. Les commandes de reliure sont faciles. Mais que se passe-t-il si ma vue contient un contrôle tel qu'un contrôle ListBox et que je dois publier un événement d'application (Utilisation de l'aggrégateur d'événements de Prism) en fonction de l'élément en cours de modification dans la liste.

si ListBox prend en charge une commande, je peux simplement la lier à une commande du ViewModel et publier l'événement. Mais Listbox n'autorise pas une telle option. Comment puis-je combler cela?

EDIT: Beaucoup de bonnes réponses.

Consultez ce lien http://blogs.Microsoft.co.il/blogs/tomershamam/archive/2009/04/14/wpf-commands-everywhere.aspx

Merci

Ariel

15
ArielBH

Une option consiste à étendre le contrôle en question et à ajouter un support pour la commande particulière dont vous avez besoin. Par exemple, j'ai modifié ListView avant pour prendre en charge l'événement ItemActivated et la commande associée.

8
Kent Boogaart

Au lieu d'essayer de lier une commande à lorsque l'élément change, j'ai examiné le problème d'une autre manière.

Si vous liez l'élément sélectionné du contrôle ListBox à une propriété du ViewModel, vous pouvez publier l'événement lorsque cette propriété est modifiée. Ainsi, le ViewModel reste la source de l'événement et il est déclenché par le changement d'élément, comme vous le souhaitez.

<ListBox ItemsSource="{Binding Items}" SelectedItem="{Binding SelectedItem}" />

...

public class ViewModel
{
    public IEnumerable<Item> Items { get; set; } 

    private Item selectedItem;
    public Item SelectedItem
    {
        get { return selectedItem; }
        set
        {
            if (selectedItem == value)
                return;
            selectedItem = value;
            // Publish event when the selected item changes
        }
}
44

Étendez le contrôle pour prendre en charge ICommandSource et décidez quelle action doit déclencher la commande.

Je l'ai fait avec Combo Box et utilisé OnSelectionChanged comme déclencheur de la commande. Premièrement, je vais montrer en XAML comment je lie la commande au contrôle ComboBox étendu que j’ai appelé CommandComboBox, puis je vais montrer le code de CommandComboBox qui ajoute la prise en charge de ICommandSource à ComboBox.

1) Utilisation de CommandComboBox dans votre code XAML:

Dans vos déclarations d'espace de nom XAML, incluez

   xmlns:custom="clr-namespace:WpfCommandControlsLibrary;Assembly=WpfCommandControlsLibrary">

Utilisez CommandComboBox à la place de ComboBox et liez-y la commande comme suit: Notez que dans cet exemple, j'ai défini une commande appelée SetLanguageCommand im my ViewModel et que la valeur sélectionnée pour cette ComboBox est définie comme paramètre de la commande.

 <custom:CommandComboBox 
    x:Name="ux_cbSelectLanguage"
    ItemsSource="{Binding Path = ImagesAndCultures}"
    ItemTemplate="{DynamicResource LanguageComboBoxTemplate}"           
    Command="{Binding Path=SetLanguageCommand, Mode=Default}"
    CommandParameter="{Binding RelativeSource={x:Static RelativeSource.Self}, Path=SelectedValue, Mode=Default}"
    IsSynchronizedWithCurrentItem="True" 
    HorizontalAlignment="Right" 
    VerticalAlignment="Center" 
    Grid.Column="1" Margin="0,0,20,0" Style="{DynamicResource GlassyComboBox}" ScrollViewer.IsDeferredScrollingEnabled="True"
 />

2) Le code pour CommandComboBox

Le code du fichier CommandComboBox.cs est inclus ci-dessous. J'ai ajouté ce fichier à une bibliothèque de classes appelée WpfCommandControlsLibrary et en ai fait un projet séparé pour pouvoir ajouter facilement les commandes extend à la solution requise pour les utiliser. Ainsi, j'ai facilement pu ajouter des contrôles WPF supplémentaires et les étendre pour prendre en charge l'interface ICommandSource.

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;

namespace WpfCommandControlsLibrary
{
   /// <summary>
   /// Follow steps 1a or 1b and then 2 to use this custom control in a XAML file.
   ///
   /// Step 1a) Using this custom control in a XAML file that exists in the current project.
   /// Add this XmlNamespace attribute to the root element of the markup file where it is 
   /// to be used:
   ///
   ///     xmlns:MyNamespace="clr-namespace:WpfCommandControlsLibrary"
   ///
   ///
   /// Step 1b) Using this custom control in a XAML file that exists in a different project.
   /// Add this XmlNamespace attribute to the root element of the markup file where it is 
   /// to be used:
   ///
   ///     xmlns:MyNamespace="clr-namespace:WpfCommandControlsLibrary;Assembly=WpfCommandControlsLibrary"
   ///
   /// You will also need to add a project reference from the project where the XAML file lives
   /// to this project and Rebuild to avoid compilation errors:
   ///
   ///     Right click on the target project in the Solution Explorer and
   ///     "Add Reference"->"Projects"->[Select this project]
   ///
   ///
   /// Step 2)
   /// Go ahead and use your control in the XAML file.
   ///
   ///     <MyNamespace:CustomControl1/>
   ///
   /// </summary>

   public class CommandComboBox : ComboBox, ICommandSource
   {
      public CommandComboBox() : base()
      {
      }

  #region Dependency Properties
  // Make Command a dependency property so it can use databinding.
  public static readonly DependencyProperty CommandProperty =
      DependencyProperty.Register(
          "Command",
          typeof(ICommand),
          typeof(CommandComboBox),
          new PropertyMetadata((ICommand)null,
          new PropertyChangedCallback(CommandChanged)));

  public ICommand Command
  {
     get
     {
        return (ICommand)GetValue(CommandProperty);
     }
     set
     {
        SetValue(CommandProperty, value);
     }
  }

  // Make CommandTarget a dependency property so it can use databinding.
  public static readonly DependencyProperty CommandTargetProperty =
      DependencyProperty.Register(
          "CommandTarget",
          typeof(IInputElement),
          typeof(CommandComboBox),
          new PropertyMetadata((IInputElement)null));

  public IInputElement CommandTarget
  {
     get
     {
        return (IInputElement)GetValue(CommandTargetProperty);
     }
     set
     {
        SetValue(CommandTargetProperty, value);
     }
  }

  // Make CommandParameter a dependency property so it can use databinding.
  public static readonly DependencyProperty CommandParameterProperty =
      DependencyProperty.Register(
          "CommandParameter",
          typeof(object),
          typeof(CommandComboBox),
          new PropertyMetadata((object)null));

  public object CommandParameter
  {
     get
     {
        return (object)GetValue(CommandParameterProperty);
     }
     set
     {
        SetValue(CommandParameterProperty, value);
     }
  }

  #endregion

  // Command dependency property change callback.
  private static void CommandChanged(DependencyObject d,
      DependencyPropertyChangedEventArgs e)
  {
     CommandComboBox cb = (CommandComboBox)d;
     cb.HookUpCommand((ICommand)e.OldValue, (ICommand)e.NewValue);
  }

  // Add a new command to the Command Property.
  private void HookUpCommand(ICommand oldCommand, ICommand newCommand)
  {
     // If oldCommand is not null, then we need to remove the handlers.
     if (oldCommand != null)
     {
        RemoveCommand(oldCommand, newCommand);
     }
     AddCommand(oldCommand, newCommand);
  }

  // Remove an old command from the Command Property.
  private void RemoveCommand(ICommand oldCommand, ICommand newCommand)
  {
     EventHandler handler = CanExecuteChanged;
     oldCommand.CanExecuteChanged -= handler;
  }

  // Add the command.
  private void AddCommand(ICommand oldCommand, ICommand newCommand)
  {
     EventHandler handler = new EventHandler(CanExecuteChanged);
     canExecuteChangedHandler = handler;
     if (newCommand != null)
     {
        newCommand.CanExecuteChanged += canExecuteChangedHandler;
     }
  }
  private void CanExecuteChanged(object sender, EventArgs e)
  {

     if (this.Command != null)
     {
        RoutedCommand command = this.Command as RoutedCommand;

        // If a RoutedCommand.
        if (command != null)
        {
           if (command.CanExecute(CommandParameter, CommandTarget))
           {
              this.IsEnabled = true;
           }
           else
           {
              this.IsEnabled = false;
           }
        }
        // If a not RoutedCommand.
        else
        {
           if (Command.CanExecute(CommandParameter))
           {
              this.IsEnabled = true;
           }
           else
           {
              this.IsEnabled = false;
           }
        }
     }
  }

  // If Command is defined, selecting a combo box item will invoke the command;
  // Otherwise, combo box will behave normally.
  protected override void OnSelectionChanged(SelectionChangedEventArgs e)
  {
     base.OnSelectionChanged(e);

     if (this.Command != null)
     {
        RoutedCommand command = Command as RoutedCommand;

        if (command != null)
        {
           command.Execute(CommandParameter, CommandTarget);
        }
        else
        {
           ((ICommand)Command).Execute(CommandParameter);
        }
     }
  }

  // Keep a copy of the handler so it doesn't get garbage collected.
  private static EventHandler canExecuteChangedHandler;

  }
}
17
eesh

L'utilisation de propriétés attachées est une excellente solution à ce type de problème. Marlon Grech a porté l'utilisation des propriétés associées au niveau supérieur en créant Comportements de commandes associées . En utilisant celles-ci, il est possible de lier toute commande existante dans un ViewModel à tout événement existant dans la vue.

C’est quelque chose que j’utilise beaucoup pour traiter des problèmes similaires avec les ListBox, dans lesquels je veux qu’ils s’ouvrent, ou qu’ils éditent ou effectuent des actions lors d’un double-clic.

Dans cet exemple, j'utilise une version plus ancienne des comportements de commande jointe, mais l'effet est le même. J'ai un style qui est utilisé pour ListBoxItems et auquel je me connecte explicitement. Cependant, il serait assez facile de créer un style d'application ou de fenêtre s'appliquant à tous les ListBoxItems définissant les commandes à un niveau beaucoup plus élevé. Ensuite, chaque fois que l'événement de ListBoxItem attaché à la propriété CommandBehavior.Event se déclenche, il déclenche plutôt la commande attachée.

<!-- acb is the namespace reference to the Attached Command Behaviors -->
<Style x:Key="Local_OpenListItemCommandStyle">
    <Setter Property="acb:CommandBehavior.Event"
            Value="MouseDoubleClick" />
    <Setter Property="acb:CommandBehavior.Command"
            Value="{Binding ElementName=uiMyListBorder, Path=DataContext.OpenListItemCommand}" />
    <Setter Property="acb:CommandBehavior.CommandParameter"
            Value="{Binding}" />
</Style>

<DataTemplate x:Key="MyView">
<Border x:Name="uiMyListBorder">
<ListBox  ItemsSource="{Binding MyItems}"
          ItemContainerStyle="{StaticResource local_OpenListItemCommandStyle}" />
</Border>
</DataTemplate>
2
rmoore

J'ai écrit des comportements (propriétés attachées) pour le faire, et il y a encore des cas où j'en ai besoin.

Dans le cas habituel cependant, en liant simplement un événement à une commande, vous pouvez tout faire dans Xaml si Blend SDK 4 est installé. Notez que vous devrez ajouter une référence à System.Windows.Interactivity.dll et redistribuer cet assembly.

Cet exemple appelle une ICommand DragEnterCommand sur le ViewModel lorsque l'événement DragEnter de la grille est déclenché:

<UserControl xmlns:i="clr-namespace:System.Windows.Interactivity;Assembly=System.Windows.Interactivity" >
    <Grid>
        <i:Interaction.Triggers>
            <i:EventTrigger EventName="DragEnter">
                <i:InvokeCommandAction Command="{Binding DragEnterCommand}" CommandParameter="{Binding ...}" />
            </i:EventTrigger>
        </i:Interaction.Triggers>
    </Grid>
</UserControl>
1
Mike Fuchs

Eh bien, personne n'a répondu. J'ai donc abandonné et déplacé l'implémentation de View en dehors du dictionnaire dans un UserControl standard, je lui ai injecté une référence à ViewModel.

Désormais, lorsque le contrôle ListBox déclenche l'événement, il appelle ViewModel et à partir de là, tout est à nouveau possible.

Ariel

1
ArielBH

Essayez d'utiliser Prism 2 .

Il vient avec de grandes extensions de commande et ouvre de nombreuses nouvelles possibilités (comme des commandes pour être lié à une arborescence visuelle).

0
Jarek Kardas