Just over a month ago, at MIX08, I presented a talk on Real-World Ajax. One of the demos I included was an Ajax templating technique to demonstrate separation of presentation and content from behavior to help manage script complexity, and to help facilitate designer/developer workflow.
The demo was based on early thoughts around what we need to do in this area. What is presented here is still quite simplistic in terms of the range of options it provides to developers. The demo worked well with the audience, and I'd love to hear any feedback people have in this area that can be fed into the design process at this early stage (I alluded to this work in comments to my Ajax vs. Silverlight post).
Here is my scenario - in my Ajax app, script issues a web request to fetch a list of data, in this case a list of bookmarks, and constructs HTML to visualize the results. This is often implemented as follows:
<div id="bookmarkList"></div>
<script type="text/javascript">
function fetchBookmarks() {
// Issue an XMLHTTP request to retrieve user's bookmarks.
}
function onBookmarksAvailable(bookmarks) {
// Update display using retrieved bookmarks. Each boomark has Url and Title properties.
var sb = new Sys.StringBuilder();
sb.append("<ul>");
for (var i = 0; i < bookmarks.length; i++) {
sb.append("<li>");
sb.append("<a href=\"");
sb.append(bookmarks[i].Url);
sb.append("\">");
sb.append(bookmarks[i].Title);
sb.append("</li>");
}
sb.append("</ul>");
$get('bookmarkList').innerHTML = sb.toString();
}
</script>
This works. However, there is a problem. The definition of the UI is now embedded in script. Its hard to design and change it. And its hard to preview it without running the page. The problem is worse if the structure of each item is more complicated than what the sample tries to do.
Here is where templating comes into the picture. Templates have been super common in server-side frameworks like ASP.NET, and are now popping up in client-side script frameworks, as Ajax apps start becoming more client-centric. With templating the developer or designer can define the structure of an item in regular markup, and the templating engine is then responsible for generating the script equivalent.
In the demo, I wrote a simple template engine, which I can use by to rewrite the script as follows:
<div id="bookmarkList">
<ul id="itemContainer"></ul>
</div>
<div id="bookmarkTemplate" style="display: none">
<ul id="itemContainer">
<li><a href="{Url}">{Title}</a>
</ul>
</div>
<script type="text/javascript">
function fetchBookmarks() {
// Issue an XMLHTTP request to retrieve user's bookmarks.
}
function onBookmarksAvailable(bookmarks) {
// Update display using retrieved bookmarks. Each boomark has Url and Title properties.
var template = new Templating.Template($get('boomarkTemplate'));
var itemContainer = $get('itemContainer', $get('bookmarkList'));
for (var i = 0; i < bookmarks.length; i++) {
var item = template.createInstance(bookmarks[i]);
itemContainer.appendChild(item);
}
}
</script>
The interesting lines are in bold. The template is defined using regular XHTML markup representing how items should appear. This can be independently edited/designed and previewed. You simply embed atomic tokens such {Url} etc. into the markup.
The script is responsible for creating the template, which effectively generates the script equivalent, (this is done once), and then as the script loops over the list of bookmarks, it uses the template to create new instances of the list items and anchors, and adds them into the DOM to generate the display.
With this we have a client-side templating mechanism. However web development is all about balancing and meeting several requirements (at once). In particular here are some of the things I also want to tackle as a developer.
- Search Engine Optimization: Ideally the page rendered by the server isn't blank from the perspective of the search engine. I demonstrated a solution in my Ajax and SEO post over year ago.
- Optimizing Networking: I don't want to spend one connection downloading a blank page, and then immediately having the script turn around to issue another connection to fetch the first page worth of data.
- Reducing Page Load Time: Sending a blank page implies some delay as scripts are downloaded, loaded and data is loaded and processed to show data to the user. This should be avoided if at all possible.
- Script-disabled browsers: If targeting an audience that might have script disabled (this is perhaps a shrinking number), then being able to still deliver a degraded but functional experience is nice.
These can be addressed through some form of server-side rendering, sort of how you've been doing using server-side data-bound controls for years. The goal however, in the context of Ajax, is to leverage a single template definition for both server-side and client-side processing. This is where server controls really shine. They allow you to do Ajax better by providing end-to-end solutions.
I've written an AjaxRepeater server control as part of the prototype. It does server-side data-binding (using the same token format we've seen above), renders the list on the server, but also generates a client-side Repeater control (using the ASP.NET Ajax UI pattern for writing controls). The client-side Repeater control consumes the same template definition, and provides the ability to dynamically add rows on the client, or reset the list of items to be displayed. The sample below illustrates using this control, along with both server-side code and client-side script working against it.
<n:AjaxRepeater runat="server" id="bookmarkList">
<ul id="itemContainer">
<li><a href="{Url}">{Title}</a>
</ul>
</n:AjaxRepeater>
<script runat="server">
void Page_Load() {
bookmarkList.DataSource = new Bookmark[] {
new Bookmark { Title="...", Url="..." }, ...
};
bookmarkList.DataBind();
}
</script>
<script type="text/javascript">
function addBookmark(title, url) {
var bookmark = { Title: title, Url: url };
$find('bookmarkList').addDataItem(bookmark);
}
</script>
The current implementation is pretty much based on what can be done via String.Format to perform substitutions of tokens in the template with actual data (I used a tweaked version of James' FormatWith code). Eventually the hope is to productize this feature, and at that point we'd like you to be able add expressions in the template as well as express conditional segments within the template. For example, one might be able to write this:
<n:AjaxRepeater runat="server" id="bookmarkList">
<ul id="itemContainer">
<li>
<a href="{Url}" class="{ IsShared ? 'bookmark shared' : 'bookmark' }">{Title}</a>
<!--- if (!IsShared) { --->
<button type="button" onclick="...">Share</button>
<!--- } --->
</li>
</ul>
</n:AjaxRepeater>
As we extend the functionality of those templates, it is important to preserve the server-side rendering capabilities. One way to accomplish this is to use the DLR and JavaScript on the server. This can actually be done quite efficiently by doing the script-generation and compilation using the DLR at page parse time, once for the lifetime of the application, in much the same way that regular ASP.NET template markup is parsed and compiled once into a delegate that can be invoked multiple times. For ongoing prototyping at this moment, I am starting to look at JScript.NET, until the DLR is part of the framework.
One key realization here is that targeting the mainline scenarios that Ajax developers encounter all the time keeps the overall solution simple and elegant for both the developer and the framework. Combining that with the right server-side support makes the solution much more interesting, as it starts to address the end-to-end scenarios with a single set of concepts and a consistent approach.
I'll reiterate that this is a prototype that I partly demo'd at MIX, and isn't meant to represent exactly what might eventually make it into the product (as we all know, things change as we iterate on ideas, and put together feature plans)... but as I mentioned earlier, if you do have thoughts you'd like to share, then go ahead, and post them below.
For those who are interested at poking around in the code for either the templating engine (which is really quite small), or the server-side control, you can download the prototype as it exists today. If you delve in deeper, you might also notice that the included script was generated using Script# (as you might have come to expect of me by now) and comes along with the originating C# source as well. If you haven't installed Script#, then simply ignore the 2nd project in the solution when it prompts you. I am hoping to get a client-side templating engine added to Script# as well a little further down the road.