Introduction
One of the major disadvantages of using XSLTs in your code is the fact that you can't use ASP.NET controls inside XSLT code. Well... at least until now!
Why can't I use ASP.NET controls inside XSLT?
You can't use ASP.NET controls inside XSLT because when the XSLT/XML transformation is made, the page that is dynamically created and contains all the ASP.NET controls is already created. Remember that ASP.NET pages are dynamically compiled on demand when first required in the context of a Web application.
A possible solution
Now, suppose you've some ASP.NET controls inside your XSLT that you want to use.
="1.0" ="UTF-8"
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:msxsl="urn:schemas-microsoft-com:xslt"
xmlns:asp="remove"
exclude-result-prefixes="msxsl vt">
<xsl:output method="html" version="4.0"/>
<xsl:template match="/">
<div>
<asp:TextBox runat="server"
ID="NomeCompleto"><xsl:value-of select="c"/></asp:TextBox>
</div>
</xsl:template>
</xsl:stylesheet>
Note that I've declared the asp
namespace so the XSLT processor knows the "new" namespace.
You have to execute the XSLT/XML transformation, store the HTML result in a string, and remove the string xmlns:asp="remove"
from it.
The next thing you need to do is execute the ParseControl
method, passing the string with the HTML of the transformation as a parameter. This method returns a reference to a control. In fact, this control has all the ASP.NET controls that were inside your XSLT.
Next, you have to add that collection of controls to your page's control collection, or if you want, to a placeholder control collection.
Remember that all this code must be executed on the Init
event of the page, because on the Init
event, the viewstate isn't already restored and server-side events aren't already fired. To get a reference to one of the newly created controls, you have to call the FindControl
method... for each control that you want to use.
private void Page_Init(object sender, System.EventArgs e)
{
string xml = string html = XMLFunctions.GetXSLXMLFile(
Server.MapPath("../XSL/A.xslt"), xml);
html = html.Replace("xmlns:asp=\"remove\"", String.Empty); Control ctrl = ParseControl(html); OutPutPlace.Controls.Add(ctrl);
Button okBtn = this.Page.FindControl("OK") as Button;
okBtn.Click += new System.EventHandler(this.OK_Click);}
Disadvantages
The fact that you've to call the ParseControl
method to get the control collection, and the calls to FindControl
to get a reference to a control.
The challenge
The challenge is to make the XSLT/XML transformations, if any, before the ASP.NET page for the current request is created.
The solution
To meet the challenge, we have to run some code inside the HTTP Pipeline before the PageHandlerFactory
runs. The page handler factory is responsible for either finding the assembly that contains the page class or dynamically creating an ad hoc assembly.
I use an HttpModule to capture all the requests to my ASP.NET pages. If the requested page has a special element that I've defined ("AspXsl:Out
"), then the module must make an XSLT/XML transformation.
Obviously, the result of the transformation can't be injected in the current page because then I would lose the information that I need to make the transformation itself, such as the XSL file and the XML file or method to invoke to get the XML.
So, I create a new page with the same name as the page that was requested and save it in a temporary directory that I've called DinTemp. In this new page, I inject the HTML of the XSLT/XML transformations.
When all transformations are done, the HTTP Pipeline work is redirected to the newly created page.
To improve performance, I save the path of the newly created file in the cache with the following FileDependecies:
- The requested page itself
- All the XSLT files that are needed to create the final ASP.NET page
This way, any changes made to one of the listed files will cause my module to recreate the temporary ASP.NET file with the result of all XSLT/XML transformations.
Time to see some code.
private void app_BeginRequest(object sender, EventArgs e)
{
HttpApplication app = (HttpApplication)sender;
HttpRequest request = app.Request;
HttpContext context = app.Context;
if(request.Url.AbsoluteUri.IndexOf("DinTemp")==-1)
{
string siteName = GetSiteName(request.Url);
string pathPageName = GetRequestedPagePath(request.Url, siteName);
if(context.Cache[pathPageName]==null)
{
#region Do work
string pageName = GetRequestedPageName(request.Url);
string dinPagePath =
context.Server.MapPath(siteName+@"DinTemp\")+pathPageName;
string html = ProcessHtml(siteName, pageName,
context.Server.MapPath( pageName ), context);
using (StreamWriter sw = new StreamWriter(dinPagePath))
sw.Write(html);
#endregion
}
RedirectToGeneratedPage(request, context, @"\DinTemp\"+pathPageName);
}
}
Great! How can I use this module in my ASP.NET pages?
The new element that this module uses is AspXsl:Out
, and has the following attributes:
Xsl
: The path to the XSL file
XmlFile
: The path to the XML file
XmlMethod
: The method of the page to call to get the XML
You can have an ASP.NET page called C.aspx with this code:
<%@ Page Trace="true" language="c#"
Codebehind="C.aspx.cs" AutoEventWireup="false" Inherits="AspXsl.C" %>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" >
<HTML>
<HEAD>
<title>C</title>
<meta name="GENERATOR" Content="Microsoft Visual Studio .NET 7.1">
<meta name="CODE_LANGUAGE" Content="C#">
<meta name="vs_defaultClientScript" content="JavaScript">
<meta name="vs_targetSchema" content="http://schemas.microsoft.com/intellisense/ie5">
</HEAD>
<body>
<form id="Form1" method="post" runat="server">
<div>
<AspXsl:Out id="target" Xsl="Xsl/C.xslt" XmlMethod="GetXml" />
</div>
<div>
<asp:Button Runat="server" ID="ok" Text="Post it!" />
</div>
<div>
<asp:Label Runat="server" ID="Msg" />
</div>
</form>
</body>
</HTML>
Notice that AspXsl:Out
element again? I'm telling my HttpModule that I have an XSLT file in "Xsl/C.xslt" and I want to transform the XML that the GetXml
method returns.
Let's look at the code-behind:
namespace AspXsl
{
public class C : System.Web.UI.Page
{
protected System.Web.UI.WebControls.Button ok;
protected System.Web.UI.WebControls.Label Msg;
protected TextBox NomeCompleto;
private void Page_Load(object sender, System.EventArgs e)
{
}
public static string GetXml(HttpRequest request){return "<c>Xml is fun</c>";}
private void ok_Click(object sender, System.EventArgs e)
{
Msg.Text = "Olá "+NomeCompleto.Text;
}
}
}
See? The code looks like the code that you have in your other ASP.NET pages! How great is that?
Conclusion
I hope that with this HttpModule, you can get the best of the two worlds: XSLT and ASP.NET. You have the code for the HttpModule and a demo web app as well to try! As always, tell what you think!