(495) 925-0049, ITShop интернет-магазин 229-0436, Учебный Центр 925-0049
  Главная страница Карта сайта Контакты
Поиск
Вход
Регистрация
Рассылки сайта
 
 
 
 
 

[Не совсем]-MVC-подход к разработке пользовательских интерфейсов в Delphi. Часть 2. Списки

Источник: habrahabr
alan008

Предыдущая статья была посвящена всего одной галочке. Пора переходить к чему-то чуть более серьезному. Сегодняшняя тема - представление списков и связь GUI-списков с внутренними данными. Статья предназначена для Delphi-разработчиков.

С чего начать

Чтобы не лить воду, перейду сразу к живому примеру, приведенному на рисунке выше. Допустим, вам нужно создать примитивную форму настройки прав пользователей. 
В левой части окна показывается список всех пользователей системы, а в правой - список прав и ролей текущего выбранного пользователя. Логика окна заключается в том, чтобы при выборе пользователя в левой части окна обновлялся список прав и ролей в правой части. Также в правой части есть кнопки "Добавить"/ "Удалить", позволяющие либо добавить пользователю новую роль или удалить выбранные существующие роли. При добавлении новых ролей появляется всплывающее окно справочника ролей, в котором можно выбрать добавляемые роли. Вот, собственно, и все.

Модель

Допустим, что внутреннее представление данных состоит из класса TUser, описывающего сотрудника, и справочника ролей, который умеет по числовому ID'у возвращать название роли. Заводить классы для ролей нецелесообразно, т.к. это слишком простая сущность:

uses
  Generics.Collections; // Чтобы можно было использовать типизированный TObjectList

type
  TIntList = array of Integer; // Данный тип объявлен в отдельном общем модуле

  TUser = class
  strict private
    FID: Integer;
    FFullFio: String;
    FRoles: TIntList;
  public
    property ID: Integer read FID;
    property FullFio: String read FFullFio;
    property Roles: TIntList read FRoles write SetRoles;
  end;

  TUsersList = class(TObjectList<TUser>)
  public
    function UserByID(const aID: Integer): TUser;
  end;

Видно, что роли пользователя представлены крайне простым образом - списком ID'ов.

Добавляю соответствующие поля классу формы:

TfmUserRights = class(TForm)
...
  lbUsers: TListBox;
  lbRoles: TListBox;    
private
  FUsers: TUsersList; 
public
  property Users: TUsersList read FUsers;
end;

Обратите внимание, что я использовал типизированный TObjectList. До Delphi 2009 такой возможности не было и TObjectList хранил всегда просто TObject'ы. При каждом обращении к элементу списка приходилось его приводить к корректному классу: FUsers[i] as TUser (ну или вариант для камикадзе: TUser(FUsers[i])). Это было неудобно и легко было допустить ошибку, выполнив преобразование не к тому классу. С появлением обобщенных типов (generics) теперь можно использовать жестко типизированный TObjectList. Это невероятно удобно! Обращаясь к элементам такого списка через FUsers[i] мы сразу получаем объект класса TUser.

Я не буду приводить код получения списка сотрудников, т.к. в каждой системе в зависимости от ее архитектуры он будет свой. Это может быть SQL-запрос к базе, обращение к какому-то клиентскому кэшу или обращение к серверу приложений (в многозвенной архитектуре). Предположим просто, что у вас есть возможность откуда-то получить этот список.

Отображение элементов списка

Итак, мы хотим получить список сотрудников и отобразить его на экране:

procedure TfmUserRights.FormCreate(Sender: TObject);
begin
  FillUsers;
end;

Метод Fill предназначен для простого [пере]заполнения списка пользователей:

procedure TfmUserRights.FillUsers;
var
  i: Integer;
begin
  FUsers.Free; // Удаляю старый список, если он был
  FUsers := GetUsers;

  lbUsers.Items.BeginUpdate;
  try
    lbUsers.Items.Clear;

    for i := 0 to Users.Count-1 do
      lbUsers.AddItem(FUsers[i].FullFio, FUsers[i]); 

    // Добавляемый элемент списка сразу получает связь с объектом FUsers[i], 
    // хотя в моем случае хватило бы и связи с ID'ами (позже вы увидите, почему)
  finally
    lbUsers.Items.EndUpdate;
  end;
end;

Простого заполнения списка сотрудников недостаточно. Нужно еще показать роли текущего выбранного сотрудника. А для этого нужно научиться определять, какой сотрудник сейчас выбран? Неопытные программисты начинают активно обращаться из разных мест к lbUsers.Items.Objects[lbUsers.ItemIndex]. Однако, если вы читали предыдущую часть статьи, то уже догадываетесь, что мы пойдем другим путем. Мы заведем у класса формы свойство, возвращающее и устанавливающее текущего выбранного сотрудника. Возвращать можно либо сам объект TUser, либо числовой ID пользователя. Возвращать ID мне показалось удобнее, хотя с этим можно поспорить.

TfmUserRights = class(TForm)
private
  FSelUserID: Integer;
public
  property SelUserID: Integer read FSelUserID write SetSelUserID;
end;

procedure TfmUserRights.SetSelUserID(const Value: Integer);
begin
  if FSelUserID <> Value then
  begin
    FSelUserID := Value;
    UpdateSelUser; // !!!
  end;
end;

Ключевой момент здесь в методе UpdateSelUser, который приводит интерфейс в состояние, при котором выбран заданный пользователь:

procedure TfmUserRights.UpdateSelUser;
var
  vSelInd: Integer;
  i: Integer;
begin
  vSelInd := -1;
  with lbUsers do
    for i := 0 to Items.Count-1 do
      if (Items.Objects[i] as TUser).ID = SelUserID then
      begin
        vSelInd := i;
        Break;
      end;
   lbUsers.ItemIndex := vSelInd;
   if SelUserID <= 0 then
      gbRoles.Caption := 'Права и роли:'
  else
      gbRoles.Caption := 'Права и роли: ' + Users.UserByID(SelUserID).FullFio

  FillUserRoles; // !!!
end;

Мы видим, что метод установки текущего пользователя всегда вызывает перезаполнение списка ролей (FillUserRoles). 

Как и в предыдущей статье, раз мы реализовали направление синхронизации Модель->Представление, то нам нужна и обратная синхронизация. Поэтому в событии OnClick списка lbUsers добавим такой код:

procedure TfmUserRights.lbUsersClick(Sender: TObject);
begin
  SelUserID := (lbUsers.Items.Objects[lbUsers.ItemIndex] as TUser).ID;
end;

При задании SelUserID, если раньше был выбран другой пользоваль, то set-метод вызовет UpdateSelUser, который в свою очередь полностью синхронизирует представление с моделью, а именно обновит список ролей. Т.е. мне уже не нужно вызывать метод обновления списка ролей изнутри обработчика lbUsersClick, все произойдет автоматически.

Приведу метод заполнения списка ролей (он тривиален):

procedure TfmUserRights.FillUserRoles;
var
  i: Integer;
  vSelUser: TUser;
begin
  lbRoles.BeginUpdate;
  try
    lbRoles.Clear;
    
    if SelUserID <= 0 then 
      Exit;

    vSelUser := Users.UserByID(SelUserID); 
    for i := 0 to High(vSelUser.Roles) do
      lbRoles.AddItem(DictRoles.NameByID(vSelUser.Roles[i]), TObject(vSelUser.Roles[i])); 
      // Тут я сделал небольшую хитрость и привязал к элементам списка не объекты, а сами ID'ы, использовав приведение их к TObject'у (это допустимо)

  finally
    SomeList.EndUpdate;
  end;
end;

Код инициализации формы я дополню ициниализацией первого пользователя в списке:

procedure TfmUserRights.FormCreate(Sender: TObject);
begin
  FillUsers;
  FSelUserID := -2; // Хочу, чтобы сработал Set-метод
  SelUserID := -1; // По умолчанию не выбираю никакого пользователя
end;

Что мы получили? Теперь обращаться к текущему выбарнному пользователю можно через SelUserID. Причем как при программной установке значения свойства SelUserID, так и при выборе пользователя через GUI-список будет автоматически обновляться список ролей.

Для работы с ролями (добавление, удаление) можно завести у класса формыеще свойство SelRoles. Его проще сделать полностью виртуальным (не заводить для него отдельное поле):

property SelRoles: TIntList read GetSelRoles write SetSelRoles;

function TfmUserRights.GetSelRoles: TIntList;
var
  i: Integer;
begin
  Result := nil;
  for i := 0 to lbRoles.Items.Count-1 do
    if lbRoles.Selected[i] then
      AddIntToList(Integer(lbRoles.Items.Objects[i]), Result); 
   // Помните про вышеописанную хитрость? На самом деле в Objects'ах  
   // сидят не объекты, а ID'ы ролей, поэтому смело привожу их к Integer
end;

procedure TfmReportMain.SetSelRoles(const aSelRoles: TIntList);
var
  i: Integer;
begin
  lbRoles.Items.BeginUpdate;
  try
    for i := 0 to lbRoles.Items.Count-1 do
      lbRoles.Selected[i] := IntInList(Integer(lbRoles.Items.Objects[i]), aSelRoles);
  finally
    lbRoles.Items.EndUpdate;
  end;
  UpdateSelRoles; // Этого метода может и не быть. В нем можно разместить код, к примеру, выводящий фразу "Выбрано N ролей" на статус баре или где-то еще
end;

Методы IntInList и AddIntToList соответственно проверяют вхождение элемента в массив и добавляют новый элемент в массив.

Добавление и удаление ролей

Добавление ролей:

procedure TfmUserRights.btAddRoleClick(Sender: TObject);
var
  vSelUser: TUser;
  vRoles: TIntList;
  vAddRoles: TIntList;
  i: Integer;
begin
  vAddRoles := nil;
  vAddRoles := TfmDictionary.GelDictIDs(DictRoles); // Получаю список ID'ов выбранных ролей из всплывающего окна справочника

  vSelUser :=  Users.UserByID(SelUserID);
  vRoles := vSelUser.Roles;
  for i := 0 to High(vAddRoles) do
    AddIntToList(vAddRoles[i], vRoles); 
  vSelUser.Roles := vRoles;

  // После добавления новых ролей сразу выделяю их в списке ролей (визуально это удобно)
  SelRoles := vAddRoles;
end;

Удаление ролей:

procedure TfmUserRights.btDelRoleClick(Sender: TObject);
var
  vSelUser: TUser;
  vDelRoles: TIntList;
  vRoles: TIntList;
  vNewRoles: TIntList;
  i, vInd: Integer;
begin
  if lbAllowRightsRoles.SelCount = 0 then
    raise Exception.Create('Необходимо выделить в списке удаляемые роли.');

  vDelRoles := SelRoles;
  vSelUser :=  Users.UserByID(SelUserID);
  vRoles := vSelUser.Roles;
  SetLength(vNewRoles, Length(vRoles)); // размер завожу про запас
  // В vNewRoles переношу только те роли, которые не входят в список удаляемых
  vInd := 0;
  for i := 0 to High(vRoles) do
  begin
    if IntInList(vRoles[i], vDelRoles) then
      Continue;

    vNewRoles[vInd] := vRoles[i];
    inc(vInd);
  end;
  SetLength(vNewRoles, vInd); // усекаю до корректного размера

  vSelUser.Roles := vNewRoles;
end;

В каком месте осуществлять сохранение изменений объекта TUser в БД решать вам. Кто-то, возможно, захочет делать это немедленно, прямо внутри SetRoles класса TUser (чтобы все изменения отражались в базе мгновенно). Кто-то реализует сохранение измененных объектов TUser при нажатии на кнопку OK в окне. Третьим вариантом является сохранение по кнопке ОК, а также при попытке переключения между пользователями, если роли текущего пользователя были изменены (т.к. приведенный выше интерфейс окна не позволяет визуально отследить, у каких сотрудников роли поменялись, а у каких - нет, при переключении с одного сотрудника на другого, что может привести к ошибке).

Итог

Получилось окно управления правами пользователей. Окно реализует следующую логику:
1) Запрос списка сотрудников.
2) Отображение списка сотрудников.
3) Получение ID'а текущего выбранного сотрудника через SelUserID.
4) Установка выбранного сотрудника по ID'у с автоматическим обновлением списка его ролей.
5) Получение списка выбранных ролей сотрудника через SelRoles.
6) Добавление и удаление ролей.

Дополнение. Обновление списка с сохранением выбранного элемента

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

Итак, допустим вы добавили еще кнопку "Обновить список сотрудников" в окно настройки прав. Очевидно, что она должна приводить к простому вызову метода FillUsers. Но ведь тогда текущий выбранный сотрудник потеряется (т.к. GUI список будет очищен и переазполнен заново), что будет очень неудобно и странно для пользователя.

procedure TfmUserRights.FillUsers;
var
  i: Integer;
  vSavedSelUserID: Integer;
begin  
  // Перед перестроением списка запоминаю текущего выбранного пользователя
  if SelUserID > 0 then
    vSavedSelUserID := SelUserID
  else
   vSavedSelUserID := -1;

  ...
  // переполучаю данные FUsers и перезаполняю список
  ...

  // Устанавливаю заново текущего пользователя
  if vSavedSelUserID > 0 then
  begin
    FSelUserID := -1;
    SelUserID := vSavedSelUserID;
  end
  else
     SelUserID := -1;
end;

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

procedure TfmUserRights.FillUsers(const aSelUserID: Integer = -1);
var
  i: Integer;
  vNeedSelUserID: Integer;
begin  
  if aSelUserID > 0 then // Если передано на ком позиционироваться, то на нем
    vNeedSelUserID := aSelUserID 
  else if SelUserID > 0 then // Иначе если выбран текущий - то на текущем
    vNeedSelUserID := SelUserID
  else
    vNeedSelUserID := -1;

  ...
  // переполучаю данные FUsers и перезаполняю список
  ...

  if vNeedSelUserID > 0 then
  begin
    FSelUserID := -1;
    SelUserID := vNeedSelUserID;
  end
  else
     SelUserID := -1;
end;

При этом FormCreate поменяется на

procedure TfmUserRights.FormCreate(Sender: TObject);
begin
  FillUsers(Config.RightsFormSavedUserID);
end;

а FormDestroy на

procedure TfmUserRights.FormCreate(Sender: TObject);
begin
  Config.RightsFormSavedUserID := SelUserID;
end;

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

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

Удачи!

Ссылки по теме


 Распечатать »
 Правила публикации »
  Обсудить материал в конференции Embarcadero »
Написать редактору 
 Рекомендовать » Дата публикации: 16.11.2012 
 

Магазин программного обеспечения   WWW.ITSHOP.RU
Delphi Professional Named User
Enterprise Connectors (1 Year term)
TeeChart for .NET with source code single license
EMS Data Export for PostgreSQL (Business) + 1 Year Maintenance
SAP® Crystal Presentation Design 2016 WIN INTL NUL
 
Другие предложения...
 
Курсы обучения   WWW.ITSHOP.RU
 
Другие предложения...
 
Магазин сертификационных экзаменов   WWW.ITSHOP.RU
 
Другие предложения...
 
3D Принтеры | 3D Печать   WWW.ITSHOP.RU
 
Другие предложения...
 
Новости по теме
 
Рассылки Subscribe.ru
Информационные технологии: CASE, RAD, ERP, OLAP
Новости ITShop.ru - ПО, книги, документация, курсы обучения
Программирование на Microsoft Access
CASE-технологии
СУБД Oracle "с нуля"
Delphi - проблемы и решения
Проект mic-hard - все об XP - новости, статьи, советы
 
Статьи по теме
 
Новинки каталога Download
 
Исходники
 
Документация
 
Обсуждения в форумах
Слот Биг Бэнг (6)
Большой взрыв как правящая теория сотворения мира. Теперь вы можете поиграть слот Big Bang в...
 
Отличается ли ДрифтКазино от беттинга? (57)
Друзья, давно заметил, что на Дрифте уже несколько месяцев во всю рекламируется и предлагается...
 
Подскажите лучшее онлайн казино (8)
Вот нашёл интересную игровую площадку, и это онлайн казино Вавада. Может кто что подсказать по...
 
Помощь по MS Access (345)
Доброе время суток. Случайно оказался на этом сайте, искал статьи по OLAP. Вижу, что...
 
Требуется Краснодар: Java -разрабочик (2)
Обязанности: Разработка корпоративных backend-приложений для автоматизации розничных продаж...
 
 
 



    
rambler's top100 Rambler's Top100