вторник, 16 октября 2007 г.

Практика с XSLT в ASP.NET

Часто возникает необходимость в небольшом сайте, но ASP.NET оказывается слишком тяжеловесен, статичный HTML не устраивает совмещением данных и представления, а PHP или SSI просто брезгуешь.

В случае же использования XSLT кодирование займет ненамного больше времени, чем в случае с HTML. В этом посте я рассмотрю 2 варианта развертывания XSLT-сайта под ASP.NET.

Трансформацию на стороне клиента не рассматриваю, потому как на любителя это. Не в том смысле, что простые пути - не наш выбор, а в том, что выставлять данные напоказ... хмм... нет.

Вариант первый: собственный рендерер страницы

Пример:

/test1/default.aspx:

<%@ Page Language="C#" AutoEventWireup="true" 
CodeFile="Default.aspx.cs" Inherits="_Default" %>
//
<root title="Page Title">
<data>
<item text="node 1 contents" />
<item text="node 2 contents" />
<item text="node 3 contents" />
</data>
</root> 

/test1/default.aspx.cs:

public partial class _Default : XslTransformPage 
{
public _Default() : base("test1.xsl") { }
}

Как видно, содержимое страницы представляет собой данные в формате XML. И страница эта наследуется от XslTransformPage, реализующего функционал XSL преобразования:

/app_code/xsltransformpage.cs:

using System;
using System.IO;
using System.Text;
using System.Xml;
using System.Xml.Xsl;
using System.Web;
using System.Web.Caching;
using System.Web.UI;
//
public class XslTransformPage : Page
{
public XslTransformPage() { }
//
public XslTransformPage(string styleSheetFile)
{
this.styleSheetFile = styleSheetFile;
}
//
private string styleSheetFile;
public string StyleSheetFile
{
get { return styleSheetFile; }
set { styleSheetFile = value; }
}
//
protected override void Render(HtmlTextWriter writer)
{
if (Cache[Request.RawUrl] != null)
{
writer.Write(Cache[Request.RawUrl].ToString());
return;
}
//
string xslPath = Server.MapPath("~/App_Code/" + styleSheetFile);
if (!File.Exists(xslPath))
throw new HttpException(404, "Cannot find " + Request.RawUrl);
//
XslCompiledTransform transform = new XslCompiledTransform();
transform.Load(xslPath);
//
StringBuilder inBuilder = new StringBuilder();
StringBuilder outBuilder = new StringBuilder();
//
using (StringWriter sw = new StringWriter(inBuilder))
using (HtmlTextWriter hw = new HtmlTextWriter(sw))
{
//Получает содержимое страницы, т.е. данные в xml
base.Render(hw);
//
using (StringReader sr = new StringReader(inBuilder.ToString()))
using (XmlReader xr = XmlReader.Create(sr))
//
using (XmlWriter xw = XmlWriter.Create(outBuilder))
transform.Transform(xr, xw);
}
//
Cache.Add(Request.RawUrl, outBuilder.ToString(),
new CacheDependency(new string[] { Request.PhysicalPath, xslPath }),
Cache.NoAbsoluteExpiration,
new TimeSpan(24, 0, 0),
CacheItemPriority.Normal, null);
//
writer.Write(outBuilder.ToString());
}
} 
Кроме преобразования здесь используется также кеширование результата преобразования. 

Вариант второй: HttpHandler на .aspx

Как, наверное, понятно, используется HttpHandler, навешиваемый на расширение .aspx, вот таким образом:

app.config:

<httpHandlers>  <add verb="*" path="*.aspx" type="XslTransformHandler"/>
</httpHandlers>

Теперь сам обработчик:

/app_code/xsltransformhandler.cs:

using System;
using System.IO;
using System.Text;
using System.Web;
using System.Web.Caching;
using System.Xml;
using System.Xml.Xsl;
//
public class XslTransformHandler : IHttpHandler
{
public void ProcessRequest(HttpContext context)
{
string path = context.Request.Path.Substring(context.Request.ApplicationPath.Length);
//
ProcessRequest(context, path);
}
//
public void ProcessRequest(HttpContext context, string path)
{
context.Response.ContentType = "text/html";
//
if (context.Cache[path] == null)
{
string xslPath = String.Empty;
string xmlPath = String.Empty;
//
string xslFile = String.Empty;
string xmlFile = String.Empty;
string xmlDir = String.Empty;
//
string[] parts = path.Contains("/") ?
path.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries) :
new string[] { path };
//
// Замечу, что файл стилей будет один для каждой папки, для корневой: 
//   default.xsl, ...
if (parts.Length == 1)
{
xslFile = "default.xsl";
xmlFile = Path.GetFileNameWithoutExtension(parts[0]) + ".xml";
xmlDir = "";
}
//   ... для вложенных - вида папка.папка...папка.xsl
else
{
xslFile = String.Join(".", parts, 0, parts.Length - 1) + ".xsl";
xmlFile = Path.GetFileNameWithoutExtension(parts[parts.Length - 1]) + ".xml";
xmlDir = String.Join("/", parts, 0, parts.Length - 1);
if (xmlDir.Length > 0)
xmlDir += "/";
}
//
xslPath = context.Server.MapPath("~/App_Code/" + xslFile);
xmlPath = context.Server.MapPath("~/App_Data/" + xmlDir + xmlFile);
//
if (!File.Exists(xmlPath) || !File.Exists(xslPath))
throw new HttpException(404, "Cannot find" + context.Request.RawUrl);
//
XslCompiledTransform transform = new XslCompiledTransform();
transform.Load(xslPath);
//
StringBuilder sb = new StringBuilder();
using (XmlReader xr = XmlReader.Create(xmlPath))
using (XmlWriter xw = XmlWriter.Create(sb))
transform.Transform(xr, xw);
//
context.Cache.Add(path, sb.ToString(),
new CacheDependency(new string[] { xmlPath, xslPath }),
Cache.NoAbsoluteExpiration,
new TimeSpan(12, 0, 0),
CacheItemPriority.Normal, null);
//
context.Response.Write(sb.ToString());
}
else
context.Response.Write(context.Cache[path].ToString());
}
//
public bool IsReusable
{
get { return false; }
}
} 

Пример:

Для запрашиваемого /test2/default.aspx данные будут распологаться в /App_Data/test2/default.xml, а стили в /App_Code/test2.xsl

Для запрашиваемого /test2/subdir/page.aspx: в /App_Data/test2/subdir/page.xml и /App_Code/test2.subdir.xsl соответственно.

А самого файла .aspx может и не существовать, либо в случае со страницами по умолчанию (например, default.aspx) он может присутствовать и быть пустым, чтобы IIS был в курсе о ее существовании.

Также, как и в случае со статичными данными, xml может быть получен и из БД (SELECT FOR XML) и вообще откуда угодно, и оба описанных метода можно приспособить в том числе и для этого.

Да, немногословно как-то получилось :)

Комментариев нет: