UPDATE (16-07-2009)
Here is a new sample website with source code, it now disposes the page object and allows for WebControl rendering.
Dynamically Loading ASP.NET User Controls with jQuery v2.zip (25.52 kb)
——————————————
This post is only relevant if you are using WebForms. If you have converted to the MVC approach, then go read about Dependency Injection or NHibernate
If however you are using WebForms or are maintaining an application that was build with WebForms. Here is a way to partially render a usercontrol via jQuery (ajax).
Introduction
User controls are a great way to group markup and functionality, but in these AJAX times, the problem and nature of User Controls, in relation to ajax, is their fixed position in the Page Control tree.
Sam Mueller describes the issues and gives his solution to dynamically loaded user controls using jQuery.
Read it here:
http://samuelmueller.com/post/2008/12/20/Dynamically-Loading-ASPNET-User-Controls-with-jQuery.aspx
Sam Muellers raises some very interesting points, however I have two things I would like to change (URL and security).
URL
I very much like that Sam Mueller has found a solution to “directly” call a user control (view) with an URL. What I do not like is the URL to be called:
/ajax.svc/renderuc?path=/usercontrols/myusercontrol.ascx
I would like to be able to call the user control directly by this kind of URL:
/usercontrols/myusercontrol.ascx
And by using a HttpHandler this is possible. I have written an example here:
1: public class AjaxUserControlHandler : IHttpHandler
2: {
3: public void ProcessRequest(HttpContext context)
4: {
5: // Get the path to the user control
6: string path = context.Request.Url.LocalPath;
7:
8: // Intialize the pseudo page and user control
9: Page page = new Page();
10: UserControl viewControl = page.LoadControl(path) as UserControl;
11:
12: // Display error if the user control was not found
13: if (viewControl == null)
14: {
15: context.Response.StatusCode = 404;
16: context.Response.Output.WriteLine("The requested url was not found");
17: return;
18: }
19:
20: // Check existense of the AjaxEnabled attribute. Only add the AjaxEnabled attribut to
21: // user controls that is safe for direct calls
22: var type = viewControl.GetType();
23: var attributes = type.GetCustomAttributes(typeof (AjaxEnabledAttribute), true);
24: AjaxEnabledAttribute attribute = attributes.FirstOrDefault() as AjaxEnabledAttribute;
25:
26: // If the attribute was not found, display an error
27: if (attribute == null)
28: {
29: context.Response.StatusCode = 403;
30: context.Response.Output.WriteLine("Access to the resource is not allowed");
31: return;
32: }
33:
34:
35: // Check if the request is valiud with regards to the requirements of the attribute.
36: // If not, display error
37: if ((attribute.Method == RequestMethodSupport.GET && context.Request.RequestType != "GET")
38: || (attribute.Method == RequestMethodSupport.POST && context.Request.RequestType != "POST"))
39: {
40: context.Response.StatusCode = 403;
41: context.Response.Output.WriteLine(string.Format("The request method {0} is not allowed.", context.Request.RequestType));
42: return;
43: }
44:
45: // Add user control to the pages control tree
46: page.Controls.Add(viewControl);
47:
48: // Disable caching, remove this if you will allow client caching
49: context.Response.CacheControl = "No-Cache";
50:
51: // Execute and return result to Output stream
52: context.Server.Execute(page, context.Response.Output, true);
53: }
54:
55: public bool IsReusable
56: {
57: get { return true; }
58: }
59: }
To make it work, you need to add the following to httpHandlers tag in the web.config.
<add verb="*" path="*.ascx" type="[NAMESPACE].AjaxUserControlHandler, [ASSEMBLY]"/>
Security
I have implemented an AjaxEnabled attribut, so that it is not possible to directly call any of the user controls (if you know the path to them) in the solution.
The attribute is simple and looks like this:
1: public class AjaxEnabledAttribute : Attribute
2: {
3: [DefaultValue(RequestMethodSupport.All)]
4: public RequestMethodSupport Method { get; set; }
5: }
6:
7: public enum RequestMethodSupport
8: {
9: All,
10: GET,
11: POST
12: }
By doing this, the handler will only allow calls to user controls that have the [AjaxEnabled[ attribute, like this:
1: [AjaxEnabled]
2: public partial class Test : System.Web.UI.UserControl
3: {
4: ...
Conclusion
With the HttpHandler, and the attribute in place, my two concerns (url and security) are dealt with.
You are now able to call a user control directly:
/usercontrols/myusercontrol.ascx
And you are only able to call user controls that have the AjaxEnabled attribute.
Any comments and suggestions are welcome btw
Ahh man, this looks good, just unable to implement your solution, could you possibly post a demo project?
Looking good!
Maybe provide a text-only version of your sources, or a zip-file for easy copy and pasting.
What is happening on line 22 and 23, ‘var’?
Thank you.
I went through the trouble of copying it for my own use, so here’s a more copy-and-paste-friendly version of the source above. Thanks again for the great ideas!
===========================================
public class AjaxUserControlHandler : IHttpHandler
{
public void ProcessRequest(HttpContext context)
{
// Get the path to the user control
string path = context.Request.Url.LocalPath;
// Intialize the pseudo page and user control
Page page = new Page();
UserControl viewControl = page.LoadControl(path) as UserControl;
// Display error if the user control was not found
if (viewControl == null)
{
context.Response.StatusCode = 404;
context.Response.Output.WriteLine("The requested url was not found");
return;
}
// Check existense of the AjaxEnabled attribute. Only add the AjaxEnabled attribut to
// user controls that is safe for direct calls
var type = viewControl.GetType();
var attributes = type.GetCustomAttributes(typeof (AjaxEnabledAttribute), true);
AjaxEnabledAttribute attribute = attributes.FirstOrDefault() as AjaxEnabledAttribute;
// If the attribute was not found, display an error
if (attribute == null)
{
context.Response.StatusCode = 403;
context.Response.Output.WriteLine("Access to the resource is not allowed");
return;
}
// Check if the request is valiud with regards to the requirements of the attribute.
// If not, display error
if ((attribute.Method == RequestMethodSupport.GET && context.Request.RequestType != "GET")
|| (attribute.Method == RequestMethodSupport.POST && context.Request.RequestType != "POST"))
{
context.Response.StatusCode = 403;
context.Response.Output.WriteLine(string.Format("The request method {0} is not allowed.", context.Request.RequestType));
return;
}
// Add user control to the pages control tree
page.Controls.Add(viewControl);
// Disable caching, remove this if you will allow client caching
context.Response.CacheControl = "No-Cache";
// Execute and return result to Output stream
context.Server.Execute(page, context.Response.Output, true);
}
public bool IsReusable
{
get { return true; }
}
}
Thank you Kyle for the copy-paste version.
Great idea and I thank you for all that work, but your example is lacking how to implement it. Kind of like all the wheels and cogs are there, but the screws are missing. Please post a sample project? thanks!
Ok, I will be posting a sample website in just a few minutes.
I am working using this and I was wondering if you were ever able to get this to work using server controls in your usercontrol. I am stuck trying to figure out how to dynamically override(if possible) the Page’s VerifyRenderingInServerForm method. Is there a way to do this dynamically?
This is a pretty nice solution, but for a high usage app/website, I would recommend adding a using statement for the Page object. Not properly disposing of it could lead to some serious memory fragmentation and performance problems.
@rac: I have updated the source code to support async. calling of a WebControl. When using this approach, it is not possible to use the ViewState for these controls, and this excludes using some standard ASP.NET controls. If you want make use of the ViewState, it would be easier to just use Microsoft ASP.NET Ajax partial rendering.
@Steve: You are correct, the first version was coded quickly and only as a sample. But I have updated the sourcecode.
Great idea and I thank you for all that work, but your example is lacking how to implement it. Kind of like all the wheels and cogs are there, but the screws are missing. Please post a sample project?
To run with localhost I made some changes…
Default.aspx:
$("#datetime").load("Controls/Datetime.ascx");
$("#datetime").load("DateTimeServerControl.sch");
AjaxServerControlHander:
string typeName = path.Substring(0, path.Length – 4);
typeName = typeName.Substring(typeName.LastIndexOf(‘/’) + 1);
It works great. Thanks Mikkel!
Instead of messing with custom attributes, you could simply use the following:
<add verb="*" path="*.ajax.ascx" type="[NAMESPACE].AjaxUserControlHandler, [ASSEMBLY]"/>
Simply name your ‘runnable’ usercontrols like that (with ‘.ajax’ in name), works the same.