Sunday, April 6, 2008

Using IScriptControl interface with UserControls in ASP.NET AJAX Applications.

Often we need to write some custom AJAX component that will serve UserControl, and will define some client side functionality that is specific only to that UserControl.

OK we can find a samples in the AJAX docs covering how to create custom inheritor from Sys.Component or Sys.UI.Control or Sys.UI.Behavior classes in javascript part of AJAX framework.

We can also find how to create custom component, extender or control on server side. But this samples cover only Custom Controls. I not use Custom Control very often during every day development. Custom Controls are supposed to be reusable and for some very specific part of the page creating a new Custom Control is not an option - we use User Controls.

I tried to use the same approach as described in the links above with User Controls, and it works fine.

I will take one of the samples from AJAX docs and show how to use it with User Controls instead.

To implement IScriptControl interface we need to make the following steps:

1) Write client side script describing how control behaves itself on the client (I am taking them for demonstration from AJAX docs website)  - this is Timer class you can find on the link.

2)Write PreRender event handler for UserControl:
protected override void OnPreRender(EventArgs e)
{
    ScriptManager.GetCurrent(Page).RegisterScriptControl(this);
    base.OnPreRender(e);
}

3) Write Render event handler for UserControl:

protected override void Render(HtmlTextWriter writer)
{
    base.Render(writer);
    ScriptManager.GetCurrent(Page).RegisterScriptDescriptors(this);
}

4) Implement IScriptControl interface for the user control:

#region IScriptControl Members

    public System.Collections.Generic.IEnumerable<ScriptDescriptor> GetScriptDescriptors()
    {
        ScriptComponentDescriptor scd =
            new ScriptControlDescriptor("Demo.Timer", ClientID);
        scd.AddProperty("enabled", true);
        scd.AddProperty("interval", 2000);
        scd.AddEvent("tick", "OnTick");
        yield return scd;
    }

    public System.Collections.Generic.IEnumerable<ScriptReference> GetScriptReferences()
    {
        ScriptReference sr = new ScriptReference("~/JScript.js");
        yield return sr;
    }

#endregion

 

That's it ! Now drag several UserControls on the page and see result:

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default.aspx.cs" Inherits="_Default" %>
<%@ Register src="WebUserControl.ascx" tagname="WebUserControl" tagprefix="uc1" %>
<html>
<head runat="server">
    <title>Untitled Page</title>
</head>
<body>
    <form id="form1" runat="server">
        <asp:ScriptManager ID="ScriptManager1" runat="server" />
        <div>
            <uc1:WebUserControl ID="WebUserControl1" runat="server" />
            <uc1:WebUserControl ID="WebUserControl2" runat="server" />
            <uc1:WebUserControl ID="WebUserControl3" runat="server" />
            <uc1:WebUserControl ID="WebUserControl4" runat="server" />
            <uc1:WebUserControl ID="WebUserControl5" runat="server" />
        </div>
    </form>
</body>
</html>

 

image

The Timer control is working, and no difficulties with creating scripts in page markup.

Let's see what source was generated for our page, first of all we can find script was linked to the page:

<script src="JScript.js" type="text/javascript"></script>

And the $create function was called for each user control:

Sys.Application.initialize();
Sys.Application.add_init(function() {
    $create(Demo.Timer, {"enabled":true,"interval":2000}, {"tick":function(sender, args) {var result = $get('WebUserControl1_divOutput'); result.innerText = parseInt(result.innerText) + 1; }}, null, $get("WebUserControl1"));
});

 

As you see with very little efforts and in elegant way we use built in AJAX IScriptControl interface to do all the work for creating and initializing Sys.Component inheritor that is implementing some non visual functionality - timer in this case.

 

Here is the full source code for code-behind file of our UserControl again:

 

using System;
using System.Web.UI;

public partial class WebUserControl : System.Web.UI.UserControl, IScriptControl
{
    protected void Page_Load(object sender, EventArgs e)
    {

    }

    protected override void OnPreRender(EventArgs e)
    {
        ScriptManager.GetCurrent(Page).RegisterScriptControl(this);
        base.OnPreRender(e);
    }

    protected override void Render(HtmlTextWriter writer)
    {
        base.Render(writer);
        ScriptManager.GetCurrent(Page).RegisterScriptDescriptors(this);
    }

    #region IScriptControl Members

    public System.Collections.Generic.IEnumerable<ScriptDescriptor> GetScriptDescriptors()
    {
        ScriptComponentDescriptor scd =
            new ScriptControlDescriptor("Demo.Timer", ClientID);
        scd.AddProperty("enabled", true);
        scd.AddProperty("interval", 2000);

        scd.AddEvent("tick",
            "function(sender, args) {var result = $get('" + 
            divOutput.ClientID + 
            "'); result.innerText = parseInt(result.innerText) + 1; }");

        yield return scd;
    }

    public System.Collections.Generic.IEnumerable<ScriptReference> GetScriptReferences()
    {
        ScriptReference sr = new ScriptReference("~/JScript.js");
        yield return sr;
    }

    #endregion
}

Hope this helps.

Technorati Tags:

5 comments:

  1. Hi. I followed your instructions but i can't do what i originaly intended to do. My point was to expose a javscript api for other controls. I wanted to do this:
    getElementById("controlid").MyFunction(). Unfortunately i always get a null reference.

    ReplyDelete
  2. Hi gooma,

    you can use the $find shortcut function to get reference to Sys.Components :

    $find('controlid').MyFunction()

    The Sys.Component have to mbe registered with 'controlid' id.

    ReplyDelete
  3. The problem is that actually there is no control with that id. Render function renders only the insides of user control. Therefore i can't call my js functions. In the rendered source i can see that the object is created
    $create(AjaxTest.ClientControl, {"PageSize":15}, null, null, $get("TestControl1"));

    But i can't get reference to it by calling $find or anything else.

    ReplyDelete
  4. Gooma,

    I ran into the same problem and I solved it by wrapping the contents of my control in a asp:Panel and then declared my ScriptControlDescriptor (in the GetScriptDescriptors method) with the client ID of that new panel.

    Dim descriptor As ScriptControlDescriptor = New ScriptControlDescriptor("MyNamespace.MyControl", Panel.ClientID)


    This is a cool trick. Thanks!

    -Scott

    ReplyDelete
  5. Yeah, really big thanks for example!

    As for trouble with $find (i.e. $find(ClientID) returns null because no client content tags generated with id=ClientID) - here is a new Render method to solve this problem (and no Panel need to put there).

    protected override void Render(HtmlTextWriter writer)
    {
    writer.AddAttribute(HtmlTextWriterAttribute.Id, this.ClientID);
    writer.RenderBeginTag(HtmlTextWriterTag.Div);
    base.Render(writer);
    writer.RenderEndTag();

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

    ReplyDelete