Articles MVVM Starter Kit (Part 1 of 3) by Erik van Bilsen

emailx45

Местный
Регистрация
5 Май 2008
Сообщения
3,571
Реакции
2,439
Credits
574
MVVM Starter Kit (Part 1 of 3)
January 22, 2018 Erik van Bilsen
[SHOWTOGROUPS=4,20]
Wikipedia: Для просмотра ссылки Войди или Зарегистрируйся

We present a framework to help you get started separating user interface from code using the Model-View-ViewModel pattern.
In this first part, I will briefly introduce the MVVM pattern, talk about data binding and start building out the Models for a sample application.
In Для просмотра ссылки Войди или Зарегистрируйся we will focus on the ViewModels and how these can be unit tested. In the Для просмотра ссылки Войди или Зарегистрируйся we look at the Views and how to bind them to ViewModels. It also shows how the same ViewModels can be used to build a VCL version and FireMonkey version of the same application, with a minimum of framework-specific code.
TL;DR
But first a general note. I know my articles are a bit (T)L, so some people DR them. In this world of bite-size snacks, I like to provide (hopefully satisfying) meals. But I understand if you only have time for a quick snack. I will try to add plenty of (sub)headings to make it easier for you to skim the text. You can always come back later for the details. If there are other things I can do to improve the experience, let me know in the comments. I will try to be accommodating.
That being said, these three articles are bit long, not because it is a terribly complicated subject, but because there is a lot to talk about. You can think of it as part introduction to MVVM and part a more detailed programming guide.
Source Code and Sample Project
As usual, you can find the code for these articles on GitHub in the repository Для просмотра ссылки Войди или Зарегистрируйся. This repository depends on our Для просмотра ссылки Войди или Зарегистрируйся repository, so be sure to pull the latest version of that repo as well. Make sure that GrijjyFoundation is in your library path (or otherwise at the same directory level as MvvmStarterKit).
We discuss the MVVM pattern using a sample application called MyTunes, which is part of the repository. This is a simple album management app that you can use to manage albums and songs. There is both a VCL and FireMonkey version of this application. They share the same Models and ViewModels, and only have different Views (forms). I suggest you open a version of this app in Delphi, since these articles regularly refer to it.
The MVVM Pattern
Model-View-ViewModel is one of several patterns that can be used to separate the user interface of your app from the business logic. It is similar to other patterns like Model-View-Controller (MVC) and Model-View-Presenter (MVP), but imho it is better in reducing the dependencies between its separate parts: a Model has no knowledge (or dependency) on the ViewModel, and the ViewModel has no knowledge (or dependency) on the View. All dependencies go in only 1 direction: from left to right in the following diagram:
Mvvm

Only the View part deals with VCL or FireMonkey controls. It binds these controls to properties of a ViewModel. The ViewModel is really just a “model of the view”. It knows nothing about the actual user interface but it does contain the logic to operate the user interface. The View uses Actions (aka commands) to trigger this logic. The ViewModel gets its data from the Model and the Model contains all logic related to this data.
In short: the Model contains data and business logic, the ViewModel contains user interface logic and the View contains UI elements (and preferable no logic at all).
Of course, the Model must still be able to present its data to the View, but it does so without directly talking to the View. Instead, it uses data binding the bind the two together to create a loose coupling. Data binding is at the heart at the MVVM pattern, and is the subject of most of this first article in the series.
For another introduction of MVVM, take a look at Malcolm Groves’ excellent CodeRage video Для просмотра ссылки Войди или Зарегистрируйся. Malcolm has is Для просмотра ссылки Войди или Зарегистрируйся as well.
Why Model-View-Whatever?
A RAD environment like Delphi makes it very easy to get into bad habits and mix code and UI together. I do it all the time in research projects and prototypes. But when it is time to build a production-quality app, separating UI from code has many benefits:
  • It greatly improves the testability of your app. User interfaces are notoriously hard to test. By making sure the user interface layer contains only a minimum amount of code, you can achieve much higher test coverage, resulting in fewer bugs and lower maintenance costs.
  • It makes it easier to develop different user interfaces for different platforms or purposes. Because the UI contains almost no code, it takes less time to create both a VCL and FMX version of your app if you want to. Or create a different desktop and mobile experience of the same app. Or create a different OEM or white label version. These scenarios are much less expensive to develop with a Model-View-Whatever pattern.
Whether you want to use MVC, MVP, MVVM or another pattern to accomplish this is a matter of personal preference and available tools. It doesn’t matter much which pattern you pick, as long as you pick one.
Why this MVVM Starter Kit?
There are some other MVVM solutions out there, such as Для просмотра ссылки Войди или Зарегистрируйся. That framework is geared towards VCL applications however, while we at Grijjy develop mostly FireMonkey applications.
You can also build your own MVVM solution on top of Delphi’s LiveBindings, as Malcolm Groves shows in his Для просмотра ссылки Войди или Зарегистрируйся.
One of the reasons we chose to create our own framework is that both LiveBindings and the data binding in DSharp can be a bit slow. They offer great flexibility through data binding expressions, but that comes at a run-time cost. Since we develop highly responsive applications, this can become problematic at times. In fact, this is one of the most common complaints about the MVVM pattern. Our data binding solution is less flexible, but more performant and arguably somewhat easier to use. If you need more complicated data binding expressions, then you can write them in Delphi code as property getters/setters of a ViewModel.
Of course, everyone has different requirements, so you usually end up creating your own MVVM solution anyway. Therefore, as the name of this post implies, this MVVM Starter Kit can be used as a base for your own solution. It already offers a lot of functionality out of the box, but you can complement it with your own additions.
Enforcing Separation
It requires some discipline to keep user interface and code really separated. You can use the following naming conventions and directory organization to help enforce the rules:
  • Create a separate directory for your Models. Start each unit in this directory with a “Model.” prefix, such as Model.Album in the MyTunes app.
  • The units in the Models directory may only “use” other Model units or RTL units. They may never use VCL, FMX, ViewModel or View units. (For this discussion, I use the term RTL units for all units that are not VCL or FMX units).
  • Also use a separate directory for your ViewModels. Start each unit in this directory with a “ViewModel.” prefix (eg. ViewModel.Album).
  • The units in the ViewModels directory may only “use” ViewModel, Model and RTL units. These units also may never use VCL, FMX or View units.
  • Finally, create a separate directory for your Views (such as forms or frames). Again, start each unit here with a “View.” prefix (eg. View.Album).
  • The units in the Views directory are the only ones that may “use” VCL or FMX units. It may also use ViewModel and Model units.
By using these rules, it will be easier to maintain a good separation of UI and code. You can take this even further by putting all Models in a separate run-time package and all ViewModels in another run-time package. If you make sure the Models package does not “require” any ViewModel, VCL or FMX packages, then you can’t even break the rules. Unfortunately, run-time packages come with their own (cross-platform deployment) issues, so this may not be worth it.
Data Binding
Data binding is at the heart of the MVVM pattern. You can bind properties of different objects together so that changes to a property of one object are automatically propagated to a property of another object. This can save a lot of code, especially if you are used to manually synchronizing the user interface with the underlying data.
Delphi provides a very powerful and flexible LiveBindings framework that does just this. But as I mentioned before, we use something a bit more light-weight. It is based somewhat on the data binding framework that Microsoft uses for their WPF, Silverlight and Xamarin GUI frameworks. This is not too surprising since Microsoft developed the MVVM pattern and they use it for all their modern (non-web) user interfaces. Some of the class and interface names we use are directly derived from Microsoft’s counterparts, although the implementation is completely different.
Observable Objects
To be useful for data binding, an object that is the source of a binding must notify any interested parties whenever one of its properties has changed. In other words, it must be “observable”. For our purposes, an object is observable if it implements the IgoNotifyPropertyChanged interface. Other objects use this interface to subscribe to notifications of property changes. You usually don’t need to implement this interface yourself. Instead, you can use a base class, such as TgoObservable, that already implements this interface.
Take a look at the TAlbum class (in the unit Model.Album) for an example. This class is derived from TgoObservable, so that other parties can listen for changes to its properties. To enable this, it should call PropertyChanged whenever one of its bindable (or observable) properties has changed. The setter for the RecordLabel property is a simple example:
1
2
3
4
5
6
7
8
procedure TAlbum.SetRecordLabel(const Value: String);
begin
if (Value <> FRecordLabel) then
begin
FRecordLabel := Value;
PropertyChanged('RecordLabel');
end;
end;
You will use this pattern all the time:
  • First check if the value is different from the current value (to avoid firing notifications if the value hasn’t actually changed).
  • Then change the backing field accordingly.
  • And finally fire the notification by calling PropertyChanged with the name of the property. This string is case-sensitive.
Binding Properties
Now, other objects can bind to the RecordLabel property of an album.
For example, the album view (the UI form used to edit an album) binds this property to the Text property of a TEdit:
1
2
3
4
5
procedure TViewAlbum.SetupView;
begin
...
Binder.Bind(ViewModel.Album, 'RecordLabel', EditRecordLabel, 'Text');
end;
It uses the TgoDataBinder.Bind method to create the binding. You should read this statement as:
“Bind ViewModel.Album.RecordLabel to EditRecordLabel.Text”
Whenever the RecordLabel property of the album changes, the Text property of the edit box is updated accordingly. By default, bindings are bidirectional. This means that when the Text property of the edit box is modified by the user, the the Album.RecordLabel property is updated as well. This only works if the TEdit control implements the IgoNotifyPropertyChanged interface as well. We will look into this in the Для просмотра ссылки Войди или Зарегистрируйся of this series.
The following image shows most data bindings for the Album view:
Mvvm-BindingProperties



[/SHOWTOGROUPS]
 
Последнее редактирование:

emailx45

Местный
Регистрация
5 Май 2008
Сообщения
3,571
Реакции
2,439
Credits
574
MVVM Starter Kit (Part 1 of 3)
January 22, 2018 Erik van Bilsen
[SHOWTOGROUPS=4,20]
Binding Sub-Properties
You can bind to sub-properties by providing a “property path” instead of a simple property name. A property path is a string of property names separated by periods. For example, the Albums (plural) view shows a list of albums. But for the selected album, it also shows the title (among other things). To bind this title to a text control, you use the property path “SelectedAlbum.Title”:
1
2
3
4
5
6
procedure TViewAlbums.SetupView;
begin
...
Binder.Bind(ViewModel, 'SelectedAlbum.Title', TextTitle, 'Text',
TgoBindDirection.OneWay);
end;
This image shows to other bindings for the Albums view:
Mvvm-BindingSubProperties

As you can see, you can also bind to properties like FontColor and Bitmap:
1
2
3
4
5
6
7
8
procedure TViewAlbums.SetupView;
begin
...
Binder.Bind(ViewModel, 'SelectedAlbum.TextColor1',
TextTitle.TextSettings, 'FontColor', TgoBindDirection.OneWay);
Binder.Bind(ViewModel, 'SelectedAlbum.Bitmap', ImageAlbumCover, 'Bitmap',
TgoBindDirection.OneWay);
end;
Note that you can only bind to sub-properties if all properties of the path, except for the last one, are of a class type. You cannot (currently) use properties of record or interface types, although I may add support for this in the future.
Properties vs Sub-Properties
There is a fundamental difference between binding to properties and binding to sub-properties. For example, there is a difference between
1Binder.Bind(ViewModel.Foo, 'Bar', EditBox, 'Text');
and
1Binder.Bind(ViewModel, 'Foo.Bar', EditBox, 'Text');
In the first example, you are binding only to the current value of ViewModel.Foo. If ViewModel.Foo is set to a different value, then the EditBox is still bound to the original value. This model is used in the first screenshot above: this dialog box is used to edit a single album, making this the preferred way to bind the properties in this case.
In the second example, whenever the value of ViewModel.Foo changes, the EditBox binds to this new Foo instance. This model is used in the second screenshot above: if the user selects a different album in the list, then the controls on the right are updated accordingly. Use this kind of binding when the object you are binding to can change at run-time. If that is not the case, then it is more efficient to use the first kind of binding instead.
Binding Direction
The TgoDataBinder.Bind method has a number of optional parameters. The first one is a binding direction, which defaults to TgoBindDirection.TwoWay, as we saw before. You can also bind in a single direction:
1
2
3
4
5
6
procedure TViewAlbum.SetupView;
begin
...
Binder.Bind(ViewModel.Album, 'IsValid', ButtonOK, 'Enabled',
TgoBindDirection.OneWay);
end;
This enables the OK button only when the TAlbum.IsValid property is True. In this case it makes no sense to bind in the other direction. And since the IsValid property is read-only, that would not work anyway (an exception is raised if you try to bind to a read-only property).
BTW: TAlbum.IsValid is a computed property, which returns True if at least a title and artist is specified. This means that the setter methods for the Title and Artist properties must also fire a notification for the IsValid property:
1
2
3
4
5
6
7
8
9
procedure TAlbum.SetTitle(const Value: String);
begin
if (Value <> FTitle) then
begin
FTitle := Value;
PropertyChanged('Title');
PropertyChanged('IsValid');
end;
end;
Binding Flags
The next optional parameter is a set of binding flags:
  • TgoBindFlag.TargetTracking: set this flag to update the source while the target property is changing. For example, when using a TEdit control, a notification is only send after the user has finished editing the text (by exiting to control for example). By setting this flag, a notification will be fired for each character the user enters in the control. For this to work, the target must implement the IgoNotifyPropertyChangeTracking interface.
  • TgoBindFlag.SourceTracking: works the same, but applies to the source instead of the target.
  • TgoBindFlag.DontApply: normally, when creating a binding, the initial value of the source property will be applied to given target property. If you don’t want this, then you can specify this flag. A reason you want to do this is if you have a one-way binding from the View to the ViewModel, but you don’t want to update the ViewModel with the current value in the View.
Our album example uses the TgoBindFlag.TargetTracking flag to update the Album.Title property for every character the user enters in the EditTitle edit box:
1
2
3
4
5
6
procedure TViewAlbum.SetupView;
begin
...
Binder.Bind(ViewModel.Album, 'Title', EditTitle, 'Text',
TgoBindDirection.TwoWay, [TgoBindFlag.TargetTracking]);
end;
Value Converter
The final optional parameter is a value converter. You can supply a converter to convert a source property value before assigning it to a target property. You can even change the data type if needed.
The data binder already takes care of a lot of “trivial” conversions. For example, you can bind an integer property to a floating-point property, then the value will be converted to floating-point on assignment. The other way around, the fractional part of a floating-point value will be ignored (truncated) when assigned to a integer property. Likewise, the binder automatically converts numbers to and from strings. It even converts from object to Boolean, where the value is True if the object is assigned, or False otherwise. This is useful for example to set the Enabled property of a button, depending on whether an object is assigned or not.
If the built-in conversions don’t suffice, you can pass a converter class. Our album example uses a (contrived) converter to convert the album title to a form caption:
1
2
3
4
5
6
procedure TViewAlbum.SetupView;
begin
...
Binder.Bind(ViewModel.Album, 'Title', Self, 'Caption',
TgoBindDirection.OneWay, [], TTitleToCaption);
end;
The converter class (TTitleToCaption in this example) consists of just two virtual class methods ConvertSourceToTarget and ConvertTargetToSource. The first one must be overridden. The second one is optional and only used for two-way bindings.
Note that you don’t create an instance of the converter class. Instead, it uses Delphi’s (unique) language feature of virtual class methods.
Our sample converter just prefixes the title with the text “Album: “:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
type
{ Prefix an album title with the text 'Album: ' }
TTitleToCaption = class(TgoValueConverter)
public
class function ConvertSourceToTarget(
const ASource: TgoValue): TgoValue; override;
end;

class function TTitleToCaption.ConvertSourceToTarget(
const ASource: TgoValue): TgoValue;
begin
Assert(ASource.ValueType = TgoValueType.Str);
Result := 'Album: ' + ASource.AsString;
end;
All values are encapsulated in a TgoValue type, so it can be used for integers, floating-point values, strings and some other types. The type of the source does not have to be the same as the type of the result. TgoValue is a very light-weight version of Delphi’s TValue type. See the unit Grijjy.Mvvm.Rtti for more information on how to use it.
This example also shows that the Album.Title property is bound to multiple targets (the caption of the form and the Text property of an edit box). This is perfectly legal. Be careful though with two-way bindings, since you can end up with multiple sources fighting to update a property of a target.


[/SHOWTOGROUPS]
 

emailx45

Местный
Регистрация
5 Май 2008
Сообщения
3,571
Реакции
2,439
Credits
574
MVVM Starter Kit (Part 1 of 3)
January 22, 2018 Erik van Bilsen
[SHOWTOGROUPS=4,20]
All values are encapsulated in a TgoValue type, so it can be used for integers, floating-point values, strings and some other types. The type of the source does not have to be the same as the type of the result. TgoValue is a very light-weight version of Delphi’s TValue type. See the unit Grijjy.Mvvm.Rtti for more information on how to use it.
This example also shows that the Album.Title property is bound to multiple targets (the caption of the form and the Text property of an edit box). This is perfectly legal. Be careful though with two-way bindings, since you can end up with multiple sources fighting to update a property of a target.
Binding Collections
The data binder is also used to bind entire collections to list-like views (such as TListBox or TListView). We leave the discussion of this to the 3rd (and final) part of this series.
Limitations
The TgoDataBinder class has some limitations compared to Delphi’s LiveBindings. Most notable, it does not support binding expressions: you can only bind one property to another.
As said before, you can bind to sub-properties however. Technically, these are also called expressions, but not the kind of expressions I mean here.
Fortunately, you can easily write your expressions in regular Delphi code as properties of a ViewModel. For example, a TAlbumTrack has a Duration property of type TTimeSpan. You cannot bind using an expression like 'Duration.Minutes', since TTimeSpan is record. But you can easily write a property that returns this value, as the TViewModelTracks class does:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type
TViewModelTracks = class(TgoObservable)
...
property SelectedTrackDurationMinutes: Integer
read GetSelectedTrackDurationMinutes
write SetSelectedTrackDurationMinutes;
end;

function TViewModelTracks.GetSelectedTrackDurationMinutes: Integer;
begin
if Assigned(FSelectedTrack) then
Result := FSelectedTrack.Duration.Minutes
else
Result := 0;
end;
Sometimes, you can achieve the same with a value converter. Choose what is most appropriate in your situation.
MyTunes Models
We conclude this first part with a quick tour of the models in the MyTunes sample application.
Mvvm-M

The MVVM pattern (like other MV* patterns) does not say much about the Model part. This is entirely application specific and mostly regarded as a black box containing your data and business logic. How you organize this part is entirely up to you: it may get the data from a local database, custom backend, REST API or by some other means. Or it may not use a “traditional” data source at all. For example, if you are developing an image editing application, your model may contain image data coming from a local image file.
MyTunes Sample Data
Since this mini series focuses on the MVVM pattern and not on data access, we keep things simple. The application does not use a database or REST server. Instead, it uses a small set of sample data that is embedded into the executable as a resource. I created this sample data by using the Для просмотра ссылки Войди или Зарегистрируйся to query the most popular albums in the US at the time of writing. The results are saved to a Google Protocol Buffer file (using Для просмотра ссылки Войди или Зарегистрируйся) and converted to a resource file. This makes it easy to load the sample data into memory with only a couple of lines of code.
See the Data.pas unit for a description of the serialization format and the code to load the data set.
The sample application does not provide a way to save any changes back to the “database”, since that is outside the scope of these articles.
Album and Track Models
The sample application has just two main entities: an album and a track. These have corresponding models called TAlbum and TAlbumTrack. These models are super simple. They just derive from TgoObservable and add bindable properties that use the PropertyChanged pattern we discussed earlier in this article.
In addition, the TAlbum model contains some simple “business logic” in the form of methods for adding and removing tracks.
This is all pretty straight-forward. Take a look at the code if you want more details.
Working with Bitmaps
The only property that warrants some more explanation is TAlbum.Bitmap. Every album has a cover image. In the “database”, this image is stored in raw JPEG format (through the RawImage property). However, to display the image, it must be converted to a bitmap. Delphi does not provide a framework-independent representation of a bitmap. Instead, it has separate TBitmap classes for the VCL and FMX frameworks. Of course, you cannot uses VCL or FMX dependencies in your models!
So TAlbum.Bitmap is declared as a TObject. The property getter uses the helper class TgoBitmap to load the raw (JPEG) data into a VCL or FMX bitmap, depending on framework:
1
2
3
4
5
6
7
8
9
10
11
function TAlbum.GetBitmap: TObject;
begin
if (FBitmap = nil) and (FRawImage <> nil) then
begin
FBitmap := TgoBitmap.Load(FRawImage);

{ Don't need raw image anymore. Release it. }
FRawImage := nil;
end;
Result := FBitmap;
end;
This is an example of a “lazy” property: the bitmap is only loaded the first time the property is accessed.
You may wonder if using TgoBitmap creates an implicit dependency on VCL or FMX. It does not. It uses a variable of a procedural type that is used to actually load the bitmap. The VCL and FMX specific MVVM units (which we get to in Для просмотра ссылки Войди или Зарегистрируйся) , set this variable to a framework-dependent bitmap loading routine.
Master Model
You usually have some sort of entry point to access the models in your application. These models could be global variables, so you can access the everywhere in the application. To keep things clean and OOP, I chose to create a “master” model, simply called TModel. This is a singleton object that gives you access to the models in your application:
1
2
3
4
5
6
7
8
type
TModel = class
public
...
class property Instance: TModel read GetInstance;

property Albums: TAlbums read FAlbums;
end;
In this example, it contains just a single property Albums, since the tracks our owned by their respective albums.
This class uses a singleton pattern, where you access the singleton through its Instance class property:
1
2
for Album in TModel.Instance.Albums do
...
The TModel class also contains some (private) business logic to load the sample data set and convert it to TAlbum and TAlbumTrack models.
Note that this is just one way to organize your models. The MVVM pattern doesn’t prescribe how you should do this. I chose to derive the models from TgoObservable so we can use data binding to bind to its properties. However, if you don’t want to do this (or you cannot do this), then you can still use the MVVM pattern. In that case, you will have to make bindable “property wrappers” in your ViewModels. These property wrappers than access the corresponding (non-bindable) properties of the underlying Model. You can argue that this is an even cleaner approach since it doesn’t pose any requirements on the Models. But it results in more code in your ViewModels (although not in your Views).

[/SHOWTOGROUPS]
 

emailx45

Местный
Регистрация
5 Май 2008
Сообщения
3,571
Реакции
2,439
Credits
574
Последнее редактирование: