|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
Note: This is an unedited contribution. If this article is inappropriate,
needs attention or copies someone else's work without reference then please
Report This Article
IntroductionCuyahoga framework has very nice approach to Web development. It has 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 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 web site. BackgroundI previously shared my module development experience with an article titled Developing Simple Issue Tracker Module here on CodeProject. In my issue tracker module I used AjaxToolkit ModalPopupExtender control. But this was an unstructured approach it was a kind of hack,I simply placed ScriptManager on my ASPX page and moved injection code of GeneralPage class to 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 web site BenimOdam.com (The web site is in Turkish). In BenimOdam.com we only use Forum and ContactUs built-in modules and much of the functionality is embedded in our own modules. These modules are mainly functioning as list and record editing modules. We used 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 users point of view. In such uncomfortable situations UpdatePanel control included with 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 much more smooth navigation and probably they will be happier. Cuyahoga InternalsCuyahoga has some principles we must keep in mind before attempting to extend the framework for Ajax support. These are
Blocks of code should be wrapped in <;pre> tags like this: Ajax Support PreperationUpdatePanel included within Microsoft Ajax distirbution and AjaxToolkit controls all require a ScriptManager control placed as the first control in your ASPX page. But Cuyahoga does not handle your modules as seperate 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 emposed by ASP.NET. 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" />
<!--[if IE]>
<style type="text/css" media="screen">
body { behavior: url(<%= Page.ResolveUrl("~/csshover.htc") %> ); /* call hover behaviour file */ }
</style>
<![endif]-->
</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>
<!-- shadow divs -->
<div id="containerleft">
<div id="containertopleft">
<div id="containerright">
<div id="containertopright">
<!-- main -->
<div id="main">
<!--
<div id="globalmenu">
<asp:placeholder id="globalMenu" runat="server"> </asp:placeholder>
</div>
-->
In the sample code above you can see that we 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 met the constraint saying that "Ajax controls need a ScriptManager and this script manager 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 ScriptManager type can not be resolved. Adding Ajax Support To Your ModulesIn theory we do not have to add ScriptManager to our module code, since we 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 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 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 straight forward we will remove ScriptManagers 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 (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 NOTE: We could prefer to modify PageEngine class so that we would inspect all controls and remove the ScriptManagers from the modules. But that would probably cause performance problems since we would have to loop with foreach on modules' Controls collection.May be some modules would not even use AJAX and that would be waste of time inspecting these modules for a ScriptManager control. ( Marker interfaces could be used to identify AJAX modules but still that would be wast of time to loop) Here is the 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 Controls collection and remove it from the collection, which enables us to avoid multiple ScriptManagers problem. IMPORTANT: Cuyahoga PageEngine class applies the template and injects your modules in the overriden OnInit function. Adding Ajax Support To Your Nodeless PagesModules are the primary mains 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 seperate 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 Cuyahoga framework provides us a base class named For 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.
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: Original version of GeneralPage class (base class of our AjaxGeneralPage) handles content loading in the overriden OnInit function (I think this was the only place in .NET 1.1 version to perform content loading). If you leave content loading code inside this function you will not be able to properly remove the ScriptManager control from your page. For details why this is not possible see ASP.NET Page Lifecylcle article on MSDN. To solve this problem we simply move the code in overriden OnInit function to OnPreInit override.That is the right place for content loading and dynamic control creation in .NET version 2.0. Our OnPreIniti function in GeneralPage class looks like this protected override void OnPreInit(EventArgs e)
{
// The GeneralPage loads it's own content. No need for the PageEngine to do that.
base.ShouldLoadContent = false;
//Init the PageEngine.
//NOTE: We replaced base.OnInit with base.OnPreIniti
base.OnPreInit(e);
// Build page.
ControlCollection col = this.Controls;
this._currentSite = base.RootNode.Site;
if (this._currentSite.DefaultTemplate != null
&& this._currentSite.DefaultPlaceholder != null
&& this._currentSite.DefaultPlaceholder != String.Empty)
{
// Load the template
this.TemplateControl = (BaseTemplate)this.LoadControl(UrlHelper.GetApplicationPath()
+ this._currentSite.DefaultTemplate.Path);
// Register css
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;
}
// Add the pagecontrol on top of the control collection of the page
this.TemplateControl.ID = "p";
col.AddAt(0, this.TemplateControl);
// Get the Content placeholder
this._contentPlaceHolder = this.TemplateControl.FindControl(this._currentSite.DefaultPlaceholder) as PlaceHolder;
if (this._contentPlaceHolder != null)
{
// Iterate through the controls in the page to find the form control.
foreach (Control control in col)
{
if (control is HtmlForm)
{
// We've found the form control. Now move all child controls into the placeholder.
HtmlForm formControl = (HtmlForm)control;
while (formControl.Controls.Count > 0)
{
this._contentPlaceHolder.Controls.Add(formControl.Controls[0]);
}
}
}
// throw away all controls in the page, except the page control
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
{
// The default template and placeholders are not correctly configured.
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)
{
// Check view permissions before adding the section to the page.
if (section.ViewAllowed(this.User.Identity))
{
// Create the module that is connected to the section.
ModuleBase module = _moduleLoader.GetModuleFromSection(section);
if (module != null)
{
if (Context.Request.PathInfo.Length > 0 && section == this._activeSection)
{
// Parse the PathInfo of the request because they can be the parameters
// for the module that is connected to the active section.
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 adds support for loading sections attached to the default site template. This code has nothing to do with AJAX support it was an improvement neede for BenimOdam.com Modifying the HttpHandler for AJAX supportAs I mentioned in Cuyahoga Internals section of the article, Cuyahoga framework registers a custom HttpHandler class named
public void ProcessRequest(HttpContext context)
{
string rawUrl = context.Request.RawUrl;
log.Info("Starting request for " + rawUrl);
DateTime startTime = DateTime.Now;
string aspxPagePath = String.Empty;
// Rewrite url
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
// Obtain the handler for the current page
aspxPagePath = rewrittenUrl.Substring(0, rewrittenUrl.IndexOf(".aspx") + 5);
IHttpHandler handler = PageParser.GetCompiledPageInstance(aspxPagePath, null, context);
// Process the page just like any other aspx page
handler.ProcessRequest(context);
// Release loaded modules. These modules are added to the HttpContext.Items collection by the ModuleLoader.
ReleaseModules();
// Log duration
TimeSpan duration = DateTime.Now - startTime;
log.Info(String.Format("Request finshed. Total duration: {0} ms.", duration.Milliseconds));
}
InstallationWe have to modify our Web.config file to enable Ajax support. If we do not add the following configuration information it is likely that
<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" />
<!--Ajax toolkit support-->
<add name=" ScriptModule"
type=" System.Web.Handlers.ScriptModule, System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
</httpModules>
</system.web>
History18 February 2008: Initiali version published
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||