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:
01.<%@ Page Language="C#"
AutoEventWireup="true" CodeFile="Default.aspx.cs"
Inherits="_Default" %>
02.
04.
06.<head runat="server">
07.<title></title>
08.<script language="javascript" type="text/javascript" src="jQuery-1.2.6.js></script>
09.</head>
10.<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:
1.protected void Page_Load(object sender, EventArgs e)
2.{
3.Page.ClientScript.RegisterClientScriptInclude(
4.typeof(_YourPageClassName_), "jQuery", ResolveUrl("~/js/jquery-1.2.6.js"));
5.}
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.
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:
1.<body>
2.<form id="form1" runat="server">
3.<asp:Literal runat="server" ID="ScriptLiteral" Visible="false">
4.<script language="javascript" type="text/javascript">
5.alert($);
6.</script>
7.</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:
1.protected void Page_Load(object sender, EventArgs e)
2.{
3.Page.ClientScript.RegisterClientScriptInclude(
4.typeof(_YourPageClassName_), "jQuery", ResolveUrl("~/js/jquery-1.2.6.js"));
5.Page.ClientScript.RegisterClientScriptBlock(
6.typeof(_YourPageClassName_), "ScriptBlock", ScriptLiteral.Text,false);
7.}
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):
1.<%@ Control Language="C#" AutoEventWireup="true" CodeFile="jQuery.ascx.cs"Inherits="Controls_jQuery" %>
(Nothing,
basically.)
Controls/jQuery.ascx.cs (complete):
01.using System;
02.using System.Web.UI;
03.
04.public partial class Controls_jQuery :
System.Web.UI.UserControl
05.{
06.protected override void OnInit(EventArgs e)
07.{
08.AddJQuery();
09.}
10.
11.private bool _Enabled = true;
12.[PersistenceMode(PersistenceMode.Attribute)]
13.public bool Enabled
14.{
15.get { return _Enabled; }
16.set { _Enabled = value; }
17.}
18.
19.void AddJQuery()
20.{
21.string minified = Minified ? ".min" : "";
22.string url = ResolveClientUrl(JSDirUrl
23.+ "jQuery-" + _Version
24.+ minified
25.+ ".js");
26.Page.ClientScript.RegisterClientScriptInclude(
27.typeof(Controls_jQuery), "jQuery", url);
28.}
29.
30.private string _jsDir = null;
31.public string JSDirUrl
32.{
33.get
34.{
35.if (_jsDir == null)
36.{
37.if (Application["JSDir"] != null)
38._jsDir = (string)Application["JSDir"];
39.else return "~/js/"; // default
40.}
41.return _jsDir;
42.}
43.set { _jsDir = value; }
44.}
45.
46.private string _Version = "1.2.6";
47.[PersistenceMode(PersistenceMode.Attribute)]
48.public string JQueryVersion
49.{
50.get { return _Version; }
51.set { _Version = value; }
52.}
53.
54.private bool _Minified = false;
55.[PersistenceMode(PersistenceMode.Attribute)]
56.public bool Minified
57.{
58.get { return _Minified; }
59.set { _Minified = value; }
60.}
61.}
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 .. %>:)
1.<%@ Page Language="C#"
AutoEventWireup="true" CodeFile="Default.aspx.cs"
Inherits="_Default" %>
2.<%@ Register
src="~/Controls/jQuery.ascx" TagPrefix="local"
TagName="jQuery" %>
(Add add just below <form runat="server">:)
1.<form id="form1" runat="server">
2.<local:jQuery runat="server"
3.Enabled="true"
4.JQueryVersion="1.2.6"
5.Minified="false"
6.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:
01.<%@ Page Language="C#"
AutoEventWireup="true" CodeFile="MyMultiForm.aspx.cs"
Inherits="MyMultiForm" %>
02.
04.
06.<head runat="server">
07.<title></title>
08.</head>
09.<body>
10.<form id="form1" runat="server">
11.<div>
12.
13.<div id="This_Goes_To_Action_A">
14.<asp:RadioButton ID="ActionARadio" runat="server"
15.GroupName="Action" Text="Action A" />
16.
17.Name: <asp:TextBox runat="server" ID="Name"></asp:TextBox>
18.
19.Email: <asp:TextBox runat="server" ID="Email"></asp:TextBox>
20.</div>
21.
22.<div id="This_Goes_To_Action_B">
23.<asp:RadioButton ID="ActionBRadio" runat="server"
24.GroupName="Action" Text="Action B" />
25.
26.Foo: <asp:TextBox runat="server" ID="Foo"></asp:TextBox>
27.
28.Bar: <asp:TextBox runat="server" ID="Bar"></asp:TextBox>
29.</div>
30.
31.<asp:Button runat="server" Text="Submit" UseSubmitBehavior="true"/>
32.
33.</div>
34.</form>
35.</body>
36.</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:
02.
04.<head runat="server">
05.<title></title>
06.</head>
07.<body>
08.<form id="form1" runat="server">
09.<%--Nothing in the form runat="server"--%>
10.</form>
11.<div>
12.
13.<div id="This_Goes_To_Action_A">
14.<input type="radio" name="cbAction" checked="checked"
15.id="cbActionA" value="A" />
16.<label for="cbActionA">Action A</label>
17.
18.<form id="ActionA" name="ActionA" action="ActionA">
19.Name: <input type="text" name="Name" />
20.
21.Email: <input type="text" name="Email" />
22.</form>
23.</div>
24.<div id="This_Goes_To_Action_B">
25.<input type="radio" name="cbAction" checked="checked"
26.id="cbActionB" value="B" />
27.<label for="cbActionB">Action B</label>
28.
29.<form id="ActionB" name="ActionB" action="ActionB">
30.Foo: <input type="text" name="Foo" />
31.
32.Bar: <input type="text" name="Bar" />
33.</form>
34.</div>
35.
36.<button onclick="if (document.getElementById('cbActionA').checked)
37.alert('ActionA would submit.'); //document.ActionA.submit();
38.else if (document.getElementById('cbActionB').checked)
39.alert('ActionB would submit.');
//document.ActionB.submit();">Submit</button>
40.
41.</div>
42.</body>
43.</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.)
1...
2.<%@ Register src="~/Controls/jQuery.ascx"
TagPrefix="local" TagName="jQuery" %>
3...
4.<form id="form1" runat="server">
5.<local:jQuery runat="server" />
6.</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.
1.<button onclick="if ($('#cbActionA')[0].checked)
2.alert('ActionA would submit.'); //$('#ActionA')[0].submit();
3.else if ($('#cbActionB')[0].checked)
4.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.
01.<script language="javascript" type="text/javascript">
02.function postFormAsync(form, fn, returnType) {
03.var formFields =
$(form).serialize();
04.
05.// set up a default POST completion routine
06.if (!fn) fn = function(response) {
07.alert(response);
08.};
09.
10.$.post(
11.$(form).attr("action"), // action attribute (url)
12.formFields, // data
13.fn, // callback
14.returnType // optional
15.);
16.}
17.</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.
1.<button onclick="if ($('#cbActionA')[0].checked)
2.postFormAsync($('#ActionA')[0]);
3.else if ($('#cbActionB')[0].checked)
4.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:
1....
2.<asp:DropDownList runat="server" ID="DataList1" />
3.<asp:DropDownList runat="server" ID="DataList2" />
4....
Code-behind /
server script:
1.protected void Page_Load(object sender, EventArgs e)
2.{
3.DataList1.DataSource = GetSomeData();
4.DataList1.DataBind();
5.
6.DataList2.DataSource = GetSomeOtherData();
7.DataList2.DataBind();
8.}
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:
1.$("#myFormPlaceholder").append($("#myControlContainer"));
Detailed example:
01....
02.<div style="display: none" id="ServerForm">
03.<%-- Server form is only used for staging, as shown--%>
04.<form id="form1" runat="server">
05.<local:jQuery runat="server" />
06.<span id="DataList1_Container">
07.<asp:DropDownList runat="server" ID="DataList1">
08.</asp:DropDownList>
09.</span>
10.<span id="DataList2_Container">
11.<asp:DropDownList runat="server" ID="DataList2">
12.</asp:DropDownList>
13.</span>
14.</form>
15.</div>
16....
17.<script language="javascript" type="text/javascript">
18....
19.$().ready(function() {
20.$("#DataList1_PlaceHolder").append($("#DataList1_Container"));
21.$("#DataList2_PlaceHolder").append($("#DataList2_Container"));
22.});
23.</script>
24.<div>
25.<div id="This_Goes_To_Action_A">
26....
27.<form id="ActionA" name="ActionA" action="callback/ActionA.aspx">
28....
29.DropDown1: <span id="DataList1_PlaceHolder"></span>
30.</form>
31.</div>
32.<div id="This_Goes_To_Action_B">
33....
34.<form id="ActionB" name="ActionB" action="callback/ActionB.aspx">
35....
36.DropDown2: <span id="DataList2_PlaceHolder"></span>
37.</form>
38.</div>
39.</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:
01.<script language="javascript" type="text/javascript">
02....
03.$().ready(function() {
04.var DataList1 = $("#<%= DataList1.ClientID %>")[0];
05.var DataList2 = $("#<%= DataList2.ClientID %>")[0];
06.$("#DataList1_PlaceHolder").append($(DataList1));
07.$("#DataList2_PlaceHolder").append($(DataList2));
08.});
09.</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:
01.<asp:Literal runat="server" Visible="false" ID="ScriptLiteral">
02.<script language="javascript" type="text/javascript">
03....
04.$().ready(function() {
05.var DataList1 = $("#{DataList1.ClientID}")[0];
06.var DataList2 = $("#{DataList2.ClientID}")[0];
07.$("#DataList1_PlaceHolder").append($(DataList1));
08.$("#DataList2_PlaceHolder").append($(DataList2));
09.});
10.</script>
11.</asp:Literal>
Code-behind / server
script:
01.protected void Page_Load(object sender, EventArgs e)
02.{
03....
04.Page.ClientScript.RegisterClientScriptBlock(
05.typeof(MyMultiForm), "pageScript",
06.ScriptLiteral.Text
07..Replace("{DataList1.ClientID}", DataList1.ClientID)
08..Replace("{DataList2.ClientID}", DataList2.ClientID));
09.}
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:
01.<script language="javascript" type="text/javascript">
02.function copyOptions(src, dest) {
03.for (var o = 0; o < src.options.length; o++) {
04.var opt = document.createElement("option");
05.opt.value = src.options[o].value;
06.opt.text = src.options[o].text;
07.try {
08.dest.add(opt, null); // standards compliant; doesn't work in IE
09.}
10.catch (ex) {
11.dest.add(opt); // IE only
12.}
13.}
14.}
15.
16.$().ready(function() {
17.var DataList1 = $("#<%= DataList1.ClientID
%>")[0];
18.copyOptions(DataList1, $("#ActionA_List")[0]);
19.copyOptions(DataList1, $("#ActionB_List")[0]); // both
use same DataList1
20.});
21.</script>
22.
23....
24.
25.<form id="ActionA" ...>
26....
27.DropDown1: <select id="ActionA_List"></select>
28.</form>
29.<form id="ActionB" ...>
30....
31.DropDown1: <select id="ActionB_List"></select>
32.</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.
01.List<string>:
02.
03.<select>
04.<% MyList.ForEach(delegate (string s) {
05.%><option><%=HttpUtility.HtmlEncode(s)%></option><%
06.}); %>
07.</select>
08.
09.Dictionary<string, string>:
10.
11.<select>
12.<% foreach (
13.System.Collections.Generic.KeyValuePair<string, string> item
14.in MyDictionary)
15.{
16.%><option value="<%=
HttpUtility.HtmlEncode(item.Value)
17.%>"><%=HttpUtility.HtmlEncode(item.Key) %></option><%
18.} %>
19.</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.