[Почти]-MVC-подход к реализации пользовательского интерфейса в Delphi. Часть 3. ОбъектыИсточник: habrahabr alan008
Начать данную статью я бы хотел с рассмотрения ошибки, а точнее с неточности, допущенной в предыдущей части статьи. Приведенный там код добавления и удаления ролей у текущего выбранного пользователя корректно изменял внутреннее состояние объекта, но никак не обновлял при этом пользовательский интерфейс. Точнее интерфейс мог бы обновиться лишь после переключения с одного пользователя на другого и обратно. Как верно подметили в комментариях, для исправления данной недоработки достаточно было вставить в код процедур btAddRoleClick, btDelRoleClick вызов метода FillUserRoles. Работать будет, но это совсем не то, что нам нужно. Такой способ плох тем, что во всех местах, где роли сотрудника могут потенциально меняться, нужно каждый раз вставлять вызов по обновлению пользовательского интерфейса. А хочется раз и навсегда забыть о необходимости что-то делать с GUI в тех местах, где мы работаем с объектом. Я хочу, чтобы GUI реагировал на изменения объекта сам и сам перерисовывался, когда я изменяю поля объекта. Для этого я расширю класс TUser следующим образом:
Я добавил в объект TUser простейшее нотифицирующее событие, которое будет уведомлять нас об изменении списка ролей сотрудника. При этом метод SetRoles класса TUser примет следующий вид:
Пока событие OnChangeRoles класса TUser не переопределено (по умолчанию FOnChangeRoles имеет значение nil), вызов DoChangeRoles просто ничего не делает. Для того, чтобы можно было как-то реагировать на данное событие, нужно присвоить объектам TUser соответствующий обработчик.
Теперь нужно навесить этот обработчик события на объекты класса TUser:
Вот вобщем-то и все :). Теперь при изменении ролей объекта будет срабатывать событие OnChangeRoles, назначенный обработчик которого будет вызывать FillUserRoles и обновлять GUI (перезаполнять список ролей). С этими правками код из предыдущей статьи будет работать корректно.
Можно ли было сделать лучше?1) В контексте предыдущей статьи мне нужно было реагировать только на изменение списка ролей, поэтому я завел конкретное событие, реагирующее только на изменение поля Roles класса TUser. Зачастую реагировать нужно на изменение не одного, а нескольких (а может быть и всех) полей объекта. В этом случае лучше было завести событие не OnChangeRoles, а просто OnChange, правда и обработчик его в этом случае должен не только перестраивать список ролей, но и обновлять любую другую информацию о пользователе, которая могла в это время отображаться в окне. Соответственно и вызов DoChange находился бы не только в SetRoles, а также и в setter'ах остальных полей объекта TUser, изменения которых хотелось бы отслеживать. И здесь главная задача не забыть добавить этот вызов DoChange при добавлении нового поля к объекту, т.к. пропустить его довольно легко.
Если этого не сделать, то при попытке изменения объекта TUser после уничтожения формы TUser может попытаться вызвать обработчик события, ссылающийся на метод уже уничтоженного объекта (формы) и в лучшем случае мы получим Access Violation. Рассмотренный способ можно считать подпиской на уведомления с одним подписчиком. Однако, это еще не полноценная подписка на уведомления. Уведомления хороши тем, что на них можно подписать сколько угодно подписчиков. Сейчас мы рассмотрим, как это делается. Давайте переключимся на другую задачу.
Уведомления с несколькими подписчикамиДанный шаблон очень часто применяется в качественно написанных MDI-приложениях (да и вообще в любых многооконных приложениях). Шаблон используется, когда в нескольких окнах системы могут отображаться одни и те же данные и при изменении этих данных через одно окно нужно чтобы они синхронно обновлялись во всех окнах. При этом данные окна не обязательно являются экземплярами окна одного класса и не обязательно имеют одинаковый пользовательский интерфейс. Напротив, окна могут быть совершенно разными. Они лишь отображают одну и ту же информацию. Например, в одном окне отображается список сотрудников, а в другом - карточка этого сотрудника, где можно изменить какие-то его характеристики. При этом требуется, чтобы по нажатию кнопки "Сохранить" в карточке сотрудника данные обновлялись бы как в карточке сотрудника, так и в общем списке сотрудников.
На вызовы SaveUser хотят реагировать все окна, в которых может отображаться какая-либо относящаяся к сотруднику информация. В этом случае классу TUserMngr придется хранить ссылки на все обработчики, которые могут подписаться на событие сохранения сотрудника:
Код реализации данных методов:
Имея такой функционал, вы можете легко подписаться на изменения интересующих вас объектов из любого окна:
Теперь, когда мы поняли, как это будет использоваться, вернемся непосредственно к моменту нотификации, т.е. к моменту срабатывания события:
Теперь при сохранении объекта TUser все формы будут уведомляться об этом, если они не забыли подписаться на соответствующее событие.
Блокировка срабатывания обработчиковПриведенный выше код является хорошим до тех пор, пока в системе не появляются операции сразу над большим количеством объектов. Возможно, не самый лучший пример: группа сотрудников прошла обучение и каждый из них получил какой-то одинаковый для всех сертификат. Мы выделяем 10 сотрудников в списке, жмем "Добавить сертификат". Далее поочередно происходит вызов UserMngr.Save для каждого из этих 10 сотрудников. При этом после сохранения каждого сотрудника срабатывает событие изменения DoUserChangeNotify, которое приводит к перестроению списка сотрудников во всех открытых окнах (а каждое перестроение будет еще приводить к перезапросу списка сотрудников из БД или с сервера приложений). В итоге сохранение изменений для 10 сотрудников будет происходить оооочень медленно и вдобавок мы получим массу миганий в открытых окнах приложения (списки будут перестраиваться по 10 раз). Сейчас я опишу простой способ, как этого избежать:
Метод нотификации при этом тоже изменится:
Через FLock отслеживается уровень блокировки (допускаются вложенные вызовы BeginUpdate..EndUpdate). FChanged - это флажок, позволяющий нам запомнить, происходило ли хотя бы один раз срабатывание события внутри сеанса блокировки. Если оно действительно происходило, то в момент выхода из сеанса блокировки (т.е. в момент вызова EndUpdate самого верхнего уровня), событие будет автоматически вызвано. Таким образом, код изменения множества объектов можно легко защитить от излишних срабатываний событий:
Подобную блокировку удобно применять и в других случаях, например, когда нужно перевести объект из одного состояния в другое, изменив при этом не одно, а несколько его полей. При этом некоторые промежуточные состояния объекта (некоторые комбинации значений полей) могут считаться недопустимыми с точки зрения GUI. Соответственно нужно не допустить, чтобы GUI вообще узнал о том, что объект проходил через такие состояния. В таком случае изменение объекта также проводится внутри сеанса его обновления, когда срабатывание событий об изменении этого объекта заблокировано.
ИтогСобытия - один из хороших приемов для связи объектов с GUI. Данный шаблон применяется не только при программировании GUI, но и во многих других случаях. В статье мы рассмотрели варианты реализации подписки на уведомления с одним и с множественными подписчиками. На этом цикл статей о программировании GUI в MVC-стиле скорее всего будет завершен. Если у кого-то остались вопросы именно по подходам к реализации GUI в Delphi, прошу оставлять их в комментариях и, возможно, данный цикл статей будет успешно продолжен. Также предлагаю в комментариях (а может и в отдельных статьях!) делиться своими приемами успешной реализации типовых задач на Delphi. И не надо закапывать никаких стюардесс, Delphi еще поживет ;) Всем хорошего дня! |