The most pramatic way to describe databinding is that properties of one object or resource are linked to those of another object or resource, typically in a "push" fashion; that is, once a property value changes, those other properties that are databound to it auto-magically have their values updated as well. One of the many things that the Atlas framework provides is that of databinding of one control on the web page to another. I got curious....how does it work?
A less pragmatic way of laying out databinding is the following:
"Object A's property value P1 is computed by method m, which uses the state of object B's property P2 when event E occurs".
ASP.Net "Atlas" supports databinding by utilitizing a binding object that is instantiated and assigned to the control whose values are to be bound (the "binder", not the "bindee"); the various properties of the binding object declare what properties are bound, what event they are to be updated by, and what translation of the data is to happen before assignment (transference) occurs. Therefore, the binding object holds onto P1, P2, m (and its parameters), and B from the above statement. When the binding object is handed to the control whose state is to be bound, then A is assigned to the binding object. Clear as mud? Here's an example:
// Create the binding on the textbox
var binding_1 = new Sys.Binding();
binding_1.set_dataContext(checkBoxBoolean);
binding_1.set_dataPath('checked');
binding_1.set_property('text');
binding_1.set_transformerArgument("Checkbox is {0}.");
binding_1.set_direction(Sys.BindingDirection.In);
The code above is taken from one of the quick-start samples provided by Microsoft and it shows the programmatic way (non-declarative way) of creating a binding object (whose type is Sys.Binding()). Note the following while taking into consideration the quoted statement from before:
1) dataContext := Object B
2) dataPath := P2
3) property := P1
4) transformerArgument := m's parameters.
The direction (set_direction()) simply states who gets and who gives in the binding relationship (who is "A" and who is "B").
What's missing, then, is the "translator" or the method "m" from above:
// This is the built-in transform ToString
binding_1.transform.add(Sys.BindingBase.Transformers.ToString);
And then the assigment of the databinding object to object "A":
// Add the bindings to the controls
textBox.get_bindings().add(binding_1);
By looking at the client source, I know that all that's happened so far is pure assignment of member variables within the newly instantiated binding object; literally no algorithms have ran and nothing has been "hooked up", so-to-speak. Nothing more happens until the sink object (object "A" in our example) has its initialize method called. The source for .initiliaze() looks like this:
this.initialize = function() {
Sys.UI.TextBox.callBaseMethod(this, 'initialize');
_text = this.element.value;
_changeHandler = Function.createDelegate(this, this._onChanged);
this.element.attachEvent('onchange', _changeHandler);
_keyPressHandler = Function.createDelegate(this, this._onKeyPress);
this.element.attachEvent('onkeypress', _keyPressHandler);
}
I intentionally highlighted the callBaseMethod() method, as it calls into the base method for the Sys.UI.Textbox type:
this.initialize = function() {
if (_bindings) {
for (var i = 0; i < _bindings.length; i++) {
_bindings[i].initialize(this);
}
}
_initialized = true;
}
And this is where the magic happens. Note how each member of the binding collection has their initialize() method called, passing in the 'this' pointer (the assignment of object "A" from earlier). Inspecting the .initialize() method of the binding type:
this.initialize = function(target) {
Sys.Binding.callBaseMethod(this, 'initialize', [ target ]);
if (this.get_automatic()) {
if ((_direction != Sys.BindingDirection.In) &&
Sys.INotifyPropertyChanged.isImplementedBy(target)) {
_targetNotificationHandler = Function.createDelegate(this, this._onTargetPropertyChanged);
target.propertyChanged.add(_targetNotificationHandler);
}
if (_direction != Sys.BindingDirection.Out) {
var source = this._getSource();
if (Sys.INotifyPropertyChanged.isImplementedBy(source)) {
_sourceNotificationHandler = Function.createDelegate(this, this._onSourcePropertyChanged);
source.propertyChanged.add(_sourceNotificationHandler);
}
this.evaluate(Sys.BindingDirection.In);
}
}
}
We see that for each binding, a "delegate" is created which wraps the this._onXXXXXPropertyChanged method (either Source or Target). The delegate is then assigned to the source object's propertyChanged delegate collection. So, when, say, a checkbox is checked (going with our earlier example), we see the following code execute within the checkbox type:
this._onClick = function() {
this.raisePropertyChanged('checked');
this.click.invoke(this, Sys.EventArgs.Empty);
}
Note the call to raisePropertyChanged. This method takes as a parameter the event name and calls the appropriate delegates in the propertyChanged Event type.
this.raisePropertyChanged = function(propertyName) {
this.propertyChanged.invoke(this, new Sys.PropertyChangedEventArgs(propertyName));
}
You can see, then, that the delegate makes sure that the callbacks are executed within the binding object when an event occurs -the whole thing is done in a two-way assignment that is somewhat as confusing as ConnectionPoints in old-school COM. What does the callback do? It takes all the values that were given to it way, way back and actually does the transfer and translation of data to from source to sink by calling the transform method (with the transform parameters). After some pre-processing, the callbacks eventually call into one method named "evaulate". It determines which direction the data is going, and calls either evaulateIn or evaulateOut. These final methods call the transform method (assigned earlier) with all the parameters (also assigned earlier) and hooks up the data values - and the end of the story is when Sys.TypeDescriptor.setProperty(_target, _property, value, _propertyKey); executes:
this.evaluate = function(direction) {
debug.assert((direction == Sys.BindingDirection.In) || (direction == Sys.BindingDirection.Out));
if (_bindingExecuting) {
return;
}
_bindingExecuting = true;
if (direction == Sys.BindingDirection.In) {
this.evaluateIn();
}
else {
this.evaluateOut();
}
_bindingExecuting = false;
}
this.evaluateIn = function() {
var targetPropertyType = Sys.TypeDescriptor.getPropertyType(_target, _property, _propertyKey);
var value = this._getSourceValue(targetPropertyType);
var canceled = false;
if (this.transform.isActive()) {
var be = new Sys.BindingEventArgs(value, Sys.BindingDirection.In, targetPropertyType, _transformerArgument);
this.transform.invoke(this, be);
canceled = be.get_canceled();
value = be.get_value();
}
if (!canceled) {
Sys.TypeDescriptor.setProperty(_target, _property, value, _propertyKey);
}
}
this.evaluateOut = function() {
throw Error.createError('evaluateOut is not supported for this binding');
}