Tip/Trick: Cool UI Templating Technique to use with ASP.NET AJAX for non-UpdatePanel scenarios

I've been having some fun playing around with the ASP.NET AJAX Beta release this weekend. 

Usually when I integrate AJAX functionality into my code I just end up using the built-in server controls that ASP.NET AJAX provides (UpdatePanel, UpdateProgress, etc) and the cool controls in the ASP.NET AJAX Control Toolkit.  Scott Hanselman had jokingly called using these AJAX controls "cheating" when he interviewed me two weeks ago for his latest podcast - since they don't require that you write any client-JavaScript for most common scenarios.

This weekend I decided to focus my coding on some of the client JavaScript pieces in the ASP.NET AJAX framework that don't use UpdatePanels at all, and to experiment with alternative ways to use the server to easily generate HTML UI that can be dynamically injected into a page using AJAX.  In the process I created what I think is a pretty useful library that can be used with both ASP.NET AJAX and other AJAX libraries to provide a nice template UI mechanism with ASP.NET, and which doesn't use or require concepts like postbacks or viewstate - while still providing the benefits of control encapsulation and easy re-use.

First Some Quick Background on the JavaScript Networking Stack in ASP.NET AJAX

To first provide some background knowledge about the client JavaScript library in ASP.NET AJAX before getting to the template approach I mentioned above, lets first walkthrough building a simple AJAX "hello world" application that allows a user to enter their name, click a button, and then make an AJAX callback to the server using JavaScript on the client to output a message:

ASP.NET AJAX includes a very flexible JavaScript network library stack with rich serialization support for .NET data-types.  You can define methods on the server to call from JavaScript on the client using either static methods on your ASP.NET Page class, or by adding a web-service into your ASP.NET application that is decorated with the [Microsoft.Web.Script.Services.ScriptService] meta-data attribute and which exposes standard [WebMethod] methods. 

For example, below is a SimpleService.asmx web-service with a "GetMessage" method that takes a string as an argument:

using System;
using 
System.Web.Services;

[Microsoft.Web.Script.Services.ScriptService]
public class SimpleService : WebService {

    [WebMethod]
    
public string GetMessage(string name) {
        
return "Hello <strong>" + name + "</strong>, the time here is: " + DateTime.Now.ToShortTimeString();
    
}
}

ASP.NET AJAX can then automatically create a JavaScript proxy class to use on the client to invoke this method and pass appropriate parameters to/from it.  The easiest way to add this JavaScript proxy class is to add an <asp:ScriptManager> control on the page and point at the web-service end-point (this control also does the work to ensure that each library only ever gets added once to a page). 

I can then call and invoke the method (passing in a value from a textbox), and setup a callback event handler to fire when the server responds using client-side JavaScript code like below.  Note: I could get fancier with the JavaScript code to eliminate some of the lines - but I'm deliberately trying to keep it clear and simple for now and avoid adding too much magic:

<html>
<head id="Head1" runat="server">
    
<title>Hello World Service</title>
    
<link href="StyleSheet.css" rel="stylesheet" type="text/css" />
    
    <
script language="javascript" type="text/javascript">
        
        
function callServer() {
            SimpleService.GetMessage( $
get("Name").value, displayMessageCallback );
        
}
    
        
function displayMessageCallback(result) {
            $
get("message").innerHTML result;
        
}
    
    
</script>
                
</head>
<body>
    
<form id="form1" runat="server">
        
        
<asp:ScriptManager ID="ScriptManager1" runat="server" >
            
<Services>
                
<asp:ServiceReference Path="~/SimpleService.asmx" />
            </
Services>
        
</asp:ScriptManager>
        
        
<h1>Hello World Example</h1>
        
        
<div>
            Enter Name: 
<input id="Name" type="text" />
            
            <
a href="javascript:callServer()">Call Server</a>

            
<div id="message"></div>
        
</div>
        
    
</form>
</body>
</html>

Now when I run the page and enter a name "Scott", the page will use AJAX to call back and dynamically update the HTML of the page without requiring any postbacks or page refreshes.

A cleaner approach to generate HTML using templates

As you can see from the example above, I can easily return HTML markup from the server and inject it into the page on the client.  The downside with the approach I am taking above, though, is that I am embedding the HTML generation logic directly within my server web method.  This is bad because: 1) it intermixes UI and logic, and 2) it becomes hard to maintain and write as the UI gets richer.

What I wanted was an easy way to perform my logic within my web service method, retrieve some data, and then pass the data off to some template/view class to generate the returned HTML UI result.  For example, consider generating a Customer/Order Manager application which uses AJAX to generate a UI list of customers like this:

I want to write server code like below from within my WebService to lookup the customers by country and return the appropriate html list UI.  Note below how the ViewManager.RenderView method allows me to pass in a data object to bind the UI against.  All UI generation is encapsulated within my View and out of my controller webmethod: 

    [WebMethod]
    
public string GetCustomersByCountry(string country)
    {
        CustomerCollection customers
DataContext.GetCustomersByCountry(country);

        if 
(customers.Count > 0)
            
return ViewManager.RenderView("customers.ascx", customers);
        else
            return 
ViewManager.RenderView("nocustomersfound.ascx");
    
}

It turns out this wasn't too hard to enable and only required ~20 lines of code to implement the ViewManager class and RenderView methods used above.  You can download my simple implementation of it here.

My implementation allows you to define a template to render using the standard ASP.NET User Control (.ascx file) model - which means you get full VS designer support, intellisense, and compilation checking of it.  It does not require that you host the usercontrol template on a page - instead my RenderView implementation dynamically cooks up a dummy Page object to host the UserControl while it renders, and captures and returns the output as a string.

For example, here is the Customer.ascx template I could write to generate the customer list output like the screen-shot above which generates a list of customer names that have links to drill into their order history:

<%@ Control Language="C#" CodeFile="Customers.ascx.cs" Inherits="Customers" %>

<div class="customers">

    
<asp:Repeater ID="Repeater1" runat="server">
        
<ItemTemplate>
        
            
<div>    
                
<a href="javascript:CustomerService.GetOrdersByCustomer('<%# Eval("CustomerID") %>', displayOrders)">
                    
<%# Eval("CompanyName") %>
                
</a>
            
</div>

        
</ItemTemplate>
    
</asp:Repeater>

</div>

And its associated code-behind file then looks like this (note: I could add view-specific formatting methods into this if I wanted to):

using System;

public 
partial class Customers : System.Web.UI.UserControl
{
    
public object Data;

    void 
Page_Load(object sender, EventArgs e)
    {
        Repeater1.DataSource 
Data;
        
Repeater1.DataBind();
    
}
}

For passing in the data to the template (for example: the customers collection above), I initially required that the UserControl implement an IViewTemplate interface that I used to associate the data with.  After playing with it for awhile, though, I instead decided to go with a simpler user model and just have the UserControl expose a public "Data" property on itself (like above).  The ViewManager.RenderView method then does the magic of associating the data object passed in to the RenderView method with the UserControl instance via Reflection, at which point the UserControl just acts and renders like normal. 

The end result is a pretty powerful and easy way to generate any type of HTML response you want, and cleanly encapsulate it using .ascx templates.

Finishing it Up

You can download the complete sample I ended up building here.  Just for fun, I added to the above customer list example by adding support for users to click on any of the customer names (after they search by country) to pop-up a listing of their orders (along with the dates they placed the order).  This is also done fully with AJAX using the approach I outlined above:

The entire application is about 8 lines of JavaScript code on the client and a total of about 15 lines of code on the server (that includes all data access).  All HTML UI generation is then encapsulated within 4 nicely encapsulated .ascx template files that I can load and databind my data to from my webmethods on demand:

Click here to download the ViewManager.RenderView implementation if you want to check it out and try it yourself.

Hope this helps,

Scott

54 Comments

  • This is great stuff - I needed exactly this a couple of weeks ago, but couldn't find an easy way to do it. I ended up using UpdatePanels and Page.LoadControl instead, thereby forsaking any possibility of having variable timed updates of an updatepanel since atlas:TimerControl, can't be controlled from server side code.

    Can't wait to try it out.

    Regards
    Jesper Hauge

  • Nice way to split data and UI. I just thought using the interface to pass data objects to controls would be cleaner. and not that hard to implement since VS help you implement quickly interface members.

    By the way, in the scripts apprears 'BLOCKED SCRIPT' in two points. I guess your blog engine blocked the 'javascript :' word... or is it a new magic word ?

  • Really nice simple example. Love it. Especially the bit where you use the RenderView sub to dynamically build and render a user control in memory. I've got a whole ton of uses for that snippet. Cheers Scott.

  • Nice article. Works fine with most of the controls. I tried putting a GridView in the ascx file and got System.Web.HttpException: Control 'ctl00_ResultsView' of type 'GridView' must be placed inside a form tag with runat=server when calling HttpContext.Current.Server.Execute(pageHolder, output, false)

    Any ideas?

  • Hi Josh,

    At first glance an Interface does seem a little cleaner. What I like about the reflection approach though is that I don't have to have the Data property be tied to being a specific datatype.

    For example, I could have Data be of type "CustomerCollection", "IEnumeration", "string", or just of type "Object". This avoids me ever having to cast it to use it, and also gives me compilation checking within the view (meaning the compiler will complain if I try and use it differently).

    Having said that, requiring an interface would only change a few lines in the implementation - so if you prefer that approach you can ceretainly do that too.

    Hope this helps,

    Scott

  • Hi Skup,

    My blog posting software (either that or blog server) didn't like the word javascript followed by a ":" in my code snippets. Probably this is due to security reasons. :-)

    The .zip file has the full source you can download as well.

    Hope this helps,

    Scott

  • Hi Steve,

    One approach would be to use ASP.NET's HTML intrinsic controls instead of the ones for things like buttons and checkboxes. These are easy to use - just add the regular HTML elements to the user control and then add an "ID" attribute and a runat="server". These won't complain about needing to be in a element.

    If you want to use a control that needs to be in a element to render, one approach you could take would be to dynamically inject a form element under the page class (within the RenderView method I built, and then add the UserControl under that). You could build your own custom derived page class that stripped out the control's rendering output if you wanted - which would then cause you to only return the exact HTML that you need.

    Hope this helps,

    Scott


  • I have been using this technique for a few months now. To avoid seeing the error "GridView must be placed inside a form tag", I use a custom control that strips out the form check like so:

    public class FormlessGridView : System.Web.UI.WebControls.GridView {
    protected override void Render(HtmlTextWriter writer) {
    this.PrepareControlHierarchy();
    this.RenderContents(writer);
    }
    }

  • Hey Scott this looks great and I am actually doing something really similar but using XSLT to generate the HTML.

    In general (performance, maintenance, etc..)How would you compare your above example vs doing the same thing using XSLT template sheets?

    Thanks,
    Greg

  • Hi Greg,

    In terms of performance, my solution above should be very fast. It is using the standard ASP.NET page infrastructure for compilation and running a page - which means there is no interpreted logic at all.

    One other benefit is that you don't need to serialize your data model to XML, which from a performance standpoint is probably the biggest issue with an XSL/T approach (which works fine if you already have data in XML format, but which might have issues if you have to first convert data to XML).

    Hope this helps,

    Scott

  • Hi Rumen,

    I think you might have me confused with someone else (I'm not Bertrand). :-)

    Telerik has actually had access to the new Atlas Beta bits for about 7 weeks now, and has had access to conference calls with the team during that time (this is true for many of the major control vendors). I'd recommend chatting with others folks at Telerik who have received the documentation and information about how to change your controls. That might help with updating them. If not, I know the Atlas team would be happy to help you.

    Hope this helps,

    Scott

  • A true gent Scott cheers.
    Much qudos to you in the way you find the time to reply to give a thorough reply to all questions on here and in asp.net forums.....and also do you day job???!!
    DO MS have a 48 hour day:)

  • Great example. I've been doing something similiar with ASPX pages as templates and using Server.Execute() to load up the content, but using ASCX is the natural way to do this... Cool!

  • does page.LoadControl() will decrease performance because it has to load the ascx pages everytime the page get called ?

  • Hi Jimmy,

    The lookup is cached, so calling Page.LoadControl shouldn't have any performance issues.

    Hope this helps,

    Scott

  • With regards to performance, since the large amount of data is transferred via Web Services instead of in the standard "Page Model", would this affect performance as well?

    will the loaded control still have the same functionality when applied with the behaviours from the Atlas Toolkit (such as the popup control?)

    Regards,

    Tim

  • Hi Tim,

    The ASP.NET AJAX web-service handler actually doesn't use XML under the wire -- instead it just returns content in a "JSON" format. This should be pretty efficient and not cause any performance problems.

    Hope this helps,

    Scott

  • Hi Andrey,

    You can use the technique I use above from any HttpHandler or even a page.

    The reason I used AJAX web-service callbacks is because they allow you transfer rich datatypes between JavaScript and the server (basically any .NET class to/from JavaScript). This can help with a lot of scenarios and is pretty easy to-do.

    Hope this helps,

    Scott

  • You talking about serialization and only one way here-"to"(there's plenty of json ser. libs exists).But I see dynamic rendering of controls-that's more important as this example show,than Atlas itself.
    I don't say Atlas is useless,but other frameworks can use this asp.net functionality too,and that much more important,than webservices or json serialization of .net objects.
    Thanks.

  • There seems to be a limit on the amount of content that a webservice can return. I am getting an error thrown by the Ajax framework (system.invalidcastexception) whenever the return > approx 100K

  • I love this approach, but I have being getting exceptions fired by the Ajax framework when then return data of the webservice exceeds about 100K ("Maximum length exceeded"). Any ideas?

  • Hi Richard,

    I'm going to send mail to find out why requests over 100k are having problems. I'll let you know what I find.

    Thanks,

    Scott

  • I'm running the first example, but I got an error. It can not recognize the function of "BLOCKED SCRIPTcallServer()"

    Could anyone explain it for me? Thanks.

  • Hi Jane,

    My blog engine replaced the "javascript:" text with the BLOCKED SCRIPT: text. If you download my sample you'll see the right code.

    Hope this helps,

    Scott

  • I'm trying to use this to load a control that contains a CollapsiblePanel, but I get the following error:

    "The control with ID 'CollapsiblePanelExtender1' requires a ScriptManager on the page. The ScriptManager must appear before any controls that need it."

    I have a ScriptManager on the original page that calls this method, but it appears that the Page object that is created in the RenderView method does not. I tried adding one programatically, but got this error:

    "Value cannot be null.\r\nParameter name: virtualPath"

    Do you have any thoughts on this?

    Thanks

  • Hi,

    viewManager class doesn't execute user control if it has several web.UI controls in it, why?
    kindly resolve this problem

    thanks

  • Great example!
    &quot;The reason I used AJAX web-service callbacks is because they allow you transfer rich datatypes between JavaScript and the server (basically any .NET class to/from JavaScript). &nbsp;This can help with a lot of scenarios and is pretty easy to-do.
    Hope this helps,
    Scott&quot;
    Hi Scott,
    Is there any way to use more complex controls (like Calendar, GridView, AJAX Accordion etc.) in your templates approach. I tried some of them with the same result - System.Web.HttpException --Error executing child request for handler &#39;System.Web.UI.Page&#39;.
    Thanks.

  • Does this work when the UserControl has controls on it that need to get posted back? I've used an update panel to dynamically load a user control via Page.LoadControl based on the selection of a drop down, but when the page is submitted I don't have a way of gaining access to the controls in the UserControl because they were dynamically rendered when the update panel refreshed rather than being contained in the initial page. I can't even get a reference to the UserControl during the postback because the page doesn't know the UserControl exists because it was created during an UpdatePanel refresh.

  • Hi,

    Thanks for the nice example!
    I downloaded ur code, simple & complete and tried to run it on my machine... it throws a variety of errors(I suspect on machine only..), starting with:
    - Line 47 Object doesn't support this method or property (Runtime error do you wish to Debug)
    - Line 54 & 77 Error 'Sys' is undefined (Runtime error do you wish to Debug)

    any help ?

  • Hi Roy,

    This technique above only allows you to use these template files in a display-only way. It doesn't support or allow post-backs.

    Hope this helps,

    Scott

  • Hi Shahul,

    The problem is that the above sample was written for ASP.NET AJAX Beta1 - and Beta2 requires an update to the web.config file to work.

    You can read about the update to make in this blog posting here: http://weblogs.asp.net/scottgu/archive/2006/11/08/ASP.NET-AJAX-1.0-Beta-2-Release.aspx

    Hope this helps,

    Scott

  • Hey Scott,

    I am getting the same error that diPietro is getting and it is happening on the ViewManager.cs line:
    HttpContext.Current.Server.Execute(pageHolder, output, false);

    I get an error back that says Error executing child request for handler 'System.Web.UI.Page'

    What am I missing here??

    Thanks, Greg
    gpbenoit [at] gmail [dot] com

  • Hey Scott,

    Sorry...I figured out that it was the same error as someone above had which is the fact that you can't have an asp control without the form tag.

    So now my question turns into what would the code look like to do the following (quoted you from above):
    "You could build your own custom derived page class that stripped out the control's rendering output if you wanted - which would then cause you to only return the exact HTML that you need."

    thanks, Greg
    gpbenoit [at] gmail [dot] com

  • Hey again Scott,

    Ok I have gotten it to work with asp controls by dynamically adding a form element and then stripping it away. All I had to do was add this to the top of the ViewManager class:

    using System.Web.UI.HtmlControls;

    And then I changed the bottom of the RenderView method to this:

    HtmlForm tempForm = new HtmlForm();
    tempForm.Controls.Add(viewControl);

    //pageHolder.Controls.Add(viewControl);
    pageHolder.Controls.Add(tempForm);

    StringWriter output = new StringWriter();
    HttpContext.Current.Server.Execute(pageHolder, output, false);

    string outputToReturn = output.ToString();
    outputToReturn = outputToReturn.Substring(outputToReturn.IndexOf(""));

    outputToReturn = outputToReturn.Substring(0, outputToReturn.IndexOf(""));

    return outputToReturn;

    ...And you're done...I hope this helps some people.

    thanks for a great post,
    Greg Benoit (Benwah)
    gpbenoit [at] gmail [dot] com

  • I've some asp.net controls that have extra java script methods attached to it. If I use the viewmanager I will lose this javascript functionality. How can I persist already defined java script functionality??

  • I have had problems passing a parameter into a component. E.g. ViewManager.RenderViewGu("~/App_Views/customers.ascx?JobNumber=" + jobNumber)

    It results in the error message "... is not a valid virtual path"

  • Hi Michael,

    The problem you are having above is that you are passing a querystring parameter to the ViewManager.RenderView method. You'll instead want to just pass the path of the .ascx file.

    Hope this helps,

    Scott

  • Scott,

    You comment above that this technique is display-only and does not support post-backs. How would I do this the ASP.NET AJAX way if I needed to dynamically load a User Control and have that control handle postbacks?

    For example, say I have a drop-down list box of possible form names that dynamically load the associated form via AJAX. Then say I need to submit the form and have its postback event handler called. I assume I need to call LoadControl again on the postback so the proper event handler is called, right? How do I do that and at what point in the page life cycle do I do it in?

    Thanks...

  • Hi Scott

    Any clues about the "There seems to be a limit on the amount of content that a webservice can return. I am getting an error thrown by the Ajax framework (system.invalidcastexception) whenever the return > approx 100K" query ?

    Thanks,
    Richard

  • Hi Richard,

    There is a "MaxJsonLength" property that you can configure to return more than 100k back from a JSON request: http://ajax.asp.net/docs/mref/3db73fe7-be41-810f-c3c7-f26b98a1b534.aspx

    Hope this helps,

    Scott

    P.S. Thanks Cristian for the pointer!

  • I cannot find a way to put Javascript inside a component and have it render using this method.

  • I've some asp.net controls that have extra java script methods attached to it. If I use the viewmanager I will lose this javascript functionality. How can I persist already defined java script functionality??

  • Hi Scott!

    Is there any possibility not to write direct web service path like this





    I mean is there any other way to register web service?

  • Hi Alex,

    You can do two other options:

    1) Rather than declaratively register services like above, you can instead programmatically access the scriptManager and register them at runtime if you prefer.

    2) You can dynamically invoke web-services from JavaScript directly without needed to register anything.

    Hope this helps,

    Scott

  • Hi George,

    I'm going to be posting my slides from the CodeMash conference later this weekend. In the Tips and Tricks talk I have an updated version of the sample that works with RC1.

    Thanks,

    Scott

  • Scott,

    I like the idea of using this 'trick', but have the following dilemma:

    If I attempt to generate the html through a static page reference (PageMethods), I get the error 'Error executiong child request for handler 'System.Web.UI.Page' on the HttpContext.Current.Server.Execute call.

    If I use a web service, it doesn't appear I have access to the session variables created on the main site, which I need.

    Any idea on how to work around this?

    Thanks much,

    - Stew

  • Hi,
    If web services are on same site you can have access to session by adding this [WebMethod(EnableSession = true)] instead plain [WebMethod]
    For the &#39;Error executiong child request for handler &#39;System.Web.UI.Page&#39; trick is (like mentioned in previous posts) to inject form into page and add your user control into that. After that I also strip out any hidden viewstate inputs to prevent and &quot;invalid state&quot; exceptions when posting pack.
    - Heiskanen

  • Since I hate passing the type Object around, try it with generics.
    Not sure if my copy/paste will look clear here, but you get the idea.


    public class ControlRender
    {
    public static string RenderControl( D data ) where T : System.Web.UI.Control, IRenderable, new()
    {
    Page pageHolder = new Page();
    T userControl = new T();
    if( data != null )
    {
    userControl.PopulateData( data );
    }

    pageHolder.Controls.Add( userControl );
    StringWriter output = new StringWriter();
    HttpContext.Current.Server.Execute( pageHolder, output, false );
    return output.ToString();
    }
    }


    public interface IRenderable
    {
    void PopulateData( T data );
    }

  • Hi scott,
    Can u tell me How to call ServerSide code from javascript.Without using any Webservices.

  • Scott, I tried using a non-AJAX callback (the original ASP.NET 2.0 callback procedures...still using them) last year in which I generated what I thought was the equivalent of a gridview table, retrieved through a callback and added to a panel. It worked, but it seemed very slow, particularly as the row count increased, compared to the standard gridview submit. Maybe it was slow because the process by which I built my "equivalent" gridview, or the resulting code, was inefficient. My question is, if I were to try this again, using rendercontrol to build the callback html, should I expect it to be relatively the same speed as the gridview submit...meaning, relatively efficient control build, and relatively efficient html? Or is there some reason that you know of relative to the internals that would still make the round trip significantly slower than the gridview submit. Any guidance on this would be appreciated. Thanks!

  • Hi Jim,

    I think it should be pretty fast. It could be that the way you were concatinating strings on the server was causing the slowdown?

    Thanks,

    Scott

  • Scott, thanks for your message. Yes, I was doing a lot of string work to build the "gridview equivalent". I suspected that that could have been the problem, and your comment allows me to assume it was and get past that. I appreciate the input on the performance because the better probability of success will allow me to move this feature up on my priority list. I'm using callbacks in a number of other ways on the page in question, and now have line of sight to getting the main panel refreshes to use them too. Cool! Thanks!

  • Has anyone succeeded in rendering a page containing a scriptmanager? I also get the errors reported by Tony Guidici above.

    TIA
    Thomas

  • Thomas,

    Still working on that one myself... had to alter my design.

    However, I'm revisiting this for the next project phase, so guidance on this issue would be helpful!

    If I figure it out, I'll let you know.

Comments have been disabled for this content.