📜 ⬆️ ⬇️

MVC approach to developing user interfaces in Delphi. Part 2. Lists



The previous article was devoted to just one tick. It's time to move on to something a little more serious. Today's topic is the presentation of lists and the association of GUI lists with internal data. The article is intended for Delphi-developers.


Where to begin

In order not to pour water, I will go straight to the living example shown in the figure above. Suppose you need to create a primitive form of setting user rights.
The list of all users of the system is shown in the left part of the window, and the list of rights and roles of the currently selected user is shown in the right. The logic of the window is that when you select a user in the left part of the window, the list of rights and roles in the right part is updated. Also in the right side there are “Add” / “Delete” buttons, which allow either to add a new role to the user or to delete selected existing roles. When you add new roles, a pop-up window appears in the role guide in which you can select the roles to be added. That's all.
')
Model

Suppose that the internal representation of the data consists of the class TUser, which describes the employee, and the directory of roles, which is able to return the name of the role by numeric ID. It’s not practical to create classes for roles. this is too simple an entity:

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; 


It can be seen that user roles are presented in an extremely simple way - a list of IDs.

I add the appropriate fields to the form class:
 TfmUserRights = class(TForm) ... lbUsers: TListBox; lbRoles: TListBox; private FUsers: TUsersList; public property Users: TUsersList read FUsers; end; 


Notice that I used a typed TObjectList. Before Delphi 2009 there was no such possibility and TObjectList always kept just TObjects. Each time a list item was accessed, it had to lead to the correct class: FUsers [i] as TUser (well, or an option for kamikazes: TUser (FUsers [i])). It was inconvenient and it was easy to make a mistake by doing the conversion to the wrong class. With the advent of generic types (generics), you can now use a hard-typed TObjectList. It is incredibly comfortable! Referring to the elements of such a list through FUsers [i], we immediately get an object of class TUser.

I will not give the code to get the list of employees, because in each system, depending on its architecture, it will be its own. This may be a SQL query to the database, a call to some client cache, or a call to an application server (in a multi-tier architecture). Suppose simply that you have the opportunity to get this list from somewhere.

Display list items

So, we want to get a list of employees and display it on the screen:
 procedure TfmUserRights.FormCreate(Sender: TObject); begin FillUsers; end; 


The Fill method is designed to simply [re] fill the user list
 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; 


Just filling out the list of employees is not enough. You still need to show the roles of the currently selected employee. And for this you need to learn to determine which employee is currently selected? Inexperienced programmers are beginning to actively contact lbUsers.Items.Objects [lbUsers.ItemIndex] from different places. However, if you read the previous part of the article, you already guess that we will go some other way. We will get a property from the form class that returns and sets the currently selected employee. You can return either the TUser object itself or a numeric user ID. Returning an ID seemed to me more convenient, although one could argue with that.

 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; 


The key point here in the UpdateSelUser method, which brings the interface to the state where the specified user is selected:

 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; 


We see that the installation method of the current user always causes the roles list to be full (FillUserRoles).

As in the previous article, since we implemented the direction of synchronization Model-> View, we also need reverse synchronization. Therefore, in the OnClick event of the lbUsers list, add the following code:
 procedure TfmUserRights.lbUsersClick(Sender: TObject); begin SelUserID := (lbUsers.Items.Objects[lbUsers.ItemIndex] as TUser).ID; end; 


When setting SelUserID, if another user was previously selected, the set method will call UpdateSelUser, which in turn will completely synchronize the presentation with the model, namely, update the list of roles. Those. I no longer need to call the update method for the list of roles from within the lbUsersClick handler, everything will happen automatically.

I will give the method of filling the list of roles (it is trivial):
 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; 


I will supplement the form initialization code with initializing the first user in the list:
 procedure TfmUserRights.FormCreate(Sender: TObject); begin FillUsers; FSelUserID := -2; // ,   Set- SelUserID := -1; //       end; 


What did we get? Now it is possible to access the currently selected user via SelUserID. Moreover, both when programmatically setting the value of the SelUserID property, and when the user is selected via the GUI list, the list of roles will be automatically updated.

To work with roles (add, delete), you can add a SelRoles property to the form class. It is easier to make it completely virtual (do not start a separate field for it):
 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; 


The IntInList and AddIntToList methods respectively check whether an element is in the array and add a new element to the array.

Adding and deleting roles

Adding Roles:
 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; 

Removing roles:
 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; 


Where to save the changes to the TUser object in the database is up to you. Someone might want to do this immediately, right inside the SetRoles class of TUser (so that all changes are reflected in the database instantly). Someone implements saving changed TUser objects when clicking on the OK button in the window. The third option is to save the OK button, as well as when trying to switch between users, if the current user’s roles have been changed (because the window interface above doesn’t visually track which employees have changed roles, and which ones haven’t, when switching from one employee to another, which can lead to an error).

Total

It turned out the user rights management window. The window implements the following logic:
1) Request a list of employees.
2) Displays a list of employees.
3) Getting the ID of the currently selected employee via SelUserID.
4) Installation of the selected employee by ID with automatic updating of the list of his roles.
5) Getting a list of selected employee roles through SelRoles.
6) Adding and deleting roles.

Addition. Refreshing the list with saving the selected item

It would be possible to stop here, but still I want to show how you can update the list of employees, without losing the current selected employee. The functionality of manually updating the list of employees can be useful if adding employees via another window, and the mechanism for automatically notifying the window for changing rights to add a new employee is not implemented. Also, a new employee can be added by another user of the system on another machine, and you do not want to restart to the rights configuration window so that the added user appears in the list.

So, let's say you added another button “Update employee list” in the rights settings window. Obviously, it should lead to a simple call to the FillUsers method. But then the current selected employee will be lost (since the GUI list will be cleared and re-filled again), which will be very inconvenient and strange for the user.

 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; 


In the future, you may need even more: remember the last selected employee between repeated logins to the rights configuration window or even between application sessions. In this case, you can add a parameter to FillUsers that determines which user to position on after rebuilding the list. In this case, the logic of remembering the current user will have to complicate a little
 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; 


At the same time, FormCreate will change to
 procedure TfmUserRights.FormCreate(Sender: TObject); begin FillUsers(Config.RightsFormSavedUserID); end; 


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


Most of the above code is invented from the head, do not judge too strictly for typos and inaccuracies. It is very similar to a real project, but in reality there are much more details in a real project that I don’t want to talk about now.

Gradually, I am getting closer to linking the classes of internal data with the GUI controls. I have not done that yet. In the next part of the article, I will look at the notification subscription template and show how the GUI interface can react to changes in the objects themselves.

Good luck!

PS

1st part of the article
3rd part of the article

Source: https://habr.com/ru/post/147198/


All Articles