Eliminating Postbacks: Setting Up jQuery On ASP.NET Web Forms and Managing Data On The Client

by Jon Davis 19. October 2008 12:11

This is a follow-up to a prior post, Keys to Web 3.0 Design and Development When Using ASP.NET. Now I want to focus solely on getting jQuery and client-side data managmeent working with ASP.NET 2.0 without ASP.NET AJAX or ASP.NET MVC.

So you're stuck with Visual Studio 2005 and ASP.NET Web Forms. You want to flex your ninja skills. You can't jump into ASP.NET MVC or ASP.NET AJAX or an alternate templating solution like PHP, though. Are you going to die ([Y]/N)? N

 

Why would you use Web Forms in the first place? Well, you might want to take advantage of some of the data binding shorthand that can be done with Web Forms. For this blog entry, I'll use the example of a pre-populated DropDownList (a <select> tag filled with <option>'s that came from the database). 

This is going to be kind of a "for Dummies" post. Anyone who has good experience with ASP.NET and jQuery is likely already quite familiar with how to get jQuery up and running. But there are a few caveats that an ASP.NET developer would need to remember or else things become tricky (and again, no more tricky than is easily mastered by an expert ASP.NET developer).

Caveat #1: You cannot simply throw a script include into the head of an ASPX page.

The following page markup is invalid:

<%@ Page Language="C#" AutoEventWireup="true"  CodeFile="Default.aspx.cs" Inherits="_Default" %> 

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> 

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title></title>
    <script language="javascript" type="text/javascript" src="jQuery-1.2.6.js></script>
</head>
<body>

It's invalid because ASP.NET truncates tags from the <head> that it doesn't recognize and doesn't know how to deal with, such as <script runat="server">. You have to either put it into the <body> or register the script with the page which will then cause it to be put into body.

Registering the script with the page rather than putting it in the body yourself is recommended by Microsoft because:

  1. It allows you to guarantee the life cycle--more specifically the load order--of your scripts.
  2. It allows ASP.NET to do the same (manage the load order) of your scripts alongside the scripts on which ASP.NET Web Forms is running. Remember that Web Forms hijacks the <form> tag and the onclick behavior of ASP.NET buttons and such things, so it does know some Javascript already and needs to maintain that.
  3. When a sub-page or an .ascx control requires a dependency script, it helps to prevent the same dependency script from being added more than once.
  4. It allows controls to manage their own scripts. More on that in a moment.
  5. It allows you to put the inclusion markup into a server language context where you can use ResolveUrl("~/...") to dynamicize the location of the file according to the app path. This is very important in web sites where a directory hierarchy--with ASP.NET files buried inside a directory--is in place.

Here's how to add an existing external script (a script include) like jQuery into your page. Go to the code-behind file (or the C# section if you're not using code-behind) and register jQuery like so:

protected void Page_Load(object sender, EventArgs e)
{
    Page.ClientScript.RegisterClientScriptInclude(
        typeof(_YourPageClassName_), "jQuery", ResolveUrl("~/js/jquery-1.2.6.js"));
} 

A hair more verbose than I'd prefer but it's not awful. In the case of jQuery which is usually a foundational dependency for many other scripts (and itself has no dependencies), you might also consider putting this on an OnInit() override rather than Page_Load, but that's only if you're adding it to a control, where its lifecycle is less predictable in Page_Load() than in OnInit(), but I'll get into that shortly.

There is a way to inject a script into the <head>, such as described here: http://www.aspcode.net/Javascript-include-from-ASPNET-server-control.aspx. However, that is even more verbose, and it's not really considered "the ASP.NET Web Forms way".

If you want to use the Page.ClientScript registration methods for page script (written inline with markup), create a Literal control and put your script tag there. Then on the code-behind you can use Page.ClientScript.RegisterClientScriptBlock().

On the page:

<body>
    <form id="form1" runat="server">
    <asp:Literal runat="server" ID="ScriptLiteral" Visible="false">
    <script language="javascript" type="text/javascript">
        alert($);
    </script>
    </asp:Literal> 

Note that I'm using a hidden (Visible="false") Literal tag, and this tag is inside the <form runat="server"> tag. Which leads me to ..

Caveat #2: ASP.NET controls can only be declared inside <form runat="server">.

Alright, so then on the code-behind file (or server script), I add:

protected void Page_Load(object sender, EventArgs e)
{
    Page.ClientScript.RegisterClientScriptInclude(
        typeof(_YourPageClassName_), "jQuery", ResolveUrl("~/js/jquery-1.2.6.js"));
    Page.ClientScript.RegisterClientScriptBlock(
        typeof(_YourPageClassName_), "ScriptBlock", ScriptLiteral.Text, false);
} 

Unfortunately, ..

Caveat #3: Client script blocks that are registered on the page in server code lack Intellisense designers for script editing.

To my knowledge, there's no way around this, and believe me I've looked. This is a design error on Microsoft's part, it should not have been hard to create a special tag like: <asp:ClientScriptBlock runat="server">YOUR_SCRIPT_HERE();</asp:ClientScriptBlock>, that registers the given script during the Page_Load lifecycle, and then have a rich, syntax-highlighting, intellisense-supporting code editor when editing the contents of that control. They added a ScriptManager control that is, unfortunately, overkill in some ways, but that is only available in ASP.NET AJAX extensions, not core ASP.NET Web Forms.

But since they didn't give us this functionality in ASP.NET Web Forms, if you want natural script editing (and let's face it, we all do), you can just use unregistered <script> tags the old-fashioned way, but you should put the script blocks either inside the <form runat="server"> element and then inside a Literal control and registered, as demonstrated above, or else it should be below the </form> closure of the <form runat="server"> element. 

Tip: You can usually safely use plain HTML <script language="javascript" type="text/javascript">...</script> tags the old fashioned way, without registering them, as long you place them below your <form runat="server"> blocks, and you are acutely aware of dependency scripts that are or are not also registered.

But scripts that are used as dependency libraries for your page scripts, such as jQuery, should be registered. Now then. We can simplify this... 

Tip: Use an .ascx control to shift the hassle of script registration to the markup rather than the code-behind file.

A client-side developer shouldn't have to keep jumping to the code-behind file to add client-side code. That just doesn't make a lot of workflow sense. So here's a thought: componentize jQuery as a server-side control so that you can declare it on the page and then call it.

Controls/jQuery.ascx (complete):

<%@ Control Language="C#" AutoEventWireup="true" CodeFile="jQuery.ascx.cs" Inherits="Controls_jQuery" %> 

(Nothing, basically.)

Controls/jQuery.ascx.cs (complete):

using System;
using System.Web.UI; 

public partial class Controls_jQuery : System.Web.UI.UserControl
{
    protected override void OnInit(EventArgs e)
    {
        AddJQuery();
    } 

    private bool _Enabled = true;
    [PersistenceMode(PersistenceMode.Attribute)]
    public bool Enabled
    {
        get { return _Enabled; }
        set { _Enabled = value; }
    } 

    void AddJQuery()
    {
        string minified = Minified ? ".min" : "";
        string url = ResolveClientUrl(JSDirUrl 
            + "jQuery-" + _Version
            + minified 
            + ".js");
        Page.ClientScript.RegisterClientScriptInclude(
            typeof(Controls_jQuery), "jQuery", url);
    } 

    private string _jsDir = null;
    public string JSDirUrl
    {
        get
        {
            if (_jsDir == null)
            {
                if (Application["JSDir"] != null)
                    _jsDir = (string)Application["JSDir"];
                else return "~/js/"; // default
            }
            return _jsDir;
        }
        set { _jsDir = value; }
    } 

    private string _Version = "1.2.6";
    [PersistenceMode(PersistenceMode.Attribute)]
    public string JQueryVersion
    {
        get { return _Version; }
        set { _Version = value; }
    } 

    private bool _Minified = false;
    [PersistenceMode(PersistenceMode.Attribute)]
    public bool Minified
    {
        get { return _Minified; }
        set { _Minified = value; }
    }
}


Now with this control created we can remove the Page_Load() code we talked about earlier, and just declare this control directly.

(Add to top of page, just below <%@ Page .. %>:)

<%@ Page Language="C#" AutoEventWireup="true"  CodeFile="Default.aspx.cs" Inherits="_Default" %>
<%@ Register src="~/Controls/jQuery.ascx" TagPrefix="local" TagName="jQuery" %> 

(Add add just below <form runat="server">:)

<form id="form1" runat="server">
<local:jQuery runat="server"
    Enabled="true"
    JQueryVersion="1.2.6"
    Minified="false"
    JSDirUrl="~/js/" /> 

Note that none of the attributes listed above in local:jQuery (except for runat="server") are necessary as they're defaulted.

On a side note, if you were using Visual Studio 2008 you could use the documentation features that enable you to add a reference to another script, using "///<reference path="js/jQuery-1.2.6.js" />, which is documented here:

There's something else I wanted to go over. In a previous discussion, I mentioned that I'd like to see multiple <form>'s on a page, each one being empowered in its own right with Javascript / AJAX functionality. I mentioned to use callbacks, not postbacks. In the absence of ASP.NET AJAX extensions, this makes <form runat="server"> far less relevant to the lifecycle of an AJAX-driven application.

To be clear,

  • Postbacks are the behavior of ASP.NET to perform a form post back to the same page from which the current view derived. It processes the view state information and updates the output with a new page view accordingly.
  • Callbacks are the behavior of an AJAX application to perform a GET or a POST to a callback URL. Ideally this should be an isolated URL that performs an action rather than requests a new view. The client side would then update the view itself, depending on the response from the action. The response can be plain text, HTML, JSON, XML, or anything else.

jQuery already has functionality that helps the web developer to perform AJAX callbacks. Consider, for example, jQuery's serialize() function, which I apparently forgot about this week when I needed it (shame on me). Once I remembered it I realized this weekend that I needed to go back and implement multiple <form>'s on what I've been working on to make this function work, just like I had been telling myself all along.

But as we know,

Caveat #4: You can only have one <form runat="server"> tag on a page.

And if you recall Caveat #2 above, that means that ASP.NET controls can only be put in one form on the page, period.

It's okay, though, we're not using ASP.NET controls for postbacks nor for viewstate. We will not even use view state anymore, not in the ASP.NET Web Forms sense of the term. Session state, though? .. Maybe, assuming there is only one web server or shared session services is implemented or the load balancer is properly configured to map the unique client to the same server on each request. Failing all of these, without view state you likely have a broken site, which means that you shouldn't abandon Web Forms based programming yet. But no one in their right mind would let all three of these fail so let's not worry about that.

So I submit this ..

Tip: You can have as many <form>'s on your page as you feel like, as long as they are not nested (you cannot nest <form>'s of any kind).

Caveat #5: You cannot have client-side <form>'s on your page if you are using Master pages, as Master pages impose a <form runat="server"> context for the entirety of the page.

With the power of jQuery to manipulate the DOM, this next tip becomes feasible:

Tip: Treat <form runat="server"> solely as a staging area, by wrapping it in <div style="display:none">..</div> and using jQuery to pull out what you need for each of your client-side <form>'s.

By "a staging area", what I mean by that is that the <form runat="server"> was necessary to include the client script controls for jQuery et al, but it will also be needed if we want to include any server-generated HTML that would be easier to generate there using .ascx controls than on the client or using old-school <% %> blocks.

Let's create an example scenario. Consider the following page:

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="MyMultiForm.aspx.cs" Inherits="MyMultiForm" %> 

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> 

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title></title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
    
        <div id="This_Goes_To_Action_A">
            <asp:RadioButton ID="ActionARadio" runat="server" 
                GroupName="Action" Text="Action A" /><br />
            Name: <asp:TextBox runat="server" ID="Name"></asp:TextBox><br />
            Email: <asp:TextBox runat="server" ID="Email"></asp:TextBox>
        </div>
        
        <div id="This_Goes_To_Action_B">
            <asp:RadioButton ID="ActionBRadio" runat="server" 
                GroupName="Action" Text="Action B" /><br />
            Foo: <asp:TextBox runat="server" ID="Foo"></asp:TextBox><br />
            Bar: <asp:TextBox runat="server" ID="Bar"></asp:TextBox>
        </div>
        
        <asp:Button runat="server" Text="Submit" UseSubmitBehavior="true" />
    
    </div>
    </form>
</body>
</html> 

And just to illustrate this simple scenario with a rendered output ..

Now in a postback scenario, this would be handled on the server side by determining which radio button is checked, and then taking the appropriate action (Action A or Action B) on the appropriate fields (Action A's fields or Action B's fields).

Changing this instead to client-side behavior, the whole thing is garbage and should be rewritten from scratch.

Tip: Never use server-side controls except for staging data load or unless you are depending on the ASP.NET Web Forms life cycle in some other way.

In fact, if you are 100% certain that you will never stage data on data-bound server controls, you can eliminate the <form runat="server"> altogether and go back to using <script> tags for client scripts. Doing that, however, you'll have to keep your scripts in the <body>, and for that matter you might even consider just renaming your file with a .html extension rather than a .aspx extension, but of course at that point you're not using ASP.NET anymore, so don't. ;)

I'm going to leave <form runat="server"> in place because .ascx controls, even without postbacks and view state, are just too handy and I'll illustrate this with a drop-down list later.

I can easily replace the above scenario with two old-fashioned HTML forms:

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="MyMultiForm.aspx.cs" Inherits="MyMultiForm" %> 

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> 

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title></title>
</head>
<body>
    <form id="form1" runat="server">
    <%--Nothing in the form runat="server"--%>
    </form>
    <div>
        
        <div id="This_Goes_To_Action_A">
            <input type="radio" name="cbAction" checked="checked" 
                id="cbActionA" value="A" />
            <label for="cbActionA">Action A</label><br />
            <form id="ActionA" name="ActionA" action="ActionA">
                Name: <input type="text" name="Name" /><br />
                Email: <input type="text" name="Email" />
            </form>
        </div>
        <div id="This_Goes_To_Action_B">
            <input type="radio" name="cbAction" checked="checked" 
                id="cbActionB" value="B" />
            <label for="cbActionB">Action B</label><br />
            <form id="ActionB" name="ActionB" action="ActionB">
                Foo: <input type="text" name="Foo" /><br />
                Bar: <input type="text" name="Bar" />
            </form>
        </div>
        
        <button onclick="if (document.getElementById('cbActionA').checked)
                            alert('ActionA would submit.'); //document.ActionA.submit();
                         else if (document.getElementById('cbActionB').checked)
                            alert('ActionB would submit.'); //document.ActionB.submit();">Submit</button>
    
    </div>
</body>
</html> 

In some ways this got a lot cleaner, but in other ways it got a lot more complicated. First of all, I had to move the radio buttons outside of any forms as radio buttons only worked within a single form context. For that matter, there's a design problem here; it's better to put a submit button on each form than to use Javascript to determine which form to post based on a radio button--that way one doesn't have to manually check to see which radio button is checked, in fact one could drop the radio buttons altogether, and could have from the beginning even with ASP.NET postbacks; both scenarios can facilitate two submit buttons, one for each form. But I put the radio buttons in to illustrate one small example of where things inevitably get complicated on a complex page with multiple forms and multiple callback behaviors. In an AJAX-driven site, you should never (or rarely) use <input type="submit"> buttons, even if you have an onsubmit handler on your form. Instead, use plain <button>'s with onclick handlers, and control submission behavior with asynchronous XmlHttpRequest uses, and if you must leave the page for another, either use user-clickable hyperlinks (ideally to a RESTful HTML view URL) or use window.location. The window.location.reload() refreshes the page, and window.location.href=".." redirects the page. Refresh is useful if you really do want to stay on the same page but refresh your data. With no form postback, refreshing the page again or clicking the Back button and Forward button will not result in a browser dialogue box asking you if you want to resubmit the form data, which is NEVER an appropriate dialogue in an AJAX-driven site.

Another issue is that we are not taking advantage of jQuery at all and are using document.getElementById() instead.

Before we continue:

Tip: If at this point in your career path you feel more confident in ASP.NET Web Forms than in "advanced" HTML and DOM scripting, drop what you're doing and go become a master and guru of that area of web technology now.

ASP.NET Web Forms is harder to learn than HTML and DOM scripting, but I've found that ASP.NET and advanced HTML DOM can be, and often are, learned in isolation, so many competent ASP.NET developers know very little about "advanced" HTML and DOM scripting outside of the ASP.NET Web Forms methodology. But if you're trying to learn how to switch from postback-based coding to callback-based coding, we literally cannot continue until you have mastered HTML and DOM scripting. Here are some great books to read:

While you're at it, you should also grab:

Since this is also about jQuery, you need to have at least a strong working knowledge of jQuery before we continue.

The key problem with the above code, though, assuming that the commented out bit in the button's onclick event handler was used, is that the forms are still configured to redirect the entire page to post to the server, not AJAXy callback-style. What do we do?

First, bring back jQuery. We'll use the control we made earlier. (If you're using master pages, put this on the master page and forget about it so it's always there.)

..
<%@ Register src="~/Controls/jQuery.ascx" TagPrefix="local" TagName="jQuery" %>
..
<form id="form1" runat="server">
    <local:jQuery runat="server" />
</form> 

Next, to clean-up, replace all document.getElementById(..) with $("#..")[0]. This is jQuery's easier to read and write way of getting a DOM element by an ID. I know it looks odd at first but once you know jQuery and are used to it, $("#..")[0] is a very natural-looking syntax.

<button onclick="if ($('#cbActionA')[0].checked)
                    alert('ActionA would submit.'); //$('#ActionA')[0].submit();
                 else if ($('#cbActionB')[0].checked)
                    alert('ActionB would submit.'); //$('#ActionB')[0].submit();">Submit</button> 

Now we need to take a look at that submit() code and replace it.

One of the main reasons why we broke off <form runat="server"> and created two isolated forms is so that we can invoke jQuery's serialize() function to essentially create a string variable that would consist of pretty much the exact same serialization that would have been POSTed to the server if the form's default behavior executed, and the serialize() function requires the use of a dedicated form to process the conversion. The string resulting from serialize() is essentially the same as what's normally in an HTTP request body in a POST method.

Note: jQuery documentation mentions, "In order to work properly, serialize() requires that form fields have a name attribute. Having only an id will not work." But you must also give your <form> an id attribute if you intend to use $("#formid").

So now instead of invoking the appropriate form's submit() method, we should invoke a custom function that takes the form, serializes it, and POSTs it to the server, asynchronously. That was our objective in the first place, right?

So we'll add the custom function.

    <script language="javascript" type="text/javascript">
        function postFormAsync(form, fn, returnType) {
            var formFields = $(form).serialize();
            
            // set up a default POST completion routine
            if (!fn) fn = function(response) {
                alert(response);
            }; 

            $.post(
                $(form).attr("action"), // action attribute (url)
                formFields,             // data
                fn,                     // callback
                returnType              // optional
                );
        }
    </script> 

Note the fn argument, which is optional (defaults to alert the response) and which I'll not use at this time. It's the callback function, basically what to do once POST completes. In a real world scenario, you'd probably want to pass a function that redirects the user with window.location.href or else otherwise updates the contents of the page using DOM scripting. Note also the returnType; refer to jQuery's documentation for that, it's pretty straightforward. 

And finally we'll change the button code to invoke it accordingly.

<button onclick="if ($('#cbActionA')[0].checked)
                    postFormAsync($('#ActionA')[0]);
                 else if ($('#cbActionB')[0].checked)
                    postFormAsync($('#ActionB')[0]);">Submit</button> 

This works but it assumes that you have a callback URL handler waiting for you on the action="" argument of the form. For my own tests of this sample, I had to change the action="" attribute on my <form> fields to test to "ActionA.aspx" and "ActionB.aspx", these being new .aspx files in which I simply had "Action A!!" and "Action B!!" as the markup. While my .aspx files also needed to check for the form fields, the script otherwise worked fine and proved the point.

Alright, at this point some folks might still be squirming with irritation and confusion about the <form runat="server">. So now that we have jQuery performing AJAX callbacks for us, I still have yet to prove out any utility of having a <form runat="server"> in the first place, and what "staging" means in the context of the tip I stated earlier. Well, the automated insertion of jQuery and our page script at appropriate points within the page is in fact one example of "staging" that I'm referring to. But another kind of staging is data binding for initial viewing.

Let's consider the scenario where both of two forms on a single page have a long list of data-driven values.

Page:

...
<asp:DropDownList runat="server" ID="DataList1" />
<asp:DropDownList runat="server" ID="DataList2" />
... 

Code-behind / server script:

protected void Page_Load(object sender, EventArgs e)
{
    DataList1.DataSource = GetSomeData();
    DataList1.DataBind();

    DataList2.DataSource = GetSomeOtherData();
    DataList2.DataBind();
} 

Now let's assume that DataList1 will be used by the form Action A, and DataList2 will be used by the form Action B. Each will be "used by" their respective forms only in the sense that their <option> tags will be populated by the server at runtime.

Since you can only put these ASP.NET controls in a <form runat="server"> form, and you can only have one <form runat="server"> on the page, you cannot therefore simply put an <asp:DropDownList ... /> control directly into each of your forms. You'll have to come up with another way.

One-way data binding technique #1: Move the element, or contain the element and move the element's container.

You could just move the element straight over from the <form runat="server"> form to your preferred form as soon as the page loads. To do this (cleanly), you'll have to create a container <div> or <span> tag that you can predict an ID and wrap the ASP.NET control in it.

Basic example:

$("#myFormPlaceholder").append($("#myControlContainer")); 

Detailed example:

...
<div style="display: none" id="ServerForm">
    <%-- Server form is only used for staging, as shown--%>
    <form id="form1" runat="server">
        <local:jQuery runat="server" />
        <span id="DataList1_Container">
            <asp:DropDownList runat="server" ID="DataList1">
            </asp:DropDownList>
        </span>
        <span id="DataList2_Container">
            <asp:DropDownList runat="server" ID="DataList2">
            </asp:DropDownList>
        </span>
    </form>
</div>
...
<script language="javascript" type="text/javascript">
...
$().ready(function() {
    $("#DataList1_PlaceHolder").append($("#DataList1_Container"));
    $("#DataList2_PlaceHolder").append($("#DataList2_Container"));
});
</script>
<div>
    <div id="This_Goes_To_Action_A">
        ...
        <form id="ActionA" name="ActionA" action="callback/ActionA.aspx">
        ...
        DropDown1: <span id="DataList1_PlaceHolder"></span>
        </form>
    </div>
    <div id="This_Goes_To_Action_B">
        ...
        <form id="ActionB" name="ActionB" action="callback/ActionB.aspx">
        ...
        DropDown2: <span id="DataList2_PlaceHolder"></span>
        </form>
    </div>
</div> 

An alternative to referencing an ASP.NET control in its DOM context by using a container element is to register its ClientID property to script as a variable and move the server control directly. If you're using simple client <script> tags without registering them, you can use <%= control.ClientID %> syntax.

Page: 

<script language="javascript" type="text/javascript">
...
$().ready(function() {
    var DataList1 = $("#<%= DataList1.ClientID %>")[0];
    var DataList2 = $("#<%= DataList2.ClientID %>")[0];
    $("#DataList1_PlaceHolder").append($(DataList1));
    $("#DataList2_PlaceHolder").append($(DataList2));
});
</script> 

If you are using a literal and Page.ClientScript.RegisterClientScriptBlock, you won't be able to use <%= control.ClientID%> syntax, but you can instead use a pseudo-tag syntax like "{control.ClientID}", and then when calling RegisterClientScriptBlock perform a Replace() against that pseudo-tag.

Page: 

<asp:Literal runat="server" Visible="false" ID="ScriptLiteral">
<script language="javascript" type="text/javascript">
    ...
    $().ready(function() {
        var DataList1 = $("#{DataList1.ClientID}")[0];
        var DataList2 = $("#{DataList2.ClientID}")[0];
        $("#DataList1_PlaceHolder").append($(DataList1));
        $("#DataList2_PlaceHolder").append($(DataList2));
    });
</script>
</asp:Literal> 

Code-behind / server script:

protected void Page_Load(object sender, EventArgs e)
{
    ...
    Page.ClientScript.RegisterClientScriptBlock(
        typeof(MyMultiForm), "pageScript", 
        ScriptLiteral.Text
        .Replace("{DataList1.ClientID}", DataList1.ClientID)
         .Replace("{DataList2.ClientID}", DataList2.ClientID));
} 

For the sake of brevity (and as a tentative decision for usage on my own part), for the rest of this discussion I will use the second of the three, using old-fashioned <script> tags and <%= control.ControlID %> syntax to identify server control DOM elements, and then move the element directly rather than contain it.

One-way data binding technique #2: Clone the element and/or copy its contents.

You can copy the contents of the server control's data output to the place on the page where you're actively using the data. This can be useful if both of two forms, for example, each has a field that use the same data.

Page:

<script language="javascript" type="text/javascript">
function copyOptions(src, dest) {
    for (var o = 0; o < src.options.length; o++) {
        var opt = document.createElement("option");
        opt.value = src.options[o].value;
        opt.text = src.options[o].text;
        try {
            dest.add(opt, null); // standards compliant; doesn't work in IE
        }
        catch (ex) {
            dest.add(opt); // IE only
        }
    }
} 

$().ready(function() {
    var DataList1 = $("#<%= DataList1.ClientID %>")[0];
    copyOptions(DataList1, $("#ActionA_List")[0]);
    copyOptions(DataList1, $("#ActionB_List")[0]); // both use same DataList1
});
</script> 

... 

<form id="ActionA" ...>
    ...
    DropDown1: <select id="ActionA_List"></select>
</form>
<form id="ActionB" ...>
    ...
    DropDown1: <select id="ActionB_List"></select>
</form>


This introduces a sort of dynamic data binding technique whereby the importance of the form of the data being outputted by the server controls is actually getting blurry. What if, for example, the server form stopped outputting HTML and instead began outputting JSON? The revised syntax would not be much different from above, but the source data would not come from DOM elements but from data structures. That would be much more manageable from the persective of isolation of concerns and testability.

But before I get into that, what if things got even more tightly coupled instead? 

One-way data binding technique #3: Mangle the markup directly.

As others have noted, inline server markup used to be pooh pooh'd when ASP.NET came out and introduced the code-behind model. But when migrating away from Web Forms, going back to the old fashioned inline server tags and logic is like a breath of fresh air. Literally, it can allow much to be done with little effort.

Here you can see how quickly and easily one can populate a drop-down list using no code-behind conventions and using the templating engine that ASP.NET already inherently offers.

List<string>:

<select>
    <% MyList.ForEach(delegate (string s) {
            %><option><%=HttpUtility.HtmlEncode(s)%></option><%
        }); %>
</select>  

Dictionary<string, string>:

<select>
    <%  foreach (
           System.Collections.Generic.KeyValuePair<string, string> item
           in MyDictionary)
        {
            %><option value="<%= HttpUtility.HtmlEncode(item.Value) 
            %>"><%=HttpUtility.HtmlEncode(item.Key) %></option><%
        } %>
</select> 

For simple conversions of lists and dictionaries to HTML, this looks quite lightweight. Even mocking this up I am impressed. Unfortunately, in the real world data binding often tends to get more complex.

One-way data binding technique #4: Bind to raw text, JSON/Javascript, or embedded XML.

In technique #2 above (clone the element and/or copy its contents), data was bound from other HTML elements. To get the original HTML elements, the HTML had to be generated by the server. Technically, data-binding to HTML is a form of serialization. But one could also serialize the data model as data and then use script to build the destination HTML and/or DOM elements from the script data rather than from original HTML/DOM.

You could output data as raw text, such as name/value pair collections such as those formatted in a query string. Working with text requires manual parsing. It can be fine for really simple comma-delimited lists (see Javascript's String.split()), but as soon as you introduce the slightest more complex data structures such as trees you end up needing to look at alternatives.

The traditional data structure to work with anything on the Internet is XML. For good reason, too; XML is extremely versatile as a data description language. Unfortunately, XML in a browser client is extremely difficult to code for because each browser has its own implementation of XML reading/manipulating APIs, much more so than the HTML and CSS compliance differences between browsers.

If you use JSON you're working with Javascript literals. If you have a JSON library installed (I like the JsonFx Serializer because it works with ASP.NET 2.0 / C# 2.0) you can take any object that would normally be serialized and JSON-serialize it as a string on the fly. Once this string is injected to the page's Javascript, you can access the data as live Javascript objects rather than as parsed XML trees or split string arrays.

Working directly with data structures rather than generated HTML is much more flexible when you're working with a solution that is already Javascript-oriented rather than HTML-oriented. If most of the view logic is driven by Javascript, indeed it is often very nice for the script runtime to be as data-aware as possible, which is why I prefer JSON because the data structures are in Javascript's "native tongue", no translation necessary.

Once you've crossed that line, though, of moving your data away from HTML generation and into script, then a whole new door is opened where the client can receive pre-generated HTML as rendering templates only, and then make an isolated effort to take the data and then use the rendering templates to make the data available to the user. This, as opposed doing the same on the server, inevitably makes the client experience much more fluid. But at this point you can start delving into real AJAX...

One-way data binding technique #5: Scrap server controls altogether and use AJAX callbacks only.

Consider the scenario of a page that starts out as a blank canvas. It has a number of rendering templates already loaded, but there is absolutely no data on the intial page that is sent back. As soon as the page is loaded, however (think jQuery's "$(document).ready(function() { ... });) you could then have the page load the data it needs to function. This data could derive from a web service URL that is isolated from the page entirely--the same app, that is, but a different relative URL.

In an ASP.NET 2.0 implementation, this can be handled easily with jQuery, .ashx files, and something like the JsonFx JSON Serializer.

From an AJAX purist perspective, AJAX-driven data binding is by far the cleanest approach to client orientation. While it does result in the most "chatty" HTTP interaction, it can also result in the most fluid user experience and the most manageable web development paradigm, because now you've literally isolated the data tier in its entirety.

Working with data in script and synchronizing using AJAX and nothing but straight Javascript standards, the door flies wide open to easily convert one-way data binding to two-way data binding. Posting back to the server is a snap; all you need to do is update your script data objects with the HTML DOM selections and then push that data object out back over the wire, in the exact same way the data was retrieved but in reverse.

In most ways, client-side UI logic and AJAX is the panacea to versatile web UIs. The problem is that there is little consistent guidance in the industry especially for the .NET crowd. There are a lot of client-oriented architectures, few of them suited for the ASP.NET environment, and the ones that are or that are neutral are lacking in server-side orientations or else are not properly endorsed by the major players. This should not be the case, but it is. And as a result it makes compromises like ASP.NET AJAX, RoR+prototype+scriptaculous, GWT, Laszlo, and other combined AJAX client/server frameworks all look like feasible considerations, but personally I think they all stink of excess, learning curve, and code and/or runtime bloat in solution implementations.

kick it on DotNetKicks.com

Currently rated 3.1 by 8 people

  • Currently 3.125/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Tags: , , ,

Web Development


 

Powered by BlogEngine.NET 1.4.5.0
Theme by Mads Kristensen

About the author

Jon Davis (aka "stimpy77") has been a programmer, developer, and consultant for web and Windows software solutions professionally since 1997, with experience ranging from OS and hardware support to DHTML programming to IIS/ASP web apps to Java network programming to Visual Basic applications to C# desktop apps.
 
Software in all forms is also his sole hobby, whether playing PC games or tinkering with programming them. "I was playing Defender on the Commodore 64," he reminisces, "when I decided at the age of 12 or so that I want to be a computer programmer when I grow up."

Jon was previously employed as a senior .NET developer at a very well-known Internet services company whom you're more likely than not to have directly done business with. However, this blog and all of jondavis.net have no affiliation with, and are not representative of, his former employer in any way.

Contact Me 


Tag cloud

Calendar

<<  October 2014  >>
MoTuWeThFrSaSu
293012345
6789101112
13141516171819
20212223242526
272829303112
3456789

View posts in large calendar