I was dissapointed, quite dissapointed actually, when I realized that the sample I was looking at was enabling clientside paging but without partial postbacks to retrieve more data; meaning that even though the client was paging data, it was requesting all of the data initially and paging through it once it arrived. I'm not sure if there is a built-in way around this, but it kind of sucks to think that all the data has to be brought down to the client first. Anyway...again, I got curious about how it all worked....
...it all starts off with a <listView> and a <dataSource> object:
<dataSource id="dataSource" autoLoad="true" serviceURL="SimpleDataService1.asmx" /> <listView id="dataContents" itemTemplateParentElementId="masterTemplate" >.....
You've seen this before, it's nothing amazingly new. One thing that does strike us as interesting, though, is that the binding for the <listView> control doesn't point directly to the <dataSource> object, but instead points to a new object named "view", and that the datapath is now "filteredData".
<bindings> <binding dataContext="view" dataPath="filteredData" property="data"/> </bindings>
What's going on here is that our binding object is pointing to something called a dataView object, which, in turn has its own dataBind object that points to the dataSource:
<dataView id="view" pageSize="12"> <bindings> <binding dataContext="dataSource" dataPath="data" property="data" /> </bindings> </dataView>
The binding path goes like this, then:
Web Service -> DataSource -> Binding -> DataView -> ListView
The reason for all this middle-man object stuff is that the datView provides the paging capabilities for the listView, and therefore presents the listView with a "view" on the data for its binding. The paging capabilities are supported by the dataNavigator object:
<dataNavigator id="pageNavigator" dataView="view"/>
Which hooks itself up to the dataView object via the dataView property. So, up to now, we have the web service and the datasource object which does all the serialization for the JSON response, the binding which binds the dataView to the JSON-created object, and the listview. The dataNavigator sits aside and generates events that tells the DataView to change the data it prevents to the ListView and to therefore refresh the contents of the listView. The system is callback-based, you create buttons that send events to the dataNavigator to go to the next page, previous page, last page, etc. and it responds by calling the proper methods on the dataView object:
this.raiseBubbleEvent = function(source, args) { var currentTarget = this.get_parent(); while (currentTarget) { if (currentTarget.onBubbleEvent(source, args)) { return; } currentTarget = currentTarget.get_parent(); }
...look closely and see the reference to this.get_parent(); The routine above is the event called when the following button is clicked:
<button id="firstPageButton" parent="pageNavigator" command="FIRSTPage"> </button>
The parent points to the pageNavigator object (declared earlier) and the command is packaged into the args of the onBubbleEvent method. The parent, in this case, is the dataNavigator object whose .onBubbleEvent method looks like this:
this.onBubbleEvent = function(source, args) { if (!_data) return false; var cmd = args.get_commandName().toLowerCase(); switch(cmd) { case "page": var arg = args.get_argument(); if (arg && String.isInstanceOfType(arg)) { arg = Number.parse(arg); } if (arg || arg == 0) { _data.set_pageIndex(arg); return true; } break;
case "nextpage": _data.set_pageIndex(_data.get_pageIndex() + 1); return true;
case "previouspage": var idx = _data.get_pageIndex() - 1; if (idx >= 0) { _data.set_pageIndex(idx); } return true;
case "firstpage": _data.set_pageIndex(0); return true;
case "lastpage": _data.set_pageIndex(_data.get_pageCount() - 1); return true; } return false; }
The point of this method is to call the appropriate methods on the dataView object to change its view on the data for the listView. This means that if you click the button whose command is to go to the next page, the event is bubbled upwards to the dataNavigator object who calls the dataView object's corresponding "nextpage" case above, which calls _data.set_pageIndex() for the next page via the dataView object. This method refreshes the content of the listView and you, effectively, get the 'next page' of data.
....again, though, this doesn't require any partial postbacks to the server, so even though the paging is quick, it's not nearly as efficient with memory consumption as I had hoped it would be.
Roundtrips are annoying, particularly when you're doing full postbacks, but they can be annoying for partial ones too; as a result, the guys at MS included a cool control in Atlas called the InitialData control whose responsibility is to package the "initial response" of a web service call into the response http stream for the first request. The thought goes like this: if the client has to wait for the page to load, and then have that page request data from a webservice and load it, then we're still being rather foolish with our client experience, so why not just package what would normally be the initial response directly into the output stream for the page itself? And that is exactly what the InitiaData control does.
The InitialData control is a full-fledged ASP.Net control, meaning it's processed server-side. It looks like this declaratively:
<atlas:InitialData runat=server id="dataSourceInitialData" servicePath="~/atlas/samples/data/SimpleDataService1.asmx" ClientDataSourceID="dataSource"> </atlas:InitialData>
Notice the "hookup" to the dataSource object via the ClientDataSourceID. The page this control was taken from had the following reference on it:
<dataSource id="dataSource" autoLoad="true" serviceURL="SimpleDataService1.asmx" />
By analyzing the response stream using fiddler of my browser's first request, I see a JSON block inserted inside <initialData> tags which lay between the opening and closing tags for the <dataSource> object:
<dataSource id="dataSourceInitialData"> <initialData><![CDATA[new Sys.Data.DataTable([new Sys.Data.DataColumn("Id",Number,null,false,true),new Sys.Data.DataColumn("Name",String,"New row",false,false),new Sys.Data.DataColumn("Description",String,"",false,false)],[{"Id":0,"Name":"0Architecture","Description":"0The need to define priority goals and use cases for Architectural planning and structuring."},{"Id":1,"Name":"1Architecture","Description":"1The need to define priority goals and use cases for Architectural planning and structuring."},{"Id":2,"Name":"2Architecture","Description":"2The need to define priority goals and use cases for Architectural planning and structuring."},{"Id":3,"Name":"3Architecture","Description":"3The need to define priority goals and use cases for Architectural planning and structuring."},{"Id":4,"Name":"4Architecture","Description":"4The need to define priority goals and use cases for Architectural planning and structuring."},{"Id":5,"Name":"5Architecture","Description":"5The need to define priority goals and use cases for Architectural planning and structuring."}, .................. </dataSource>
The client script for the <dataSource> object supports a property called "initialData" which, programmatically, gets set to this JSON block by the client Atlas runtime. When the <dataSource> object is initialized, the following script block is run:
this.load = function() { if (_initialData) { this.set_data(Sys.Serialization.JSON.deserialize(_initialData)); _initialData = null; return; } .....
This first statement detects if initialData exists, and if so, prompts the JSON deserializer to come into play and create us an object and set the result to the "data" property. It then sets _initialData to null to prevent further use of this JSON block.
Bear with me, this is going deep.
You'll start to understand why sometimes it's hard for people to get ahold of me on weeknights (or even weekends) because the pursuit of understanding these types of things is pretty high with me. I started in on the Atlas samples this last week, to kind of act as a refresher for some of the things I learned at MIX '06, and couldn't stop asking myself how this worked and how that worked - I have been progressing very slowly through the samples, but have been going very deep into each one of them. The one I'm about to show you I did a couple days ago - but am kind of excited to explain because it really harnesses a lot of what the new Atlas framework is all about. The sample is here (if you wish to follow along).
First, let's take a look at a piece of the XML Script that's to be placed into the practice page:
<dataSource id="dataSource" autoLoad="true" serviceURL="SampleDataService.asmx" propertyChanged="onChange"/> <listView id="dataContents" itemTemplateParentElementId="masterTemplate" propertyChanged="onChange"> <bindings> <binding dataContext="dataSource" dataPath="data" property="data"/> </bindings> <layoutTemplate> <template layoutElement="masterTemplate"/> </layoutTemplate> <itemTemplate> <template layoutElement="masterItemTemplate"> <label id="masterIndex"> <bindings> <binding dataPath="$index" transform="Add" property="text"/> </bindings> </label> <label id="masterName"> <bindings> <binding dataPath="Name" property="text"/> </bindings> </label> <label id="masterDescription"> <bindings> <binding dataPath="Description" property="text"/> </bindings> </label> </template> </itemTemplate> <emptyTemplate> <template layoutElement="masterNoDataTemplate"/> </emptyTemplate> </listView>
....but first, I digress: The XML code above is actually downloaded into the client browser verbatim; that's right - it's not parsed/read/interpreted/etc by the ASP.Net server runtime at all but instead sent down to the client as is. It's encapsulated within <script> tags and given the script type of text/xml-script. The browser leaves this alone and does not attempt to process it - so the xml block simply goes down to the client. It is the client code executing within the browser starting when the page loads that actually parses through and builds the required java script objects to layout, in memory, the representation of what you see in XML above. After the objects have been laid out, then the client environment begins to execute and the pieces fall as they may....end of digression....
Follow along the XML script above: the Atlas client framework builds an instance of a listView object, and then begins creating various databinding objects and assigning them to it. If you look closely, you see that immediately before the xml script laying out the structure for the listView object there exists a <dataSource> element that instructs the Atlas client framework to build a datasource instance. Of major importance within the <dataSource> element is the serviceURL attribute pointing to SimpleDataService1.asmx: a webservice page; this is important because all of the data that the listview consumes is actually fetched via this webservice, with the <dataSource> object acting as a proxy, so-to-speak. Now, if you follow the binding path (remember that dataContext is the source object for the binding), you'll understand where my initial question came from:
....Okay, look closely, you see that the following has a dataContext:
<bindings> <binding dataContext="dataSource" dataPath="data" property="data"/> </bindings>
Whilst the following does not (yes, I just used the word "whilst" - now get over it):
<bindings> <binding dataPath="Description" property="text"/> </bindings>
The logic goes like this: the first binding object binds the listView to the <dataSource> (which is, in turn, bound to the webservice), and the second binding binds one of the item template elements of the listView to the listView itself. In short: HUH? See, it's confusing. We have a lot of binding to bindings to dataSources to webServices to (whatever) going on here and it's kind of bit hard to swallow at first. A whole dump of questions follow: How is it that a binding can go without a dataContext attribute, how is it that the Atlas framework can databind to a webservice, what information is downloaded by the webservice and how, given what we already know about databinding, does the whole damn system work? Arg, right? Yeah - arg. Why is this all important? I dunno' about you - I but ain't debugging this crap until I understand a bit about it; so I decided to have some present pain for future gain.
One of the questions was simple to answer: when the dataContext attribute is missing, it means that 'this' is the datasource, meaning the current object. For situations where templates are utilized, then, an item within the template can bind to the main object (in this case the listView) by simply omitting the dataContext attribute. Look closely and see that the following is synonymous with the second binding statement from above:
<bindings> <binding dataContext="dataContents" dataPath="Description" property="text"/> </bindings>
Note that the listView object is named "dataContents".
The real meat of the whole xml script block above, however, is all the databinding that occurs. And, most curiously, the databinding to a webservice. How does this work? Again, I cracked open the Atlas runtime client code and started inspecting. First, I looked at what happens when you tell a <binding> object to bind to a dataPath. If you look back on my previous post, you'll see that I discovered that, eventually, a <binding> object will call into one of two methods depending on whether it's binding to an inside or outside source, and in our example, it's binding to an outside source and therefore the following bit of script gets eventually gets executed:
..... var sourcePropertyType = Sys.TypeDescriptor.getPropertyType(propertyObject, propertyName);
var value = this._getTargetValue(sourcePropertyType); if (value != null) { Sys.TypeDescriptor.setProperty(propertyObject, propertyName, value); } .....
Here you see that Sys.TypeDescriptor.getPropertyType is called to retrieve particular bits of type information. Eventually, after getting the value of the property being "grepped", we then call Sys.TypeDescriptor.setProperty to complete the databinding. Okay, nice and simple right (well, remember you're also seeing a very small sliver of the complete code)? Anyway.....remember this fact: in order to databind, we must get type information of the object we're binding to and use that type information to access properties (properties are essential to the binding). So, if an item within the itemTemplate of a listView is bound to the listView as a data source, whatever data source listView provides must be an object that exposes properties. Good? Okay....
...the journey continued, then, to discover what the listView was exposing as a data source (for the <bindings> of its itemTemplate to bind to). Well, if you look closely at this line, you see that the data property (which is the data source) of the listView is completely tied to the data property of the <dataSource> object:
<bindings> <binding dataContext="dataSource" dataPath="data" property="data"/> </bindings>
Because, remember, that the dataPath is the source property, and property is the destination property of the databinding with the dataContext being the source object. So, the data flow goes like this:
WebService -> ListView -> Items of ListView
Ah - but we say, the DATA of the listview is bound to a webservice - but the webservice, THEN, must have properties for us to BIND to because databinding requires properties (i.e. strongly typed objects). That means that the javascript core of Atlas is viewing this web service as something strongly typed. But, how? The answer is simple, and a friend of mine by the name of JSON showed me (no, I didn't misspell it). JSON stands for JavaScript Object Notation - or basically a lightweight way to express javascript objects in a serialized fashion - therefore, in a fashion that one can take any javascript object, serialize it, send it, and have it deseralized by a waiting browser engine. Basically, it's a data interchange format for javascript. Therefore, if, in theory, our webservice were to return its result in JSON format, the javascript engine could parse it, build an object, and our databinding methods of earlier will "just work". And this is EXACTLY what happens, take a look at the following code snippet from the <dataSource> code in the atlas script runtime:
Sys.Net.ServiceMethod.invoke(_serviceURL, "GetData", null, {parameters: _parameters, loadMethod: _loadMethod}, Function.createDelegate(this, onLoadComplete), Function.createDelegate(this, ready));
If we follow this call down (namely, inspect the .invoke) method of Sys.Net.ServiceMethod, we see that the service is called asynchronously, and that the completion method attempts to parse the result as a JSON object by eventually executing this method:
this.get_object = function() { if (!_resultObject) { _resultObject = Sys.Serialization.JSON.deserialize(this.get_data()); } return _resultObject; }
So....In Summary: The individual items of the itemTemplate for the listview bind to the listview, whose data object is bound to the data object of the <dataSource> object, which makes a request to a webservice and, first, attempts to parse the response as a JSON object and build all the appopriate type information, et al in memory for the binding infrastructure to work properly. Everything just takes suit from there on out. But, don't take my word for it, check out this dump that Fiddler gave me while watching the client do a partial postback to the webservice to retrieve information (note that the Content-Type is JSON, and that the body is a javascript object serialized out for us):
HTTP/1.1 200 OK Cache-Control: private, max-age=0 Date: Mon, 01 May 2006 03:05:04 GMT Content-Type: application/json; charset=utf-8 Server: Microsoft-IIS/6.0 X-Powered-By: ASP.NET X-AspNet-Version: 2.0.50727 Vary: Accept-Encoding
new Sys.Data.DataTable([new Sys.Data.DataColumn("Id",Number,null,false,true),new Sys.Data.DataColumn("Name",String,"New row",false,false),new Sys.Data.DataColumn("Description",String,"",false,false)],[{"Id":0,"Name":"ListView","Description":"A control to display data using templates."},{"Id":1,"Name":"Window","Description":"A control to display dialogs."},{"Id":2,"Name":"Weather","Description":"A control to display local weather."},{"Id":3,"Name":"New row","Description":"sdf"}])
ASP.Net Atlas gives Javascript the concept of a "strong type" in much the same way that .Net strongly types objects: through the use of metadata. Occasionally in the MS core javascript code you may see something that looks like this:
var sourcePropertyType = Sys.TypeDescriptor.getPropertyType(propertyObject, propertyName);
Or, possibly, something like this:
Sys.TypeDescriptor.setProperty(propertyObject, propertyName, value);
This begs the question "what is Sys.TypeDescriptor"? It is very similar to System.Type in the .Net runtime in that an instance of it can encapsulate all relevant information regarding a type (such as its methods, properties, etc.). Under the hood, the .getPropertyType method looks like this:
Sys.TypeDescriptor.getPropertyType = function(instance, propertyName, key) { if (instance == null) { throw Error.createError('instance is null in TypeDescriptor.getPropertyType'); }
if (Sys.ICustomTypeDescriptor.isImplementedBy(instance)) { return Object; }
if (key) { return Object; }
if ((propertyName == null) || (propertyName.length == 0)) { throw Error.createError('propertyName is null'); }
var td = Sys.TypeDescriptor.getTypeDescriptor(instance);
var propertyInfo = td._getProperties()[propertyName]; debug.assert(propertyInfo, String.format('Property "{0}" not found on object of type "{1}"', propertyName, Object.getTypeName(instance)));
return propertyInfo.type; }
The most interesting part in the above method is the call into Sys.TypeDescriptor.getTypeDescriptor, and then the use of the returned type descriptor to _getProperties() and return the .type on the returned property of name 'propertyName'. By following this call chain, you eventually see that, for each object, the method .getDescriptor is being called (so that when you want to get the type information for a particular object, the getTypeDescriptor eventually calls getDescriptor). What gets returned is, an INSTANCE of the following:
Sys.TypeDescriptor = function() { var _properties = { }; var _events = { }; var _methods = { }; var _attributes = { };
this._getAttributes = function() { return _attributes; }
this._getEvents = function() { return _events; }
this._getMethods = function() { return _methods; }
this._getProperties = function() { return _properties; } }
This, like, System.Type in the .Net runtime, maintains an internal list of all type information related to an object within the clientside Atlas framework. Objects build this instance when asked for it like this:
this.getDescriptor = function() { var td = Sys.Data.DataSource.callBaseMethod(this, 'getDescriptor');
td.addProperty('data', Object); td.addProperty('autoLoad', Boolean); td.addProperty('initialData', String); td.addProperty('isDirtyAndReady', Boolean, true); td.addProperty('isReady', Boolean, true); td.addProperty('loadMethod', String); td.addProperty('rowCount', Number, true); td.addProperty('serviceURL', String); td.addProperty('parameters', Object, true); td.addProperty('serviceType', Sys.Data.ServiceType); td.addMethod('load'); td.addMethod('save'); td.addEvent('dataAvailable', true);
return td; }
If you see above, the .type of "isReady" is a boolean. Therefore, Sys.TypeDescriptor = System.Type and works in many similar ways.
Today I got the following compile-time error:
"Unrecognized attribute 'appliesTo'. Note that attribute names are case-sensitive. "
When trying to compile an application using Microsoft ATLAS. The problem was that, for some reason, Visual Studio was using an old beta2 version of the Microsoft.Web.Atlas.dll module on my machine in lieu of the latest April CTP available here: http://www.microsoft.com/downloads/details.aspx?FamilyId=B01DC501-B3C1-4EC0-93F0-7DAC68D2F787&displaylang=en.
...Once I installed the package above on the machine, everything worked; apparently this machine hasn't been updated with the latest bits in quite some time.
Microsoft built into ATLAS some fairly interesting frameworks (collectively known as the script core) that can be utilized programmatically to make the language feel more .Net-ish for .Net developers. For example, .Net programmers are use to being able to go .trim() on every string object; and we're also use to ICollection symantics (.Add, .Remove, .Contains, etc). The script core added this functionality and gave programming javascript in the Atlas framework a more OO feel. But, the WAY Microsoft accomplished this was of interest to me, and so I cracked open the code for the Script Core (AtlasRuntime.js) and peered inside.
If the following bit of code (which executes perfectly fine under IE7) looks odd to you, then you won't understand how MS did it:
function page_load(){ Function.prototype.googliebah = "10"; alert(page_load.googliebah); }
Look very carefully above, see that I'm calling a method ON a method!?! The point to take from this statement is that everything is an object in javascript, including methods (this isn't a ground-breaking idea, it's been around for quite some time, actually). The next point to take away from this is that I, basically, extended functionality that existed within the javascript language at runtime; namely, gave the 'function' keyword extra functionality. :)
Okay, so we can do more, take this for example:
function page_load(){ Array.prototype.extraValue = 2; var arr = new Array(1); arr[0] = 1; alert(arr.extraValue); }
Note how I extended the capabilities of the "Array" object in javascript (which is a built-in datatype). The same capabilities exist for methods too, meaning you can add methods (which include .Add, .Remove, .Contains, etc.) to datatypes built into the language. Now, what about our own types? Javascript is prototypical, meaning that its type-system is founded atop prototypes, or objects that it types properties and methods from. Given that a function is an object, and you create an object in javascript like this:
function MyObject() { this.MyValue = 10; }
function page_load(){ var mo = new MyObject(); }
You can very easily add a .Whatever() method to ALL datatypes that are instantiated in the script runtime by simply tagging onto the Function prototype we saw earlier a .Whatever() method. Any object that is created using the 'new' keyword, then, (so long as it sees the javascript code that extends the Function prototype) will have that method. So......say that Microsoft wanted to add a .trim() function to the String datatype in javascript so that us .Net programmers that are used to it can go ahead and use it; well, peer inside AltasRuntime.js and see them adding it (taken from AtlasRuntime.js):
String.prototype.trim = function() { return this.trimRight().trimLeft(); } ...and, folks, that's how they're doing it. Happy coding.
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'); }
|