Introduction
The Cuyahoga framework has very nice approach to web development. It has a bunch of built-in modules, and you can develop your own modules in a couple of hours. If you have some experience with NHibernate and/or some other web framework, your module development may even take less than an hour. My product site PragmaSQL Online runs on top of the Cuyahoga framework, and it took me just a couple of hours to bring this site up and running. Although Cuyahoga is a very nice framework and I love Cuyahoga development, I shall admit that you may experience some problems while applying some advanced topics like AJAX to Cuyahoga. In this article, I will show you a simple and structured way to add AJAX support to your Cuyahoga website.
Background
I previously shared my module development experience with an article titled Developing a Simple Issue Tracker Module here on CodeProject. In my issue tracker module, I used the AjaxToolkit ModalPopupExtender control. But, this was an unstructured approach. It was a kind of a hack, I simply placed ScriptManager
on my ASPX page and moved the injection code of the GeneralPage
class to the OnPreInit
method from OnInit
. This was the right choice and it saved the day, this module is still online and is working very well.
Nowadays, I am working on another website, BenimOdam.com (the website is in Turkish). In BenimOdam.com, we only use the Forum and ContactUs built-in modules, and much of the functionality is embedded in our own modules. These modules mainly function as list and record editing modules. We have used the MultiView
and View
controls to provide tabbed browsing functionality, and I guess much of you know that MultiView
does a post back while switching between views, and this post back may be annoying from the user's point of view. In such uncomfortable situations, the UpdatePanel
control included with the Microsoft AJAX distribution (previously known as Atlas) provides a nice and easy to apply solution. You simply put your controls, MultiView
in our case, inside an UpdatePanel
, and you are done. Your users will experience a much more smooth navigation, and probably they will be happier.
Cuyahoga Internals
Cuyahoga has some principles we must keep in mind before attempting to extend the framework for AJAX support. These are:
- Template User Controls with placeholders are used to identify different parts of your pages.
- A custom HttpHandler (
PageHandler
) is used to process requests and a custom UrlWriter
to rewrite raw URLs. - Page structure (sections) and modules contained within the sections are resolved from the database.
- Injection is used to build the resulting pages.
- Modules are designed as User Controls, and you must inherit your module control from
BaseModuleControl
. - You can use the
GeneralPage
base class to build custom ASPX pages not related to any Cuyahoga node that uses the default site template.
AJAX Support Preparation
The UpdatePanel
included within the Microsoft AJAX distribution and the AjaxToolkit controls all require a ScriptManager
control placed as the first control in your ASPX page. But, Cuyahoga does not handle your modules as separate ASPX pages, and injects your module inside the template you are using. As I mentioned above, a template has placeholder controls that make the different parts of your pages. As a result, it is not guaranteed that the ScripManager
control you placed as the first control in your module will also be placed as the first control in the rendered page. Being the first control in the resulting page is a very tight constraint imposed by ASP.NET. The solution to this situation is placing a default ScriptManager
control inside your template user controls as the first control. Here is a sample template User Control code:
<%@ Control Language="c#" AutoEventWireup="false" Inherits="Cuyahoga.Web.UI.BaseTemplate" %>
<%@ Register TagPrefix="cc1" Namespace="Cuyahoga.ServerControls.Navigation"
Assembly="Cuyahoga.ServerControls.Navigation" %>
<%@ Register Assembly="System.Web.Extensions, Version=1.0.61025.0,
Culture=neutral, PublicKeyToken=31bf3856ad364e35"
Namespace="System.Web.UI" TagPrefix="asp" %>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01
Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<title> <asp:literal id="PageTitle" runat="server"> </asp:literal> </title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<asp:literal id="MetaTags" runat="server" />
<asp:literal id="Stylesheets" runat="server" />
</head>
<body>
<form id="t" method="post" runat="server">
<asp:ScriptManager ID="DefaultScriptManager" runat="server">
</asp:ScriptManager>
<div id="container">
<div id="header">
<div id="logo">
<img width="120px" height="100px" alt="BenimOdam.com"
src="http://www.mydomain.com/Templates/Bo/Images/bo_logosmaller.gif"/>
</div>
<div>
<span id="titletext"> Ev arkadaşı ve ev
arayanların buluşma noktası.</span>
</div>
<div id="searcharea">
<asp:placeholder id="searchinput" runat="server">
</asp:placeholder>
</div>
</div>
<div id="nav">
<cc1:menu id="mnuMain" runat="server"
MenuCss = "~/Templates/Bo/Css/Menu.css"> </cc1:menu>
</div>
<div id="containerleft">
<div id="containertopleft">
<div id="containerright">
<div id="containertopright">
<div id="main">
In the sample code above, you can see that we have added a ScriptManager
named DefaultScriptManager
as the first control of the form. That means, DefaultScriptManager
will be injected to all our Cuyahoga pages using this template. As a result, we meet the constraint that says that "AJAX controls need a ScriptManager
and this ScriptManager
must be the first control in the ASPX page".
Note: Do not forget to register System.Web.Extensions.dll, else you will get an exception telling you that the ScriptManager
type can not be resolved.
Adding AJAX Support to your Modules
In theory, we do not have to add a ScriptManager
to our module code, since we have added a default ScriptManager
to our template which will automatically be injected to all of our Cuyahoga pages. But in practice, we will probably want designer support while developing our modules, and if you do not include the ScriptManager
in your module control, the designer will complain and refuse to render the AJAX control, which can make us feel uncomfortable (you can still design your module markup without designer support). When you add a ScriptManager
to your module, the resulting Cuyahoga page will contain more than one ScriptManager
, one from the template and one or many from your modules. Another constraint about the ScriptManager
says that "Only one ScriptManager
can be used in a page", and we have to find a way to remove additional ScriptManager
instances and leave only one ScriptManager
in the resulting page. The solution here is straightforward, we will remove the ScriptManager
s placed in our modules and leave only the DefaultScriptManager
placed in our template control.
Limitations: If you want to use custom JavaScript code for AJAX handling in your module, you have to register your scripts to your ScriptManager
's (the one included in your module code) Scripts
collection. In this case, you will have to rethink the solution I proposed. May be, you will have to invent some interaction that places your custom scripts in the DefaultScriptManager
before removing the ScriptManager
from your module.
As I mentioned above, all of your Cuyahoga modules must be inherited from BaseModuleControl
. But, we have to find a way to remove ScriptManager
s from our module code before they are injected to the template we are using. The solution is to create another base class (AjaxBaseModuleControl
) which supports ScriptManager
removal functionality. AjaxBaseModuleControl
is inherited from BaseModuleControl
and overrides the AddedControl
method. In the AddedControl
function, we try to catch the ScriptManager
control after it is added to the Controls
collection of our module, and remove it from from the collection so that the multiple ScriptManager
s problem is avoided.
Note: We could prefer to modify the PageEngine
class so that we would inspect all controls and remove the ScriptManager
s from the modules. But, that would probably cause performance problems since we would have to loop with a foreach
on the modules' Controls
collection. May be, some modules would not even use AJAX, and that would be a waste of time inspecting those modules for ScriptManager
controls. ( Marker interfaces could be used to identify AJAX modules but still that would be waste of time to loop.)
Here is the AjaxBaseModuleControl
code:
namespace Cuyahoga.Web.UI
{
public class AjaxBaseModuleControl:BaseModuleControl
{
protected override void AddedControl(Control control, int index)
{
if (control.GetType() == typeof(ScriptManager))
this.Controls.RemoveAt(index);
else
base.AddedControl(control, index);
}
}
}
AjaxBaseModuleControl
is simple and straightforward, we simply catch the ScriptManager
after it is added to the Controls
collection, and remove it from the collection, which enables us to avoid the multiple ScriptManager
s problem.
Important: The Cuyahoga PageEngine
class applies the template and injects your modules in the overridden OnInit
function. I would recommend you move the OnInit
code to the overridden OnPreInit
function. That is not necessary for module level AJAX support, but the reason will be more clear when I explain page level AJAX support.
Adding AJAX Support to your Nodeless Pages
Modules are the primary means of Cuyahoga development. But, it is obvious that only modules may not meet all your requirements. For example, you would list records with a module and deploy a separate nodeless page for record editing. We call the record editing page nodeless because this page is not attached to any node in our site structure. For such cases, the Cuyahoga framework provides us a base class named GeneralPage
. You inherit your nodeless page from GeneralPage
, and Cuyahoga automatically applies the default site template (CSS styles and the structure of the page based on the default template) to your page. Actually, your page code is injected, not rendered.
For a nodeless page example, please go to Pragma Issue Tracker and try to view an issue from the issue list. The page used to view a specific issue is a nodeless page, and it is not included in the site structure; we simply redirect to this page from our module, and Cuyahoga injects the page code automatically.
In order to add AJAX support to our nodeless pages, we have to apply the same ideas.
- Add a default
ScriptManager
as the first control to the resulting page. - Automatically remove the
ScriptManager
, added during design time, before Cuyahoga injects our page's source.
The first item was already applied by putting a default ScriptManager
to our template control. For the second item, we create another base class named AjaxGeneralPage
which is inherited from GeneralPage
, and override the AddedControl
method to intercept and catch the ScriptManager
included in our nodeless page.
Here is the code:
namespace Cuyahoga.Web.UI
{
public class AjaxGeneralPage:GeneralPage
{
protected override void AddedControl(Control control, int index)
{
if (control.GetType() == typeof(HtmlForm))
TryToRemoveScriptManager(control as HtmlForm);
else
base.AddedControl(control, index);
}
private void TryToRemoveScriptManager(HtmlForm frm)
{
int idx = -1;
for (int i = 0; i <frm.Controls.Count; i++)
{
if (frm.Controls[i] is ScriptManager)
{
idx = i;
break;
}
}
if (idx >= 0)
frm.Controls.RemoveAt(idx);
}
}
}
Please be warned that we do not catch the ScriptManager
directly as that was the case in AjaxBaseModuleControl
. We catch the HtmlForm
control included within the page and search for ScriptManager
in the form's Controls
collection.
Important note: The original version of the GeneralPage
class (the base class of our AjaxGeneralPage
) handles content loading in the overridden OnInit
function (I think this was the only place in the .NET 1.1 version to perform content loading). If you leave the content loading code inside this function, you will not be able to properly remove the ScriptManager
control from your page. For details of why this is not possible, see the ASP.NET Page Lifecylcle article on MSDN. To solve this problem, we simply move the code in the overridden OnInit
function to the OnPreInit
override. That is the right place for content loading and dynamic control creation in .NET version 2.0.
Our OnPreInit
function in the GeneralPage
class looks like this:
protected override void OnPreInit(EventArgs e)
{
base.ShouldLoadContent = false;
base.OnPreInit(e);
ControlCollection col = this.Controls;
this._currentSite = base.RootNode.Site;
if (this._currentSite.DefaultTemplate != null
&& this._currentSite.DefaultPlaceholder != null
&& this._currentSite.DefaultPlaceholder != String.Empty)
{
this.TemplateControl =
(BaseTemplate)this.LoadControl(UrlHelper.GetApplicationPath()
+ this._currentSite.DefaultTemplate.Path);
string css = UrlHelper.GetApplicationPath()
+ this._currentSite.DefaultTemplate.BasePath
+ "/Css/" + this._currentSite.DefaultTemplate.Css;
RegisterStylesheet("maincss", css);
if (this._title != null)
{
this.TemplateControl.Title = this._title;
}
this.TemplateControl.ID = "p";
col.AddAt(0, this.TemplateControl);
this._contentPlaceHolder = this.TemplateControl.FindControl(
this._currentSite.DefaultPlaceholder) as PlaceHolder;
if (this._contentPlaceHolder != null)
{
foreach (Control control in col)
{
if (control is HtmlForm)
{
HtmlForm formControl = (HtmlForm)control;
while (formControl.Controls.Count > 0)
{
this._contentPlaceHolder.Controls.Add(formControl.Controls[0]);
}
}
}
while (col.Count > 1)
{
col.Remove(col[1]);
}
}
#region // Ali Ozgur (07-02-2008): Load sections that are related to the template
foreach (DictionaryEntry sectionEntry in _currentSite.DefaultTemplate.Sections)
{
string placeholder = sectionEntry.Key.ToString();
Section section = sectionEntry.Value as Section;
if (section != null)
{
BaseModuleControl moduleControl =
CreateModuleControlForSection(section);
if (moduleControl != null)
{
((PlaceHolder)
this._templateControl.Containers[placeholder]).
Controls.Add(moduleControl);
}
}
}
#endregion
}
else
{
throw new Exception("Unable to display page because" +
" the default template is not configured.");
}
}
#region // Ali Ozgür 07-02-2008 : Load sections that are related to the template
private BaseModuleControl CreateModuleControlForSection(Section section)
{
if (section.ViewAllowed(this.User.Identity))
{
ModuleBase module = _moduleLoader.GetModuleFromSection(section);
if (module != null)
{
if (Context.Request.PathInfo.Length > 0 &&
section == this._activeSection)
{
module.ModulePathInfo = Context.Request.PathInfo;
}
return LoadModuleControl(module);
}
}
return null;
}
private BaseModuleControl LoadModuleControl(ModuleBase module)
{
BaseModuleControl ctrl = (BaseModuleControl)this.LoadControl(
UrlHelper.GetApplicationPath() + module.CurrentViewControlPath);
ctrl.Module = module;
return ctrl;
}
#endregion
In the GeneralPage
code snippet presented above, you will notice regions of code that add support for loading sections attached to the default site template. This code has nothing to do with AJAX support; it was an improvement needed for BenimOdam.com.
Modifying the HttpHandler for AJAX support
As I mentioned in the Cuyahoga Internals section of the article, the Cuyahoga framework registers a custom HttpHandler class named PageHandler
to handle page requests. This handler is needed as a result of the injection practice used in the framework. Cuyahoga does not actually render physical pages or user controls. The page structure is retrieved from the database (sections, and modules within these sections) and modules are instantiated during runtime, and the final page is constructed by Cuyahoga by injecting the module code and the page template to a resulting page. Since there is only one physical page called Default.aspx (actually, there are some more physical pages as Error.aspx and Install.aspx), all page requests must be handled by a custom HttpHandler and resolved so that proper pages with proper sections and modules can be constructed at runtime.
The PageHandler
class implements the IHttpHandler
interface and the IRequiresSessionState
marker interface. PageHandler
utilizes Cuyahoga'a custom UrlRewriter
class which is used to rewrite requested URLs. UrlRewriter
produces URLs that are meaningful for the framework and used for building the right result page. But unfortunately, HTTP requests caused by AJAX calls can not be handled properly by PageHandler
because Cuyahoga's UrlRewriter
can not rewrite the right URL for AJAX calls, which in turn results in a resource not found exception thrown by the handler. To overcome this problem, we have to slightly modify the PageHandler
class' ProcessRequest
function. Here is the code:
public void ProcessRequest(HttpContext context)
{
string rawUrl = context.Request.RawUrl;
log.Info("Starting request for " + rawUrl);
DateTime startTime = DateTime.Now;
string aspxPagePath = String.Empty;
UrlRewriter urlRewriter = new UrlRewriter(context);
string rewrittenUrl = urlRewriter.RewriteUrl(rawUrl);
#region //Ali Ozgur: This is an ajax request, so we have to realign the rewritten url.
if (context.Request["HTTP_X_MICROSOFTAJAX"] != null)
{
int idx = rewrittenUrl.ToLowerInvariant().IndexOf("/default.aspx");
if (idx >= 0)
{
rewrittenUrl = rewrittenUrl.Substring(idx, rewrittenUrl.Length - idx);
}
}
#endregion
aspxPagePath = rewrittenUrl.Substring(0, rewrittenUrl.IndexOf(".aspx") + 5);
IHttpHandler handler =
PageParser.GetCompiledPageInstance(aspxPagePath, null, context);
handler.ProcessRequest(context);
ReleaseModules();
TimeSpan duration = DateTime.Now - startTime;
log.Info(String.Format("Request finshed. Total duration: {0} ms.",
duration.Milliseconds));
}
Installation
We have to modify our Web.config file to enable AJAX support. If we do not add the following configuration information, it is likely that we will get a "Sys not defined" error when our module tries to execute AJAX related code.
<system.web >
<httpHandlers >
<remove verb="*" path="*.asmx" />
<add verb="*" path=" Error.aspx"
type=" System.Web.UI.PageHandlerFactory" />
<add verb="*" path=" *.aspx"
type=" Cuyahoga.Web.HttpHandlers.PageHandler, Cuyahoga.Web" />
<add verb="*" path=" *.asmx" validate=" false"
type=" System.Web.Script.Services.ScriptHandlerFactory, System.Web.Extensions,
Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
<add verb="*" path=" *_AppService.axd" validate=" false"
type=" System.Web.Script.Services.ScriptHandlerFactory, System.Web.Extensions,
Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
<add verb=" GET,HEAD" path=" ScriptResource.axd"
type=" System.Web.Handlers.ScriptResourceHandler, System.Web.Extensions,
Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
validate=" false" />
</httpHandlers>
<httpModules>
<add type=" Cuyahoga.Web.HttpModules.AuthenticationModule, Cuyahoga.Web"
name=" AuthenticationModule" />
<add type=" Cuyahoga.Web.HttpModules.CoreRepositoryModule, Cuyahoga.Web"
name=" CoreRepositoryModule" />
<add name=" NHibernateSessionWebModule"
type=" Castle.Facilities.NHibernateIntegration.Components.SessionWebModule,
Castle.Facilities.NHibernateIntegration" />
<add name=" ScriptModule"
type=" System.Web.Handlers.ScriptModule, System.Web.Extensions,
Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
</httpModules>
</system.web>
History
- 18 February 2008: Initial version published.