Workflow and multi binding with #D.MVVM!
In every example for MVVM, you find the three blocks diagram explaining the binding between the View, the ViewModel, and the Model.
If you dig deeper into a real-world application, you may use the 1:1:1 relation, but in most cases, you need it very differently.
1:1:1 means One View connected to one ViewModel, connected to one Model!
Let's start with the View:
Most of my views are not simple TForms but much more assembled on small subframes. The subframes are also TForms and not TFrames, because of the recurring problems with frames (in the past). But that is not the topic.
The View will be created in the composition root - or elsewhere - and then?
Every subframe must also be created and placed into the target frame of the parent view.
So far so good. Of course, this is all handled by my framework.
In some cases, every frame should have its own ViewModel. But what if not?
Well, keep this in mind and start at the other end of this chain...
We have a database where every row is just an entry of our beloved TPerson, with the normal address fields.
Part of this TPerson is a list of telephone numbers handled in a different table and perhaps also a list of bank accounts also in a different table.
As we all know - I hope - the Model is not our Database interface, the Model will get the interface to the database for local or remote storage. This could be an interface to a local database or to a RESTfull web service or something else!
Let us talk concretely for the moment:
To store a value into a database or send it as JSON you need the field-value. For the complete row and perhaps for the subtables you need a list of field-values and the field-names or at least the index position in the param list of the query.
We will name this: Place X.
Place X need the Name, Street, Zip and more... And of course, the two lists for the subtables.
To be able to get this right you have to collect the data. A good place would be the Model. Wait... The Model?
So for all the different fields and from at least 3 frames, we have to collect the data in only one Model!
To store the data, the database should do a transaction to keep the subtables in sync with the address table. This would be a nightmare if 3 Models would do this separately.
At this point, we have our View with 3 subframes (Address, Telefonlist, Bankaccountlist) and only one Model.
1(3) : ? : 1
The View is not the place to collect the data from the subframes. Should we bind the subframes to one ViewModel?
In this case, we get our example relation of 1:1:1. That would be nice, but will it fit our needs? One reason to separate our View into SubViews is the possibility to swap out a subview to a different version. To check if the subframe data is sufficient to be stored, I would like to have a simple ViewModel per frame. Let's collect:
PersonView -> PersonViewModel -> PersonModel <- Database/Rest Interface
AddressView -> AddressViewModel -> ?
TelefonView -> TelefonViewModel -> ?
BankView -> BankViewModel -> ?
The PersonViewModel has no fields (or just two buttons).
Even if we do not store data in visual controls, we must define the controls. Lets name this Place B,C,D for the three subviews, and Place A perhaps for the PersionView that only has a clear and save button.
In the PersonViewModel we have to declare a Field/Property canSave (Place A1). Inside the PersonViewModel we must be able to check the three other ViewModel so every ViewModel from the subviews should be connected to the "parent" ViewModel.
In the AddressViewModel we have to declare the Field/Properties for the Address. (Place B1) And the same for the two other ViewModels (Place C1, Place D1).
In a best-case scenario, we would skip the Models for the subviews - if not we would also have Place B2, C2 and D2.
Is MVVM slower in development because you have to type everything three times?
Normally: Yes, it is!
Don't forget we're doing MVVM to get things faster developed!
How can we archive this?
One major point for MVVM is Testability. Perhaps we are slower in the first place, but with a huge project and a lot of Models - if we have really good test coverage - we are much faster than other projects after a short period of time. Especially if you're doing TDD.
My idea is auto bind by name, this saves you a lot of time. The other idea is now to store field values on the flow from component to the final database.
Let's take a look at the full development cycle and for simplicity name it in Delphi terms:
Create a TForm for the subframes, name it TPerson.View
Create a TForm for the address, name it TPersonAddress.View
Create a TForm for the telephone numbers, name it TPersonTelephone.View
Create a TForm for the bank accounts, name it TPersonBank.View
In the composition.root: Register the View's and implement the create for the Forms.
Create the 4 ViewModels.
As the ViewModels are "just" classes derived from TViewModel, we only need to implement a property for all components on the TForms. This is the place where we would like to store our values. To speed up things, we don't create local fields as
FName : String;
we create this local field as
FName : TProperty<String>;
Auto-Binding will do the rest and connects the Name : TEdit from the Form to this "property"!
Create an empty TPerson.Model as TPersonModel = Class(TModel).
At this point - if you're doing TDD - I would create a TPerson.Model.Test.pas and also a TPerson.ViewModel.Test.pas. But this is a topic for a different blog post.
The base TViewModel class has a procedure call ViewModelSet that is called if the ViewModel is set to the View.
If this event is fired, because you set the property in the composition root like:
Result := TPersonView.Create;
LViewModel := TPersonViewModel.Create;
LViewModel.Model := TPersonModel.Create(TPersonDB.Create('local'));
Result.ViewModel := LViewModel; // here
By setting the ViewModel Property at the View, the Framework is doing the auto depending subframe load. So if everything is ready to go you have the ViewModelSet method that is called to setup the multi bind.
So we have:
1(3) : 1(3) : 1 chain!
If our MainView / MainViewModel wants to create our PersonView it just calls "Show('PersonView')" as part of the Navigation Service - Bang - everything will be created and everything is wired-up for the View and the subviews.
Now - how to transfer the Data from the View to the ViewModel to the Model to the DB and even from SubViewModels? Sounds complicated!
Let's compare RAD vs. MVVM!
In a "normal" RAD App you will have to define some kind of data structure to keep your data in memory. This could be a Class or a Record with all your field-values. In our case, there will also be some kind of TList<TBank> or TArray<TTelephone>. To get your data into your data structure you can use the live bindings, to a dataset or in the old days perhaps a DBEdit to do this the direct way. Really horrible, because while viewing the data, the database connection must be kept open or copied to a mem-table. Perhaps you have done it the old way by manual setting the values to the component and back by calling a method like:
FormToData, DataToForm where you have to set every field to the component.
EditName.Text := Data.Name;
EditStreet.Text := Data.Street;
Data.Name := EditName.Text
Data.Stree := EditStreet.Text;
Even if you use the live bindings this could get messy if you have many controls on a Form, and of course, it is horrible to maintain, because all changes are in the DFM/FMX file not so easy to merge in your repository.
Designing the Form is the same for RAD and MVVM.
Instead of creating a Class to hold your data, in MVVM, you create a ViewModel. (Perhaps some tutorial says that the data is stored in the Model, but this is not my approach). Creating the ViewModel should take the same time as creating your Memory-Class. The auto-bind will be faster the every other RAD approach.
1:0 for #D.MVVM.
Autoload of depending Frames would be 2:0 for #D.MVVM, but you could use real frames so this would not count. But setting up subframes in the composition root is easier to maintain, so let's call it even and don't count this. So we are still at 1:0.
Using the ViewModel for a Unit-Test to check if the Form works as expected: 2:0 for #D.MVVM. This should count 3:0 because Unit-Test with the RAD-Form is not really an option.
So for the design part, we are faster with #D.MVVM than with the pure RAD approach.
And this is where the problems begin: We don't want to redefine all the fields in the model and thus double our effort and lose our time advantage. We also want to save ourselves the typing work and don't want to double our source code, which we have to maintain in parallel. But in some cases, we have to convert fields to be able to write them correctly into the database. The same is with the database itself. We don't want to create a separate definition for our database by copying the field from the ViewModel.
Perhaps you use a tool to design your database or some kind of Modelmaker stuff to create your DB and Classes! From my point of view, this is not the way to go. I want to have everything in the source to keep track of it over the source repository.
How can we archive this?
Well, one idea is to use an on-the-fly created data context that is able to receive or set the property fields in the ViewModel and also convert the data-fields while sending them from the ViewModel to the Model and after that to our database interface. If we finally have a wizard in place for creating ViewModels and Models insight the IDE, it will be a piece of cake to do this. Without a wizard perhaps we have to rebuild the structure only one more time to create a universal data-structure that is able to deal with our ViewModel, our Model, and our CRUD-Database.
Without this approach let's collect our places where we have to copy all the fields. We need a copy in the Model (A2, B2, C2, D2) and in our database (at least in the SQL statement to create the DB and in every query to read or fill the values) (A3, B3, C3, D3) and as we remember Place X for the JSON.
This sounds like overkill and throws a bad light on MVVM. 4 Places to copy all fields... Horrible
That's why I do it in a different way with my #D.MVVM framework. Create your ViewModel, use the ViewModel definition, and some attributes to define an ORM-Like Model for the database and also for the Model, define some converter methods to handle or change field or field-types. That's it!
No need to define any fields in the Model: 2:0 for #D.MVVM.
Just define converter if needed: 3:0 for #D.MVVM.
Create the database with just one definition: 4:0 for #D.MVVM.
And don't forget this is VCL and FMX. The next step is a View to ViewModel wizard to convert your legacy VCL app to VCL-MVVM or FMX.
This could all be done with the pure #D.MVVM Framework. Combining this with my FDK - The Firemonkey Development Kit, that contains many units and functions that are also usable with VCL, provides the intuitive MVVM developer with a tool that can not only create even the most complicated application with numerous interfaces in a very short time but also keep it maintainable for many years.
Sounds interesting, butvit would by way more if you add some code snippets or, better yet, an exampleReplyDelete
One of my next Youtube Video will show this! In these days everything takes more time..Delete
Thanks for this great article.ReplyDelete
I faced the same problem and am happy to see I had the same approach as you.
I tend to put all business logics in the Models. Using the ViewModels only to bind data to the Views and the Models.
The dbModule is seen as a Model.
When I started to use lists, it became obvious that I would break the 1:1:1 schema as, if we take your TPerson example, the list is a set of Persons.
Then I had a Model for handling the lists and a Model for the Person entity.
MVVM slows down writing the code, for sure, but the main benefits are 1- it forces you to really be smart in constructing your code, much more than in RAD and 2- ad said, unit testing!
In the end, you become a better developer and your app will be more stable and better.
Totally convinced about that.
Better developer is one benefit the other is: You have to think twice before implementing "something". With RAD you are too quickly tempted by a double click and put in some bad code that is not testable at all.