PageRenderTime 39ms CodeModel.GetById 16ms RepoModel.GetById 1ms app.codeStats 0ms

/src/XsltView.cs

https://github.com/Q42/XsltViewEngine
C# | 223 lines | 158 code | 31 blank | 34 comment | 13 complexity | 78663fd2c98be8594629fc3c2883f93d MD5 | raw file
  1. using System;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using System.Linq;
  5. using System.Reflection;
  6. using System.Web;
  7. using System.Web.Caching;
  8. using System.Web.Mvc;
  9. using System.Xml;
  10. using System.Xml.Linq;
  11. using System.Xml.XPath;
  12. using System.Xml.Xsl;
  13. using System.Web.Mvc.Html;
  14. using System.Linq;
  15. using System.Text;
  16. namespace Q42.Mvc.XsltViewEngine
  17. {
  18. public delegate object PluginConstructor(ViewContext viewContext, IViewDataContainer viewDataContainer);
  19. public sealed class Utf8StringWriter : StringWriter
  20. {
  21. public override Encoding Encoding { get { return Encoding.UTF8; } }
  22. }
  23. public class XsltView : IView, IViewDataContainer
  24. {
  25. private static string defaultViewsPath = resolvePath("~/Views");
  26. private XslCompiledTransform xsl = null;
  27. private XsltArgumentList arguments = null;
  28. private ViewContext viewContext = null;
  29. private Dictionary<string, PluginConstructor> pluginConstructors = null;
  30. public XsltView(ControllerContext controllerContext, string partialPath, Dictionary<string, PluginConstructor> pluginConstructors)
  31. {
  32. xsl = getXsl(partialPath);
  33. arguments = new XsltArgumentList();
  34. this.pluginConstructors = pluginConstructors;
  35. }
  36. public XsltView(ControllerContext controllerContext, string viewPath, string masterPath, Dictionary<string, PluginConstructor> pluginConstructors)
  37. {
  38. xsl = getXsl(viewPath);
  39. arguments = new XsltArgumentList();
  40. this.pluginConstructors = pluginConstructors;
  41. }
  42. #region IView Members
  43. public void Render(ViewContext viewContext, TextWriter writer)
  44. {
  45. this.viewContext = viewContext;
  46. // add plugins, model, viewdata and tempdata
  47. Plugin.AddAll(arguments);
  48. addObject("Model", viewContext.ViewData.Model);
  49. foreach (string key in viewContext.ViewData.Keys)
  50. addObject(key, viewContext.ViewData[key]);
  51. foreach (string key in viewContext.TempData.Select(pair => pair.Key))
  52. addObject(key, viewContext.TempData[key]);
  53. // add our html helper wrapper
  54. arguments.AddExtensionObject("urn:HtmlHelper", new HtmlHelperWrapper(viewContext, this));
  55. foreach (KeyValuePair<string, PluginConstructor> plugin in pluginConstructors)
  56. arguments.AddExtensionObject(plugin.Key, plugin.Value(viewContext, this));
  57. // set the content type based on the xsl
  58. HttpResponseBase response = viewContext.HttpContext.Response;
  59. string mediaType = getMediaType();
  60. // when media-type is set to an xsl file, transform to xml, otherwise to response
  61. #region optional xsl pipelining
  62. while (!String.IsNullOrEmpty(mediaType) && mediaType.Contains(".xsl"))
  63. {
  64. string transformXsl = resolvePath(mediaType);
  65. XmlDocument output = new XmlDocument();
  66. using (XmlWriter tempWriter = XmlWriter.Create(output.CreateNavigator().AppendChild()))
  67. {
  68. xsl.Transform(new XDocument().CreateReader(), arguments, tempWriter);
  69. }
  70. xsl = getXsl(transformXsl);
  71. mediaType = getMediaType();
  72. }
  73. #endregion
  74. // set the mediatype
  75. response.ContentType = String.IsNullOrEmpty(mediaType) ? "text/html" : mediaType;
  76. // use this helper class to avoid creating a document that advertises itself incorrectly as utf-16
  77. // see: http://stackoverflow.com/questions/1564718/using-stringwriter-for-xml-serialization#1564727
  78. StringWriter sw = new Utf8StringWriter();
  79. using (XmlWriter xmlWriter = XmlWriter.Create(sw, xsl.OutputSettings))
  80. {
  81. xsl.Transform(new XDocument().CreateReader(), arguments, xmlWriter);
  82. }
  83. // clean up plugin namespaces
  84. string result = sw.ToString();
  85. result = Plugin.StripXmlNamespaces(result);
  86. writer.Write(result);
  87. }
  88. #endregion
  89. /// <summary>
  90. /// Returns an Xsl file
  91. /// </summary>
  92. /// <param name="fileName"></param>
  93. /// <returns></returns>
  94. private static XslCompiledTransform getXsl(string fileName)
  95. {
  96. // complete the fileName
  97. fileName = resolvePath(fileName);
  98. // check cache for an available xsl
  99. string cacheKey = "Q42.Mvc." + fileName;
  100. XslCompiledTransform xsl = HttpRuntime.Cache[cacheKey] as XslCompiledTransform;
  101. if (xsl == null)
  102. {
  103. xsl = new XslCompiledTransform(true);
  104. try
  105. {
  106. xsl.Load(fileName);
  107. }
  108. catch(Exception ex)
  109. {
  110. throw new Exception("Error loading " + fileName, ex);
  111. }
  112. // make cache dependant on filechanges within the view path
  113. // also try to add the current filename as it might live outside of ~/Views (defaultViewsPath);
  114. var viewDir = Path.GetDirectoryName(fileName);
  115. var viewDirs = GetDirectories(viewDir);
  116. var defaultViewDirs = GetDirectories(defaultViewsPath);
  117. var dirList = viewDirs.Concat(defaultViewDirs).DistinctBy(s => s);
  118. HttpRuntime.Cache.Insert(cacheKey, xsl, new CacheDependency(dirList.ToArray()));
  119. }
  120. return xsl;
  121. }
  122. private static IEnumerable<string> GetDirectories(string path)
  123. {
  124. if (!Directory.Exists(path))
  125. return Enumerable.Empty<string>();
  126. DirectoryInfo dir = new DirectoryInfo(path);
  127. DirectoryInfo[] dirs = dir.GetDirectories("*", SearchOption.AllDirectories);
  128. List<string> dirList = new List<string>();
  129. dirList.Add(dir.FullName);
  130. foreach (DirectoryInfo d in dirs) dirList.Add(d.FullName);
  131. return dirList;
  132. }
  133. /// <summary>
  134. /// Resolves a virtual path to an absolute path
  135. /// </summary>
  136. /// <param name="path">Virtual path to resolve</param>
  137. /// <returns></returns>
  138. private static string resolvePath(string path)
  139. {
  140. if (path.StartsWith("~"))
  141. {
  142. string root = HttpRuntime.AppDomainAppPath.Replace("/", "\\");
  143. path = path.Replace("~", root + "\\").Replace("\\\\", "\\");
  144. }
  145. return new FileInfo(path).FullName;
  146. }
  147. /// <summary>
  148. /// Retrieves the media type set by the xsl's output element.
  149. /// This is done by reflection due to its protection level.
  150. /// </summary>
  151. /// <returns>String</returns>
  152. private string getMediaType()
  153. {
  154. try
  155. {
  156. return typeof(XmlWriterSettings).GetField("mediaType", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(xsl.OutputSettings) as string;
  157. }
  158. catch
  159. {
  160. return null;
  161. }
  162. }
  163. /// <summary>
  164. /// Adds an object as parameter to the xsl, so it can be obtained from within xsl by a global param
  165. /// definition with the same name.
  166. /// The object will be serialized automatically if needed to.
  167. /// </summary>
  168. /// <param name="name"></param>
  169. /// <param name="obj"></param>
  170. private void addObject(string name, object obj)
  171. {
  172. if (obj == null) return;
  173. if (obj is ValueType || obj is String || obj is XmlNode)
  174. arguments.AddParam(name, "", obj);
  175. else
  176. {
  177. XDocument xObj = obj as XDocument;
  178. if (xObj != null)
  179. arguments.AddParam(name, "", xObj.Document);
  180. else
  181. arguments.AddParam(name, "", ((XmlDocument)Serializer.ToXml(obj)).DocumentElement);
  182. }
  183. }
  184. #region IViewDataContainer Members
  185. public ViewDataDictionary ViewData
  186. {
  187. get { return viewContext.ViewData; }
  188. set { throw new NotSupportedException(); }
  189. }
  190. #endregion
  191. }
  192. }