[Не совсем]-MVC-подход к разработке пользовательских интерфейсов в Delphi. Часть 2. СпискиИсточник: habrahabr alan008
Предыдущая статья была посвящена всего одной галочке. Пора переходить к чему-то чуть более серьезному. Сегодняшняя тема - представление списков и связь GUI-списков с внутренними данными. Статья предназначена для Delphi-разработчиков.
С чего начатьЧтобы не лить воду, перейду сразу к живому примеру, приведенному на рисунке выше. Допустим, вам нужно создать примитивную форму настройки прав пользователей.
МодельДопустим, что внутреннее представление данных состоит из класса TUser, описывающего сотрудника, и справочника ролей, который умеет по числовому ID'у возвращать название роли. Заводить классы для ролей нецелесообразно, т.к. это слишком простая сущность:
Видно, что роли пользователя представлены крайне простым образом - списком ID'ов. Добавляю соответствующие поля классу формы:
Обратите внимание, что я использовал типизированный TObjectList. До Delphi 2009 такой возможности не было и TObjectList хранил всегда просто TObject'ы. При каждом обращении к элементу списка приходилось его приводить к корректному классу: FUsers[i] as TUser (ну или вариант для камикадзе: TUser(FUsers[i])). Это было неудобно и легко было допустить ошибку, выполнив преобразование не к тому классу. С появлением обобщенных типов (generics) теперь можно использовать жестко типизированный TObjectList. Это невероятно удобно! Обращаясь к элементам такого списка через FUsers[i] мы сразу получаем объект класса TUser. Я не буду приводить код получения списка сотрудников, т.к. в каждой системе в зависимости от ее архитектуры он будет свой. Это может быть SQL-запрос к базе, обращение к какому-то клиентскому кэшу или обращение к серверу приложений (в многозвенной архитектуре). Предположим просто, что у вас есть возможность откуда-то получить этот список.
Отображение элементов спискаИтак, мы хотим получить список сотрудников и отобразить его на экране:
Метод Fill предназначен для простого [пере]заполнения списка пользователей:
Простого заполнения списка сотрудников недостаточно. Нужно еще показать роли текущего выбранного сотрудника. А для этого нужно научиться определять, какой сотрудник сейчас выбран? Неопытные программисты начинают активно обращаться из разных мест к lbUsers.Items.Objects[lbUsers.ItemIndex]. Однако, если вы читали предыдущую часть статьи, то уже догадываетесь, что мы пойдем другим путем. Мы заведем у класса формы свойство, возвращающее и устанавливающее текущего выбранного сотрудника. Возвращать можно либо сам объект TUser, либо числовой ID пользователя. Возвращать ID мне показалось удобнее, хотя с этим можно поспорить.
Ключевой момент здесь в методе UpdateSelUser, который приводит интерфейс в состояние, при котором выбран заданный пользователь:
Мы видим, что метод установки текущего пользователя всегда вызывает перезаполнение списка ролей (FillUserRoles). Как и в предыдущей статье, раз мы реализовали направление синхронизации Модель->Представление, то нам нужна и обратная синхронизация. Поэтому в событии OnClick списка lbUsers добавим такой код:
При задании SelUserID, если раньше был выбран другой пользоваль, то set-метод вызовет UpdateSelUser, который в свою очередь полностью синхронизирует представление с моделью, а именно обновит список ролей. Т.е. мне уже не нужно вызывать метод обновления списка ролей изнутри обработчика lbUsersClick, все произойдет автоматически. Приведу метод заполнения списка ролей (он тривиален):
Код инициализации формы я дополню ициниализацией первого пользователя в списке:
Что мы получили? Теперь обращаться к текущему выбарнному пользователю можно через SelUserID. Причем как при программной установке значения свойства SelUserID, так и при выборе пользователя через GUI-список будет автоматически обновляться список ролей. Для работы с ролями (добавление, удаление) можно завести у класса формыеще свойство SelRoles. Его проще сделать полностью виртуальным (не заводить для него отдельное поле):
Методы IntInList и AddIntToList соответственно проверяют вхождение элемента в массив и добавляют новый элемент в массив.
Добавление и удаление ролейДобавление ролей:
Удаление ролей:
В каком месте осуществлять сохранение изменений объекта TUser в БД решать вам. Кто-то, возможно, захочет делать это немедленно, прямо внутри SetRoles класса TUser (чтобы все изменения отражались в базе мгновенно). Кто-то реализует сохранение измененных объектов TUser при нажатии на кнопку OK в окне. Третьим вариантом является сохранение по кнопке ОК, а также при попытке переключения между пользователями, если роли текущего пользователя были изменены (т.к. приведенный выше интерфейс окна не позволяет визуально отследить, у каких сотрудников роли поменялись, а у каких - нет, при переключении с одного сотрудника на другого, что может привести к ошибке).
ИтогПолучилось окно управления правами пользователей. Окно реализует следующую логику:
Дополнение. Обновление списка с сохранением выбранного элементаЗдесь можно было бы и остановиться, но все-таки хочется показать, как можно обновлять и сам список сотрудников, не теряя при этом текущего выбранного сотрудника. Функциональность ручного обновления списка сотрудников может быть полезной, если добавление сотрудников производится через другое окно, а механизм автоматической нотификации окна изменения прав о добавлении нового сотрудника не реализован. Также нового сотрудника может добавить другой пользователь системы на другой машине, а вам не хочется перезаходить в окно настройки прав, чтобы добавленный пользователь появился в списке. Итак, допустим вы добавили еще кнопку "Обновить список сотрудников" в окно настройки прав. Очевидно, что она должна приводить к простому вызову метода FillUsers. Но ведь тогда текущий выбранный сотрудник потеряется (т.к. GUI список будет очищен и переазполнен заново), что будет очень неудобно и странно для пользователя.
В дальнейшем может потребоваться еще большее: запоминать последнего выбранного сотрудника между повторными входами в окно настройки прав или даже между сеансами работы приложения. В этом случае в FillUsers можно добавить параметр, определяющий, на каком пользователе нужно спозиционироваться после перестроения списка. При этом логику запоминания текущего пользователя придется немного усложнить:
При этом FormCreate поменяется на
а FormDestroy на
Большая часть вышеприведенного кода придумана из головы, не судите слишком строго за опечатки и неточности. Он очень похож на реальный проект, но на самом деле в реальном проекте гораздо больше деталей, о которых сейчас говорить не хочется. Постепенно я все ближе подхожу к тому, чтобы связать с GUI-контролами сами классы внутренних данных. Пока еще я этого не сделал. В следующей части статьи я рассмотрю шаблон подписки на уведомления и покажу, как GUI-интерфейс может реагировать на изменения самих объектов. Удачи! |