Новый подход разработки компонентов FireMonkey “Контрол – Модель – Презентация”. Часть 1

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


Предисловие

Разбор нового подхода начнем с примера. Рассмотрим TCalendar. Это визуальный компонент отображения календаря, предназначенный для отображения и выбора пользователем даты. Для своей реализации этот компонент использует TListBox с многоколоночным режимом для отображения дней в месяце. А дополнительные кнопки TButton и TPopupBox служат для выбора месяца, года и навигации между соседними месяцами. Ничего сложного в этом компоненте нет.

2015-03-25 13-29-13 Project4 - Elbrus Developer Build - Unit13

Этот компонент заполняет TListBox значениями дней на основании текущей даты и задаёт свои обработчики событий на нажатия кнопок и выбора дня, года, месяца.

Если на Windows, OSX и Android способ выбора даты и общая схема отображения ничем не отличается. То iOS дал нам совершенно другой способ выбора даты, через «барабан».

Календарь iOS 7, iOS 8

И тут возник важный вопрос, как совместить в одном контроле TCalendar несколько способов отображения даты и принципиально разных способов выбора даты.

Самый просто вариант – это смешать две реализации внутри одного компонента TCalendar. Это естественно привело бы к значительному усложнению кода, спагетти коду и плохой сопроводимости. Помимо этого класс будет содержать вспомогательные поля и данных, которые не нужны для использования в текущей реализации отображения и тд.

Второй вариант – создание кроссплатформенного сервиса. Каждая реализации будет скрыта за конкретной реализацией сервиса. Такой подход используется в некоторых компонентах: TWebBrowser, пикеры. Применимый вариант, но концепция сервисов FMX позволяет в каждый момент времени иметь только одну реализацию сервиса. А что, если пользователь захочет для отображения даты на iOS использовать классический подход, а для выбора даты использовать «барабан»?

Это значит, что в идеале мы должны обеспечить:

  1. Существовавние двух независимых реализаций работы календаря
  2. Раздельное и не зависимое друг от друга существование реализаций.
  3. Позволить создавать любое количество реализаций.

Основная проблема разработки компонентов я вижу в том, что если мы хотим расширить функциональность штатного компонента, то мы должны создать новый компонент:

  1. Создать новый на основе штатного новый компонент, расширив его функциональность
  2. Зарегистрировать его в RAD Studio
  3. Использовать свой компонент вместо штатного.

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

Примечание. Хотя бы каждый разработчик Делфи в свое время делал свою кнопку ;-) Шутка

Поэтому попробуем избежать этого и сделать так, чтобы была возможность расширения компонента без необходимости создавать новый расширенный компонент с обязательной установкой. Тут как раз и появляется новый вариант, который подразумеваем разделение компонента на:

  1. Модель с данными
  2. Способы отображения данных и организация их ввода для пользователя

Как вы уже могли понять, это будет очень сильно напоминать MVC (Model — View — Controller), но применимо к контролам.

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

type

{ TCalendarModel }

  TCalendarModel = class(TDataModel)
  public const
    DefaultCalDayOfWeek = TCalDayOfWeek.dowLocaleDefault;
  private
    FDate: TDate;
    FFirstDayOfWeek: TCalDayOfWeek;
    FTodayDefault: Boolean;
    FWeekNumbers: Boolean;
    FOnChange: TNotifyEvent;
    FOnDateSelected: TNotifyEvent;
    FOnDayClick: TNotifyEvent;
    procedure SetDate(const Value: TDate);
    procedure SetFirstDayOfWeek(const Value: TCalDayOfWeek);
    procedure SetTodayDefault(const Value: Boolean);
    procedure SetWeekNumbers(const Value: Boolean);
  public
    constructor Create; override;
    property DateTime: TDate read FDate write SetDate;
    property FirstDayOfWeek: TCalDayOfWeek read FFirstDayOfWeek write SetFirstDayOfWeek;
    property TodayDefault: Boolean read FTodayDefault write SetTodayDefault;
    property WeekNumbers: Boolean read FWeekNumbers write SetWeekNumbers;
    property OnChange: TNotifyEvent read FOnChange write FOnChange;
    property OnDateSelected: TNotifyEvent read FOnDateSelected write FOnDateSelected;
    property OnDayClick: TNotifyEvent read FOnDayClick write FOnDayClick;
  end;

О том, что такое TDataModel, мы поговорим чуть позже.

Отображение календаря и способ выбора даты вынесем в отдельный класс FMX.Calendar.Style.TStyledCalendar. Этот класс будем называть стилевым представлением календаря. “Стилевым” – означает, что это представление будет использовать для реализации концепцию стилей. Обратите внимание, что теперь этот класс содержит кнопки выбора даты, лист бокс для отображения и выбора даты и поля выбора года и месяца, а не TCalendar.

Что же из себя теперь представляет TCalendar? Он содержит модель и представление. Теперь если мы хотим создать другой вариант календаря, например, использовать барабаны, мы создаем другое представление и на iOS используем нативный контрол UIDatePicker. FMX.Calendar.iOS.TiOSNativeCalendar – это реализация календаря с использованием нативного контрола. Скажу даже больше, что нативная презентация TiOSNativeCalendar является по сути полностью нативным контролом, то есть UIDatePicker. Таким образом работая с TiOSNativeCalendar вы работаете на пряму с нативным календарем.

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

Взаимодействие “Контрол -> Презентация”, “Контрол -> Модель”, “Модель <-> Презентация”

Все взаимодействия между частями происходят при помощи использования делфи сообщений (TObject.Dispatch). Выбор сообщений для взаимодействия между частями выбран не случайно и продиктован двумя важными достоинствами:

  1. Протокол. Использование сообщений позволяет в получателе перехватывать только те сообщения, которые для него актуальны и эмулировать понятие протокола в ObjectiveC. Если мы сравним использование сообщений с интерфейсом, то интерфейс обязывает нас реализовать все методы, даже те, которые могут не иметь смысл в той или иной реализации, а сообщения — нет. Так, например, нативный календарь не поддерживает отображение номеров недель WeekNumbers и TodayDefault. А стилевая реализация — да. Если бы мы использовать интерфейс, как способ взаимодействия между контролом и представлением, то нам бы пришлось включить в интерфейс два метода по уведомлению, что эти два свойства поменялись. В свою очередь, каждое представление должно было бы их реализовать.
  2. Полностью сняты требования к базовому классу представлений. То есть представлением может быть любой класс вплоть до TObject. Это особенно актуально при использовании в качестве базового класса TOCLocal (Mac, iOS) и TJavaLocal (Android). Эти классы предназначены для связи делфи с нативным классами. Одним из ограничением этих классов является — не возможность подмешивать интерфейсы, не отвечающие методам нативных контролов. А значит, если бы мы использовали интерфейсы, то нам бы пришлось сделать цепочку Контрол -> Адаптер (реализующий интерфейс взаимодействия) -> Нативный контрол.

FMX.Presentation.Messages.TMessageSender — это базовый класс, добавляющий удобную работу с отправкой сообщений с данными любого типа. Главными особенностями являются:

  1. Отправка сообщений приемнику с данными любого типа.
  2. Отправка сообщений и получение обратно результата от приемника. То есть мы отправили сообщение на запрос данных от приемника, и приемник синхронно вернул нам эти данные.
  3. Возможность отключать/включать отправку сообщений.
  4. Возможность создавать получателя сообщений внутри или задавать его снаружи.

Например, если я хочу отправить кому-то данные мне достаточно написать:

SendMessage<TDateTime>(MM_DATE_CHANGED, Value);

А если я хочу получить какие-то данные, то так:

PresentationProxy.SendMessageWithResult<TSizeF>(PM_GET_RECOMMEND_SIZE, Result);

Например, если пользователь выбрал дату в представлении, то представлении должно обновить данные о дате в моделе. Чтобы избежать получения презентаций уведомления об измененении даты, которую презентация сама моделе и указала, есть возможность отключить уведомления на это время.

Model.DisableNotify;
try
  Model.DateTime := Date;
finally
  Model.EnableNotify;
end;

Это самое главное назначение класса TMessageSender.

Модель -> Презентация

Модель при изменении данных отсылает уведомление презентации через посылку сообщений с соответствующим кодом. Если вы меняете дату в календаре через TCalendar.Date это действие изменяет дату в модели, что в свою очередь отправляет уведомление представлению MM_DATE_CHANGED.

procedure TCalendarModel.SetDate(const Value: TDate);
begin
  if not SameDate(FDate, Value) then
  begin
    FDate := Value;
    SendMessage<TDateTime>(MM_DATE_CHANGED, Value);
  end;
end;

В свою очередь представление перехватывает это сообщение и выполняет отображение этой даты.

TStyledCalendar = class(TStyledPresentation)
private
// ....
{ Messages }
  procedure MMDateChanged(var AMessage: TDispatchMessageWithValue<TDateTime>); message MM_DATE_CHANGED;
//...

Нативное представление для iOS:

TiOSNativeCalendar = class(TiOSNativeControl)
private
//...
protected
  { Messages From Model}
  procedure MMDateChanged(var AMessage: TDispatchMessageWithValue<TDateTime>); message MM_DATE_CHANGED;
// ...

Презентация -> Модель

Презентация получает модель через сообщение PM_SET_MODEL. Поэтому она полностью имеет прямой доступ к модели. Поэтому обратная связь идет на прямую.

Контрол -> Презентация

А что делать, если контролу требуется выполнить действие по обработке данных. Например, если TComboBox требуется программно открывать выпадающий список TComboBox.DropDown. То контрол должен об этом сказать презентации, так как сам контрол ничего не знает о том, как открывать выпадающий список и он даже не владеет им. Для этих целей контрол может так же отправить уведомление/ действие презентации при помощи сообщений. Для отправки сообщений контролом презентации используется расширение класса TMessageSenderTPresentationProxy.

TPresentationProxy – это посредник между представлением и контролом. Контрол всю работу над представлением ведет через TPresentationProxy путем отправки через него сообщений. Этот класс также выполняет первоначальную связь с презентацией, передает презентации модель и контрол.

У каждого представления есть свой класс TPresentationProxy. Например у стилевого представления календаря есть TStyledCalendarProxy и TiOSCalendarProxy. Каждый класс прокси создает внутри себя уже нужное представление:

{ TStyledCalendarProxy }

  TStyledCalendarProxy = class (TPresentationProxy)
  protected
    function CreateReceiver: TObject; override;
  end;

// ....

{ TStyledCalendarProxy }

function TStyledCalendarProxy.CreateReceiver: TObject;
begin
  Result := TStyledCalendar.Create(nil);
end;

CreateReceiver — создает представление и задает его в качестве получателя сообщений.

Таким образом, подведем промежуточный итог:

  1. Данные контрола хранятся в модели;
  2. За способ отображения и ввода данных пользователя отвечает представление;
  3. Контрол содержит модель и презентацию;
  4. Модель при изменении в ней данных отправляет презентации уведомление при помощи сообщений;
  5. Контрол может отправлять нотификации и запросы данных презентации при помощи сообщений;
  6. Контрол знает класс своей модели;
  7. Контрол ничего не знает о своем представлении. То есть нет никакой привязки к конкретному классу;
  8. Презентация получает модель, в момент ее установки контролу;

Осталось нам разобраться, как осуществляется установка и выбор представления.

TPresentedControl – основа презентационного подхода

TPresentedControl – это основа для использования концепции с моделью и представлениями. Этот класс обеспечивает:

  1. Создание модели
  2. Автоматический выбор и загрузку представления по строковому идентификатору
  3. Выполняет первичную инициализацию представления.
  4. Обеспечивает связь с представлением через отправку сообщений. TPresentationProxy.
  5. И осуществляет первичную передачу данных о контроле представлению. При изменении свойств. Например, переключение видимости, изменение позиции, изменение размера и тд.

За работу с представлением в TPresentedControl отвечают методы:

  1. DefinePresentationName – получение название представления
  2. LoadPresentation – поиск, создание и загрузка представлени по названию
  3. InitPresentation Первичная инициализация представления на стороне контрола
  4. UnloadPresentation – выгрузка представления.
  5. HasPresentationProxy – Загружено представление или нет
  6. PresentationProxy — Доступ к прокси представления
  7. ControlType – какое представление нужно загружать. Platform – нативное, Styled – стилизованное. Влияет на название представления в DefinePresentationName.

За работу с моделью в TPresentedControl:

  1. DefineModelClass – возвращает класс модели контрола. По умолчанию TDataModel. Для календаря — TCalendarModel
  2. Model – доступ к модели
  3. GetModel<T: TDataModel> — Получение модели нужного класса.

За генерацию названия представления отвечает метод: DefinePresentationName. По умолчанию он генерирует названия следующим образом:

  1. Берет имя класса и убирает первую букву T.
  2. Добавляет суффикс:
    1. ControlType = Style: ‘-style’
    2. ControlType= Platform: ‘-native’

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

Фабрика представлений или выбор произвольного представления

Все представления хранятся в фабрике FMX.Presentation.Factory.TPresentationProxyFactory. Фабрика имеет набор методов:

  1. Для регистрации собственных представлений;
  2. По замене стандартных FMX представлений на свои (Расширение для 3D-party);
  3. Проверка наличия представления;
  4. Получение экземпляра представления по его названию.

Представление запрашиваются по строковому идентификатору. На самом деле, фабрика регистрирует не представления, а их прокси TPresentationProxy. Конкретный прокси конкретного представления сам создает уже нужное представление и устанавливает с ним связь.

На диаграмме ниже представлено, как контрол с поддержкой нового подхода TPresentedControl загружает представление через фабрику.

Диаграмма последовательности загрузки TPresentedControl представления

Диаграмма последовательности загрузки TPresentedControl представления

Заключение

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

 Пример работы AutoComplete в TEdit

 

Новый подход разработки компонентов FireMonkey “Контрол – Модель – Презентация”. Часть 1: 3 комментария

  1. Уведомление: Fire Monkey - Yaroslav Brovin » Создание нативных представлений для iOS. TSpinBox и UIStepper. Часть 3

  2. Уведомление: Новый подход разработки компонентов FireMonkey “Контрол – Модель – Презентация”. Часть 2. TEdit с автозавершением | DelphiFeeds.ru 2.0

  3. Уведомление: Создание нативных представлений для iOS. TSpinBox и UIStepper. Часть 3 | DelphiFeeds.ru 2.0

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *