Tuesday, June 2, 2009

JavaScript and Server Controls 101

After my previous post called JavaScript and User Controls 101, I had several people ask me if I could also talk about server controls. That is a great point so I threw together a quick little application to demonstrate server controls.

A common interview question is, "What is the difference between a user and a server control?" Of course, the server control is "server side" but can also have client side script. A server control is a truly atomic control that can be referenced in an external DLL and comes with its own properties and scripts. It can only be manipulated via the properties (both runtime and design-time) that it exposes. A user control, on the other hand, is a composite object that participates explicitly in page life-cycle events (init, load, render, etc) and can contain many other controls.

Server controls are typically used for fine-grained functionality. For example, a form that requests a block of information is most likely a user control, while a field in the form that is designed to allow entry of a file name might be a special server control (these are also referred to as ASP.NET controls).

For our example, we are going to make a new server control called a "BlurTextbox." The purpose of this control is to raise a callback event any time it loses focus. This will send the controls value to the server for some type of processing. Whatever the server returns will be evaluated as JavaScript.

In essence, you could use this to validate something on the server side and return a call to show an error message or an alert. Imagine, for example, entering a record with a unique name, and you want to give the illusion of "client-side validation" by showing if the name violates this constraint before the user even tabs from the form.

The logical way to extend a server control is to provide a client side representation. The AJAX framework provides extensive support for doing this, so let's get started.

First, we are going to inherit our control from a TextBox. I add a local reference to the ScriptManager that I can use in several places (this is the control we use to interact with the AJAX framework). I added a single property to my control called "ControlCallbackID." This property will have the client identifier for the control is going to respond to our callback request. The server control itself cannot plug into this (but it could expose events or delegates, that's beyond the scope of this example). To listen to the callback, we simply set this to the client ID of the control that is listening. In our example, it will be the actual page that hosts the control. Note the standard pattern to persist the property in the ViewState property bag.

    public class BlurTextbox : TextBox
    {
        private ScriptManager _sm;

        public string CallBackControlID
        {
            get
            {
                if (ViewState[CALLBACKCONTROLID] == null)
                {
                    return string.Empty;
                }

                return ViewState[CALLBACKCONTROLID].ToString();
            }

            set
            {
                ViewState[CALLBACKCONTROLID] = value;
            }
        }
       ...

The next step is to implement IScriptControl. This identifies the control as having a client-side counterpart. IScriptControl asks us to implement IEnumerable<ScriptDescriptor> GetScriptDescriptors() and IEnumerable<ScriptReference> GetScriptReference().

Script Descriptors

The script descriptors allow us to bind our server-side properties to the client-side JavaScript object. We start by identifying the fully qualified name of the object (we will have the namespace, OnBlurTextBox.TextBox, match on both the client and server). Then we add our properties by calling AddProperty with the client-side name of the property and the value for the property.

public IEnumerable<ScriptDescriptor> GetScriptDescriptors()
{
   ScriptControlDescriptor descriptor = new ScriptControlDescriptor(
                GetType().FullName, ClientID);
   descriptor.AddProperty(JS_CALLBACKCONTROLID, CallBackControlID);
   return new ScriptDescriptor[] { descriptor };
}

Script References

The next piece to implement is a list of references for script for the control. In this case, I only have one JavaScript to include. It is an embedded resource with the same name as the control itself. Therefore, I can register it with the full namespace and the extension (.js) as well as the assembly. Don't forget that you must go into Properties -> AssemblyInfo.cs and add an [assembly: WebResource...] reference for this. The ScriptReference object has several overloads and can handle a URL, local, or embedded resource for the script (this is a good place, for example, to also include a library if you are using those functions along with your control).

public IEnumerable<ScriptReference> GetScriptReferences()
{           
   ScriptReference reference = new ScriptReference(string.Format({0}.js",GetType().FullName),
   GetType().Assembly.GetName().FullName);
   return new[] { reference };
        }

Now we need to register these with the AJAX script manager. In PreRender we register the control itself, letting AJAX know it must call our overrides and wire up a client-side object.

protected override void OnPreRender(EventArgs e)
        {
            if (!DesignMode)
            {
                _sm = ScriptManager.GetCurrent(Page);

                if (_sm == null)
                {
                    throw new HttpException(ERROR_SCRIPTMANAGER);
                }
               
                _sm.RegisterScriptControl(this);
            }

            base.OnPreRender(e);
        }

In Render, at the last possible moment, we register the descriptors to emit the properties.

 protected override void Render(HtmlTextWriter writer)
        {
            if (!DesignMode)
            {               
                _sm.RegisterScriptDescriptors(this);
            }

            base.Render(writer);
        }

That's it for the server control ... the rest is done on the client. Let's take a look at the client-side script:

/// 

Type.registerNamespace("OnBlurTextBox");

// we extend a textbox and really just add a property (callbackcontrolid) and 
// an event (onblur)
OnBlurTextBox.BlurTextbox = function(element) {
    OnBlurTextBox.BlurTextbox.initializeBase(this, [element]);
    // this is the property to refer to the corresponding callback control
    this._callBackControlID = null;
    this._oldBlur = null; 
}

// prototype for the clientside representation
OnBlurTextBox.BlurTextbox.prototype = {
    initialize: function() {
        OnBlurTextBox.BlurTextbox.callBaseMethod(this, 'initialize');
        // bind the handler for calling the callback on blur
        this._onblurHandler = Function.createDelegate(this, this._onBlur);
        // store any existing "blur" event
        this._oldBlur = this.get_element().onblur;
        // register the event handler
        $addHandlers(this.get_element(),
            { 'blur': this._onBlur },
            this);
    },
    dispose: function() {
        //Add custom dispose actions here
        $clearHandlers(this.get_element());
        OnBlurTextBox.BlurTextbox.callBaseMethod(this, 'dispose');
    },
    oldBlur: function() {
        if (this._oldBlur) {
            this._oldBlur();
        }
    },
    // getter for callback binding
    get_callBackControlID: function() {
        return this._callBackControlID;
    },
    // setter for callback binding
    set_callBackControlID: function(value) {
        if (this._callBackControlID != value) {
            this._callBackControlID = value;
            this.raisePropertyChanged('callBackControlID');
        }
    },
    // wire into the blur event to trigger the callback
    _onBlur: function(e) {
        if (this.get_element() && !this.get_element().disabled) {
            // callback to the server and then simply evaluate what is returned as javascript
            WebForm_DoCallback(this.get_callBackControlID(), this.get_element().value, function(args, ctx) {
                eval(args);
                ctx.oldBlur(); 
            }, this, null, false);
        }
    }
}

// register the class
OnBlurTextBox.BlurTextbox.registerClass('OnBlurTextBox.BlurTextbox', Sys.UI.Control);

// let 'em know we're loaded
if (typeof (Sys) !== 'undefined') Sys.Application.notifyScriptLoaded();

There's a lot going on here, but it's not as complicated as it may first appear. First, we register the namespace for our control. Next, we create a prototype for instantiating the object. Notice that it takes a parameter called "element". This is the actual DOM element it will be bound to. Because we are extending a Textbox, the element will be an input element. The prototype is the JavaScript way of typing an object. The object has getters and setters as well as methods and handlers. The first thing to notice is how we initialize and expose getters and setters for the callbackControlID. Next, take a look at the "blur handler" which creates a delegate and registers the event. Finally, note our private field "_oldBlur" that holds the original blur event so we can chain to whatever is done by the consuming page or control.

The _onBlur event is straightforward. We make sure we have a reference to the valid input element and that it is not disabled. Then, we simply invoke WebForm_DoCallback. We pass the callback control (if this isn't set, the callback just gets ignored), the value of the element (input box) we are bound to, and pass an inline function for the callback to evaluate whatever was sent by the server and then call the original "blur" event. Note that we pass the AJAX object itself as "context" (the third to last parameter). This allows us to reference our specific instance on the callback to get the original blur event.

Finally, we register the class with the AJAX framework and notify that this script has been loaded.

Whew! That was a bit, but how do we use it? This is the fun part.

Open up Default.aspx. You'll notice I've inlined a bit of JavaScript. One function sets the focus for the control (using the timeout function for IE 6.0 compatibility). The other inserts text dynamically into an unordered list. We register the control at the top by pointing to the namespace and the assembly and giving it a prefix. Then, we embed two of our custom "blurtextbox" controls. I'll explain the button that does nothing in a minute. We also make sure we have a ScriptManager and that's it.

Now to the code. On the code-behind, I implement ICallbackEventhandler so I can process the events raised by the new controls. In the Page_Load I get a callback reference to make sure the page is listening. I also take the ClientID of the page and set it to the custom controls so they have their reference for the callback. Finally, I add a blur event to the second textbox. I'm basically asking it to set the focus back to the first textbox. This will demonstrate how our custom control does not override the events we set. The reason I put the dummy button on the page is because without it, the last tab on IE would set the focus to the address bar and I'd lose my ability to set focus on the page. By having a button, the default focus would go to the button, then I intercept this and set it back to the first textbox. If you tab through the textboxes, you should find you are alternating between the first and second.

The callback event itself is quite simple: I simply take the value and respond with a call to my function to insert the list item. This will basically build the unordered list on the page each time you tab or click out a text box. You can try typing "This[TAB]Is[TAB]A[TAB]Test" and you should see this:

  • This
  • Is
  • A
  • Test

That's it ... to summarize:

  1. Build your server control
  2. Implement IScriptControl to wire in your properties and script references
  3. Build your javascript to encapsulate whatever client-side functionality you need the control to exhibit
  4. Include the control in a page

That's all there is to JavaScript with server controls.

You can see the working application in action at http://apps.jeremylikness.com/onblurtextbox.

If you view the source, you can learn a lot more about how the AJAX framework interacts with server controls. The start of the page looks pretty much like our .aspx template, with a few added pieces such as the viewstate and event arguments. You'll notice that unlike our original embedded JavaScript (WebResource) we now have some ScriptResource references as well.

Our custom control looks just like an ordinary input box, nothing special. You can even see the blur event that we wired in:

   <div>
        Type something, then TAB out of the textbox<br /> 
        <input name="_bTextBox" type="text" id="_bTextBox" /> 
        <input name="_bTextBox2" type="text" id="_bTextBox2" onblur="setNewFocus('_bTextBox');" />    
        <input type="button" id="btn" value=" " />
        <br />
        (You can also try clicking between the textboxes)
    </div>

The key is at the bottom. Notice that there are two add_init calls to a function called $create. There you'll see something familiar: the reference to the callback control (pointing to the page as __Page, and the $get function to get a reference to the input boxes. This is where everything is bound (the prototype and actual call are in the script references that were included earlier).

Jeremy Likness

No comments:

Post a Comment