MVVM Starter Kit (Part 2 of 3) by Erik van Bilsen
January 24, 2018 Erik van Bilsen
January 24, 2018 Erik van Bilsen
[SHOWTOGROUPS=4,20]
In this second part of our MVVM mini series we look at ViewModels and how they can be unit tested. We assume you already read the Для просмотра ссылки Войдиили Зарегистрируйся, where we talked about the MVVM pattern and data binding.
We discuss ViewModels and Views separately, leaving the discussion of Views to the Для просмотра ссылки Войдиили Зарегистрируйся. Usually, you will develop your ViewModels and Views in parallel, but it may be beneficial to think about the ViewModel before you start building the View. It helps avoid the temptation to put (too much) logic in the View.
ViewModels
A ViewModel is the middleman between the Model and the View:
A ViewModel has access to the model and can request data from it, or ask it to run business logic. Recall that the Model does not have access to the ViewModel though. The model can only indirectly update the ViewModel (or View) through data binding, which is usually initiated by the View. Likewise, the ViewModel does not have access to the View. The ViewModel contains the logic to update the state of the View, but these state changes are usually propagated via data binding. This UI logic is triggered by the View, either by calling a method of the view or by using actions (aka commands).
From a technical perspective, there isn’t that much difference between a Model and a ViewModel. Both are models: one models the business logic and the other one models the UI logic. In our Для просмотра ссылки Войдиили Зарегистрируйся repository, there isn’t even a separate base class for ViewModels. Like Models, ViewModels usually derive from TgoObservable so they can be a source for data binding. Of course, it may make sense to create your own base class for ViewModels in your applications.
ViewModels in MyTunes
We discuss a couple of different ways to model your ViewModels using the MyTunes sample application in the MvvmStarterKit repository. I suggest you open the MyTunes project group in Delphi as you follow along this article. This project group contains an FMX and VCL version of the application, as well as unit tests for the Models and ViewModels.
The MyTunes app has 3 Views and 3 corresponding ViewModels: The main view shows a list of albums and allows to user to add, delete or edit an album. A second view is used to edit the properties of an album. It also has an option to edit the tracks, which opens the third view.
View(Model) vs Form
In this example, there is a one-to-one correspondence between a View(Model) and a form. That does not have to be the case however. In more complex applications, a form can contain multiple logical views, each responsible for discrete parts of the application. For example, a form may host a tab control, where each page in the tab control provides distinct functionality.
There are multiple ways you can model this. You can use one single form (or View) with all controls for all pages in the tab control, but with multiple ViewModels: one for each page in the tab control. You can also put the contents of each page of the tab control into a separate frame (which is also a View), and create a ViewModel for each frame. You can even regard a single control (such as a list view) as a View, and have a separate ViewModel for just that control.
You will have to find some balance between flexibility and complexity when you model your application. Using more granular ViewModels may make it easier to port your application to different platforms, or to make OEM customized versions. But it increases complexity and can make it more difficult to keep track of how all parts are connected.
TViewModelTracks
We’ll start with the ViewModel for editing tracks in MyTunes. This TViewModelTracks is used by TViewTracks and provides the following bindable properties:
The ViewModel maintains a list of tracks in its Tracks property. This property is of type TEnumerable<TAlbumTrack> so you can only read its contents. It is backed by an object of type TgoObservableCollection<TAlbumTrack>. An observable collection is a collection that implements the IgoNotifyCollectionChanged interface, so that it can be bound to list-like controls, such as a TListView in the example above. It is similar to the TgoObservable class (and corresponding IgoNotifyPropertyChanged interface) we discussed in the first part, but it applies to a collection instead of a single entity. We will talk a bit more about this in the third part of this series.
The View is responsible for binding the properties of the ViewModel to the controls, as will also be shown in the Для просмотра ссылки Войдиили Зарегистрируйся. Suffice to note here that Genres, TrackNumber and Name are bound as sub-properties (eg. ‘SelectedTrack.Name’), whereas SelectedTrackDurationSeconds and SelectedTrackDurationMinutes are bound as “regular” properties. As said in the first part, these are “computed” properties that are calculated in their getter methods.
Adding and Deleting Tracks
The ViewModel provides the UI logic to add and delete tracks. This logic is provided as “bindable actions”. These are regular methods that the View can bind to using TAction components (as we will show in the next part). There are two types of bindable methods:
The implementation of these methods is pretty trivial:
The AddTrack and DeleteTrack methods also call the property setter for the SelectedTrack property. This will fire a couple PropertyChanged notifications, which the View will respond to by updating the selected item in the list view and the controls on the right side of the form:
As you can see, you must also send notifications for any computed properties that depend and the newly selected track. Sub-properties (such as ‘SelectedTrack.Name’) are handled automatically.
[/SHOWTOGROUPS]
In this second part of our MVVM mini series we look at ViewModels and how they can be unit tested. We assume you already read the Для просмотра ссылки Войди
We discuss ViewModels and Views separately, leaving the discussion of Views to the Для просмотра ссылки Войди
ViewModels
A ViewModel is the middleman between the Model and the View:
A ViewModel has access to the model and can request data from it, or ask it to run business logic. Recall that the Model does not have access to the ViewModel though. The model can only indirectly update the ViewModel (or View) through data binding, which is usually initiated by the View. Likewise, the ViewModel does not have access to the View. The ViewModel contains the logic to update the state of the View, but these state changes are usually propagated via data binding. This UI logic is triggered by the View, either by calling a method of the view or by using actions (aka commands).
From a technical perspective, there isn’t that much difference between a Model and a ViewModel. Both are models: one models the business logic and the other one models the UI logic. In our Для просмотра ссылки Войди
ViewModels in MyTunes
We discuss a couple of different ways to model your ViewModels using the MyTunes sample application in the MvvmStarterKit repository. I suggest you open the MyTunes project group in Delphi as you follow along this article. This project group contains an FMX and VCL version of the application, as well as unit tests for the Models and ViewModels.
The MyTunes app has 3 Views and 3 corresponding ViewModels: The main view shows a list of albums and allows to user to add, delete or edit an album. A second view is used to edit the properties of an album. It also has an option to edit the tracks, which opens the third view.
View(Model) vs Form
In this example, there is a one-to-one correspondence between a View(Model) and a form. That does not have to be the case however. In more complex applications, a form can contain multiple logical views, each responsible for discrete parts of the application. For example, a form may host a tab control, where each page in the tab control provides distinct functionality.
There are multiple ways you can model this. You can use one single form (or View) with all controls for all pages in the tab control, but with multiple ViewModels: one for each page in the tab control. You can also put the contents of each page of the tab control into a separate frame (which is also a View), and create a ViewModel for each frame. You can even regard a single control (such as a list view) as a View, and have a separate ViewModel for just that control.
You will have to find some balance between flexibility and complexity when you model your application. Using more granular ViewModels may make it easier to port your application to different platforms, or to make OEM customized versions. But it increases complexity and can make it more difficult to keep track of how all parts are connected.
TViewModelTracks
We’ll start with the ViewModel for editing tracks in MyTunes. This TViewModelTracks is used by TViewTracks and provides the following bindable properties:
The ViewModel maintains a list of tracks in its Tracks property. This property is of type TEnumerable<TAlbumTrack> so you can only read its contents. It is backed by an object of type TgoObservableCollection<TAlbumTrack>. An observable collection is a collection that implements the IgoNotifyCollectionChanged interface, so that it can be bound to list-like controls, such as a TListView in the example above. It is similar to the TgoObservable class (and corresponding IgoNotifyPropertyChanged interface) we discussed in the first part, but it applies to a collection instead of a single entity. We will talk a bit more about this in the third part of this series.
The View is responsible for binding the properties of the ViewModel to the controls, as will also be shown in the Для просмотра ссылки Войди
Adding and Deleting Tracks
The ViewModel provides the UI logic to add and delete tracks. This logic is provided as “bindable actions”. These are regular methods that the View can bind to using TAction components (as we will show in the next part). There are two types of bindable methods:
- Procedures without parameters. These execute the action.
- Functions without parameters, returning a Boolean. These are predicates that indicate if an action can be executed.
The implementation of these methods is pretty trivial:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | procedure TViewModelTracks.AddTrack; var Track: TAlbumTrack; begin Track := TAlbumTrack.Create; FTracks.Add(Track); SetSelectedTrack(Track); end; procedure TViewModelTracks.DeleteTrack; begin Assert(Assigned(FSelectedTrack)); FTracks.Remove(FSelectedTrack); SetSelectedTrack(nil); end; function TViewModelTracks.HasSelectedTrack: Boolean; begin Result := Assigned(FSelectedTrack); end; |
1 2 3 4 5 6 7 8 9 10 | procedure TViewModelTracks.SetSelectedTrack(const Value: TAlbumTrack); begin if (Value <> FSelectedTrack) then begin FSelectedTrack := Value; PropertyChanged('SelectedTrack'); PropertyChanged('SelectedTrackDurationMinutes'); PropertyChanged('SelectedTrackDurationSeconds'); end; end; |
[/SHOWTOGROUPS]