- 1 Preface
- 2 SmartBinding comes to the rescue
- 2.1 Example – Simple property binding
- 2.2 Example – Binding to and from record data
- 2.3 Example – Binding with lists of objects
- 2.4 Example – Binding with datasets
- 2.5 Example – Binding to a list box or combo box
- 2.6 Example – Binding to a grid
- 2.7 Example – functions as binding source and/or destination
- 2.8 Example – Augmenting bindings
- 2.9 Enabling, disabling, unbinding and rebinding
- 3 Prologue
One of Delphi’s forces (and weaknesses) has been the ability to do data binding between a TDataset and specially designed dataaware controls. It made it very easy, in VCL, to quickly develop a datadriven GUI, although from purist views, the end result often ended up with a mix of view and model parts intermixed with control parts in the same unit, in one big spaghetti mashup.
Firemonkey is really cool
When Firemonkey came along, Embarcadero decided to no longer provide the TDataset/TDatasource based data binding, but instead provided a much cooler looking framework called live binding, which have additional flexibility but at the expense of a more complicated binding process. Specially in the first several versions of it, binding stuff were quite complicated, because various classes had to be instantiated, somewhat confusing properties set and all the stuff linked together by hand.
It led to lenghty explanations and posts attempting to explain the inner workings of live binding.
Later versions introduced a very nice (at least on the surface) live binding designtime editor, which made it fairly easy to link objects and properties together. Behind the scenes it however continued to rely on complex class instantiations, property settings and object hierarchies to work. But it was much nicer and easier to use…. at least for fairly simple forms with not too many controls in play.
I prefer making new apps using Firemonkey, simply because it has some design abilities that makes for a more modern looking application, and it also opens up executing those apps on various platforms, which in my belief has a life extending potential for the apps. In practice Firemonkey turned out fairly well, and gave nice sideeffects, where some platforms availability suddenly broadened the potential use cases for my applications. One application could sometimes work very well on both mobile platforms and as kiosk mode like Windows applications.
Although my workdays often exceeds 15 hours, I’m actually lazy by nature (just ask my wife!), and I absolutely despise having to make the same work twice. Hence Firemonkey came as a tool to enable my future laziness by being able to reuse with little extra effort.
Live binding problem #1
What I however discovered, was that although live binding seems beautiful on the surface, it quickly turned out to be quite ugly.
The designtime drag/drop designer became exponentially slower and slower in use, making it unbearable to use for more complex forms typical in applications where business style grid like data is to be presented in a non grid way, hence requiring separate controls and such.
The binding is still fairly easy to do at designtime, it is just very very slow reacting and it is with a larger number of controls getting unusable in practice.
Having a larger number of controls also impairs the overview in the designer, even though it does support some layer management. But it’s just slow to use.
Live binding problem #2
The problems with live binding unfortunately do not stop there.
In a program where one want to separate view from model from control, it is virtually impossible to code in a nice way that enables speedy refreshing of data, while not having live binding affecting performance greatly.
For example, if using a TDataset, doing EnableControls and DisableControls have absolutely no effect on live bound controls. If your TDataset originally contained say 3000 rows, and you want to refresh those 3000 rows from a database or an application server, then due to the way live binding works, it will cause your poor TDataset’s Post to run 9.000.000 record navigations due to Resync being called by Post, while large buffers has been allocated for TDatalink’s which are the actual glue between the TDataset and the DB live binding source classes.
It makes an otherwise speedy interface crumble to an almost complete halt by several orders of magnitude.
So how to avoid that? Well one would think DisableControls would do the trick… alas it doesn’t.
What about avoiding the Resync? Unfortunately that is not an option at all. It’s part of TDataset’s way of working.
So you could stop using TDataset’s! Yes… but TDataset’s are actually very nice containers for storing data and the history of the data, specially when using kbmMemTable (used by kbmMW) as the storage. It makes for a very easy way to figure out what to insert, update, delete etc. when the time comes to that in the data’s visual life time.
But there must be a way? Yes. I did find one….. Completely disconnect the TDataset from the TBindSourceDB before the refresh operation, and reconnect it after. The problem with that, is that you now in most cases will have to make your model/control part that handles your data access to the database or application server, aware about the existence of a TBindSourceDB, so it can be disconnected and reconnected at the right moments.
Perhaps one can “handle” this “nicely” with some callback IoC techniques, but I really don’t want to have to remember to circumvent structural design issues in code, every single time I’m going to write a new application. So it is in my opinion a mitigation of a problem, not a solution, one one have to remember to mitigate each and every time new data is going to be worked on.
Livebinding problem #3
Ok… I have to make the applications work, and the end users don’t really care how it is working behind the scenes, as long as it works and looks pretty. So I bite the bullet and mitigate problem #2. But remember there is really no nice mitigation for problem #1 without entering attempting live binding between separate TFrame and other such stuff, that even more removes focus on the actual functionality of the application, to be able to handle all the plumbing to make live binding work seamlessly.
But then enters problem #3. What if I want to refactor the visual controls? What happens with the live binding? If your controls are deleted/moved to another form, your live binding disappears, and you will have to manually remember all the bindings you made at design time, and redo them later on after the refactoring. It makes refactoring the GUI a true PITA.
But then don’t use the designer… make the binding in code! That would be a typical answer to both problem #1 and #3… and that takes us all the way back to how live binding was used from day 1…. having to remember and instantiate the correct object hierarchies, set the correct somewhat obfuscated properties … and pray. Making accessing and viewing the data much more cumbersome than I like.
I just frigging want to focus on the actual business aspects (visual, algorithms and data) of the application, not to have it drown in all sorts of stuff draining my mental capabilities, and in fact hindering my way towards delivering something stable, speedy and business wise feature rich to the customers.
Livebinding problem #4
Live binding is event oriented. That means, each and every time something sneezes in your app, it might trigger live binding code that may result in another part of your apps controls or data getting updated which in turn may trigger yet more events being fired. Most Delphi coders probably have tried the simple circular never ending event loop having one TEdit.OnChange enter data into another TEdit.Text which in turn also have an OnChange event handler, which is supposed to update the first TEdits Text property. It is an easy way to spend all your computers CPU power on one core and does not make for a nice GUI for your end users.
The idea about binding data to visual controls should IMO handle this issue, to ensure that it is easy to get your data presented, and to handle user input. Unfortunately using an event based setup, makes it very likely that you will end up in circular event trains which you again have to code your way out of to handle. Again taking time away from actually making business code.
“Well Kim, you have done nothing except complain. Do something about it then if you are so damned negative about live binding, grumpy ol’ man!’
SmartBinding comes to the rescue
And so I did. I realised about a year ago, the above issues that affects my productivity in a negative way, and started to brainstorm and prototype a bit to see if I could find some solution. At first I was thinking about improving live binding, because it is already there… but I soon came to the conclusion that the design in itself was wrong, and there would never be a fix that solved all 4 above problems.
It needed a new thinking… a new approach. After some tinkering, I had the basic structure in place for what is now called kbmMW SmartBinding and which will be included as beta code in the upcoming kbmMW release.
The design goals for SmartBinding was:
- Must be easy to use
- Must minimise or completely remove boiler plate code.
(do you see the trend here?… kbmMW has since v5 been all about making things easier!)
- Must have good performance
- Must have a low CPU, memory and size footprint
- Must not result in endless circular event trains
- Should work with all sorts of data and controls
- Should be flexible and extendable
- Must be near real time
- Must be easily refactor-able
- Should play well with kbmMW’s other features
- Should be usable even without using kbmMW’s other features
All those goals has now been achieved in the current beta version.
SmartBinding is based on a poll/update strategy rather than a strict event oriented code bound methodology. Done right it fits well with the above requirements.
Let’s see some action then!
Example – Simple property binding
We start out gently… Assume we have these controls on a form.
We want to be able to type stuff in Edit1 (TEdit) and have it reflected in Edit2 (TEdit), Button1 and Label1, and we want to be able to type stuff in Edit2 and have it reflected in Edit1 (which in turn further reflects to Button1 and Label1 as already mentioned). A simple binding case.. but fairly complex in an event driven binding context.
This is the code needed to bind:
Binding.Bind(Edit1,'Text',Label1,'Caption'); Binding.Bind(Edit1,'Text',Button1,'Caption'); Binding.Bind(Edit1,'Text',Edit2,'Text',[mwboTwoWay]);
I mean… that’s ALL that is needed to bind those controls together!
The first 2 arguments are the source instance and property name, and the next 2 are the destination instance and property name.
What about the Binding instance…where does that come from? kbmMW SmartBinding default comes with an instantiated singleton Binding:TkbmMWBindings which can be used immediately. See later for more information.
The last line also includes an optional flag, that indicates that the binding is two ways, so changing one side will automatically change the other too.
Basically all string, boolean, floating point, int64 and integer properties can be easily bound this way with auto conversion where SmartBinding make sure to automatically convert data between the different types as needed. Other types of data can be bound too, but the source and destination properties will then have to be of same type (there are ways around that too… see later).
What about thread safety? kbmMW SmartBinding maintain two pools of bindings, one where binding polling and updates can run completely async in a separate thread, and one where the binding polling and updates must run in the main GUI thread. When you bind, SmartBinding automatically recognise if you are binding to and/or from TControl‘s, and assign those bindings to the appropriate binding pool, thus ensuring correct operation.
It is important to know, that each poll and update pass will be fully done before a next one will happen. The async pool will be immeidately started and run alongside the sync pool, but the complete pass will have to finish before being considered done and next update pass will be attempted according to the scheduled interval. See prologue for more information about the interval.
Example – Binding to and from record data
kbmMW SmartBinding can also bind to regular objects or even records of data. Just make sure that the data is constantly available for as long as the binding exists. For that reason kbmMW SmartBinding also contains easy unbind and rebind features.
Continuing our example from above, lets also update a global record:
type TData = record FData1:string; end; var data:TData; ... Binding.Bind(Edit1,'Text',@data,TypeInfo(TData),'FData1'); Binding.Bind(@data,TypeInfo(TData),'FData1',EditN,'Text');
All changes to Edit1.Text is now also automatically populated to the FData1 field in the data record, and similarly all changes to the data record’s FData1 field is automatically shown in the EditN’s Text property.
Again.. what about thread safety?
The above example is obviously somewhat naive. However since both bindings are referencing a TControl descendant, the polling and update of the data record is also done in the main application/GUI thread, and thus, unless you have another thread accessing data.FData1, this will work safely.
If other threads are getting into play, you will obviously have to do regular thread data locking. You can check TkbmMWLock for one good tool to assist with that.
kbmMW SimpleBinding of course also supports object instances in any combination as source and/or destination.
Example – Binding with lists of objects
This example shows how we can bind to a list of objects. First lets declare a list with some data in it.
type TLine = class private FName:string; FAddress:string; public constructor Create(const AName:string; const AAddress:string); property Name:string read FName write FName; property Address:string read FAddress write FAddress; end; TLines = TObjectList<TLine>; var lines:TLines; ... lines:=TLines.Create; lines.Add(TLine.Create('Hans','Hansvej 1')); lines.Add(TLine.Create('Jens','Jensvej 1')); lines.Add(TLine.Create('Frederik','Frederikvej 1')); lines.Add(TLine.Create('Jonas','Jonasvej 1'));
Now let’s bind:
var bnd:IkbmMWBinding; begin Binding.Clear; bnd:=Binding.Bind(lines,'Name',Edit1,'Text'); Binding.Bind(lines,'Address',Edit2,'Text'); if bnd.Navigator<>nil then bnd.Navigator.First; end;
Here I have introduced a couple of new things, namely removing all existing bindings using Binding.Clear and navigation.
Remember if you have global variables that contains references to a IkbmMWBinding you must set those to nil before calling Binding.Clear. If not, specially when dealing with datasets as sources (see later), kbmMW will not be able to reliably track shared datasets between bindings.
Any call to Binding.Bind returns a IkbmMWBinding interface which can be used to manipulate that particular binding. One useful thing that IkbmMWBinding provides, is access to a Navigator property of type IkbmMWBindingNavigator, which in turn, gives easy access to navigate data that can be navigated… for example lists as in this case. If the binding source is not a list, the navigator is nil.
So you can do bnd.Navigator.First/Last/Next/Previous, get bookmarks to which you can return to and more.
The navigator is common and singular for all bindings to a particular source. Different navigable sources have their own navigator instances.
Example – Binding with datasets
A next logical step is to be able to bind with TDataset descendants. So let us make an example source by loading biolife.csv into a TkbmMemTable. Notice we could have chosen any TDataset descendant class.
var mt:TkbmMemTable; csv:TkbmCSVStreamFormat; begin csv:=TkbmCSVStreamFormat.Create(nil); try mt:=TkbmMemTable.Create(nil); mt.LoadFromFileViaFormat('biolife.csv',csv); finally csv.Free; end;
And let’s make the bindings to the edit controls (you may be able to guess how it’s done by now 😉 )
Binding.Clear; bnd:=Binding.Bind(mt,'Category',Edit1,'Text',[mwboTwoWay]); Binding.Bind(mt,'Species Name',Edit2,'Text',[mwboTwoWay]); if bnd.Navigator<>nil then bnd.Navigator.First;
Again exactly same way to bind. In this case we have told the bindings to be two way, essentially making Edit1 and Edit2 behave the same way as if it was the old dataware TDBEdit controls. Again we have optional access to the navigator and can easily navigate the dataset.
Example – Binding to a list box or combo box
Sometimes one want to use the source list/dataset (or selected parts of it) to populate a TList or TCombobox (or descendant).
For this example we also want to synchronise the controls so selecting in one of them is automatically reflected in the other.
Binding.Clear; Binding.Bind(mt,'Species Name',ComboBox1,'Items'); Binding.Bind(mt,'@',ComboBox1,'ItemIndex',[mwboTwoWay]); bnd:=Binding.Bind(mt,'Common_Name',ListBox2,'Items'); Binding.Bind(mt,'@',ListBox2,'ItemIndex',[mwboTwoWay]); if bnd.Navigator<>nil then bnd.Navigator.First;
First we bind one field from the dataset mt, to the items string list of the TCombobox, and we bind another field to the items string list of the TList. But what we have also done, is introducing the @ (at) indicator. When used as the source property, we ask kbmMW SmartBinding to refer to the position of the source list/dataset. So we bind the position of the source to the ItemIndex (position) of the TList and TCombobox. Further we have told SmartBinding that we want it to be two ways. Hence not only does changing the position in the source navigator update the position in the TList and TCombobox, but selecting something in the controls also automatically updates the source list/datasets position essentially in this case ensuring the two controls are in automatic sync with each other and their source.
Example – Binding to a grid
No SmartBinding without binding a grid…
In this case we bind the dataset mt from before, to columns of a grid, and as a little bit of extra spice, we also bind a TEdit to the grid’s RowCount property, so we dynamically, at runtime, can change the number of shown rows.
Binding.Clear; bnd:=Binding.Bind(mt,'Category',StringGrid1,'#1'); Binding.Bind(mt,'Species Name',StringGrid1,'#2'); Binding.Bind(mt,'@',StringGrid1,'@',[mwboTwoWay]); // Show position number Binding.Bind(mt,'@',StringGrid1,'#0'); // Bind to rowcount for easy on the fly change at runtime Binding.Bind(leRowCount,'Text',StringGrid1,'RowCount'); if bnd.Navigator<>nil then bnd.Navigator.First;
It is still the same way we bind as we are getting used to, but now a #n syntax is revealed. It simply refers to the column number (starting with 0) in the grid, that is bound to.
Using the navigator, the grid will now act almost exactly as if it was a TDBGrid. You can scroll thru the source dataset, and the grid will automatically update. Changing the selected row within the grid will also automatically update the position of the source dataset, because we twoway bound the @ of the source dataset to the @ of the grid.
Changes to the current record will in the source dataset will automatically be reflected in the grid and had we bound two ways, the data entered into the current row in the grid would be reflected back to the source.
Example – functions as binding source and/or destination
Not only objects, data or datasets can act as source or destination for a binding. Anonymous functions can also.
// Show calling function when Edit1.Text is changed. Binding.Bind(Edit1,'Text',function(const AProxy:TkbmMWBindingCustomProxy; var AValue:TValue):boolean begin Log.Debug('Got change from binding to Edit1: '+AValue.ToString); Result:=true; end);
The above example essentially works as a kind of event handler where the code in the anonymous function will be called every time a change has been detected in Edit1.Text. It will in this case log the situation using the kbmMW logging framework TkbmMWLog.
Next example is how to use a function as the source for data.
Binding.Bind(function(const AProxy:TkbmMWBindingCustomProxy; var AValue:TValue):boolean begin AValue:=Random(100); Result:=true; end,Edit1,'Text');
Now the anonymous function is repeatedly called. Every time the function returns a new changed value, Edit1.Text will be updated. How often the function is polled depends on the setting of the property UpdateFrequency of the Binding instance which we use to define the bindings with. Default the frequency is 10 times per second, but you can always change the frequency to match your liking.
The above line will change the update frequency to once per second.
Example – Augmenting bindings
What if you want to define a binding that takes a numerical value from one source, and outputs that to a label but formatted differently?
You will use the ToDestinationExpression method available on the resulting interface returned from the Binding.Bind function.
// Show calling function to populate Edit1.Text and format its look. Binding.Bind(function(const AProxy:TkbmMWBindingCustomProxy; var AValue:TValue):boolean begin AValue:=Random(100); Result:=true; end,Edit1,'Text') .ToDestinationExpression('"Hello "+data');
In this case we simply add on to the previous function binding example, and asks kbmMW SmartBinding to augment the data, on the way tot he destination according to the string expression given in the ToDestinationExpression function. This example results in Edit1.Text containing the value ‘Hello ‘ and a random number.
The string expression is quite feature rich, as it is based on the same expression handling capabilities that kbmMW is taking advantage of elsewhere, that originates from kbmMemTables capable SQL parser and evaluator. In this case, we however only support the math like expression part, not the SQL itself. But you can use all regular operations you would expect to be able to use, including many nice conversion, regular expression, math, conditional evaluation and more functions. You can read more about supported functions and how to extend the expression parser and evaluator with your own user defined functions, here (user defined functions and kbmMemSQL).
As a binding can be two way, there is a need to also be able to format or perhaps unformat the value when it is going back to the source. Hence a ToSourceExpression function also exists.
Binding.Bind(Edit1,'Text',Edit2,'Text',[mwboTwoWay]) .ToDestinationExpression('"Hello "+data') .ToSourceExpression('Mid(data,7)');
This example grabs what is in Edit1.Text and puts it into Edit2.Text with the text ‘Hello ‘ prefixed. However it also recognise changes made in Edit2.Text and moves that text to Edit1.Text after first having removed the first 6 characters of it. This type of two way binding would often make event driven binding go nuts, because of the potential endless event train happening in the TEdit control by the changes. However kbmMW SmartBinding is not affected by those events, and ensures the updates with minimum effort.
Enabling, disabling, unbinding and rebinding
Sometimes you may want to prevent a binding to do its job. If the prevention is only supposed to be temporary, then one way is to disable it.
var bnd:IkbmMWBinding; begin bnd:=Binding.Bind(....); ... bnd.Disable:=true; ... bnd.Disable:=false; end;
If you want permanently to disable it, you might as well remove it. For that purpose the Unbind methods exists.
The above will unbind Edit1 from being a source for any bindings.
And the above will unbind Edit2 from being a destination for any bindings.
You can also unbind using the IkbmMWBinding you received when issuing the Bind method.
var MyBinding:IkbmMWBinding; begin MyBinding:=Binding.Bind(....); ... Binding.Unbind(MyBinding);
If you are not binding to anonymous functions, you can also unbind using the exact same arguments as the bind
Binding.Bind(Edit1,'Text',Edit2,'Text'); ... Binding.Unbind(Edit1,'Text',Edit2,'Text');
Finally you may want to rebind. Rebind essentially makes it possible to modify a binding from using one source or destination instance to another. It is specially interesting when binding to transient records or objects
The above changes any bindings which references the record or memory buffer ‘data‘, and update those bindings to instead reference the record or memory buffer ‘data2‘.
Similarly you can rebind a control
All bindings referencing Edit1 will now instead reference NewEdit1.
As you may now have noticed, the syntax for runtime binding is consistent and simple, and makes it easy to refactor bindings when user interfaces or controls are refactored.
I did earlier on mention that in addition to the existing thread safe Binding singleton, you can choose to make your own binding manager instances. The reason to do that can include that your want different bindings to be updated at different intervals of some reason, or that you want very easy access to drop or recreate all bindings for for example a frame in one simple go without affecting all other bindings defined in other frames, and without having to explicitly unbind each of them.
var myBindingMgr:TkbmMWBindings; begin myBindingMgr:=TkbmMWBindings.Create(1000); ... myBindingMgr.Free;
The above example creates another binding manager which only polls every second. Remember to free your own created binding managers when you do not need them any longer.
There are many more ideas in my head about making binding easier and add more features to it, but this is what will be included as beta code in next full release of kbmMW Enterprise Edition.
If you like our products and posts, please share the posts with everyone you know could benefit from them!
C4D is in it for making coding applications easier, letting you focus on the business features rather than the plumbing. Why? Because I hate doing plumbing when I’m developing end user code. So I’m actually not developing all this stuff for you, but selfishly and egoistically for my self, hoping you will like it too 🙂
Oh.. whats the meaning of that featured image?
Well.. it can be interpreted many ways… Here are some
- Smartbinding coming to the rescue for those fallen off the live binding cliff
- When binding you need to be sure the bind (tether) is safe and will not cause you dangerous problems
- Make your own interpretation 🙂
47,576 total views, 4 views today