Чудесный паттерн 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>
исходники находятся
здесь
code-behind не запрещен в MVVM. Для таких операций как Drag&Drop, Focus и других не стандартных вещей удобнее будет использовать code-behind.
ОтветитьУдалитьСам паттерн подразумевает отказ от использования код-бихайнда
Удалить