WPF MVVM: Команды просты. Как соединить View и ViewModel с RoutedEvent - PullRequest
15 голосов
/ 10 мая 2009

Предположим, у меня есть представление, реализованное как DataTempate внутри словаря ресурсов. И у меня есть соответствующая ViewModel. Обязательные команды просты. Но что, если мое представление содержит элемент управления, такой как ListBox, и мне нужно опубликовать событие всего приложения (с помощью агрегатора событий Prism) на основе изменяемого элемента в списке.

если ListBox поддерживает команду, я мог бы просто привязать ее к команде в ViewModel и опубликовать событие. Но Listbox не позволяет такую ​​опцию. Как мне это преодолеть?

EDIT: Много отличных ответов.

Взгляните на эту ссылку http://blogs.microsoft.co.il/blogs/tomershamam/archive/2009/04/14/wpf-commands-everywhere.aspx

Спасибо

Ariel

Ответы [ 7 ]

44 голосов
/ 17 мая 2009

Вместо того, чтобы пытаться связать команду с изменением элемента, я посмотрел на проблему по-другому.

Если вы привязали выбранный элемент ListBox к свойству в ViewModel, то при изменении этого свойства вы можете опубликовать событие. Таким образом, ViewModel остается источником события, и оно вызывается изменением элемента, что вам и нужно.

<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
        }
}
17 голосов
/ 12 мая 2009

Расширить элемент управления для поддержки ICommandSource и решить, какое действие должно вызвать команду.

Я сделал это с помощью поля со списком и использовал OnSelectionChanged в качестве триггера для команды. Сначала я покажу в XAML, как связать команду с расширенным ComboBox элемента управления, который я назвал CommandComboBox, затем я покажу код для CommandComboBox, который добавляет поддержку ICommandSource в ComboBox.

1) Использование CommandComboBox в вашем коде XAML:

В ваших объявлениях пространства имен XAML есть

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

Используйте CommandComboBox вместо ComboBox и привяжите команду к нему следующим образом: Обратите внимание, что в этом примере у меня есть определенная команда SetLanguageCommand im my ViewModel, и я передаю выбранное значение для этого ComboBox в качестве параметра в команда.

 <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) Код для CommandComboBox

Код для файла CommandComboBox.cs приведен ниже. Я добавил этот файл в библиотеку классов с именем WpfCommandControlsLibrary и сделал его отдельным проектом, чтобы я мог легко добавлять любые команды расширения в любое решение, необходимое для их использования, и поэтому я мог легко добавлять дополнительные элементы управления WPF и расширять их для поддержки интерфейса 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;

  }
}
8 голосов
/ 11 мая 2009

Один из вариантов - расширить рассматриваемый элемент управления и добавить поддержку для конкретной команды, которая вам требуется. Например, я изменил ListView перед для поддержки события ItemActivated и связанной команды.

2 голосов
/ 12 мая 2009

Отличным решением проблемы такого типа является использование Attached Properties. Марлон Греч поднял использование Attached Properties на следующий уровень, создав Attached Command Behaviors . Используя их, можно связать любую Команду, существующую в ViewModel, с любым Событием, существующим в представлении.

Это то, что я часто использую для решения аналогичных проблем со списками, где я хочу, чтобы они открывались, редактировались или выполняли какие-либо действия двойным щелчком мыши.

В этом примере я использую старую версию Attached Command Behaviors, но эффект тот же. У меня есть стиль, который используется для ListBoxItems, к которому я явно обращаюсь. Однако было бы достаточно легко создать стиль приложения или окна, применяемый ко всем элементам ListBoxItems, который устанавливает команды на гораздо более высоком уровне. Затем, всякий раз, когда событие для ListBoxItem, присоединенного к свойству CommandBehavior.Event, срабатывает, оно вместо этого запускает присоединенную команду.

<!-- 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>
1 голос
/ 25 марта 2013

Я писал для этого поведения (прикрепленные свойства), и до сих пор есть случаи, когда они мне нужны.

Однако в обычном случае, просто привязав событие к команде, вы можете делать все в Xaml, если у вас установлен Blend SDK 4. Обратите внимание, что вам нужно будет добавить ссылку на System.Windows.Interactivity.dll и распространить эту сборку.

В этом примере вызывается ICommand DragEnterCommand для ViewModel при возникновении события DragEnter в Grid:

<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 голос
/ 10 мая 2009

Ну, никто не ответил. Поэтому я сдался и переместил реализацию View вне словаря в обычный UserControl, я вставил ему ссылку на ViewModel.

Теперь, когда ListBox запускает Событие, он вызывает ViewModel, и оттуда все возможно снова.

Ariel

0 голосов
/ 10 мая 2009

Попробуйте использовать Prism 2 .

Он значительно расширяет возможности командования и открывает множество новых возможностей (например, команды привязки к визуальному дереву).

...