Freitag, September 16, 2005

Anatomy of an AJAX Control

AJAX Web Control architecture

Writing an AJAX enabled control (here AJAX Control) is as easy as writing another AJAX enabled web applications. The only difference lies in the kind of separating HTML, JavaScript and the clueing stuff like AJAX Actions and Server calls into the right places so that the control can be reused in other places.

If you just want to separate a part of your application so that other members of the team can work on it separately then you can place all the code of the control into the User Control’s *.ascx file.

You should be familiar with the technique of the <ajax:Lookup ... LookUpService="OrteLookup" ... />


What you see here is NOT HTML code but a descriptive declaration of rendering some HTML code here. When using ASP.NET User Controls these attributes do not get automatically rendered as HTML attributes. Instead the ASP.NET framework matches them to properties of the class that builds the control on the server. So any attribute that is used as a parameter must also be defined as a field or property of the control’s class to make the value available on the server.

<%@ Control Language="C#" ... %>
<script runat="server">
  public string lookupservice = "DefaultService.asmx";
  ...
</script>

To make it available on the client the values of these members must then be written out in the HTML code that is generated by this control:

<input ... lookupservice="<%=this.lookupservice %>" ... />

The consequence of this is that the default-values for parameters that are not specified in the source code must be specified in the initialization of the class members and that values assigned to in the JavaScript prototype objects are always overridden.

Writing specific HTML code for a User Control is simply done by writing it down at the end of the *.ascx file. It can be as complex as you like it to be.

Be sure to also add the unique id of the control into the generated HTML code:

id="<%=this.UniqueID %>"

An ASP.NET User control doesn’t automatically create an outer HTML object. It is also possible to generate multiple objects in a row. In this case the JavaScript behaviour is attached to the object that is assigned the unique id.

If you need a reference to another web resource you can use the ResolveUrl method of the Page object:

src="<%=Page.ResolveUrl("~/controls/images/drop.gif") %>"

Programming the Behaviour

The specific JavaScript behaviour that should be used to implement the client-side functionality for a User Control should be implemented in a separate JavaScript include file. This is not strictly necessary but is good for the overall performance because it can be cached in the browser.

I use the same name as the *.ascx file for this control specific include file and place them all into the ~/controls folder.

To attach the behaviour to the html object a small JavaScript fragment is also part of the rendered HTML code:

< script defer="defer" type="text/javascript">
  jcl.LoadBehaviour("<%=this.UniqueID %>", LookUpBehaviour);
</script >

AJAX Actions

The AJAX Action that is used by the Control can be declared as a part of the Behaviour’s prototype object. In the LookUp Control you can find the _fillAction property that is the declaration object of the AJAX Action.

// declare an AJAX action to the lookup service
_fillAction: {
  delay: 100,
  prepare: function(fld) { fld.savevalue = fld.value; return (fld.value); },
  call: null, // is assigned later
  finish: function (val, fld) {
    if (fld.savevalue != fld.value) {
      ajax.Start(fld._fillAction, fld); // again
    } else {
      var dd = fld.CreateDropdown(fld);
      fld.FillDropdown(dd, val);
    } // if
  }, // finish
  onException: proxies.alertException
}, // _fillAction

If you only have one single instance of the Control on the page you will not get into trouble. If there are multiple instances of the Control on the same page these actions definitions will be shared by all controls so you better make a copy of it using the jcl.CloneObject function because they will use different WebServices.

The reference to the WebService should also be set in the init function after the page was loaded because it is not guaranteed whether the WebService proxies are already setup correctly when the behaviour is attached.

this._fillAction = jcl.CloneObject(this._fillAction);
this._fillAction.call = proxies[this.lookupservice].GetPrefixedEntries;

Registering the script includes

Before the HTML text is send to the client all the JavaScript include files that are needed by the control must be registered on the page. This can be done in the OnPreRender method:

protected override void OnPreRender(EventArgs e) {
  base.OnPreRender(e);

  ...

  // register the JavaScripts includes without need for a Form.
  if (!Page.ClientScript.IsClientScriptBlockRegistered(Page.GetType(), "CommonBehaviour")) {
    Page.ClientScript.RegisterClientScriptBlock(Page.GetType(), "CommonBehaviour", String.Empty);
    ((HtmlHead)Page.Header).Controls.Add(new LiteralControl("<script type='text/javascript' src='"
      + Page.ResolveUrl("~/controls/jcl.js")
      + "'><" + "/script>\n"));
  } // if

  if (!Page.ClientScript.IsClientScriptBlockRegistered(this.GetType(), "MyBehaviour")) {
    Page.ClientScript.RegisterClientScriptBlock(this.GetType(), "MyBehaviour", String.Empty);
    ((HtmlHead)Page.Header).Controls.Add(new LiteralControl("<script type='text/javascript' src='"
      + Page.ResolveUrl("~/controls/LookUp.js")
      + "'><" + "/script>\n"));
  } // if
} // OnPreRender
p>All the code fragments can be found in the third part of the samples:

OrteLookUp.aspx is the implementation of the page,

~/controls/LookUp.ascx is the first visible control and

~/controls/LookUp.js is the corresponding JavaScript Behaviour.

Kommentare:

Marco hat gesagt…

I also see some ajax implementations that uses httphanderls (like ajax.net). You're focusing on webservices only. What exactly is the reason that you this path?

Thanks

Marco

MatHertel hat gesagt…

The communication between the client and the server is realized by using the standard WebServices infrastructure with SOAP messages and WSDL services descriptions instead of building a new server-side mechanism for good reasons.

Writing a server side framework must not be done. There are already a lot of them but the WebService based on SOAP and WSDL is widely accepted as a standard.

Before implementing a new proprietary core feature with a high complexity I think it makes sense to search for existing technology in the common frameworks that I can rely on.

I use ASP.NET for my engine to build the server side part. The engine itself is a client-side implementation in JavaScript. Because I expose WebServices to the client-side AJAX engine it should be possible to port this part of the overall application architecture to another server platform if needed.

Writing server-side ports can be a nightmare regarding security. Many security risks of the past years came through open ports that do not work as expected and could be used for different purpose.

By just NOT implementing a server side communication framework I leave this task to Microsoft. – Of course it is still necessary to write good and secure code inside the methods.

I haven’t found a situation until now where WebServices do not fit into the architecture.

The technologies around SOAP and WSDL are at a solid usable level but also still evolving. I expect to see a native universal service client in browser type applications and I hope we will not have to use the basic XMLHTTPRequest Object in the future and will participate in this evolving when building web applications.
(Mozilla/Firefox has already a built-in but very buggy object to do this and Microsoft has as COM object part of the Office suite).

Why do you ask for httphandlers ? What feature do you want to reach ?