Creating ASP.NET Ajax ‘Scriptable’ Server Controls – Overview

ASP.NET Ajax gives us great powers of encapsulation on the client that help us build richer, more client-centric applications.  Richer because we can build atop a ‘richer bedrock’… to borrow a phrase from Dino Esposito.  Much of this comes about because of the new client-based Type system that we have to work with, and so it becomes much easier to bundle up ‘classes’ and get reuse from them. 

 

Get the code for this article here:

http://cid-15630f96cb7d86c1.skydrive.live.com/embedrowdetail.aspx/Public/ScriptableServerControls.zip

 

To get even wider reuse from our JavaScript Types, ASP.NET Ajax provides us with some server side goo that makes it easier for us to package client-centric controls as server controls.  In this article I will show you how to create a custom TextBox control which has a Tags property and which can match those tags at runtime in the client.  To do this we will need to create a custom TextBox control (TaggerTextBox) and a custom ASP.NET Ajax control which will represent the behaviour and the client-side interface of the text box in the client. 

To start with, you should create a solution with 2 projects – 1 for the web site and 1 for your custom server controls.

SSC0

To create the Web.Controls project, use the ASP.NET Ajax Server Control template because it comes with some boiler plate code to get you started.

SSC1

In the Web.Controls project, add a new class named TaggerTextBox, derive it from TextBox and implement a special ASP.NET Ajax interface called IScriptControl.

namespace ScriptableServerControls.Web.Controls {

    public class TaggerTextBox : TextBox, IScriptControl {

        public IEnumerable<ScriptDescriptor> GetScriptDescriptors() {
            throw new NotImplementedException();
        }

        public IEnumerable<ScriptReference> GetScriptReferences() {
            throw new NotImplementedException();
        }
    }
}

As you can see, implementing the IScriptControl interface requires us to add 2 methods to our class – GetScriptDescriptors and GetScriptReferences.  Script references are where we get to return a reference to an embedded JavaScript file which contains our client control code.  Script descriptors are a server side API which gives us strongly typed access to emitting the $create statement that will get injected into the client to represent an instance of our JavaScript class.  We’ll get to both of those in a minute, for now, let’s add the 2 properties that give our custom TextBox its special powers:

public string TagsToMatch {
    get { return (string)ViewState["TagsToMatch"]; }
    set { ViewState["TagsToMatch"] = value; }
}

public string ResultElementID {
    get { return (string)ViewState["ResultElementID"]; }
    set { ViewState["ResultElementID"] = value; }
}

public string HiddenFieldStateElementID {
    get { return (string)ViewState["HiddenFieldStateElementID"]; }
    set { ViewState["HiddenFieldStateElementID"] = value; }
}

public string SelectedValues {
    get {
        HiddenField ctl = this.FindControl(this.HiddenFieldStateElementID) as HiddenField;

        if (ctl == null)
            return "";

        return ctl.Value.TrimEnd(new char[] { ‘,’ });
    }
    set {
        HiddenField ctl = this.NamingContainer.FindControl(this.HiddenFieldStateElementID) as HiddenField;
        if (ctl != null)
            ctl.Value = "";
    }
}

As you can see, our TextBox will take a list of TagsToMatch.  It will use this list at runtime to match words that the user types into the control and display matches in another element with the ID of ResultElementID.  This could be a DIV or SPAN element somewhere else on the page.  In this case I’ve also provided another element reference called HiddenFieldStateElementID which a user would bind to a HiddenField to maintain state of matched tags (in a production example, you would provide a more encapsulated version of HiddenField).  Finally we have a server side method called SelectedValues which we can use to surface the values that were persisted in the HiddenField.

The Client Behaviour

Now it’s time to leave our server control for a moment and to think about how our control will work in the clients browser.  To kick things off, add a new JavaScript file to the Web.Controls project named TaggerTextBox.js (the name is not particularly relevant), and include it as an Embedded Resource:

SSC2

Now we can implement the body of our TaggerTextBox.js file – which will be the behaviour that we wish to bind to our server control TextBox at runtime.  The following snippet of code is a mouthful, but don’t be too overwhelmed by it.  It’s simply the boilerplate code that is required to create a new ASP.NET Ajax client Type with getter and setter accessors for each property that we wish to expose.  In this case, it’s 1 property for each of those Server Control properties:

/// <reference name="MicrosoftAjax.js" />

Type.registerNamespace("MarkItUp.Web") ;

MarkItUp.Web.TaggerTextBox = function(element) {
    MarkItUp.Web.TaggerTextBox.initializeBase(this, [element]) ;
    this._resultElement = null ;
    this._stateElement = null ;
    this._tagsToMatch = ” ;
    this._internalTagsToMatchArray = [] ;
}

MarkItUp.Web.TaggerTextBox.prototype = {

    initialize : function() {
        MarkItUp.Web.TaggerTextBox.callBaseMethod(this, ‘initialize’) ;
        var func = new Function.createDelegate(this, this.handleKeyPress) ;
        $addHandler(this.get_element(), ‘keyup’, func) ;
    },

    dispose : function() {
        $clearHandlers(this.get_element()) ;
        MarkItUp.Web.TaggerTextBox.callBaseMethod(this, ‘dispose’) ;
    },

    get_TagsToMatch : function() {
        return this._tagsToMatch ;
    },

    set_TagsToMatch : function(value) {
        var str = value.toLowerCase() ;
        this._internalTagsToMatchArray = str.split(‘,’) ;
        this._tagsToMatch = str ;
    },

    get_HiddenFieldStateElementID : function() {
        return this._stateElement ;
    },

    set_HiddenFieldStateElementID : function(value) {
        this._stateElement = value ;
    },

    get_ResultElementID : function() {
        return this._resultElement ;
    },

    set_ResultElementID : function(value) {
        this._resultElement = value ;
    },

    handleKeyPress : function(e) {
        $get(this._resultElement).innerText = this.get_element().value ;
    }

}

MarkItUp.Web.TaggerTextBox.registerClass("MarkItUp.Web.TaggerTextBox", Sys.UI.Control) ;

As you can see, I’ve also taken the liberty of binding an event handler to the keyup event of the underlying HTML element that represents my server control at runtime and cleaning it up in the dispose method.  The handleKeyPress event handler simply echoes the contents of the text box back to the user whenever they press a key but this is where we will write our parsing logic to check tags at runtime.

Let’s flip back to the server for a moment…

Referencing our JavaScript file

Before we go ahead and write too much tricky JavaScript code, why don’t we go ahead and finish off our server control so that we can test it out in our website.  First, we add the Script descriptors – whose job it is to map the server side values out into our $create statement.  ScriptDescriptors come in a few different flavours:

SSC3 

Each of those Addblah methods, allows us to add a property reference which will ultimately end up in the corresponding slot of a $create statement:

SSC4

In our case, we will return the values that were set from the properties that we exposed like so:

 

public IEnumerable<ScriptDescriptor> GetScriptDescriptors() {
    ScriptControlDescriptor descriptor = new ScriptControlDescriptor("MarkItUp.Web.TaggerTextBox", this.ClientID);
    descriptor.AddProperty("TagsToMatch", this.TagsToMatch);
    descriptor.AddElementProperty("ResultElementID", this.ResultElementID);
    descriptor.AddElementProperty("HiddenFieldStateElement", this.HiddenFieldStateElementID);

    yield return descriptor;
}

Now for the Script reference stuff that will ensure that our JavaScript file is emitted at runtime – without that, the above $create code will fail with a message something like… "Namespace MarkItUp not found" (or something along those lines).  Returning the Script reference requires 2 steps:

  1. Add an assembly attribute which marks the embedded resource as a script resource
  2. Return the reference via the interface method

The assembly attribute for our Javascript file can either be at the head of our code file (above the namespace declaration) or, more professionally, in the AssemblyInfo file:

[assembly: WebResource("ScriptableServerControls.Web.Controls.TaggerTextBox.js", "text/javascript")]

Notice that the WebResource ‘path’ is the name of the file, prefixed with the namespace of the assembly.

Now we can return the reference via the interface method:

public IEnumerable<ScriptReference> GetScriptReferences() {
    yield return new ScriptReference(
        Page.ClientScript.GetWebResourceUrl(
            typeof(TaggerTextBox),
            "ScriptableServerControls.Web.Controls.TaggerTextBox.js"
            )
        );
}

The last thing to do is to call our GetScriptDescriptors and GetScriptReferences methods from within the relevant lifecycle points in the control:

 

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

    ScriptManager manager = ScriptManager.GetCurrent(this.Page);
    manager.RegisterScriptControl(this);
}

protected override void Render(HtmlTextWriter writer) {
    base.Render(writer);

    ScriptManager manager = ScriptManager.GetCurrent(this.Page);
    manager.RegisterScriptDescriptors(this);
}

 

At this point, build your solution to see that everything is working fine.  If it is, add a project reference to your Web.Controls project from the Web project and then after we add the following Register line into a test page, we can reference our control too:

<%@ Register Namespace="ScriptableServerControls.Web.Controls" Assembly="ScriptableServerControls.Web.Controls" TagPrefix="cc" %>

We’ll wire up our control with some TagsToMatch, attach to a hidden field element and bind it to an output element:

<asp:HiddenField ID="hdnField" runat="server" />

<cc:TaggerTextBox id="tt1" runat="server"
    HiddenFieldStateElementID="hdnField"
    ResultElementID="output"
    TagsToMatch="Silverlight, Ajax, ASP.NET"
     />
<div id="output"></div>

 

When we run the demo and start typing (don’t forget to add a ScriptManager first), our text should be echoed in the output element like so:

SSC5

The remainder of the logic for this control simply requires writing some parsing logic to parse the textbox text and look for matches against the TagsToMatch, let’s do a dirt simple implementation of that now.  Go back to the JavaScript file and add the following code the key press handler:

handleKeyPress : function(e) {

    this._resultElement.innerText = ” ;
    var str = this.get_element().value.toLowerCase() ;

    for( var i=0; i<this._internalTagsToMatchArray.length; i++ ) {
        var tag = this._internalTagsToMatchArray[i] ;
        if( str.indexOf(tag) != -1 ) {
            this._resultElement.innerHTML +=
                String.format("Found {0}<br />", tag) ;
        }
    }
}

Now when we re-run our application, we see the following results:

SSC6

That’s it for now, but over the next few days I’ll post a few more demo’s and examples of different types of scriptable server controls that you might want to create.  If you have any ideas for things that you’d like to see, please feel free to drop me a line.

Advertisements

~ by D on December 11, 2007.

2 Responses to “Creating ASP.NET Ajax ‘Scriptable’ Server Controls – Overview”

  1. What a great, simple explaination Darren. This is an invaluable sample and introduction.
    Thanks,
    John.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

 
%d bloggers like this: