FreeMarker is a quite success template engine in Java world. It is used with Apache Struts, an MVC framework, as a view engine. ASP.NET MVC is using Web Form as the default view engine. The problem is the view become a spaghetti of HTML and C# snippets fairly quickly, just like classic ASP and PHP.
FreeMarker.Net is a project that ports the FreeMaker as an ASP.NET MVC view engine
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
FreeMarker provides an
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
They are similar but not in the same interface, all of them are like this:
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.