Майже -mvc-підхід до реалізації призначеного для користувача інтерфейсу в delphi

Для цього я розширю клас TUser наступним чином:

Я додав у об'єкт TUser найпростіше нотифікує подія, який повідомить нас про зміну списку ролей співробітника. При цьому метод SetRoles класу TUser прийме наступний вигляд:

Поки подія OnChangeRoles класу TUser НЕ перевизначити (за замовчуванням FOnChangeRoles має значення nil), виклик DoChangeRoles просто нічого не робить. Для того, щоб можна було якось реагувати на цю подію, потрібно привласнити об'єктам TUser відповідний обробник.
Цей оброблювач логічно завести у класу форми:

Тепер потрібно навісити цей обробник події на об'єкти класу TUser:

Ось вобщем-то і все :). Тепер при зміні ролей об'єкта буде спрацьовувати подія OnChangeRoles, призначений обробник якого буде викликати FillUserRoles і оновлювати GUI (перезаполнять список ролей). З цими правками код з попередньої статті буде працювати коректно.

Чи можна було зробити краще?

1) В контексті попередньої статті мені потрібно було реагувати тільки на зміну списку ролей, тому я завів конкретну подію, що реагує тільки на зміну поля Roles класу TUser. Найчастіше реагувати потрібно на зміну не одного, а декількох (а може бути і всіх) полів об'єкта. В цьому випадку краще було завести подія не OnChangeRoles, а просто OnChange, правда і обробник його в цьому випадку повинен не тільки перебудовувати список ролей, але і оновлювати будь-яку іншу інформацію про користувача, яка могла в цей час відображатися у вікні. Відповідно і виклик DoChange знаходився б не тільки в SetRoles, а також і в setterах інших полів об'єкта TUser, зміни яких хотілося б відслідковувати. І тут головне завдання не забути додати цей виклик DoChange при додаванні нового поля до об'єкта, тому що пропустити його досить легко.
2) Виходячи з принципів безпечного програмування, якщо ми реєструємо оброблювач події (як ще кажуть, "підписуємося" на подію), то ми повинні потім цю підписку прибрати ( "разрегістріровать" обробник), тобто повернути OnChangeRoles в початковий стан або на худий кінець в nil. Чи потрібно виконувати цю розреєстрації, в кожному випадку вирішується індивідуально. В першу чергу це залежить від співвідношення часу життя об'єктів TUser і об'єкта форми. Якщо форма живе довше TUserа, то в принципі розреєстрації не обов'язкова. Якщо ж, навпаки, TUser може ще пожити і після знищення форми, то звичайно в OnDestroy у форми потрібно прописати щось в дусі

Якщо цього не зробити, то при спробі зміни об'єкту TUser після знищення форми TUser може спробувати викликати оброблювач події, що посилається на метод вже знищеного об'єкта (форми) і в кращому випадку ми отримаємо Access Violation.
3) Коли ми працюємо зі списками об'єктів, привласнювати обробник кожному об'єкту не завжди зручно. Якщо елементи списку знають про самому списку (наприклад, посилаються на нього через Ownerа), можна зробити, щоб DoChange об'єктів TUser просто викликав Owner.DoChange, а настроюється подія (property FOnChange) завести вже у самого списку (у TObjectListа). Хоча це вобщем-то нічого за змістом не змінює.

Повідомлення з декількома передплатниками

Даний шаблон дуже часто застосовується в якісно написаних MDI-додатках (та й взагалі в будь-яких багатовіконних додатках). Шаблон використовується, коли в декількох вікнах системи можуть відображатися одні й ті ж дані і при зміні цих даних через одне вікно потрібно щоб вони синхронно оновлювалися в усіх вікнах. При цьому дані вікна не обов'язково є екземплярами вікна одного класу і не обов'язково мають однаковий інтерфейс. Навпаки, вікна можуть бути абсолютно різними. Вони лише відображають одну і ту ж інформацію. Наприклад, в одному вікні відображається список співробітників, а в іншому - картка цього співробітника, де можна змінити якісь його характеристики. При цьому потрібно, щоб після натискання кнопки "Зберегти" в картці співробітника дані оновлювалися б як в картці співробітника, так і в загальному списку співробітників.
Шаблон множинної передплати на повідомлення зручно застосовувати при наявності долгоживущего об'єкта. Його час життя повинно бути свідомо більше часу життя тих об'єктів, які підписуються на повідомлення від нього. Припустимо, у нас є якийсь клас-менеджер, який відповідає за роботу з співробітниками (зокрема за збереження змін об'єктів TUser в базу):

Код реалізації даних методів:

Тепер, коли ми зрозуміли, як це буде використовуватися, повернемося безпосередньо до моменту нотифікації, тобто до моменту спрацьовування події:

Блокування спрацьовування оброблювачів

Наведений вище код є хорошим доти, поки в системі не з'являються операції відразу над великою кількістю об'єктів. Можливо, не найкращий приклад: група співробітників пройшла навчання та кожен з них отримав якийсь однаковий для всіх сертифікат. Ми виділяємо 10 співробітників в списку, тиснемо "Додати сертифікат". Далі по черзі відбувається виклик UserMngr.Save для кожного з цих 10 співробітників. При цьому після збереження кожного співробітника спрацьовує подія зміни DoUserChangeNotify, яке призводить до перебудови списку співробітників у всіх відкритих вікнах (а кожне перестроювання буде ще приводити до перезапроса списку співробітників з БД або з сервера додатків). В результаті збереження змін для 10 співробітників відбуватиметься дуууже повільно і до того ж ми отримаємо масу миганий у відкритих областях програми (списки будуть перебудовуватися по 10 разів). Зараз я опишу простий спосіб, як цього уникнути:

Метод нотифікації при цьому теж зміниться:

Через FLock відстежується рівень блокування (допускаються вкладені виклики BeginUpdate..EndUpdate). FChanged - це прапорець, що дозволяє нам запам'ятати, чи відбувалося хоча б один раз спрацьовування події всередині сеансу блокування. Якщо воно дійсно відбувалося, то в момент виходу з сеансу блокування (тобто в момент виклику EndUpdate самого верхнього рівня), подія буде автоматично викликано.

Таким чином, код зміни безлічі об'єктів можна легко захистити від зайвих спрацьовувань подій:

Подібну блокування зручно застосовувати і в інших випадках, наприклад, коли потрібно перевести об'єкт з одного стану в інший, змінивши при цьому не одне, а кілька його полів. При цьому деякі проміжні стану об'єкта (деякі комбінації значень полів) можуть вважатися неприпустимими з точки зору GUI. Відповідно потрібно не допустити, щоб GUI взагалі дізнався про те, що об'єкт проходив через такі стани. В такому випадку зміна об'єкта також проводиться всередині сеансу його поновлення, коли спрацьовування подій про зміну цього об'єкта заблоковано.

Всім гарного дня!