Building ASP.NET AJAX Controls (Pt 6 - Moving things to the server)

Links to the other posts in this series

In Part 3 we pretty much had the skeleton of a working control that would show an image in its default state and a Virtual Earth map centred at a specified location (presumably the location of the thing in the image) on mouseover. Unfortunately, to use this control in its current state means worrying about calling $create() in Application init, wiring up various properties and providing some suitable markup to target.

What I want to do is package this as an ASP.NET server control to make it really easy to use, re-use across applications and distribute to my friends and family (they always appreciate a nice ASP.NET server control). Referring back to an earlier post in the series, I want to be able to just add some markup like the following:

     <cc1:VEMapControl ID="VEMapControl2" runat="server" 
        ImageUrl='~/Photos/Image1.png'
        Lat='52.0' 
        Lon='-1.0' />

What that server control needs to do is inject the right scripts into the page and automatically call $create for me (as well as possibly hosting some resources and allowing me to declaratively set properties).

There are three things an ASP.NET AJAX server control needs to do:

  • Register itself with the ScriptManager on the page
  • Declare ScriptDescriptors
  • Declare ScriptReferences

This introduces a couple of new concepts - ScriptDescriptors allow the configuration and generation of the $create() statement to instantiate the client component. ScriptReferences allow us to specify which scripts need to be rendered on the page for our control to function (so in our case that's going to be the Virtual Earth script and our own VEMapControl script).

Then there's the relationship between the server class (or interface) and the client component type. If we want to generate a Sys.UI.Behavior the server control should derive from ExtenderControl or implement IExtenderControl. If we want a Sys.UI.Control (or a Sys.Component) then the server control should derive from ScriptControl or implement IScriptControl (see below).

Picture1

It's less work if you can derive from the abstract ExtenderControl or ScriptControl classes but of course if you want to derive from another base class (Button for example, or CompositeControl) then you have to implement IExtenderControl / IScriptControl and do a little extra work.

What do these interfaces / classes look like? Here's an implementation of IExtenderControl:

Picture2

ie there are two methods to implement: GetScriptDescriptors and GetScriptReferences each of which returns an IEnumerable of the relevant type. "yield return" in C# just provides a convenient mechanism to return an IEnumerable.

As it's an extender we need to create a ScriptBehaviorDescriptor (see below for the relationship between ScriptDescriptors and client components). In the constructor we pass the name of the client type we want and the (client) ID of the target control (we get passed a reference to the target control as a parameter). These are used by $create() as the first and last parameter respectively.

The ScriptDescriptor allows us to add properties (and events) that will be rendered as part of the $create() statement. In this way we can "bind" server control properties to client component properties. In this case we're ensuring that $create() gets rendered with the properties parameter set to { Size : Value_Of_Server_Control_Size_Property }.

For the ScriptReference in the above example, we pass the path to the required script file. We'll look at embedding resources such as scripts and images in our assembly in another post.

Picture3

If we're implementing the interface (IExtenderControl / IScriptControl) the extra work we have to do relates to registration with the ScriptManager (this is done automagically for you by the abstract class implementations). The implementation used in ExtenderControl is shown below (from Reflector):

 protected internal override void  OnPreRender (EventArgs e)
{
  base.OnPreRender(e);
  this.RegisterWithScriptManager();
}
 protected internal override void  Render (HtmlTextWriter writer)
{
  base.Render(writer);
  this.IPage.VerifyRenderingInServerForm(this);
  if (!base.DesignMode)
  {
    this.ScriptManager.RegisterScriptDescriptors(this);
  }
}

And a simplified version of RegisterWithScriptManager() might be something along the lines of:

 private void RegisterWithScriptManager()
{
    ScriptManager sm = ScriptManager.GetCurrent(this.Page);

    if (sm == null)
    {
        throw new InvalidOperationException("A ScriptManager is required");
    }

    sm.RegisterScriptControl(this);
}

In OnPreRender() we first make sure there is a ScriptManager on the page and then call RegisterScriptControl() to register with the ScriptManager. In Render() we first verify that we're rendering inside a <form runat=server> and then call RegisterScriptDescriptors() which requests the list of required scripts from our control and renders them to the page.

Let's make this a bit more concrete. For the VEMapControl, the implementation of the IScriptControl interface might look like:

 public IEnumerable<ScriptDescriptor> GetScriptDescriptors()
{
    ScriptControlDescriptor scd =
        new ScriptControlDescriptor("ServerControls.VEMapControl", this.ClientID);

    scd.AddProperty("lat", this.Lat);
    scd.AddProperty("lon", this.Lon);

    yield return scd;
}

public IEnumerable<ScriptReference> GetScriptReferences()
{
    yield return new ScriptReference("~/JavaScript/VEMapControl.js");
    yield return new ScriptReference("https://dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=6");
}

Implement the ScriptManager registration as described above and the rest of the work we have to do is "normal" ASP.NET server control stuff (implement properties, override CreateChildControls() etc). Rather than bloat this post anymore, I'll post the full implementation in a new post.

Technorati Tags: asp.net,windows live