пятница, 25 марта 2011 г.

MVVM и события интерфейса

Чудесный паттерн MVVM совершенно не говорит нам каким образом мы должны отлавливать и реагировать на некоторые действия пользователя. Например совершенно непонятно каким образом понять навел ли пользователь на некоторый регион интерфейса или как отловить событие Drag-and-Drop, не зная ничего о представлении. В WPF на эти случаи есть code-behind методы, однако они запрещены в MVVM. На помощь приходит система связывания триггеров и команд коих в интернете найдется с десяток и все друга на друга похожи, по крайней мере идеей. Остановлюсь на библиотеке System.Windows.Interactivity. Если я не ошибаюсь, поставляется вместе с покетом Microsoft Expression Blend SDK for .NET4, автоматом ставится с Expression Studio 4. Итак, первым дело нужно включить в xaml разметку пространство имен с расширениями
 xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"  
теперь к любому контролу можно прикрепить коллекцию Interaction.triggers, а в нее вложить треггеры которые будут вылавливать нужное нам событи. Название события указывается в триггере в поле EventName. Например, если в контроле нужно поймать событие "MouseEnter", разметка будет выглядеть так
 <i:Interaction.Triggers>  
      <i:EventTrigger EventName="MouseEnter">  
           ...  
      </i:EventTrigger>  
 </i:Interaction.Triggers>  
вместо "..." используем "i:InvokeCommandAction" к которой можно привязать любую команду как в предыдущем посте. Получится что-то вроде такого
 <i:Interaction.Triggers>  
      <i:EventTrigger EventName="MouseEnter">  
           <i:InvokeCommandAction Command="{Binding SomeCommand}"/>  
      </i:EventTrigger>  
 </i:Interaction.Triggers>  
В качестве примера реализуем простенькую рисовалку. ViewModel будет состоять из текущей недорисованной кривой, коллекции законценных кривых и трех команд. Вместе и инициализацией класс выглядит так
 using System.Collections.ObjectModel;  
 using System.ComponentModel;  
 using System.Windows;  
 using System.Windows.Input;  
 using System.Windows.Media;  
 using Microsoft.Practices.Prism.Commands;  
 namespace CommandBehaviorAPP  
 {  
      class ViewModel : INotifyPropertyChanged  
      {  
           PointCollection _currentCurve = null;  
           public ObservableCollection<PointCollection> CurvesCollection { get; protected set; }  
           public DelegateCommand StartDrawCommand { get; protected set; }  
           public DelegateCommand<IInputElement> SetNextPointCommand { get; protected set; }  
           public DelegateCommand EndDrawCommand { get; protected set; }  
           public ViewModel()  
           {  
                CurvesCollection = new ObservableCollection<PointCollection>();  
                SetNextPointCommand = new DelegateCommand<IInputElement>(  
                     inputElement =>  
                     {  
                          if (Mouse.LeftButton == MouseButtonState.Released)  
                          {  
                               StopDraw();  
                               return;  
                          }  
                          if (inputElement != null)  
                          {  
                               _currentCurve.Add(Mouse.GetPosition(inputElement));  
                               CurvesCollection.Remove(_currentCurve);  
                               CurvesCollection.Add(_currentCurve);  
                          }  
                     },  
                     point => _currentCurve != null);  
                StartDrawCommand = new DelegateCommand(  
                     () =>  
                     {  
                          _currentCurve = new PointCollection();  
                          SetNextPointCommand.RaiseCanExecuteChanged();  
                     });  
                EndDrawCommand = new DelegateCommand(  
                     () =>  
                     {  
                          StopDraw();  
                     });  
           }  
           void StopDraw()  
           {  
                _currentCurve = null;  
                SetNextPointCommand.RaiseCanExecuteChanged();  
           }  
           #region INotifyPropertyChanged Members  
           public event PropertyChangedEventHandler PropertyChanged;  
           protected void OnPropertyChanged(string propertyName)  
           {  
                if (PropertyChanged != null)  
                {  
                     PropertyChanged(this, new PropertyChangedEventArgs(propertyName));  
                }  
           }  
           #endregion  
      }  
 }  
Немного кривой выглядит команда "SetNextPointCommand". В качестве аргумента эта команда принимает элемент интерфейса относительно которого мы найдем текущие координаты мыши. Вторая неясность связанна с постоянным дерганием текущей кривой из коллекции
 ...  
 CurvesCollection.Remove(_currentCurve);  
 CurvesCollection.Add(_currentCurve);  
 ...  
такое поведение сделал из-за того, что PointCollection не является ObservableCollection. Чтобы не придумывать хитроумные механизмы и не усложнять программу решил сделать самым простым способом. Разметка кривой при условии, что в DataContext будет находится PointCollection выглядит так
 <DataTemplate x:Key="CurveTemplate">  
      <Path Stroke="Black" StrokeThickness="1">  
           <Path.Data>  
                <PathGeometry>  
                     <PathGeometry.Figures>  
                          <PathFigureCollection>  
                               <PathFigure IsClosed="False" StartPoint="{Binding .[0]}">  
                                    <PathFigure.Segments>  
                                         <PolyLineSegment Points="{Binding }"/>  
                                    </PathFigure.Segments>  
                               </PathFigure>  
                          </PathFigureCollection>  
                     </PathGeometry.Figures>  
                </PathGeometry>  
           </Path.Data>  
      </Path>  
 </DataTemplate>  
ну исамое главное, разметка основного контрола
 <Grid MinHeight="500" MinWidth="500" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" >  
      <ItemsControl HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Background="Azure" x:Name="MainCanvas"  
                          ItemsSource="{Binding CurvesCollection}" ItemTemplate="{StaticResource CurveTemplate}">  
           <i:Interaction.Triggers>  
                <i:EventTrigger EventName="PreviewMouseMove" >  
                     <i:InvokeCommandAction Command="{Binding SetNextPointCommand}" CommandParameter="{Binding ElementName=MainCanvas}"/>  
                </i:EventTrigger>  
                <i:EventTrigger EventName="PreviewMouseDown">  
                     <i:InvokeCommandAction Command="{Binding StartDrawCommand}"/>  
                </i:EventTrigger>  
                <i:EventTrigger EventName="PreviewMouseUp">  
                     <i:InvokeCommandAction Command="{Binding EndDrawCommand}"/>  
                </i:EventTrigger>  
                <i:EventTrigger EventName="MouseLeave">  
                     <i:InvokeCommandAction Command="{Binding EndDrawCommand}"/>  
                </i:EventTrigger>  
           </i:Interaction.Triggers>  
           <ItemsControl.ItemsPanel>  
                <ItemsPanelTemplate>  
                     <Canvas/>  
                </ItemsPanelTemplate>  
           </ItemsControl.ItemsPanel>  
      </ItemsControl>  
      <ContentControl Content="{Binding CurrentCurve}" ContentTemplate="{StaticResource CurveTemplate}"/>  
 </Grid>  
исходники находятся здесь

2 комментария:

  1. code-behind не запрещен в MVVM. Для таких операций как Drag&Drop, Focus и других не стандартных вещей удобнее будет использовать code-behind.

    ОтветитьУдалить
    Ответы
    1. Сам паттерн подразумевает отказ от использования код-бихайнда

      Удалить