Contents
Preface
In a previous post, I introduced SmartBinding as a new very easy to use and intelligent binding framework for Delphi. The introduction included binding objects, lists, general data and visual controls, and how to use navigators, all from code.
This post will focus on new SmartBinding features (SmartBinding v2) included in the next kbmMW release, expected shortly.
Binding with one line of code
What makes kbmMW’s “Smart” features smart, is that they focus on removing all the boilerplate code, making it easy to focus on the actual functional requirements. Doing SmartBinding by code is very easy… but it can be made even easier, which the following demo will show.
I have made a simple form, containing a TLabel, a TButton and two TEdits.
Let’s try to run it:
When I type in Edit3 (the top edit box), the remaining controls’s captions and texts are automatically updated. Thats not really new… I already showed this in the previous SmartBinding blog post.. didn’t I?
Well… yes… and no…. In the previous blog post there were a number of Binding.Bind… lines to bind the controls together.
In this example there is only one single line of code in the OnCreate event of the TForm:
Binding.AutoBind(self);
How is that possible? kbmMW SmartBinding is now able to check string properties of objects and controls, for binding instructions.
One binding instruction can be seen clearly in the label,s Caption property: {Edit3.Text}
Simply by having that text there, the label will be bound to receive its data from the Text property of the control Edit3. The button is bound the same way to Edit3. The second TEdit is also bound to Edit3, but in this case via a two way binding: {Edit3.Text, twoway:true}
Hence it is now visually easy to see where controls are getting at least their visual data from.
Multibinding
The problem with this approach, is that it seems not possible to bind non string type properties. But fortunately there is a nice solution to that too. You are not limited to just binding to the string property itself, you can bind anything via the string property.
That brings us to page two in the demo app.
This shows how to specify multiple bindings in one string property. The Caption property of the TButton control, now contains an array of 2 bindings, one where Edit1.Text is bound to Label2.Caption, and one where Edit1.Text is bound to Label1.Caption. Typing in Edit1 will update the two labels as expected.
You may notice that the button is not really end user friendly now, because we (mis)used its Caption to contain the binding array. This is easily solvable by one of these methods:
- Dont use the TButton.Caption for the binding, but instead use some other string property that is not visible at runtime
- Bind the TButton.Caption to something that produce what you will want to see
- Use the value syntax
The value syntax is simply adding a binding with a value:
[{value:"Button"},{bind:Edit1.Text, to:Label2.Caption}, { bind: Edit1.Text, to: Label1.Caption}]
Running it now it will look like this:
The button now have a nice label. We have used a constant string as the value, but we could also have used a reference to a configuration entry. See the blogs about kbmMW’s Configuration manager.
It is also possible to specify to destination and to source expressions by adding exprToSrc:”expression” and/or exprToDest:”expression” string properties to the relevant binding section. Further a binding can be set to be default disabled by setting disabled:true.
Binding and data separation
Wouldn’t it be nice if you were able to design your GUI even before you have any real data to put into it? In other words to be able to develop live mockups that can be transformed into actual applications at a flick of a switch?
That’s where binding/data separation comes into play, which brings us to next page of the demo.
On this page, there are 8 labels, of which 7 looks like they have some sort of funny looking binding on them.
I have, by using the @ syntax, told kbmMW SmartBinding that it should bind with a data placeholder named test.
Lets run it:
What happens? In a live system you will see that the bindings getting data from @test.Value2 and @test.Value3 is populated with static data, while the remaining bindings produce seemingly random data. This is an example of data from kbmMW’s SmartBinding data generator.
Somewhere before calling AutoBind we must define data placeholders that we may be using.
var data:IkbmMWBindingData; begin data:=TkbmMWBindingDataGenerator.Create('{'+ ' value1:10,'+ ' value2:"abc",'+ ' value3:88.4,'+ ' value4: &random {'+ ' type:number,'+ ' random:true,'+ ' min:10000,'+ ' max:100000,'+ ' step:5,'+ ' interval:250'+ ' },'+ ' value5: *random,'+ ' value6: *random,'+ ' value7: *random,'+ ' value8: {'+ ' type: values,'+ ' random: true,'+ ' values: ["AAA","BBB","CCC","DDD","EEE"]'+ ' }'+ '}'); Binding.DefineData('test',data); Binding.AutoBind(self); end;
By calling DefineData, we setup a link between the name ‘test’ and something that will produce and accept data to and from the bindings, which in this case is an instance of TkbmMWBindingDataGenerator.
It accepts a string, containing simplified YAML describing the various named properties this generator should produce data for. Each of the named properties can either be a constant value, like value1, value2 and value3, or an object which describes how data for that particular named property is generated.
Looking at value4 we can see that its an object (it starts and ends with curly braces), and that it contains a number of properties, which define the data generation.
type can currently be one of:
- simple
This is simply static data. Any named property that has only static data associated with it is automatically understood as type simple. - number
This is a generator that will return a number of some sort. Other properties define how. - values
This is a generator that will return one of many defined values. Other properties define how.
If its number then these additional properties can be used:
- random:true/false
If true will generate a random value between min and max, rounded to the nearest step.
If false will generate an incremental value starting at min (or value if given) and incrementing with step on each cycle. - min:n
n is a numerical value. Sets lower boundary for numerical values returned by this generator. - max:n
n is a numerical value. Sets upper boundary for numerical values returned by this generator. - step:n
n is a numerical value which indicates the step or rounding to be done depending on if its a random or incremental value. - interval:n
n is a numerical value in msecs indicating how often the value should change, interval:250 means that it will at most change 4 times a second. - decimals:n
Defines the number of decimals after the . to return. The generator will produce an integer value, and divide it with 10^n to produce the decimals. Default 0. - wrap:true/false
If true and its not a random value, then the generator will automatically wrap around when the boundaries are reached.
If false, the value reached at the boundary will become static. - reverse:true/false
If true, it will return data in reverse order. - value😡
x is the initial value to return.
If its values, then these additional properties can be used:
- random:true/false
If true will pick a value at random.
If false will pick next value. - step:n
n is a numerical value which indicates the step or rounding to be done depending on if its a random or incremental value. - wrap:true/false
If true and its not a random value, then the generator will automatically wrap around when the boundaries of the values array are reached.
If false, the value reached at the boundary will become static. - reverse:true/false
If true, it will return data in reverse order. - interval:n
n is a numerical value in msecs indicating how often the value should change, interval:250 means that it will at most change 4 times a second. - values:[…,…,….]
An array of values that can be returned for this named property.
kbmMW supports adding other custom data generator types.
One special syntax we see with the named property value4, is the YAML anchor (&random). When defined, other named property values can refer to the same definition by using the *random syntax. Thats the reason why value5,6 and 7 all also return random values.
So now we understand how to define data generators. But how to replace the data generator with true data when it is available in the development process?
It is simple. Eg.
type TTest = class private FVal1:kbmMWNullable<integer>; FVal2:kbmMWNullable<string>; FVal3:kbmMWNullable<double>; FVal4:kbmMWNullable<double>; FVal5:kbmMWNullable<double>; FVal6:kbmMWNullable<double>; FVal7:kbmMWNullable<integer>; FVal8:kbmMWNullable<string>; public constructor Create; property Value1:kbmMWNullable<integer> read FVal1 write FVal1; property Value2:kbmMWNullable<string> read FVal2 write FVal2; property Value3:kbmMWNullable<double> read FVal3 write FVal3; property Value4:kbmMWNullable<double> read FVal4 write FVal4; property Value5:kbmMWNullable<double> read FVal5 write FVal5; property Value6:kbmMWNullable<double> read FVal6 write FVal6; property Value7:kbmMWNullable<integer> read FVal7 write FVal7; property Value8:kbmMWNullable<string> read FVal8 write FVal8; end; ... constructor TTest.Create; begin inherited; FVal1:=21; FVal2:='TESTING'; FVal3:=11.1; FVal4:=22.2; FVal5:=33.3; FVal6:=44.4; FVal7:=55; FVal8:='TESTING more!'; end; ... begin testdata:=TTest.Create; Binding.DefineData('test',testdata); end;
When this code is called, we, on the fly, replace the data generator with data from the object testdata. Hence clicking on the “Redefine test data’ button will immediately result in this:
Hence we have now shown a true separation of concerns, which makes it possible to independently design the GUI and control code, and possibly as a separate task by another person, develop the data layer.
That brings us to the last page of the demo.
This is also a binding separation demo.
In this case we have defined a list of objects as our initial binding data:
TLine = class private FVal1:kbmMWNullable<string>; FVal2:kbmMWNullable<integer>; FVal3:kbmMWNullable<double>; public property Val1:kbmMWNullable<string> read FVal1 write FVal1; property Val2:kbmMWNullable<integer> read FVal2 write FVal2; property Val3:kbmMWNullable<double> read FVal3 write FVal3; end; TLines = TObjectList<TLine>; ... FLines:=TLines.Create; ... var line1,line2,line3:TLine; begin Flines:=TLines.Create; line1:=TLine.Create; line2:=TLine.Create; line3:=TLine.Create; line1.Val1:='Hej 1'; line1.Val2:=1; line1.Val3:=1.1; line2.Val1:='Hej 2'; // line2.Val2.IsNull:=true; line2.Val3:=2.2; line3.Val1:='Hej 3'; line3.Val2:=3; line3.Val3:=3.3; Flines.Add(line1); Flines.Add(line2); Flines.Add(line3); Binding.DefineData('test2',FLines); end;
And we have (in code this time for the fun of it) bound the FLines data to the visual controls:
Binding.Bind(Flines,'Val1',edVal1,'Text',[mwboTwoWay]).Navigator; Binding.Bind(Flines,'Val2',edVal2,'Text',[mwboTwoWay]); Binding.Bind(Flines,'Val3',edVal3,'Text',[mwboTwoWay]);
The Next and Prev buttons contain this code:
// The Prev buttons eventhandler: var nav:IkbmMWBindingNavigator; begin nav:=Binding.GetDataNavigator('test2'); if nav<>nil then nav.Previous; end; // The Next buttons eventhandler: var nav:IkbmMWBindingNavigator; begin nav:=Binding.GetDataNavigator('test2'); if nav<>nil then nav.Next; end;
This code is slightly different from what I showed in the first blog post in the sense that I do not store the binding and its navigator for later use, instead I ask for the navigator when I need it, based on the name of the defined binding data.
Running it, it will act as we are used to:
We can scroll thru the values with the Prev and Next buttons.
However…. what if we now want to replace this mockup list with true data from a database?
For the demo, we simulate data from a database with the contents of a memory table.
mt:=TkbmMemTable.Create(nil); mt.FieldDefs.Add('Val1',ftString,30); mt.FieldDefs.Add('Val2',ftInteger); mt.FieldDefs.Add('Val3',ftFloat); mt.FieldDefs.Add('Val4',ftString,30); mt.CreateTable; mt.Open; mt.AppendRecord(['value 1',1,111.1,'value 1_2']); mt.AppendRecord(['value 2',2,222.2,'value 2_2']); mt.AppendRecord(['value 3',3,333.3,'value 3_2']); mt.AppendRecord(['value 4',4,444.4,'value 4_2']);
Now we just need that data “switched in” instead of our TLines based data. The event handler of the “Redefine test2 data” looks like this:
Binding.DefineData('test2',mt);
Clicking it, the screen will immediately look like this:
All bindings that previously was linked to the FLines instance, has now been relinked to the memory table instance, and we can continue to click Prev and Next to scroll thru the data.
What else?
In addition to the above autobinding features which also supports fetching data from the configuration framework using the $(configpath) syntax that is supported many other places in kbmMW, ,SmartBinding v2 also includes:
- Support for kbmMWNullable types
- GroupedBy(..), NamedBy(..) binding methods, which can be used for identifying and manipulating bindings. In string based autobinding, provide the group as group:”groupname” and name as name:”bindingname”. There can be multiple bindings with same name and group.
- Custom binding data generator registration via the public singleton BindingGeneratorRegistrations
- Improved detection of focused control to handle two way binding while typing, more gracefully.
- procedure UnbindBindings(const ABindings:TList<IkbmMWBinding>);
Unbind a list of bindings in one go - procedure EnableBindings(const ABindings:TList<IkbmMWBinding>; const AEnable:boolean);
Enable or disable a list of bindings in one go - procedure UnbindByGroup(const AGroup:string);
Unbind bindings based on group name. - procedure UnbindByName(const AName:string);
Unbind bindings based on binding name. - procedure EnableByGroup(const AGroup:string; const AEnable:boolean);
Enable/disable bindings based on group name. - procedure EnableByName(const AName:string; const AEnable:boolean);
Enable/disable bindings based on binding name. - function BindingsByGroup(const AGroup:string):TList<IkbmMWBinding>;
Return a list of bindings based on group name. - function BindingsByName(const AName:string):TList<IkbmMWBinding>;
Return a list of bindings based on binding name.
Epilogue
With the easy support of binding and data separation, it makes it much easier to do functional mockups (both of data providing servers and GUI applications), which can be converted into production code by a simple flick of a switch.
It can be used in many ways. One way could be to be able to auto translate all texts on a GUI by either binding to a simple static set of texts coming from a generator, or by creating a new custom translation generator, where you select the current language, and thus what is returned to be displayed and how (sizes and placement of controls can be bound same way).
Autobinding reduces boiler plate coding to an absolute minimum, while still living up to the requirement of easy refactoring of the GUI, simply because the binding definitions in most cases will follow the specific control, as long as the binding is defined in one of the controls string properties. In the cases where the binding is defined in other controls (visual or not) string properties, you will at least not loose the binding definition by simply refactoring the controls.
It opens up for easy access to load and save bindings from/to files, to handle very late binding, which can be used for very dynamic application customer specialization upon installation rather than on compilation.
/kbm C4D
Hej igen,
Two more things:
1) Now you’re talking about a “DisableD” property, so please disregard my comment about that on the previous post in this series. Maybe that was also just a tyop. 🙂
2) Am I right in surmising that if one wants to use Smart Bindings with ordinary Controls but can’t decide which property to put the binding string into, one can just subclass any TControl and add one’s own String property, calling it “BindString” or something (or whatever one likes, since apparently the Binder uses RTTI to go through all String properties looking for validly formatted binding strings)?
2) Yup… any string property can be used, also your own.