Libraries · Patterns · Uncategorized

MVVM Starter Kit (Part 3 of 3)

In this 3rd and final part of the MVVM series we tie everything together by looking at the Views and how to bind controls and actions to their ViewModels. We show that by putting most UI logic in the ViewModel, we can easily create both an FMX and VCL version of the same application with almost no code duplication.

You may want to review part 1 (where we introduce MVVM, data binding and Models) and part 2 (where we focus on ViewModels and unit testing).

Views

When you start a new project in Delphi, you often start by designing a form. We take the opposite approach here and save the Views for last. And for a reason: thinking more in terms of Models and ViewModels makes it easier to avoid the pitfalls of putting (too much) code in the Views. Of course, in practice you will probably build out the Views and ViewModels at the same time, but it is good to take a step back and decide what code belongs in the View and what code should be part of the ViewModel.

Remember that one of the main reasons for using a pattern like MVVM is that it makes it easier to unit test UI and business logic. So ideally, you should have as little code in the Views as possible. In Delphi terms, this means your Views should only have code that manipulates VCL or FMX controls, or calls VCL or FMX APIs.

Mvvm-V

As a quick reminder: the View part is the only part of your application that can access VCL or FMX functionality. It uses the ViewModel through data binding and actions. The ViewModel contains the logic to operate the UI and uses the Model for its data and business logic.

We already talked extensively about data binding in part 1. However, we focused mostly about binding individual values. In the next section, we look at how you can bind collections of objects, and how you can bind TAction components to execute methods in the ViewModel.

Binding Collections

You have seen the following image before. It shows the data bindings of the tracks View (TViewTracks) and its ViewModel (TViewModelTracks):

Mvvm-TViewModelTracks

The Views in the sample applications won’t win any design awards (although they may qualify for an equivalent of the Golden Raspberry), but that is not the point here. They are purely for demonstrating a couple of ways to create Views and ViewModels. The designs are especially bad for mobile platforms. But the beauty of MVVM is that you can design completely different forms for a mobile experience while reusing your existing Models and ViewModels.

We briefly discussed that you can bind certain types of collections to list-like controls such as TListView.

For this to work, the collection must implement IgoNotifyCollectionChanged. You can implement this interface yourself, or you can use the TgoObservableCollection<T> class that implements it for you. This class is very similar to a generic list, but it sends notifications whenever an item is added, deleted or modified, or when the collection is cleared or rearranged somehow. The list-like controls use these notifications to update their view state.

In the MyTunes app, the tracks are stored in a TgoObservableCollection<TAlbumTrack>, and thus can be bound to a list-like control:

procedure TViewTracks.SetupView;
begin
  ...
  { Bind collections }
  Binder.BindCollection<TAlbumTrack>(ViewModel.Tracks,
    ListViewTracks, TTemplateTrack);
end;

It uses the TgoDataBinder.BindCollection<T> method to create the binding. It expects 3 arguments:

  • The source collection to bind. The source must derive from TEnumerable<T>, which many collections such as TList<T> do. You generally want to provide a collection that also implements the IgoNotifyCollectionChanged interface (such as TgoObservableCollection<T> described above).
  • The target View to bind to. This is a class that implements the IgoCollectionViewProvider interface. We haven’t discussed this interface since you generally don’t need to implement it yourself. It is implemented in controls like TListBox and TListView.
  • A template class that defines the mapping between properties of each item in the collection and the corresponding item in the View. We’ll introduce this class next.

Data Templates

When populating a list view, you must decide what data you want to show for each item in the list. In both VCL and FMX list views, each item usually has a caption and image index (in case an image list is used). In FMX list views, you often also provide a string with details. In the screenshot above, you can see that the caption of each list view item is set to the title of the track, and the details are set to the duration of the track. We don’t use image indices in this example.

To create this mapping, you derive a class from TgoDataTemplate, and pass that class as the last argument to the TgoDataBinder.BindCollection<T> method. Like the value converters discussed in part 1, this class only contains virtual class methods that you can override. You don’t have to instantiate the class.

You must always override the GetTitle method and can optionally override the GetDetail and GetImageIndex methods. All methods have a plain TObject parameter that you must typecast to the type of objects in your collection.

This is easier shown with an example. The TTemplateTrack class overrides the GetTitle and GetDetail methods to map the track name to the title and the track duration to the details:

type
  TTemplateTrack = class(TgoDataTemplate)
  public
    class function GetTitle(const AItem: TObject): String; override;
    class function GetDetail(const AItem: TObject): String; override;
  end;

class function TTemplateTrack.GetTitle(const AItem: TObject): String;
begin
  Assert(AItem is TAlbumTrack);
  Result := TAlbumTrack(AItem).Name;
end;

class function TTemplateTrack.GetDetail(const AItem: TObject): String;
var
  Track: TAlbumTrack absolute AItem;
begin
  Assert(AItem is TAlbumTrack);
  Result := Format('%d:%.2d', [Track.Duration.Minutes,
    Track.Duration.Seconds]);
end;

Binding Actions

As you know by now, most UI logic is contained in the ViewModel. In the previous part, we talked about action methods and predicates. The View calls these methods to perform the UI logic. It can do this directly, for example in response to a button press. However, you probably use a TActionList list and TAction‘s to organize the actions a user can take.

If that is the case, then you can use the data binder to bind these actions to the action methods and predicates in the ViewModel. You can either use TgoDataBinder.BindAction or TAction.Bind. Both do the same thing.

In the tracks View, we bind two actions:

procedure TViewTracks.SetupView;
begin
  ...
  { Bind actions }
  ActionAddTrack.Bind(ViewModel.AddTrack);
  ActionDeleteTrack.Bind(Self.DeleteTrack, ViewModel.HasSelectedTrack);
end;

The ActionAddTrack action is bound to the AddTrack method of the ViewModel. Similarly, the ActionDeleteTrack action is bound to a DeleteTrack method. However, this method is implemented in the View instead of the ViewModel (which I will explain in a bit). You can also see that there is an optional second parameter to the Bind method. This is a predicate that is used to determine if the action should be enabled or disabled. In the MyTunes app, it makes sense to only enable the Delete action if there is a track selected. The HasSelectedTrack predicate is implemented in the ViewModel.

UI Logic in the View

As a small side track: the reason that the DeleteTrack method is implemented in the View is that we want to ask the user for a confirmation before deleting the track. This confirmation requires a VCL or FMX dialog box, so it cannot be part of the ViewModel. The following code is FMX specific:

procedure TViewTracks.DeleteTrack;
begin
  Assert(Assigned(ViewModel.SelectedTrack));
  TDialogService.MessageDialog(
    Format('Are you sure you want to delete track "%s"?',
      [ViewModel.SelectedTrack.Name]),
    TMsgDlgType.mtConfirmation, [TMsgDlgBtn.mbYes, TMsgDlgBtn.mbNo],
    TMsgDlgBtn.mbNo, 0,
    procedure(const AResult: TModalResult)
    begin
      if (AResult = mrYes) then
        ViewModel.DeleteTrack;
    end);
end;

In the end, it calls the DeleteTrack method of the ViewModel.

However, with MVVM we want to move as much code as possible from the View to the ViewModel. That means that ideally you would want a version of MessageDialog that does not depend on the VCL or FMX. We could achieve this with the use of a procedural type (as we did for TgoBitmap in part 1). A better solution is probably to have something like a framework-independent TDialogService-like class that you can override for specific frameworks or for mocking and unit testing. I have not yet added something like this to the MVVM Starter Kit, but it would be a nice addition.

View Factory

As you may recall from the previous part, a ViewModel uses a View factory to create Views for a specific task. We showed the following TViewModelAlbum.EditTracks method:

procedure TViewModelAlbum.EditTracks;
var
  Clone: TAlbumTracks;
  ViewModel: TViewModelTracks;
  View: IgoView;
begin
  Clone := TAlbumTracks.Create;
  try
    Clone.Assign(Album.Tracks);
    ViewModel := TViewModelTracks.Create(Clone);

    { The view becomes owner of the view model }
    View := TgoViewFactory.CreateView('Tracks', nil, ViewModel);
    View.ExecuteModal(
      procedure (AModalResult: TModalResult)
      begin
        if (AModalResult = mrOk) then
          Album.SetTracks(Clone);
        Clone.DisposeOf;
      end);
  except
    Clone.DisposeOf;
    raise;
  end;
end;

Here, the ViewModel uses TgoViewFactory to create a View with the identifier ‘Tracks’. For this to work, there must be a View that is registered with the factory using this identifier.

A View here is any class that implements the IgoView interface. As we saw in the previous part, this does not have to be an actual VCL or FMX form. It can also be a mock view for the purpose of unit testing.

The Views in the MyTunes app register themselves with the factory in the initialization sections of their units. For example, the ‘Tracks’ View is registered at the end of the View.Tracks unit:

unit View.Tracks;
...

initialization
  TgoViewFactory.Register(TViewTracks, 'Tracks');

end.

IgoView and TgoFormView

As said, all Views must implement the IgoView interface, and TViewTracks is no exception. Again, you could implement this interface yourself. But it is probably easier to derive your forms from the generic TgoFormView<TVM> class, which implements this interface for you. The type parameter TVM is the type of the ViewModel used by the View. For example, TViewTracks is declared as follows:

type
  TViewTracks = class(TgoFormView<TViewModelTracks>)
     ...
  end;

The usual steps for creating a new View are:

  • Create a new (FMX or VCL) form.
  • Add the unit Grijjy.Mvvm.Views.Fmx (or Grijjy.Mvvm.Views.Vcl) to the uses clause.
  • Change the parent class of the form from TForm to TgoFormView<X>, where X is the type of the corresponding ViewModel.

Using TgoFormView as a base class has a couple of advantages:

  • It implements the IgoView interface for you.
  • It declares a property called ViewModel for you that contains the ViewModel for the View.
  • It creates a TgoDataBinder object for you, which is available through the Binder property.
  • The FireMonkey version has a GrayOutPreviousForm property. When set to True (the default), then when a form is shown modally on desktop platforms, it will gray out the previously active form to make it clear its it not accessible. The following screen shot shows an example.

Mvvm-GrayOut

Setup Data Bindings

The TgoFormView<TVM> class has a virtual method called SetupView that you should override to setup your data bindings. This method is called automatically when the form is created and its ViewModel is assigned. The complete implementation for TViewTracks looks like this:

procedure TViewTracks.SetupView;
begin
  { Bind properties }
  Binder.Bind(ViewModel, 'SelectedTrack', ListViewTracks, 'SelectedItem');
  Binder.Bind(ViewModel, 'SelectedTrack.Name', EditName, 'Text',
    TgoBindDirection.TwoWay, [TgoBindFlag.TargetTracking]);
  Binder.Bind(ViewModel, 'SelectedTrack.TrackNumber',
    SpinBoxTrackNumber, 'Value');
  Binder.Bind(ViewModel, 'SelectedTrackDurationMinutes',
    SpinBoxDurationMinutes, 'Value');
  Binder.Bind(ViewModel, 'SelectedTrackDurationSeconds',
    SpinBoxDurationSeconds, 'Value');
  Binder.Bind(ViewModel, 'SelectedTrack.Genres', MemoGenres, 'Text');
  Binder.Bind(ViewModel, 'SelectedTrack', ListBoxDetails, 'Enabled',
    TgoBindDirection.OneWay);

  { Bind collections }
  Binder.BindCollection<TAlbumTrack>(ViewModel.Tracks,
    ListViewTracks, TTemplateTrack);

  { Bind actions }
  ActionAddTrack.Bind(ViewModel.AddTrack);
  ActionDeleteTrack.Bind(Self.DeleteTrack, ViewModel.HasSelectedTrack);
end;

You should bind any properties, collections and actions in this method.

The process for the main form is a little bit different. Usually, when a form is created using a view factory, you create the ViewModel first and then create the View using the factory (refer back to the TViewModelAlbum.EditTracks code above). However, the main form is not created with a view factory. Instead, Delphi creates it for you at application startup. In that case, the form creates its own ViewModel. This is how the main form of the MyTunes app does this:

constructor TViewAlbums.Create(AOwner: TComponent);
begin
  inherited;
  ...
  InitView(TViewModelAlbums.Create, True);
end;

It creates a ViewModel inline and passes it to InitView, with the True argument indicating that the View becomes owner of the ViewModel. InitView will in turn call SetupView, which you override to setup the data bindings.

As you can see, you setup your data bindings in code. You could also create a data binding component that you can drop onto a form to create the bindings visually, as you would with Live Bindings or the DSharp framework. Personally, I prefer creating the bindings in code since imho it gives you a better insight into what is going on.

Data Binding Aware Controls

There is still one big elephant in the room. As mentioned in the first part, any object can be the target of a data binding. But to be the source of a data binding, it needs to implement the IgoNotifyPropertyChanged interface. This means that the visual controls also need to implement this interface, so that property changes can be propagated to any bound objects.

Of course, the FMX and VCL controls don’t implement this interface, so we need to create custom versions of these controls that do implement the interface. The usual way to do this is to create your own controls by deriving them from FMX or VCL controls, and implementing the IgoNotifyPropertyChanged interface. You would then put those controls into a design-time package and install that package into the IDE.

Unfortunately, it is not that simple for FMX controls. FireMonkey uses the actual name of the control classes as identifiers for the style system. This means that when you derive your own class and give it a different name, then the visual style of the control gets messed up at run-time. Although this can often be remedied by overriding the GetDefaultStyleLookupName method, there are other cases where the class name is used. For example, implementations of the IFMXDefaultPropertyValueService.GetDefaultPropertyValue method use the class name of a control to customize behavior (see TCocoaTouchMetricsServices.GetDefaultPropertyValue in the unit FMX.Platform.Metrics.iOS for an example).

Interposer Controls

So instead, we use a (dirty) trick by giving the derived control classes the exact same name as the original FMX or VCL classes. These are also called interposer classes. For example, our modified TSpinbox has the following declaration:

type
  TSpinBox = class(FMX.SpinBox.TSpinBox, IgoNotifyPropertyChanged)
  protected
    { IgoNotifyPropertyChanged }
    function GetPropertyChangedEvent: IgoPropertyChangedEvent;
    ...
  end;

We use the fully qualified name of the base class to avoid a name collision.

While being dirty, this trick has a couple of advantages:

  • You don’t have to create and install a separate package for the Delphi IDE.
  • You can use Delphi’s standard controls to layout your forms.
  • It is easier to add new controls without having to rebuild the package.
  • It is also easier to create your own versions of 3rd party controls this way.

However, this trick also has a disadvantage (besides being dirty): you must manually manage the uses clauses of your Views, as I’ll explain below.

In our MVVM Starter Kit framework, we have put all our interposer FMX controls inside the unit Grijjy.Mvvm.Controls.Fmx. We did the same for the VCL controls in the unit Grijjy.Mvvm.Controls.Vcl.

At Grijjy we mostly develop FMX apps, so I didn’t put much effort in the custom VCL controls.

However, since our interposer controls have the same names as the original controls, we must make sure that Delphi uses the correct version when running the application. You do this by ensuring that the Grijjy.Mvvm.Controls.Fmx (or Vcl) unit is listed after all other FMX (or VCL) units in the uses clause. For example, the uses clause of the Album View of the MyTunes app looks like this:

unit View.Album;

interface

uses
  System.SysUtils,
  ..
  FMX.Types,
  FMX.Controls,
  ...
  FMX.Colors,
  Grijjy.Mvvm.Controls.Fmx, // MUST be listed AFTER all other FMX.* units!
  Grijjy.Mvvm.Views.Fmx,
  ViewModel.Album;

If you don’t do this, then you will get an exception such as:

“Class X must implement IgoNotifyPropertyChanged to support data binding”

when the form is created.

You may have to revisit the uses clause as you design your form. When you put a new control onto the form, Delphi may add a new unit to the uses clauses, which breaks your custom order. I know this is not ideal, but it is the price to pay for this dirty hack. You may prefer to create a package with your custom controls and deal with the “class name” issue in a different way.

Sample Interposer Control

We show a simple example of how to implement the IgoNotifyPropertyChanged interface in an interposer control. You can use this as a template for implementing this interface in your own (custom or 3rd party) controls.

The Grijjy.Mvvm.Controls.Fmx unit already implements many interposer controls for you. You only need to read this section if you plan to create your own interposer controls.

The TSwitch interposer control sends a notification whenever the IsChecked property changes. It does so by overriding the DoSwitch method:

type
  TSwitch = class(FMX.StdCtrls.TSwitch, IgoNotifyPropertyChanged)
  private
    FOnPropertyChanged: IgoPropertyChangedEvent;
  protected
    procedure DoSwitch; override;
  protected
    { IgoNotifyPropertyChanged }
    function GetPropertyChangedEvent: IgoPropertyChangedEvent;
  end;

procedure TSwitch.DoSwitch;
begin
  if Assigned(FOnPropertyChanged) then
    FOnPropertyChanged.Invoke(Self, 'IsChecked');
  inherited;
end;

function TSwitch.GetPropertyChangedEvent: IgoPropertyChangedEvent;
begin
  if (FOnPropertyChanged = nil) then
    FOnPropertyChanged := TgoPropertyChangedEvent.Create;

  Result := FOnPropertyChanged;
end;

In general, making a control data-binding-aware requires these steps:

  • Implement the IgoNotifyPropertyChanged interface and its GetPropertyChangedEvent method.
  • The implementation of GetPropertyChangedEvent is boilerplate. You almost always use the same code as in the listing above.
  • Decide for which properties you want to send a change notification. Usually, most controls have just 1 or 2 properties that represent its main data, such as the Text of an edit control or the IsChecked property of a switch.
  • Usually, the base class has a virtual method or event that is responsible for updating these main properties, such as DoSwitch in this example. Override that method (or assign the event) and fire a notification in the implementation.

For many FireMonkey controls, the situation is a little more complicated since the main data is not stored inside the control it self, but inside a “data model class”. That data model class then has methods you need to override to send the notification. This is not too complicated. Take a look at the TEdit interposer control in Grijjy.Mvvm.Controls.Fmx for an example.

For list-like controls, you have to do a bit more work. In addition to implementing IgoNotifyPropertyChanged, you also need to implement IgoCollectionViewProvider. I will not go into the details here since this article is long enough as it is already. Look at TListBox or TListView for implementation details.

Same App, Different Views

We conclude this series by looking at how you can use the MVVM pattern to more quickly and easily create different versions of the same application.

In the MVVM Starter Kit repository, you will find a FMX and VCL version of the same application. Both share the same Models and ViewModels, and only have different Views. The Views have only a minimum amount of code however (mostly just setting up the data bindings), making it more cost effective to create different versions of the same app:

Mvvm-DifferentViews

In practice, it may not make much sense to create both a VCL and FMX version of the same app. But it often does make sense to create both a desktop and mobile version of the same app. You can use Delphi’s “View Selector” to customize the look and feel of a single form for multiple platforms, but you can only take this so far. You can have different property values for different platforms, or hide certain controls on certain platforms. You can even move controls to different places for different platforms. However, you cannot use different parents for a control for different platforms. In addition, not all platform-specific changes you make are persisted.

But more importantly, a mobile app usually demands a completely different experience than a desktop app, which is difficult to achieve by just changing some properties or hiding some controls. For example, the MyTunes app has a terrible mobile user experience (although its desktop user experience isn’t great either). In those cases, you are better off creating completely different forms for the mobile and desktop user interfaces. However, if you design your ViewModels well, then those different forms can use the same ViewModel, dramatically cutting down development and maintenance time.

Roll Your Own Pattern

As said at the very beginning of this series, our MVVM Starter Kit is just that: a Starter Kit. It is not a complete MVVM solution and you will probably need to augment or modify it to suit your needs.

If you are looking for a more complete solution (for VCL), then I suggest you take a look at DSharp.

Or maybe you prefer to create an MVVM framework using Delphi’s LiveBindings instead. In that case, I hope these articles provide some background to get started.

Or you may decide that MVVM is not for you. You may prefer MVC or MVP or a custom decoupling pattern, which is fine too.

In the end, I do hope you’ll see the value of decoupling UI from code. Whatever method you use to accomplish this is up to you.

7 thoughts on “MVVM Starter Kit (Part 3 of 3)

  1. iam sory to ask not related to this current blog but i am going to give up using delphi, if you dont mind – this blog is my favorite-. can solve my problem. my problem bring me crazy, whenever i write program always useless, very sad. why my simple program just tabcontrol inside no code at all, every time app resume from background or screen off my app (all my app including real program) frezze after restore from background. please do help

    Like

    1. I’m sorry to hear that. You should file a bug report with Embarcadero (https://quality.embarcadero.com) with details about the Delphi version you use and the platform(s) it happens on. There are currently some known issues with the latest Delphi Tokyo version and Android. If the issue only happens on Android, then there will be a fix available soon. You can also look at http://delphiworlds.com/2017/12/android-app-hang-fix/ for more information about this.

      Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s