WPF MVVM : Commands are easy. How to Connect View

2019-01-21 17:26发布

问题:

Suppose I have a view implemented as a DataTempate inside a resource Dictionary. And I have a corresponding ViewModel. Binding Commands are easy. But what if my View contains a control such as a ListBox, and I need to Publish an application wide event (Using Prism's Event Aggreagtor) based on the Item being Changed on the List.

if ListBox supports a command I could just bind it to a command in the ViewModel and publish the event. But Listbox doesn't allow such an option. How do I bridge this?

EDIT: Many great answers.

Take a look at this link http://blogs.microsoft.co.il/blogs/tomershamam/archive/2009/04/14/wpf-commands-everywhere.aspx

Thanks

Ariel

回答1:

One option is to extend the control in question and add support for the particular command you require. For example, I've modified ListView before to support the ItemActivated event and related command.



回答2:

Instead of trying to bind a command to when the item changes, I looked at the problem another way.

If you bind the selected item of the ListBox to a property in the ViewModel, then when that property is changed you can publish the event. That way the ViewModel remains the source of the event and it is triggered by the item changing, which is what you want.

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


回答3:

Extend the control to support ICommandSource and decide which action should trigger the command.

I did this with Combo Box and used OnSelectionChanged as the trigger for the command. First I will show in XAML how I bind the command to the extended Control ComboBox which I called CommandComboBox, then I will show the code for CommandComboBox that adds the support for the ICommandSource to ComboBox.

1) Using CommandComboBox in your XAML code:

In your XAML namespace declarations include

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

Use the CommandComboBox in place of ComboBox and bind the command to it like so: Note that in this example I have a defined a command called SetLanguageCommand im my ViewModel and I am passing the selected value for this ComboBox as the parameter to the command.

 <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) The code for CommandComboBox

The code for the file CommandComboBox.cs is included below. I added this file to a Class Library called WpfCommandControlsLibrary and made it a separate project so I could easily add any extend commands to whatever solution needed to use them and so I could easily add additional WPF Controls and extend them to support the ICommandSource inteface.

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;

  }
}


回答4:

A great solution to this type of problem comes from the usage of Attached Properties. Marlon Grech has taken the usage of Attached Properties to the next level by creating Attached Command Behaviors. Using these it is possible to bind any Command existing in a ViewModel to any Event existing in the view.

This is something I use a lot to deal with similar issues with ListBoxes, where I want them to open, or edit or do some action on a double click.

In this example I'm using an older version of Attached Command Behaviors, but the effect is the same. I have a style that is used for ListBoxItems which I am explicitly keying to. However, it would be easy enough to create a application or window wide style applying to all ListBoxItems that sets the commands at a much higher level. Then, whenever the event for the ListBoxItem attached to the CommandBehavior.Event property would fire, it instead fires off the attached Command.

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


回答5:

Well, nobody answered. So I've gave up and moved the implementation of the View outside the Dictionary into a regular UserControl, I've injected him a reference to the ViewModel.

Now when the ListBox fire the Event it's calls the ViewModel and from there everything is possible again.

Ariel



回答6:

I have been writing behaviors (attached properties) to do this, and there are still cases where I need them.

For the usual case however, simply binding an event to a command, you can do everything in Xaml if you have Blend SDK 4 installed. Note that you will have to add a reference to System.Windows.Interactivity.dll, and to redistribute this assembly.

  • Expression Blend SDK for .NET 4
  • Microsoft SDKs (for future reference)

This example is invoking an ICommand DragEnterCommand on the ViewModel when the DragEnter event of the Grid is fired:

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


回答7:

Try using Prism 2.

It comes with great extensions to commanding and opens many new posibilites (like commands to being tied to visual tree).