MVC approach to developing user interfaces in Delphi. Part 1. Checkmark
- Tutorial

I will not write beautiful introductions, because the article is not entertaining, but rather technical. In it, I want to turn to the user-interface programming techniques of classic Delphi desktop applications in MVC style. This is an introductory article in a subsequent series.
I ask those few who still use this development environment under cat.
By classic applications, I mean desktop GUI applications for Windows based on VCL. About the FireMonkey framework that has appeared in new versions of Delphi, let someone else write an article.
User interfaces are very diverse. And if on the web they are generally a complete menagerie, then in desktop applications everything happens more conservatively. Of course, the developers of some applications (see Skype, Mikogo, Office 2010) continue to come up with all sorts of visual tricks that are designed to further improve usability, but for most of us (people of old school) the standard Windows and VCL controls invented probably back in the days of Windows 3.1:
- button (TButton)
- checkmark (TCheckBox)
- switch (radio button, TRadioButton)
- single-line input field (TEdit)
- multi-line input field (TMemo or TRichEdit)
- all kinds of combined controls (TSpinEdit, TDateTimeEdit)
- multi-page controls (TPageControl, TTabControl)
- grids
- panels, group boxes, bevels, shapes, ImageBoxes, etc.
When creating the user interface, the main task is to come up with such a representation of the program’s internal data using the above elements so that it is convenient for the user to work with this data. Well, or on the other hand, come up with a set of elements with which the user could tell the program what the program should hear from him.
Designing an interface in terms of layout of elements is in itself a rather complicated and important task. The interface is the face and the main way for user interaction with your program (not counting its removal by Ctrl + Alt + Del :)). That is why it is always necessary to design the interface iteratively, receiving feedback every time from users, is it convenient for them to work with your program, do you have to click on the mouse 200 times, go into the sub windows (which at the last moment are also modal), 10 enter the same data once because they are not saved when you re-enter the window, etc. etc. (I am sure that a sophisticated reader can give many more examples of what the user interface should not be :)).
It is said that smart guys across the ocean who approach interface design seriously and thoroughly also use technologies such as tracking mouse movements when working with the program (so that they won’t take them back in vain), counting clicks, or even tracking the direction of the user's gaze (if eyes begin to run in random directions, this should be alarming).
But this article is not about these miracles. I will allow myself to assume that you have already figured out how this or that window will look. You just need to somehow connect this view with the internal data of the program. And I would like to talk specifically about the ways of this connection.
Let's start with the primitive. Check mark.
Suppose you have a checkmark in some window (TCheckBox), reflecting the choice of one of two options. To speak not of spherical horses in a vacuum, we give it some meaning. Let it be a window for importing some data from files of a certain directory into the database. A checkmark will reflect whether to delete imported files after the operation is completed. Then let's call our checkmark
cbNeedDeleteFiles: TCheckBox;
By the way, giving prefixes to control names based on the type of control is very convenient. For example, cb for TCheckBox, rb for TRadioButton, bt for TButton. If you install the cnPack extension package in Delphi , then when you place the next control on the form, a window pops up with a proposal to immediately rename this control in accordance with your rules. This avoids the dominance of lying on the form of Button87, CheckBox32, etc.
As a rule, after placing the control in the right place on the form, the programmer sighs with relief and calms down. Now he can access cbNeedDeleteFiles.Checked from anywhere in the program to find out whether the checkmark is ticked or not. Probably, the programmer will not access the Checked property in a single place: when creating a window, he may want to set the default state of this property or its saved value, then in the main place (where the import is performed) you need to check this attribute again, and finally, where to save the value of this attribute when closing the window in order to restore the checkmark state the next time the window is opened. You may also need to programmatically change the value of this attribute based on some other conditions. For example, a user may ask so that this checkbox is always automatically displayed when selecting a directory with input files, if this directory contains only files of one strictly defined type or if the names of all the directory files satisfy a certain mask. And so, in the heap of program places, something like this appears:
if cbNeedDeleteFiles.Checked then ...
if Something then
cbNeedDeleteFiles.Checked := True;
if SomethingElse then
cbNeedDeleteFiles.Checked := False;
At first glance, there is nothing wrong with that. But as long-term practice shows, it is AWESOME. This is called tight linking to the user interface. Suppose you later have to replace TCheckBox with two radio buttons: “Delete imported files” and “Do not delete imported files." This doesn’t make much sense, but you can do it for better visualization or as part of refactoring before adding the third state of this setting like “Delete files only if there are no import errors”. And at that moment you’ll have to put some code on working with RadioButons in the heap of places where you used to contact cbNeedDeleteFiles.Checked.
How to avoid this?
On the network for a long time and a lot of trumpeting about MVC , MVP , MVVM . As if these are such miraculous techniques, following which you can program the user interface “correctly” and not have the crap described above. In fact, these are only approaches that really help, but which can be implemented in completely different ways in different programming languages and even in one language. Those. these are rather tips on which side is best for user interface programming.
If you look at the abbreviations once again, you can see that all three have the letters M (Model, model) and V (View, view). Speaking in a very simple language, the model is the internal data of the program, and the presentation is external (user interface). Returning to the check mark, it is obvious that the Boolean value is the internal representation of this check mark. The decision about the attribute of which class this value should be stored is taken individually in each case. For example, it can be a TConfig class that provides access to program settings. However, in simple cases, it is enough to create the corresponding attribute just for the form class:
TfmImport = class(TForm)
...
private
...
FNeedDeleteFiles: Boolean;
public
...
property NeedDeleteFiles: Boolean read FNeedDeleteFiles write SetNeedDeleteFiles;
end;
Next, you need to associate the state of the NeedDeleteFiles property with the state of the visual component (TCheckBox) cbNeedDeleteFiles. This is conveniently done through the set property method:
procedure TfmImport.SetNeedDeleteFiles(const Value: Boolean);
begin
if FNeedDeleteFiles <> Value then
begin
FNeedDeleteFiles := Value;
cbNeedDeleteFiles.Checked := FNeedDeleteFiles;
end;
end;
Why the FNeedDeleteFiles <> Value condition is needed, I will explain a bit later. The main thing is that now, when assigning a value to the NeedDeleteFiles property, we will automatically be ticked (this is almost an MVC model - we change the value of the model element, and the view changes automatically). But this connection is only one way - from internal data to the interface. It is still necessary to achieve feedback - from the view (i.e. from the checkmark) to the model. To do this, write the following code in the OnClick handler of our checkbox:
procedure TfmImport.cbNeedDeleteFilesClick(Sender: TObject);
begin
NeedDeleteFiles := cbNeedDeleteFiles.Checked;
end;
Those. the action on the view (in this case, clicking on the checkmark) will bring the model into line with the current state of the view. However, the model never trusts the view and therefore causes the state of the view to be brought back to the state of the model (forcibly sets cbNeedDeleteFiles.Checked: = FNeedDeleteFiles. Nothing wrong with this. And we even insured ourselves if we checked if FNeedDeleteFiles <> Value that the visual control the OnClick handler will call again, in fact, it will not do this, because there is a similar check:
procedure TCustomCheckBox.SetChecked(Value: Boolean);
begin
if Value then
State := cbChecked
else
State := cbUnchecked;
end;
procedure TCustomCheckBox.SetState(Value: TCheckBoxState);
begin
if FState <> Value then
begin
FState := Value;
...
end;
end;
Now we have the state of the TfmImport.NeedDeleteFiles property synchronized with the daw state cbNeedDeleteFiles.Checked in both directions. In all places of the program where we used to access cbNeedDeleteFiles.Checked, you should now access the NeedDeleteFiles property. This allows us to completely forget that the representation of the NeedDeleteFiles element is a CheckBox. You can’t even imagine how wonderful it is. Subsequently, we can replace the CheckBox with two radio buttons or anything, and we need to rewrite it only with the SetNeedDeleteFiles set-method (Model -> View direction) and a handler that fires when the view state changes, i.e. visual components (View -> Model direction).
I missed such an important point as the initial synchronization of the value of the NeedDeleteFiles property with the state of the visual component. Of course, if when you open the window, your daw will either always be set or always unchecked, you can simply set the correct state in DesignTime, and assign the corresponding value to the FNeedDeleteFiles field in OnCreate of the form class. However, this is not very reliable (you need to follow this, it is easy to avoid a discrepancy), therefore, in OnCreate, it is better for the form class to place the following code:
procedure TfmImport.FormCreate(Sender: TObject);
begin
FNeedDeleteFiles := False; // Намеренно присваиваю значение, отличающееся от того, которое хочу задать, чтобы сработал set-метод
NeedDeleteFiles := True; // Тут сработает set-метод и синхронизирует GUI с присвоенным значением (выставит галочку)
end;
In the next part of the article I will try to talk about more complex cases: working in the MVC style with lists of elements (TListBox, TCheckListBox, TComboBox) and about the pitfalls when remembering the state of the visual elements of a window when it is closed.
UPD The second part of the
UPD article has been added . The third part of the article is added .