The idea is compiling FreeMarker to .Net assembly with IKVM and create a wrapper to wrap .Net objects so that FreeMarker can understand.
Compiling FreeMarker
It is a strength forward process. It can be done with one command:
ikvmc freemarker.jar -target:library
Separating Necessary IKVM Libraries
Only the following libraries is required for development:
- IKVM.OpenJDK.Beans.dll
- IKVM.OpenJDK.Charsets.dll
- IKVM.OpenJDK.Core.dll
- IKVM.OpenJDK.SwingAWT.dll
- IKVM.OpenJDK.Text.dll
- IKVM.OpenJDK.Util.dll
- IKVM.Runtime.dll
Wrapping .Net Object
FreeMarker does not directly deal with the objects. Instead, it deals with the
TemplateModel
objects. There are a few template models to be implemented:- TemplateBooleanModel
- TemplateDateModel
- TemplateHashModel
- TemplateMethodModelEx
- TemplateNumberModel
- TemplateScalarModel
- TemplateSequenceModel
ObjectWrapper
interface to wrap the raw objects into TemplateModel
.The
NetObjectModel
is actually is a TemplateHashModel
public class NetObjectModel : StringModel, TemplateModel, TemplateHashModel { Dictionary<string, PropertyInfo> props = new Dictionary<string, PropertyInfo>(); Dictionary<string, MethodModel> methods = new Dictionary<string, MethodModel>(); Dictionary<string, ExtensionMethodModel> extensionMethods = new Dictionary<string, ExtensionMethodModel>(); public NetObjectModel(object data, NetObjectWrapper wrapper) : base(data, wrapper){ var type = data.GetType(); foreach (var p in type.GetProperties()) { props.Add(p.Name, p); } foreach (var m in type.GetMethods()) { if (!methods.ContainsKey(m.Name)) { methods.Add(m.Name, new MethodModel(data, wrapper, m.Name)); } } } public virtual TemplateModel get(string key) { if (props.ContainsKey(key)) { return wrapper.wrap(props[key].GetGetMethod() .Invoke(data, null)); } else if (methods.ContainsKey(key)) { return methods[key]; } else if (wrapper.ExtensionTypes.Count > 0) { if (!extensionMethods.ContainsKey(key)) { extensionMethods[key] = new ExtensionMethodModel(data, wrapper, key); } return extensionMethods[key]; } else { return TemplateModel.__Fields.NOTHING; } } public virtual bool isEmpty() { return props.Count == 0; } }
To adapt the ASP.NET objects, three more template model are created:
- HttpApplicationStateModel
- HttpRequestModel
- HttpSessionStateModel
public class HttpApplicationStateModel : NetObjectModel, TemplateModel, TemplateHashModel { public HttpApplicationStateModel( object data, NetObjectWrapper wrapper) : base(data, wrapper) { } public override TemplateModel get(string key) { HttpApplicationStateBase dic = data as HttpApplicationStateBase; if (dic.Keys.Cast<string>().Contains(key)) { return wrapper.wrap(dic[key]); } else { return base.get(key); } } public override bool isEmpty() { IDictionary<string, object> dic = data as IDictionary<string, object>; return dic.Count == 0 && base.isEmpty(); } }View Engine
The view engine is fairly simple, it simply initialize the FreeMarker configuration.
public class FreemarkerViewEngine : VirtualPathProviderViewEngine { Configuration config; public FreemarkerViewEngine(string rootPath, string encoding = "utf-8") { config = new Configuration(); config.setDirectoryForTemplateLoading( new java.io.File(rootPath)); AspNetObjectWrapper wrapper = new AspNetObjectWrapper(); config.setObjectWrapper(wrapper); base.ViewLocationFormats = new string[] { "~/Views/{1}/{0}.ftl" }; config.setDefaultEncoding(encoding); base.PartialViewLocationFormats = base.ViewLocationFormats; } protected override IView CreatePartialView( ControllerContext controllerContext, string partialPath) { return new FreemarkerView(config, partialPath); } protected override IView CreateView( ControllerContext controllerContext, string viewPath, string masterPath) { return new FreemarkerView(config, viewPath); } }
View
FreemarkerView
implements IView
to render the content. It simply create the dictionary as variable bindings and invoke the FreeMarker.public class FreemarkerView : IView{ public class ViewDataContainer : IViewDataContainer { private ViewContext context; public ViewDataContainer(ViewContext context) { this.context = context; } public ViewDataDictionary ViewData { get { return context.ViewData; } set { context.ViewData = value; } } } private freemarker.template.Configuration config; private string viewPath; public FreemarkerView(freemarker.template.Configuration config, string viewPath) { this.config = config; this.viewPath = viewPath; } public void Render(ViewContext viewContext, System.IO.TextWriter writer) { Template temp = config.getTemplate(viewPath.Substring(2)); Dictionary<string, object> data = new Dictionary<string, object>{ {"model", viewContext.ViewData.Model}, {"session", viewContext.HttpContext.Session}, {"http", viewContext.HttpContext}, {"request", viewContext.HttpContext.Request}, {"application", viewContext.HttpContext.Application}, {"view", viewContext}, {"controller", viewContext.Controller}, {"url", new UrlHelper(viewContext.RequestContext)}, {"html", new HtmlHelper(viewContext, new ViewDataContainer(viewContext))}, {"ajax", new AjaxHelper(viewContext, new ViewDataContainer(viewContext))}, }; Writer output = new JavaTextWriter(writer); temp.process(data, output); output.flush(); } }Configuration
Configuration is as simple as adding the view engine to the view engine collections.
protected void Application_Start() { AreaRegistration.RegisterAllAreas(); // ****** Optionally, you can remove all other view engines ****** //ViewEngines.Engines.Clear(); ViewEngines.Engines.Add(new FreemarkerViewEngine(this.Server.MapPath("~/"))); RegisterGlobalFilters(GlobalFilters.Filters); RegisterRoutes(RouteTable.Routes); }
Extension Methods
ASP.NET MVC relies on extension method quite heavily. In the
NetObjectWrapper
, the following code is added to find the extension methods:public virtual void AddExtensionNamespace(string ns) { var types = this.ExtensionTypes; foreach (var a in AppDomain.CurrentDomain.GetAssemblies()) { try { foreach (var t in a.GetExportedTypes()) { if (!types.Contains(t) && t.IsClass && t.Name.EndsWith("Extensions") && t.Namespace == ns && t.GetConstructors().Length == 0) { types.Add(t); } } } catch { } } }In the view engine's constructor, it reads the namespaces specified under
system.web/pages
sections in web.config:PagesSection section = (PagesSection)WebConfigurationManager .OpenWebConfiguration("~/") .GetSection("system.web/pages"); if (section != null) { foreach (NamespaceInfo info in section.Namespaces) { wrapper.AddExtensionNamespace(info.Namespace); } }An
ExtensionMethodModel
class is created to find the appropriate method to invoke:foreach (var type in wrapper.ExtensionTypes) { MethodInfo method = type.GetMethod( methodName, BindingFlags.Public | BindingFlags.Static, Type.DefaultBinder, argTypes, null); if (method != null) { cache.Add(key, method); return wrapper.wrap(method.Invoke(target, args.ToArray())); } }Localization
ResourceManagerDirectiveModel
(a TemplateDirectiveModel
) and ResourceManagerModel
are created so that we could do something like this:<@resource type="Freemarker.Net.MvcWeb.App_GlobalResources.PersonResource, Freemarker.Net.MvcWeb"/>${res.Title}
ResourceManagerDirectiveModel
is getting the ResourceManager
from the resource and put it into res
template variable:public void execute(freemarker.core.Environment env, java.util.Map parameters, TemplateModel[] models, TemplateDirectiveBody body) { if (parameters.containsKey("type")) { TemplateScalarModel scalar = (TemplateScalarModel)parameters.get("type"); var type = Type.GetType(scalar.getAsString()); env.setVariable("res", env.getObjectWrapper() .wrap(type.GetProperty("ResourceManager", BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public) .GetGetMethod(true) .Invoke(null, null))); } }Adding More Directives
To all more directives can be added in the future, MEF is used. The directive implementation only needs to export and implement the
ImportableDirective
interface.[Export(typeof(ImportableDirective))] public class ResourceManagerDirectiveModel : TemplateDirectiveModel, ImportableDirective {The view engine will import them in the constructor:
public class FreemarkerViewEngine : VirtualPathProviderViewEngine { Configuration config; [ImportMany] IEnumerable<ImportableDirective> directives; public FreemarkerViewEngine(string rootPath, string encoding = "utf-8") { // initialize the config // ... // import the extension methods' namespaces // ... // import the directives var dir = new DirectoryInfo( Path.Combine(rootPath, "bin")); var catalogs = dir.GetFiles("*.dll") .Where(o => o.Name != "freemarker.dll" && !(o.Name.StartsWith("IKVM.") || o.Name.StartsWith("Freemarker.Net"))) .Select(o => new AssemblyCatalog( Assembly.LoadFile(o.FullName)) ); var container = new CompositionContainer( new AggregateCatalog( new AggregateCatalog(catalogs), new AssemblyCatalog( typeof(ImportableDirective).Assembly) )); container.ComposeParts(this); }
The source code is available in CodePlex. Please visit http://freemarkernet.codeplex.com/.
P.S.: The bonus of this project is we have not only a new view engine in ASP.NET MVC, but also a new template engine in .Net Framework. Maybe someday we could use FreeMarker to generate code instead of T4 in Visual Studio SDK.
No comments:
Post a Comment