📜 ⬆️ ⬇️

MVC approach to the implementation of the user interface in Delphi. Part 3. Objects


In the previous parts of the article ( 1 , 2 ), I showed how to work with the internal data of the application and the user interface through one entry point - the model. Model changes are automatically reflected in the user interface. At the same time, to simplify, as a model, I used simple form property classes, whose setter can bring the GUI interface to the current state of the model. In this part of the article I will show how the interface can react to changes in the objects themselves within the application.


To start this article, I would like to consider the error, or rather the inaccuracy made in the previous part of the article. The code for adding and deleting roles for the currently selected user presented there correctly changed the internal state of the object, but did not update the user interface at all. More precisely, the interface could be updated only after switching from one user to another and vice versa. As correctly noted in the comments, to correct this flaw, it was enough to insert the call to the FtUUrRoles method in the btAddRoleClick, btDelRoleClick procedure code. It will work, but this is not at all what we need. This method is bad because in all places where employee roles can potentially change, you need to insert a call to update the user interface each time. And I want to forget once and for all about the need to do something with the GUI in the places where we work with the object. I want the GUI to react to changes to the object itself and redraw itself when I change the fields of the object.

To do this, I will extend the TUser class as follows:
TUser = class private ... FOnChangeRoles: TNotifyEvent; protected ... procedure DoChangeRoles; public ... property OnChangeRoles: TNotifyEvent read FOnChangeRoles write FOnChangeRoles; end; procedure TUser.DoChangeRoles; begin if Assigned(FOnChangeRoles) then FOnChangeRoles(Self); end; 

')
I added the simplest notification event to the TUser object, which will notify us of changes in the employee's role list. In this case, the SetRoles method of the TUser class will look as follows:
 procedure TUser.SetRoles(Value: TIntList); begin if not SameIntLists(Roles, Value) then begin Roles := Value; DoChange; //   end; end; 


As long as the OnChangeRoles event of the TUser class is not overridden (by default, FOnChangeRoles is nil), the DoChangeRoles call simply does nothing. In order to be able to somehow react to this event, you need to assign the appropriate handler to the TUser objects.
It is logical to get this handler at the form class:
 procedure TfmUserRights.ProcRolesChanged(Sender: TObject); begin FillUserRoles; end; 


Now we need to hang this event handler on the objects of the TUser class:
 procedure TfmUserRights.FillUsers; var i: Integer; begin FUsers.Free; //   ,    FUsers := GetUsers; for i := 0 to FUsers.Count-1 do FUsers[i].OnChangeRoles := ProcRolesChanged; ... end; 


Here, in general, that's all :). Now, when the object's roles are changed, the OnChangeRoles event will fire, the assigned handler of which will call FillUserRoles and update the GUI (refill the list of roles). With these corrections, the code from the previous article will work correctly.

Could it have been better?

1) In the context of the previous article, I needed to respond only to a change in the list of roles, so I started a specific event that responds only to a change in the Roles field of the TUser class. Often you need to respond to changing not one, but several (and maybe all) fields of the object. In this case, it was better to start the event not OnChangeRoles, but simply OnChange, although the handler in this case should not only rebuild the list of roles, but also update any other information about the user that could be displayed in the window at that time. Accordingly, the call to DoChange would not only be in SetRoles, but also in the setters of the rest of the TUser object fields, which I would like to monitor for changes. And here the main task is not to forget to add this call to DoChange when adding a new field to the object, since miss it pretty easy.
2) Based on the principles of secure programming, if we register an event handler (as they say, “subscribe” to an event), then we have to remove this subscription (“unregister” the handler), i.e. return OnChangeRoles to its original state or at worst in nil. Whether it is necessary to carry out this deregistration is decided individually in each case. First of all it depends on the ratio of the lifetime of TUser objects and the form object. If the form lives longer TUser'a, then in principle deregistration is not required. If, on the contrary, TUser can still live after the destruction of the form, then of course in OnDestroy the form needs to be written in the spirit of
  for i := 0 to FUsers.Count-1 do FUsers[i].OnChangeRoles := nil; 

If this is not done, then when trying to change a TUser object after destroying a form, TUser may try to call an event handler that refers to the method of an already destroyed object (form) and in the best case we will get Access Violation.
3) When we work with lists of objects, assigning a handler to each object is not always convenient. If the list items know about the list itself (for example, they refer to it through the Owner), you can make TUser doChange objects simply call Owner.DoChange, and start a custom event (property FOnChange) at the list itself (on TObjectList) . Although this in general does not change anything in meaning.

The considered method can be considered as a subscription to notifications with one subscriber. However, this is not a full subscription to notifications. Notifications are good because you can subscribe to as many subscribers as you want. Now we look at how this is done. Let's switch to another task.

Notifications with multiple subscribers

This template is often used in well-written MDI applications (and indeed in any multi-window applications). The template is used when the same data can be displayed in several windows of the system, and when changing this data through one window, it is necessary that they are synchronously updated in all windows. However, these windows are not necessarily instances of a window of the same class and do not necessarily have the same user interface. On the contrary, the windows can be completely different. They only display the same information. For example, a list of employees is displayed in one window, and a card of this employee is displayed in another, where you can change some of its characteristics. At the same time, it is required that by pressing the “Save” button on the employee card, the data would be updated both in the employee card and in the general list of employees.
Template multiple subscription to the notification is convenient to use when there is a long-lived object. His lifetime should be obviously longer than the lifetime of those objects that subscribe to notifications from him. Suppose we have a class manager responsible for working with employees (in particular, saving changes to TUser objects in the database):
 TUsersMngr = class public procedure SaveUser(aUser: TUser); end; 


All windows, in which any employee-related information can be displayed, want to respond to calls to SaveUser. In this case, the TUserMngr class will need to store references to all handlers that can subscribe to the employee save event:
 TUsersMngr = class private FNotifiers: array of TNotifyEvent; public procedure RegChangeNotifier(const aProc: TNotifyEvent); procedure UnregChangeNotifier(const aProc: TNotifyEvent); function NotifierRegistered(const aProc: TNotifyEvent): Boolean; end; 


Code of implementation of these methods:
 procedure TUsersMngr.RegChangeNotifier(const aProc: TNotifyEvent); var i: Integer; begin if NotifierRegistered(aProc) then Exit; i := Length(FNotifiers); SetLength(FNotifiers, i+1); FNotifiers[i] := aProc; end; procedure TUsersMngr.UnregChangeNotifier(const aProc: TNotifyEvent); var i: Integer; vDel: Boolean; begin //  ,       (    RegChangeNotifier),     vDel := False; for i := 0 to High(FNotifiers) do if vDel then FNotifiers[i-1] := FNotifiers[i] else if (TMethod(aProc).Code = TMethod(FNotifiers[i]).Code) and (TMethod(aProc).Data = TMethod(FNotifiers[i]).Data) then vDel := True; if vDel then SetLength(FNotifiers, Length(FNotifiers) - 1); end; function TUsersMngr.NotifierRegistered( const aProc: TNotifyEvent): Boolean; var i: Integer; begin //       TMethod   for i := 0 to High(FNotifiers) do if (TMethod(aProc).Code = TMethod(FNotifiers[i]).Code) and (TMethod(aProc).Data = TMethod(FNotifiers[i]).Data) then begin Result := True; Exit; end; Result := False; end; 



With this functionality, you can easily subscribe to changes to objects of interest from any window:
 procedure TUsersListForm.FormCreate(Sender: TObject); begin ... UsersMngr.RegChangeNotifier(ProcUsersChanges); end; procedure TUsersListForm.FormDestroy(Sender: TObject); begin UsersMngr.UnregChangeNotifier(ProcUsersChanges); ... end; procedure TUsersListForm.ProcUsersChanged(Sender: TObject); begin RefillUsersList; end; 


Now that we have understood how this will be used, let us go back directly to the moment of notification, i.e. at the time of the event:
 procedure TUsersMngr.SaveUser(aUser: TUser); begin if aUser.Changed then begin ... //   aUser   ... DoUserChangeNotify; //   end; end; procedure TUsersMngr.DoUserChangeNotify; var i: Integer; begin for i := 0 to High(FNotifiers) do FNotifiers[i](Self); end; 


Now when you save the TUser object, all forms will be notified of this if they have not forgotten to subscribe to the corresponding event.

Handler lockout

The above code is good as long as there are no operations in the system at once on a large number of objects. Perhaps not the best example: a group of employees was trained and each of them received some kind of certificate that was the same for all. We select 10 employees in the list, click "Add certificate". Next, the UserMngr.Show call is alternated for each of these 10 employees. At the same time, after each employee is saved, the DoUserChangeNotify change event is triggered, which causes the list of employees to be rebuilt in all open windows (and each rebuild will still cause the list of employees to be re-requested from the database or from the application server). As a result, saving changes for 10 employees will be sooooo slow and in addition we will get a lot of blinks in the open application windows (the lists will be rearranged 10 times). Now I will describe a simple way to avoid this:
 TUsersMngr = class private FLock: Integer; FChanged: Boolean; public procedure BeginUpdate; procedure EndUpdate; end; procedure TUsersMngr.Create; begin ... FLock := 0; FChanged := False; end; procedure TUsersMngr.BeginUpdate; begin Inc(FLock); end; procedure TUsersMngr.EndUpdate; begin Assert(FLock > 0); Dec(FLock); if (FLock = 0) and Changed then DoUserChangeNotify(Self); end; 


The notification method will also change:
 procedure TUsersMngr.DoUserChangeNotify; var i: Integer; begin if FLock > 0 then //      begin FChanged := True; // ,     Exit; end; FChanged := False; for i := 0 to High(FNotifiers) do FNotifiers[i](Self); end; 


Through FLock, the blocking level is tracked (nested calls to BeginUpdate..EndUpdate are allowed). FChanged is a flag that allows us to remember whether an event occurred within the lock session at least once. If it did occur, then at the moment of leaving the blocking session (i.e., at the moment when the EndUpdate of the top level is called), the event will be automatically triggered.

Thus, the change code of a set of objects can be easily protected from unnecessary event triggering:
 UsersMngr.BeginUpdate; try for i := 0 to FSomeUsers[i] do UsersMngr.Save(FSomeUsers[i]); finally UsersMngr.EndUpdate; end; 


It is convenient to use such a lock in other cases, for example, when it is necessary to transfer an object from one state to another, changing at the same time not one, but several of its fields. However, some intermediate object states (some combinations of field values) may be considered invalid from the point of view of the GUI. Accordingly, it is necessary not to let the GUI know at all that the object passed through such states. In this case, the change of the object is also carried out within the session of its update, when the triggering of events about the change of this object is blocked.

Total

Events are one of the good techniques for connecting objects to a GUI. This template is used not only when programming the GUI, but in many other cases. In the article, we looked at options for implementing a subscription to notifications with one and with multiple subscribers. On this cycle of articles on programming GUI in MVC-style is likely to be completed. If someone has any questions regarding the approaches to the implementation of the GUI in Delphi, please leave them in the comments and maybe this series of articles will continue successfully. I also propose in the comments (and maybe in separate articles!) To share their techniques for the successful implementation of typical tasks in Delphi. And do not bury any flight attendants , Delphi still live;)

Have a nice day, everyone!

PS Links to previous parts of the article:
Part 1. Tick.
Part 2. Lists.

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


All Articles