Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Разработка графического пользовательского интерфейса в соответствии с паттерном Model-View-Viewmodel на платформе Windows Presentation Foundation. Осн

.pdf
Скачиваний:
0
Добавлен:
12.08.2026
Размер:
1 Мб
Скачать
☆

public static RoutedCommand CommandAddBlockUnlocated = new RoutedCommand();

//--------------------------------------------

public static RoutedCommand CommandAddBlockLocated = new RoutedCommand();

//--------------------------------------------

public static RoutedCommand CommandSelectBlock = new RoutedCommand();

//--------------------------------------------

public static ExecutedRoutedEventHandler ExecuteCommandAddBlockUnlocated =

AddBlockUnlocated; //--------------------------------------------

public static ExecutedRoutedEventHandler ExecuteCommandAddBlockLocated = AddBlockLocated;

//--------------------------------------------

public static ExecutedRoutedEventHandler ExecuteCommandSelectBlock = SelectBlock;

//--------------------------------------------

static void AddBlockUnlocated

(

object parameter, ExecutedRoutedEventArgs e

)

{

ViewModel view = e.Parameter as ViewModel; Point p = new Point

(

view.PlotArea.ActualWidth / 2, view.PlotArea.ActualHeight / 2

);

AddBlock(view, p); e.Handled = true;

}

//--------------------------------------------

static void AddBlockLocated

(

object parameter, ExecutedRoutedEventArgs e

)

{

ViewModel view = e.Parameter as ViewModel; Point p = Mouse.GetPosition(view.PlotArea); AddBlock(view, p);

e.Handled = true;

31

}

//--------------------------------------------

static void AddBlock(ViewModel view, Point p)

{

ContentControl blockWrapper = new ContentControl();

blockWrapper.Template = view.BlockTemplate; ModelBlock b = new ModelBlock();

b.Name = "block #" + view.PlotArea.Children.Count.ToString(); blockWrapper.DataContext = b; MouseGesture mg = new MouseGesture(); mg.MouseAction = MouseAction.LeftClick; MouseBinding mb = new MouseBinding(); mb.Gesture = mg;

mb.Command = ViewModel.CommandSelectBlock; mb.CommandParameter = view; blockWrapper.InputBindings.Add(mb); Canvas.SetLeft(blockWrapper, p.X); Canvas.SetTop(blockWrapper, p.Y); view.PlotArea.Children.Add(blockWrapper);

}

//--------------------------------------------

static void SelectBlock

(

object parameter, ExecutedRoutedEventArgs e

)

{

ViewModel view = e.Parameter as ViewModel; new ViewMovingElement().StartMoving

(

e.OriginalSource as FrameworkElement );

e.Handled = true;

}

//--------------------------------------------

public static readonly DependencyProperty PlotAreaProperty = DependencyProperty.Register

(

"PlotArea",

typeof(Canvas),

typeof(ViewModel)

);

32

//--------------------------------------------

public Canvas PlotArea

{

get

{

return GetValue(PlotAreaProperty) as Canvas;

}

set

{

SetValue(PlotAreaProperty, value);

}

}

//--------------------------------------------

public static readonly

DependencyProperty BlockTemplateProperty = DependencyProperty.Register

(

"BlockTemplate",

typeof(ControlTemplate),

typeof(ViewModel)

); //--------------------------------------------

public ControlTemplate BlockTemplate

{

get

{

return GetValue(BlockTemplateProperty) as ControlTemplate;

}

set

{

SetValue(BlockTemplateProperty, value);

}

}

//--------------------------------------------

}

Действия по фактическому добавлению объектов в контейнер PlotArea заключаются в том, что сначала создается новый объект ContentControl, затем для него устанавливаются шаблон визуального представления (свойство Template) и контекст данных (свойство

33

DataContext). Шаблон представления определен на XAML и доступен CLR-коду через свойство BlockTemplate класса ViewModel, а в качестве контекста данных устанавливается новый объект ModelBlock. Блоки различаются по именам (свойство Name класса ModelBlock), и для генерации новых имен используется текущее количество дочерних элементов контейнера (свойство PlotArea.Children.Count).

Для доступа ко всем этим объектам требуется ссылка на экземпляр класса ViewModel – такая ссылка обеспечивается параметром команды. (Далее будет показано, как задается этот параметр на XAML.)

Элемент GUI, отображающий созданный блок, должен быть подключен к обработке событий пользовательского ввода. Для этого создается связывание щелчка левой кнопкой мыши (жест LeftClick) с ко-

мандой CommandSelectBlock. Этот MouseBinding-объект добавляется в коллекцию InputBindings объекта ContentControl. Таким обра-

зом, при щелчке левой кнопкой мыши на каком-либо элементе, определенном визуальным представлением блока, автоматически будет инициироваться команда CommandSelectBlock.

Для позиционирования блока в пределах рабочей области PlotArea вызываются статические методы класса Canvas (SetLeft и SetTop). Координаты курсора мыши можно получить из статического метода Mouse.GetPosition, а поскольку координаты элемента GUI в WPF имеют смысл только относительно какого-либо контейнера, то метод GetPosition получает в качестве параметра ссылку на PlotArea.

Все действия по перетаскиванию блока инкапсулирует CLR-класс

ViewMovingElement:

class ViewMovingElement

{

//--------------------------------------------

bool dragging; //--------------------------------------------

Point eventPosition;

34

//--------------------------------------------

public void StartMoving

(

FrameworkElement elementToControl

)

{

if (!dragging)

{

dragging = true; elementToControl.MouseLeftButtonUp +=

OnMouseLeftButtonUp; elementToControl.MouseMove +=

OnMouseMove;

FrameworkElement canvasContainer = elementToControl.Parent as FrameworkElement;

eventPosition = Mouse.GetPosition(canvasContainer);

Mouse.Capture(elementToControl);

}

}

//--------------------------------------------

void OnMouseLeftButtonUp

(

Object sender, MouseButtonEventArgs e

)

{

dragging = false; FrameworkElement feThis =

sender as FrameworkElement; Mouse.Capture(null);

feThis.MouseLeftButtonUp -= OnMouseLeftButtonUp; feThis.MouseMove -= OnMouseMove;

e.Handled = true;

}

//--------------------------------------------

void OnMouseMove

(

Object sender, MouseEventArgs e

)

{

if (dragging && e.LeftButton == MouseButtonState.Pressed)

{

35

FrameworkElement feThis = sender as FrameworkElement;

FrameworkElement canvasContainer = feThis.Parent as FrameworkElement;

Point thisPosition = e.GetPosition(canvasContainer);

Vector shift = thisPosition - eventPosition; double left = Canvas.GetLeft(feThis); double top = Canvas.GetTop(feThis); Canvas.SetLeft(feThis, left + shift.X); Canvas.SetTop(feThis, top + shift.Y); eventPosition = thisPosition;

}

e.Handled = true;

}

//--------------------------------------------

}

В этом классе реализован следующий алгоритм. Во время вызова метода StartMoving выполняется проверка, находится ли объект в фазе dragging (перетаскивание), для чего используется соответствующий флажок. Если нет, то происходит переключение в эту фазу. Текущая позиция курсора мыши (тип данных Point) заносится в переменную eventPosition. Подключается прослушивание событий MouseLeftButtonUp и MouseMove на визуальном элементе, инициировавшем выполнение команды CommandSelectBlock и вызов метода StartMoving. Устанавливается «перехват» событий ввода-вывода от мыши (mouse capture), чтобы этот элемент мог реагировать на перемещение курсора мыши даже за пределами рабочей области. При обработке со-

бытия MouseLeftButtonUp (метод OnMouseLeftButtonUp) выполня-

ются в некотором смысле противоположные действия: осуществляется выход из фазы перетаскивания, снимается перехват (и вообще прослушивание) событий от мыши. После этого объект ViewMovingElement может быть утилизирован сборщиком мусора.

Пока блок находится в фазе перетаскивания, вызывается обработчик события MouseMove (метод OnMouseMove). Действия этого метода

36

сводятся к вычислению смещения мыши относительно предыдущей позиции и модификации положения перетаскиваемого визуального элемента относительно родительского контейнера Canvas.

Выше было показано связывание жеста LeftClick с командой CommandSelectBlock в CLR-коде в императивном стиле, поскольку нужно было ассоциировать жест с новым элементом GUI, не присутствующим в разметке на XAML. С другой стороны, команды добавления нового блока ассоциируются с самим окном, выступающим в роли «источника» блоков, эта связь статична и является в некотором смысле атрибутом окна, поэтому связывание удобно описать на XAML:

<Window.InputBindings>

<MouseBinding

Gesture="LeftClick"

Command="{x:Static

custom:ViewModel.CommandAddBlockLocated}" CommandParameter="{StaticResource View}" />

<KeyBinding

Key="N"

Modifiers="Control"

Command="{x:Static

custom:ViewModel.CommandAddBlockUnlocated}" CommandParameter="{StaticResource View}"

/>

</Window.InputBindings>

Свойство CommandParameter объектов MouseBinding и KeyBinding получает ссылку на объект ViewModel (созданный непосредственно в разметке на XAML как статический ресурс с ключом “View”), поэтому обработчики команд могут пользоваться этим параметром для доступа к модели представления.

Помимо связывания команд с событиями пользовательского ввода, требуется также связывание команд с конкретными делегатамиобработчиками, что является окончательным звеном цепочки передачи информации от сигналов ввода к алгоритмам их обработки. Эта связь

37

создается на XAML в виде коллекции объектов CommandBinding (свой-

ство Window.CommandBindings):

<Window.CommandBindings>

<CommandBinding

Command="{x:Static

custom:ViewModel.CommandAddBlockUnlocated}"

Executed="{x:Static

custom:ViewModel.ExecuteCommandAddBlockUnlocated}"

/>

<CommandBinding

Command="{x:Static

custom:ViewModel.CommandAddBlockLocated}"

Executed="{x:Static

custom:ViewModel.ExecuteCommandAddBlockLocated}"

/>

<CommandBinding

Command="{x:Static

custom:ViewModel.CommandSelectBlock}"

Executed="{x:Static

custom:ViewModel.ExecuteCommandSelectBlock}"

/>

</Window.CommandBindings>

Здесь используются только события Executed, являющиеся сигналами к исполнению команд. Делегаты для их обработки определены как статические поля класса ViewModel.

38

Классовая декомпозиция MVVM

При реализации GUI в соответствии с паттерном MVVM на WPF можно использовать различные наборы CLR-классов. Рассмотрим некоторые типичные конфигурации. Класс Model во всех вариантах представляет модель предметной области. Компонент View ни в одном из вариантов не выделяется в отдельный класс и ему соответствует вся совокупность XAML-фрагментов.

Вариант A. Классы Model, ViewModel

Это минималистичная схема для несложных приложений, управляемых несколькими командами. Класс ViewModel предоставляет для View контекст данных – некоторый набор связываемых свойств (dependency properties). Обработчики команд и событий определяются также в классе ViewModel, а связывание команд и событий (CommandBindings и InputBindings) описывается дизайнером на XAML.

Вариант B. Классы Model, ViewModel, ViewController

Основной недостаток варианта A состоит в смешивании внутри класса ViewModel контекста данных и программной логики поведения. Вариант B устраняет этот недостаток, вынося алгоритмы реакции на события и команды за пределы ViewModel. Таким образом, ViewModel по-прежнему предоставляет все связываемые свойства для использования на XAML посредством {Binding ...}, т.е. определяет программ-

ный интерфейс для взаимодействия с View, но не осведомлен о реализации обработчиков. Аспект реализации фактического поведения GUI инкапсулируется в классе ViewController. Одновременно это позво-

ляет сделать класс ViewModel неосведомленным даже о существовании

39

класса Model, что также можно считать серьезным преимуществом варианта B с точки зрения структурной гибкости.

Вариант C. Классы Model, ViewModel, ViewController, ViewEvents, ViewCommands

В этом варианте происходит дальнейшее разделение аспектов поведения программы. В программных проектах с развитым GUI монолитный класс ViewController может оказаться неудобным по причине неестественного смешивания в нем разноуровневых каналов пользовательского управления. Выполнение одних задач инициируется обобщенными командами (более высокий, абстрактный уровень), других – конкретными событиями от физических устройств ввода (относительно низкий уровень). Схемы подключения делегатов-обработчиков и алгоритмы обработки в одном и другом случаях разные, поэтому логично разнести их по разным классам. Следует отметить, что, в принципе, возможно, но далеко не всегда эффективно искусственное формирование абстрактных команд на основе низкоуровневых событий. Например, можно устроить приложение таким образом, что при каждом перемещении мыши генерировалась бы специально определенная команда, тогда применение варианта C не имеет смысла. Однако производительность такого решения низкая, а накладные расходы на связывание – завышенные.

Рассмотрим пример построения приложения на основе варианта C как наиболее гибкого. В процессе разработки этого примера станут более отчетливыми роли каждого из объектов — участников взаимодействия.

ViewController является адаптером модели, позволяющим применять, возможно даже параллельно, разные типы моделей с разными программными интерфейсами. Такая структура полностью изолирует класс Model от остальных составляющих блока ViewModel, т.е. View-

40

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]