вторник, 31 августа 2010 г.

Реализация шаблона MVC на примере игры типа Packman – Часть 3

В прошлой статье были описаны модели основных сущностей нашей игры типа Pаckman. В этой статье мы поговорим о таком элементе паттерна MVC как “представление”.

Понятие “представления” в шаблоне Model-View-Controller

Представление – это абстрактный суперкласс View реализует общие поведения представления. Встроенный набор компонентов подкласса представления (например, StandardSystemView – стандартное окно и TextEditorView – текстовый редактор). Представления могут быть вложенными для разработки сложного пользовательского интерфейса. Например, StandardSystemView (т.е. окна) содержит компонент представления, называемые TextEditorView.

Каждое представление может иметь модель и контроллер. Встроенные компоненты по умолчанию имеют заранее определенный класс контроллера для реализации своего стандартного поведения. Чтобы создать экземпляр компонента, вам нужно настроить свою модель и отобразить  компонент. Компонент затем будет инициализировать по умолчанию контроллер со своим экземпляром модели, и регистрироваться в ней для получения сообщений. При закрытии компонента, освобождаются все вложенные компоненты. Когда представление освободилось, оно удаляет свои подписки на сообщения из модели. Если вы хотите чтобы компоненты имели своё уникальное поведения, вы можете создать свой контроллер для него.

В обязанности Представления входит отображение данных полученных от Модели. Обычно Представление имеет свободный доступ к Модели и может брать из нее данные, однако это доступ только на чтение, ничего менять в Модели или даже просто вызывать методы, приводящие к изменению ее внутреннего состояния, Представлению позволять нельзя. В случае активной Модели, Представление может подписаться на события изменения Модели и перерисовываться, забрав измененные данные, при получении соответствующего оповещения. Для взаимодействия с Контроллером, представление, как правило, реализует некий интерфейс, известный Контроллеру, что позволяет менять представления независимо и иметь несколько представлений на Контроллер. Вообще, подмена или изменение Представления самая часто встречающаяся задача, по сути это и есть та причина, по которой придумывают различные паттерны разделения Модели и Представления.

Использование обобщенного MVC паттерна сводится к следующим шагам. Во-первых, разработчик определяет свойство модели – любой класс, представляющий данные предметной области и наполненный определенной бизнес-логикой приложения, так называемую модель предметной области (Domain Model). В дальнейшем данный класс будет подставляться в качестве параметра P класса Model – модель готова. Во-вторых, разработчик определяет представление, которое будет отображать данную модель. Класс представления наследуется от класса View, которому в качестве параметра P подставляется свойство модели, определенное на предыдущем шаге. Если необходимо работать с представлением предметной области, разработчик определяет представление, которое наследуется от класса ObjectView<P>. В качестве параметра P класса ObjectView подставляется тот же тип – свойство модели. Остальные задачи: выбор экранной формы отображения данных модели, инициализация ее, а также реакция экранной формы на изменение отображаемой модели полностью лежат на разработчике.

Свойство модели, которое необходимо модели, программист в любом случае формирует – это данные предметной области и логика разрабатываемого приложения. А предлагаемый framework со своей стороны по умолчанию побуждает разработчика выделять данные предметной области и по необходимости бизнес-логику приложения в отдельный компонент, чем по умолчанию уменьшает связность. С одной стороны, обобщенный MVC паттерн оставляет задачи создания визуальных форм представлений программисту, но, с другой стороны, обеспечивает двустороннюю связь отображения с данными приложения (отображение не только может изменять данные, но и обновляется при изменении данных) не обременяя разработчика тонкостями относительно сложной событийной модели MVC.

Приведём пример кода модели и его представления, суть которых описывалась выше.

/// <summary>

/// Класс модели

/// </summary>

/// <typeparam name="P">Тип модели</typeparam>

public class Model<P> where P : new()

{

private P property = new P();

/// <summary>

/// Свойство типа модели

/// </summary>

public virtual P Property

{

get

{

return property;

}

set

{

property = value;

}

}

}

/// <summary>

/// Класс представления модели

/// </summary>

/// <typeparam name="P">Тип модели</typeparam>

public abstract class View<P> where P : new()

{

private P model = new P();

/// <summary>

/// Свойство

/// </summary>

public P Model

{

get { return model; }

set

{

model = value;

Update();

}

}

/// <summary>

/// Обновить

/// </summary>

protected abstract void Update();

/// <summary>

/// Деструктор

/// </summary>

~View()

{

}

}

/// <summary>

/// Контроллер

/// </summary>

/// <typeparam name="P">свойство модели</typeparam>

public class Controller<P> where P: new() {

}

Проектирование и программирование представлений моделей игры

Теперь давайте напишем базовое представление уже для наших сущностей. Как и в проектировании моделей, мы создадим представление статического объекта, т.е. не двигающегося по полю игры и представление динамических объектов. Начнём с класса ViewMapObject, описывающего представление для статических объектов. В данном классе создаётся главное свойство типа PictureBox, содержащее в себе картинку объекта и конструктор, который размещает данную картинку на игровом поле. Также в этом классе осуществляется подписка на событие изменение координат и описан обработчик изменения координат, котором вызывается метод Show (рисование картинки объекта). Ещё описан виртуальный метод изменения картинки в соответствии с его направлением, который реализуем в более конкретных классах.

namespace PackMan

{

/// <summary>

/// Предсталение статического объекта

/// </summary>

class ViewMapObject:View<MapObject>

{

/// <summary>

/// Картинка объекта

/// </summary>

protected PictureBox picBox = new PictureBox();

/// <summary>

/// Конструктор

/// </summary>

/// <param name="map">Игровое поле</param>

public ViewMapObject(Panel map)

{

map.Controls.Add(picBox);

picBox.Height = Model.Height;

picBox.Width = Model.Width;

}

/// <summary>

/// Подписка на событие изменение координат

/// </summary>

public void Subscribe()

{

this.Model.PositionChanged += new EventHandler(OnPositionChanged);

OnPositionChanged(this, new EventArgs());

}

/// <summary>

/// Отписка от события изменение координат

/// </summary>

private void Unsubscribe()

{

this.Model.PositionChanged -= new EventHandler(OnPositionChanged);

}

/// <summary>

/// Обработчик события изменение координат

/// </summary>

/// <param name="sender">Объект,сгенеривший событие</param>

/// <param name="e">Аргументы события</param>

private void OnPositionChanged(object sender, EventArgs e)

{

Show();

}

/// <summary>

/// Нарисовать картинку объекта

/// </summary>

private void Show()

{

SetImage(Model.Position);

}

delegate void SetImageCallback(Point p);

/// <summary>

/// Подвинуть картинку элемента в соответствии с его координатами

/// </summary>

/// <param name="p"></param>

private void SetImage(Point p)

{

if (this.picBox.InvokeRequired)

{

SetImageCallback d = new SetImageCallback(SetImage);

picBox.Invoke(d, new object[] { Model.Position });

}

else

{

ChangePicture();

this.picBox.Location = p;

}

}

/// <summary>

/// Изменить картинку в соответтствии с его направлением

/// </summary>

protected virtual void ChangePicture() { }

/// <summary>

/// Обновить.

/// </summary>

protected override void Update()

{

Show();

}

}

}

Теперь реализуем класс представление двигающегося объекта ViewDynamicMapObject. Класс очень похож на предыдущий но в шаблон View уже передаётся DynamicMapObject.

namespace PackMan

{

/// <summary>

/// Представление дви жущегося объекта

/// </summary>

class ViewDynamicMapObject:View<DynamicMapObject>

{

/// <summary>

/// Картинка объекта

/// </summary>

protected PictureBox picBox = new PictureBox();

/// <summary>

/// Конструктор

/// </summary>

/// <param name="map">Игровое поле</param>

public ViewDynamicMapObject(Panel map)

{

map.Controls.Add(picBox);

picBox.Height = Model.Height;

picBox.Width = Model.Width;

}

/// <summary>

/// Подписка на событие изменение координат

/// </summary>

public void Subscribe()

{

this.Model.PositionChanged += new EventHandler(OnPositionChanged);

OnPositionChanged(this, new EventArgs());

}

/// <summary>

/// Отписка от события изменение координат

/// </summary>

private void Unsubscribe()

{

this.Model.PositionChanged -= new EventHandler(OnPositionChanged);

}

/// <summary>

/// Обработчик события изменение координат

/// </summary>

/// <param name="sender">Объект,сгенеривший событие</param>

/// <param name="e">Аргументы события</param>

private void OnPositionChanged(object sender, EventArgs e)

{

Show();

}

/// <summary>

/// Нарисовать картинку объекта

/// </summary>

private void Show()

{

SetImage(Model.Position);

}

delegate void SetImageCallback(Point p);

/// <summary>

/// Подвинуть картинку элемента в соответствии с его координатами

/// </summary>

/// <param name="p"></param>

private void SetImage(Point p)

{

if (this.picBox.InvokeRequired)

{

SetImageCallback d = new SetImageCallback(SetImage);

picBox.Invoke(d, new object[] { Model.Position });

}

else

{

ChangePicture();

this.picBox.Location = p;

}

}

/// <summary>

/// Изменить картинку в соответтствии с его направлением

/// </summary>

protected virtual void ChangePicture() { }

/// <summary>

/// Обновить.

/// </summary>

protected override void Update()

{

Show();

}

}

}

Сейчас мы уже можем описать конкретные классы представлений движущихся объектов, таких как пакмен и танк. Они практически одинаковые, разница заключается лишь в картинках, взятых из ресурсов приложения. Также в классах описано изменение картинки объекта при повороте модели.

/// <summary>

/// Класс пакмена

/// </summary>

class ViewPackMan:ViewDynamicMapObject

{

/// <summary>

/// Конструктор

/// </summary>

/// <param name="map">Поле игры</param>

public ViewPackMan(Panel map):base(map)

{

picBox.Image = Resources.PackManD;

}

/// <summary>

/// Смена картинки при смене направления движения

/// </summary>

protected override void ChangePicture()

{

switch (Model.DirectionNow)

{

case (int)Direction.Up:

{

picBox.Image = Resources.PackManU;

break;

}

case ((int)Direction.Down):

{

picBox.Image = Resources.PackManD;

break;

}

case (int)Direction.Right:

{

picBox.Image = Resources.PackManR;

break;

}

case (int)Direction.Left:

{

picBox.Image = Resources.PackManL;

break;

}

default:

break;

}

}

}

/// <summary>

/// Представление танка

/// </summary>

class ViewTank : ViewDynamicMapObject

{

/// <summary>

/// Конструктор

/// </summary>

/// <param name="map">Поле игры</param>

public ViewTank(Panel map):base(map)

{

picBox.Image = Resources.TankU;

}

/// <summary>

/// Смена картинки при смене направления движения

/// </summary>

protected override void ChangePicture()

{

switch (Model.DirectionNow)

{

case (int)Direction.Up:

{

picBox.Image = Resources.TankU;

break;

}

case ((int)Direction.Down):

{

picBox.Image = Resources.TankD;

break;

}

case (int)Direction.Right:

{

picBox.Image = Resources.TankR;

break;

}

case (int)Direction.Left:

{

picBox.Image = Resources.TankL;

break;

}

default:

break;

}

}

}

Более простыми являются представление яблока и стены так как они статические объекты. Разница заключается в том что яблоко появляется на поле при его создании (т.е. в конструкторе описано добавление картинки на игровое поле map.Controls.Add(picBox)), а стена добавляется на поле с помощью массива символов, который я опишу в следущих статьях.

/// <summary>

/// Представление яблока

/// </summary>

class ViewApple : ViewMapObject

{

/// <summary>

/// Конструктор

/// </summary>

/// <param name="map">Поле игры</param>

public ViewApple(Panel map):base(map)

{

picBox.Image = Resources.Apple;

map.Controls.Add(picBox);

}

}

/// <summary>

/// Класс представления статического объекта

/// </summary>

class ViewWall: ViewMapObject

{

/// <summary>

/// Конструктор

/// </summary>

/// <param name="map">Игровое поле</param>

public ViewWall(Panel map):base(map)

{

picBox.Image = Resources.Wall;;

}

}

Итак, у нас есть модели и представления всех сущностей игры, остаётся только написать самую главную часть модель и представление самой игры. А также контроллер, с помощью которого можно будет управлять пакмэном.

Комментариев нет:

Отправить комментарий