Creating reusable UserControls with Client-side code

4 posts, 0 answers
  1. Stuart Hemming
    Stuart Hemming avatar
    1622 posts
    Member since:
    Jul 2004

    Posted 02 Jan 2013 Link to this post

    Requirements

    RadControls version All
    .NET version All
    Visual Studio version All
    programming language C#/Javascript
    browser support

    all browsers supported by RadControls


    As .NET programmers we are used to creating classes that can be reused, right? And if we are working in the web space, we do that with controls - UserControls (.ascx controls) especially - a lot too.

    But have you noticed how messy things can get if you have a UserControl based on service-side controls that also use a lot of JavaScript?

    Lets look at an example. The following is a trivial control based on the RadTextBox that refuses entry unless the value starts with a predefined character...

    <telerik:RadScriptBlock runat="server" ID="RadScriptBlock1">
        <script type="text/javascript">
            function OnValueChanging(sender, e)
            {
                var StartingCharacterSet = "<%=this.StartingCharacterSet %>"
                var value = e.get_newValue();
                if (StartingCharacterSet.indexOf(value.substring(0, 1)) == -1)
                {
                    e.set_cancel(true);
                    sender.set_value("");
                }
            }
        </script>
    </telerik:RadScriptBlock>
    <asp:Label runat="server" ID="Label1" AssociatedControlID="RadTextBox1" />
    <telerik:RadTextBox runat="server" ID="RadTextBox1">
        <ClientEvents OnValueChanging="OnValueChanging" />
    </telerik:RadTextBox>

    public partial class TextBoxControl1 : System.Web.UI.UserControl
    {
        string _startingCharacterSet = string.Empty;
     
        public string StartingCharacterSet
        {
            get
            {
                return _startingCharacterSet;
            }
            set
            {
                _startingCharacterSet = value;
                Label1.Text = String.Format("A word starting [{0}]", value);
            }
        }
    }


    Let's put it on a page...

    <Common:TextBox runat="server" ID="TextBox1" StartingCharacterSet="aA" />

    If we run it (Webform1.aspx in the attached project) then it does what we expect.

    Hurrah for us.

    OK, now let's create a page with 2 instances of the control on it, but with different values for the 'StartingCharacterSet'...

    <Common:TextBox runat="server" ID="TextBox1" StartingCharacterSet="aA" /><br />
    <Common:TextBox runat="server" ID="TextBox2" StartingCharacterSet="bB" />

    and run it (Webform2.aspx in the attached project).

    Try entering a word beginning with "A" in the first text box. It doesn't work does it? What about if you try a word beginning with "B" in the 2nd one? Yes?

    Try entering a "B" word in the first textbox. It accepts it doesn't it?

    We can see why if we look at the generated HTML source...

    <script type="text/javascript">
        function OnValueChanging(sender, e)
        {
            var StartingCharacterSet = "aA"
            var value = e.get_newValue();
            if (StartingCharacterSet.indexOf(value.substring(0, 1)) == -1)
            {
                e.set_cancel(true);
                sender.set_value("");
            }
        }
    </script>
    <label for="ctl00_ContentPlaceHolder1_TextBox1_RadTextBox1" id="ContentPlaceHolder1_TextBox1_Label1">A word starting [aA]</label>
    <span id="ctl00_ContentPlaceHolder1_TextBox1_RadTextBox1_wrapper" class="riSingle RadInput RadInput_Office2007" style="width:160px;"><input id="ctl00_ContentPlaceHolder1_TextBox1_RadTextBox1" name="ctl00$ContentPlaceHolder1$TextBox1$RadTextBox1" size="20" class="riTextBox riEnabled" type="text" /><input id="ctl00_ContentPlaceHolder1_TextBox1_RadTextBox1_ClientState" name="ctl00_ContentPlaceHolder1_TextBox1_RadTextBox1_ClientState" type="hidden" /></span><br />
         
    <script type="text/javascript">
        function OnValueChanging(sender, e)
        {
            var StartingCharacterSet = "bB"
            var value = e.get_newValue();
            if (StartingCharacterSet.indexOf(value.substring(0, 1)) == -1)
            {
                e.set_cancel(true);
                sender.set_value("");
            }
        }
    </script>
    <label for="ctl00_ContentPlaceHolder1_TextBox2_RadTextBox1" id="ContentPlaceHolder1_TextBox2_Label1">A word starting [bB]</label>
    <span id="ctl00_ContentPlaceHolder1_TextBox2_RadTextBox1_wrapper" class="riSingle RadInput RadInput_Office2007" style="width:160px;"><input id="ctl00_ContentPlaceHolder1_TextBox2_RadTextBox1" name="ctl00$ContentPlaceHolder1$TextBox2$RadTextBox1" size="20" class="riTextBox riEnabled" type="text" /><input id="ctl00_ContentPlaceHolder1_TextBox2_RadTextBox1_ClientState" name="ctl00_ContentPlaceHolder1_TextBox2_RadTextBox1_ClientState" type="hidden" /></span>


    Our page has 2 functions with the same name. Each definition encountered is treated as a redefinition of the existing function; after all, we can only have a single function with a given name. So, whilst each of our TextBox controls is configured differently, when the page is run, they call the same version of the JavaScript function.

    There are a number of ways around this problem. 

    I'm going to show you how I do it.

    We need a JavaScript 'class' we can associate with our UserControl, one that can have multiple instances on the same page. At the same time we want to ensure that we aren't sending duplicate data down the wire to the client (bandwidth is getting cheaper, but still, waste is waste, right?)

    What I do is start by creating a JavaScript object named for the UserControl is it to work with. What's more, I - generally - create it in a namespace that matches the namespace the UserControl lives in. I use this 'template' to get me started ...

    Type.registerNamespace("NAMESPACE");
     
    // Constructor
    NAMESPACE.CLASS_NAME = function (element)
    {
        NAMESPACE.CLASS_NAME.initializeBase(this, [element]);
    }
     
    NAMESPACE.CLASS_NAME.prototype =
    {
        // Release resources before control is disposed.
        dispose: function ()
        {
            NAMESPACE.CLASS_NAME.callBaseMethod(this, 'dispose');
        },
     
        initialize: function ()
        {
            NAMESPACE.CLASS_NAME.callBaseMethod(this, 'initialize');
        }
    }
     
    NAMESPACE.CLASS_NAME.registerClass('NAMESPACE.CLASS_NAME', FULLY_QUALIFIED_BASE_CLASS_NAME);
     
    if (typeof (Sys) !== 'undefined')
    {
        Sys.Application.notifyScriptLoaded();
    }


    I'm simply basing my JavaScript control on 'Sys.UI.Control', so a quick search and replace gets me this ...

    Type.registerNamespace("RadControlsWebApp1.Controls");
     
    // Constructor
    RadControlsWebApp1.Controls.TextBoxControl2 = function (element)
    {
        RadControlsWebApp1.Controls.TextBoxControl2.initializeBase(this, [element]);
    }
     
    RadControlsWebApp1.Controls.TextBoxControl2.prototype =
    {
        // Release resources before control is disposed.
        dispose: function ()
        {
            RadControlsWebApp1.Controls.TextBoxControl2.callBaseMethod(this, 'dispose');
        },
     
        initialize: function ()
        {
            RadControlsWebApp1.Controls.TextBoxControl2.callBaseMethod(this, 'initialize');
        }
    }
     
    RadControlsWebApp1.Controls.TextBoxControl2.registerClass('RadControlsWebApp1.Controls.TextBoxControl2', Sys.UI.Control);
     
    if (typeof (Sys) !== 'undefined')
    {
        Sys.Application.notifyScriptLoaded();
    }


    Before we move on, lets have a quick look at the constructor. 

    This article tells us that if we inherit from Sys.UI.Control (or from another object that, ultimately, inherits from Sys.UI.Control) then we should pass it a DOM element that the instantiated object will be associated with.

    We want this (JavaScript) object to be associated with our UserControl. To that end, I 'wrap' the UserControl in a simple span by adding this to the code-behind of the UserControl ...

    protected override void Render(HtmlTextWriter writer)
    {
        writer.Write(string.Format(@"<span id=""{0}"">", this.ClientID));
        base.Render(writer);
        writer.Write(@"</span>");
    }

    I can then $get this control and pass it to the JavaScript object's constructor.

    As I do this a lot, I have a base class that all of my UserControls inherit from and this override lives there so I don't have to worry about forgetting it.

    OK. Next thing to go is to call the JavaScript object's constructor. We do this in the markup of the UserControl using the Sys.Component $create method.

    We add the call to $create in a function called by the Sys.Application's init event.

    To do this we add code like this to our UserControl's markup...

    <telerik:RadScriptBlock>
        <script type="text/javascript">
            Sys.Application.add_init(
                function applicationInitHandler(sender, args)
                {
                    $create(RadControlsWebApp1.Controls.TextControl2, null, null, null, $get('<%= this.ClientID %>'));
                }); 
        </script>
    </telerik:RadScriptBlock>


    Before continuing, we need to acknowledge a couple of roadblocks and how we get around them. The first of these is the fact that our original version of our UserControl referenced a server value

    var StartingCharacterSet = "<%=this.StartingCharacterSet %>"


    and we can't do that if our JavaScript object's source is in a separate file. However, we can still reference that server value in our UserControl and pass it on to our JavaScript object either in the constructor or after the object has been created.

    So lets add a property to our JavaScript object...

    // Constructor
    RadControlsWebApp1.Controls.TextBoxControl2 = function (element)
    {
        RadControlsWebApp1.Controls.TextBoxControl2.initializeBase(this, [element]);
        this._startingCharacterSet = null;
    }
     
    RadControlsWebApp1.Controls.TextBoxControl2.prototype =
    {
        get_startingCharacterSet: function()
        {
            return this._startingCharacterSet;
        },
         
        set_startingCharacterSet: function(value)
        {
          this._startingCharacterSet = value;
        },
        // ...
    }


    Now we need to assign a value to it when the object is instantiated.

    The $create method takes a number of parameters. The first is the type being created and the 2nd is a JSON object that describes the object's properties and their values. So, we can rewrite the constructor like this ...

    $create(RadControlsWebApp1.Controls.TextControl2, {startingCharacterSet : "<%=this.StartingCharacterSet %>"}, null, null, $get('<%= this.ClientID %>'));


    As an alternative, we might want to create an object separately, assign that object to a variable and pass that in to the constructor, or we might simply want to assign values to the property after the constructor has been called. To do this we need to $find the object so, we could rewrite the constructor like this ...

    $create(RadControlsWebApp1.Controls.TextControl2, null, null, null, $get('<%= this.ClientID %>'));
    $find('<%= this.ClientID %>').set_startingCharacterSet("<%=this.StartingCharacterSet %>");

    Next add any methods we want in our control object. In this case we want something to handle the OnValueChanging event so we add a method...

    RadControlsWebApp1.Controls.TextControl2.prototype =
    {
        OnValueChanging: function(sender, e)
        {
            var value = e.get_newValue();
            if (this._startingCharacterSet.indexOf(value.substring(0, 1)) == -1)
            {
                e.set_cancel(true);
                sender.set_value("");
            }
        },
        //...
    }


    That's it, right?

    Not quite. We still need to modify the UserControl's markup to point to the relevant method on our control object. This is where we hit another roadblock. We need to reference a method in out UserControl's markup on a object instance that doesn't yet exist. Here we need to use a namespaced function. We use this functionality like a static method in C#. This article on StackOverflow has a nice clear description of this. So, we modify our control object to include this ...

    RadControlsWebApp1.Controls.TextControl2.OnValueChanging = function (sender, e)
    {
        var parent = sender.get_parent();
        parent.OnValueChanging(sender, e);
    }

    And our UserControl's markup to look like this ...

    <%@ Control Language="C#" AutoEventWireup="true" CodeBehind="TextBoxControl2.ascx.cs" Inherits="RadControlsWebApp1.Controls.TextBoxControl2" %>
    <telerik:RadScriptBlock runat="server" ID="RadScriptBlock1">
        <script type="text/javascript">
            Sys.Application.add_init(function (sender, args)
            {
                $create(RadControlsWebApp1.Controls.TextControl2, null, null, null, $get('<%= this.ClientID %>'));
                $find('<%= this.ClientID %>').set_startingCharacterSet("<%=this.StartingCharacterSet %>");
            }); 
        </script>
    </telerik:RadScriptBlock>
    <asp:Label runat="server" ID="Label1" AssociatedControlID="RadTextBox1" />
    <telerik:RadTextBox runat="server" ID="RadTextBox1">
        <ClientEvents OnValueChanging="RadControlsWebApp1.Controls.TextControl2.OnValueChanging" />
    </telerik:RadTextBox>


    What's happening here is the ClientEvent for the RadTextBox is pointing to the static method RadControlsWebApp1.Controls.TextControl2.OnValueChanging. In the implementation of this method we call get_parent() on the sender parameter. Because of the way we defined the control object to be associated with the UserControl the parent is our control object. Having now got a reference to the specific instance, we can call a method on it.

    All we need to do now is ensure that our page, the one containing our UserControl, includes a reference to the script file containing the source for our control object and we're done.

    Clearly this is a trivial example, but the principle's apply regardless of how many controls you have in your UserControl. As an example, a search control in a web application might contain multiple textboxes, radio or checkbox buttons, comboboxes and pickers. It might even include other UserControls. This technique will allow you to write client-side code that is associated with your server-side controls in a way that is much more manageable than just adding endless JavaScript code to your .ascx UserControls   

    The example project does not include the Telerik DLLs;

    -- 
    Stuart
  2. Andrey
    Admin
    Andrey avatar
    836 posts

    Posted 07 Jan 2013 Link to this post

    Hello,

    Thank you Stuart for taking the time to create this Code-library project and for supporting the Telerik community. This project will definitely help our user to better understand script controls usage and will jumpstart their development process. Your Telerik points have been updated accordingly.

    Regards,
    Andrey
    the Telerik team
    If you want to get updates on new releases, tips and tricks and sneak peeks at our product labs directly from the developers working on the RadControls for ASP.NET AJAX, subscribe to their blog feed now.
  3. Eric Mooiweer
    Eric Mooiweer avatar
    4 posts
    Member since:
    Feb 2010

    Posted 07 Apr 2014 Link to this post

    Brilliant post ! I'm using this pattern in my own project now. Thanks a lot.
  4. Stuart Hemming
    Stuart Hemming avatar
    1622 posts
    Member since:
    Jul 2004

    Posted 07 Apr 2014 in reply to Eric Mooiweer Link to this post

    Eric,

    I'm glad you found the article useful.
Back to Top