This is where the MVVM pattern has its benefits. Of course in WPF this pattern can shine because you have XAML but also in Delphi this pattern is worth a closer look. This pattern is also known as Presentation Model. The main benefit over MVC or MVP is that view and viewmodel have no dependency on each other. You can design the GUI independently from the business logic and just bind them together later. And there is the keyword that makes this pattern work: binding.
There are several amazing frameworks for MVVM development in .Net like Caliburn Micro or Prism. Caliburn consists of several different pieces to easily build applications the MVVM way. One of them is the ModelViewBinder. It takes the view (like a form or a frame) and a viewmodel (like a datamodule or another business object) and creates bindings to connect these two using naming conventions. For example the Lastname property of the viewmodel is bound to the Lastname edit on the view, the Orders property which may be a list gets bound to a listview called Orders. You could also bind the Customer.Address.Street property to the Customer_Address_Street edit on the view. All this gets powered by a DI container that puts all the pieces together, also by convention over configuration. If you have a viewmodel called CustomerDetailsViewModel there should be a CustomerDetailsView (there are actually several different conventions and you can add more if you like).
DSharp presentation model makes it possible to use the powerful spring DI container and data bindings to easily build applications that are easy to test and to develop. It sure is just scratching the surface yet but when you take a look at the ContactManager sample in the svn repository you can get a basic idea of what is possible.
Enough with just talking theory. Let's create a simple application!
Step 1 - The View
After creating a new VCL application (FMX is pretty similar - there are just not that many controls supported out of the box yet) we add some controls on the form so it looks like this:
After saving the unit the source looks like this:
unit CalculatorViewForm; interface uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, DSharp.Bindings, DSharp.Bindings.VCLControls; type TCalculatorView = class(TForm) BindingGroup1: TBindingGroup; CalcOperator: TComboBox; CalcResult: TEdit; Calculate: TButton; Label1: TLabel; LeftOperand: TEdit; RightOperand: TEdit; Error: TEdit; end; implementation {$R *.dfm} initialization TCalculatorView.ClassName; end.
The only thing you actually have to write there after adding the components including the binding group is adding the unit DSharp.Bindings.VCLControls.pas (only if you don't have DSharp bindings VCL designtime package installed which inserts that unit automatically) and the line in the initialization part of the unit. This is necessary so the linker does not remove this class because it actually is not referenced anywhere (similar to the RegisterComponent you do if you are working with the DI container in the classic way).
Step 2 - The ViewModel
Now we create the viewmodel - the class that actually does the work and holds all the states of for the UI.
For our simple calculator we just need some fields and a method:
unit CalculatorViewModel; interface uses CalculatorInterfaces, DSharp.PresentationModel.ViewModelBase, SysUtils; type TCalcOperator = (Add, Subtract, Multiply, Divide); TCalculatorViewModel = class(TViewModelBase, ICalculatorViewModel) private FLeftOperand: Double; FRightOperand: Double; FCalcOperator: TCalcOperator; FCalcResult: Double; FError: string; public procedure Calculate; property LeftOperand: Double read FLeftOperand write FLeftOperand; property RightOperand: Double read FRightOperand write FRightOperand; property CalcOperator: TCalcOperator read FCalcOperator write FCalcOperator; property CalcResult: Double read FCalcResult write FCalcResult; property Error: string read FError write FError; end; implementation { TCalculatorViewModel } procedure TCalculatorViewModel.Calculate; begin try case FCalcOperator of Add: FCalcResult := FLeftOperand + FRightOperand; Subtract: FCalcResult := FLeftOperand - FRightOperand; Multiply: FCalcResult := FLeftOperand * FRightOperand; Divide: FCalcResult := FLeftOperand / FRightOperand; end; FError := ''; except on E: Exception do begin FError := E.Message; FCalcResult := 0; end; end; DoPropertyChanged('CalcResult'); DoPropertyChanged('Error'); end; initialization TCalculatorViewModel.ClassName; end.
We inherit from the TViewModelBase class which has some mechanics built-in that we need for the whole thing to work (like inheriting from TComponent which is necessary for the lifetime management and implementing several interfaces the framework needs). If you are just interested in creating something similar to the passive view and constructing the classes yourself and just using DSharp bindings you can just inherit from TPropertyChangedBase or some other class and wire things up yourself (or use the ViewModelBinder to do that without the rest of the framework).
We actually have no setters for the properties in this viewmodel because the changes just happen when you click the calculate button. Did you notice we named the properties exactly like the controls? That is not by accident. As I told you earlier the ViewModelBinder looks for components and properties it can bind together and it does it by their names.
One thing here is not obvious. We added the 4 operators to the items of the combobox earlier and we named them exactly the same (not the classic hungarian notation for enums here though). This is because the ViewModelBinder binds to the Text property of a TComboBox (you can change that to ItemIndex if you like - just edit the line in DSharp.PresentationModel.VCLConventionManager.pas). Then you can name the enums and the items differently. This may also make more sense when you localize the items in the combobox but it depends on how the combobox is set up. In the future the ViewModelBinder might consider the options of the combobox.
The calculation method is actually pretty simple. At the end it sends the notifications of the changed properties: CalcResult and Error. Keep in mind that this example does not show any kind of validations. You can still write rubbish into the edits. Since internally a conversion is done from string to double (bindings have a default value converter unless you specify one yourself) the value is not sent to the viewmodel if the converter cannot convert it into a double. How validations can be done and shown in the UI can be seen in the Validations sample.
Now let's take a look at the last unit of this example:
unit CalculatorInterfaces; interface uses DSharp.ComponentModel.Composition; type [InheritedExport] ICalculatorViewModel = interface ['{03AF7AE1-CCDF-4F06-9074-919F6C759DBE}'] end; implementation end.
The InheritedExport attribute tells the DI container to register every class it finds that implements this interface. That is why we had to add the line in the initialization part of the viewmodel unit. Because that class also is referenced nowhere and the linker would just throw it out. Why not doing the registration of the class there instead? That would actually create a dependency on the DI container and you have no chance to remove that registration for unit testing without code changes.
Remember to add a guid to the interface, otherwise the DI container will complain.
But wait - that interface does not have any methods. Yes, for our example it actually does not need them. Because bindings currently can only work between objects the interface is cast to the implementing object behind the scenes. Keep in mind that you cannot do interface delegation and binding to such interfaces. Also in our example we don't call anything on that interface because the Calculate method (of the object) is bound to the button. For a more complex scenario and actually using the interface methods look at the ContactManager example.
Step 3 - Putting the pieces together and starting up
Let's take a look at the dpr file now:
program Calculator; uses Forms, CalculatorViewForm in 'CalculatorViewForm.pas' {CalculatorView}, CalculatorViewModel in 'CalculatorViewModel.pas', CalculatorInterfaces in 'CalculatorInterfaces.pas', DSharp.PresentationModel.VCLApplication; {$R *.res} begin Application.Initialize; Application.Start<ICalculatorViewModel>; end.
We have to make some modifications here. By adding the DSharp.PresentationModel.VCLApplication (or FMXApplication) unit we add the Start<T> method to TApplication. That is the method that starts it all up as the name implies. You have to specify the root viewmodel of your application. From there on you can build everything you like - manually or using the presentation model. We removed the other methods except Application.Initialize. I have to admit that I am not totally happy with that solution right now because it kind of breaks something in the IDE regarding editing the project options (only things like Title that result in the IDE editing the dpr file). Everything else still works, no worry. The Initialize call has to stay there because removing it would actually remove the theming from the application (another weird thing related to the source of the dpr file).
Let's start the application and look if it works (if you don't want to do all the steps by yourself you can find the source in the repository as always). For now I am not going into the implementation details - I leave that for a future post to explain how all the different pieces are working together to make it a bit clearer and less "magic".
DSharp presentation model is available for Delphi 2010 (without aspects) and higher (working on 64bit for XE2) - if you experience any problems feel free to send me an email or file an issue on the project page - I usually test it on all platforms but sometimes some of the nasty compiler or rtl bugs may break something.
Stefan,
ReplyDeletewill DSharp work with D2009, too, or is this a no-go?
Michael
Unfortunately no, sorry. That is because it's using the enhanced RTTI introduced in Delphi 2010. Also generics which are also heavily used are even more broken in 2009 than in 2010.
ReplyDeleteAs I said while ago, parts of DSharp like the bindings could be modified to use the old RTTI to only work with published properties for example. But most of the stuff somewhere makes use of TValue for example which is part of the enhanced RTTI.
I was thinking about this a bit and I will look if I at least can make the bindings available for earlier versions of Delphi (maybe Lazarus too?).
ReplyDeleteHow would you like that?
Stefan, My name is Paulo and I'm Editor of Clube Delphi Magazine, here in Brasil. I'm writing an article about your project and I would like to ask permission for reproducing this demo on magazine. DSharp is fantastic!
ReplyDeleteHi Paulo,
Deletethanks for your kind words. Of course you can do that. If you have any questions feel free to contact me by mail. Too bad I don't understand Portuguese. It seems there is a great delphi community in your country.
I also can announce that there is some pretty huge improvements to the presentation model in the labs.
This is Paulo again. Stefan, you are right. Here in Brazil there is a huge Delphi community. About improvements, I would like to know about them also, so I could write in future, a "second part" of my article, showing the improvements :)
DeleteAgain, thank you for your permission.