Library

From NDjango
Jump to: navigation, search

As of version v0.9.1.0 the NDjango external interface has been changed. The primary objectives of the interface refactoring were to improve usability and simplify interface especially for non F# users.

NDjango Integration

The central point of NDjango integration is the TemplateManagerProvider class. This class provides thread safe access to all resources it manages: tag and filter definitions, settings, current template loader, etc. It also provides template managers, which in turn can render templates.

All templates ever rendered by any of the managers created by a particular provider are cached in a thread safe way in the provider. Template managers also cache templates but they do it without thread locking, eliminating the synchronization overhead at the expense of requiring that template managers should only be used from a single thread. If a manager does not have a template in its cache, it will have to request it from the provider which will always involve locking.

Template Manager Provider

Here is a partial definition of the TemplateManagerProvider class:

<source lang="csharp"> namespace NDjango { public class TemplateManagerProvider { public TemplateManagerProvider(); public ITemplateManager GetNewManager(); public TemplateManagerProvider WithFilter(string name, ISimpleFilter filter); public TemplateManagerProvider WithLoader(ITemplateLoader new_loader); public TemplateManagerProvider WithSetting(string name, object value); public TemplateManagerProvider WithTag(string name, ITag tag); } } </source>

The parameterless constructor allows for creating of the 'initial' template manager provider. It will have all default filters, tags and settings applied. To change any of the settings or register additional tags and filters you can use the "with" methods. 'With' methods do not affect the existing provider, but rather create a new clean instance of the provider with new settings/filters/tags/loader. The content of the template cache will not be copied over, so parsing of the templates will be repeated against the new settings when they are requested. The old provider as well as any of the template managers it spawned remain unaffected and will keep working in the context of the old configuration. The method GetNewManager is used to get an instance of a new template manager,

Template Manager

Template managers implement the following interface:

<source lang="csharp"> namespace NDjango.Interfaces { public interface ITemplateManager { TextReader RenderTemplate(string path, IDictionary<string, object> context); } } </source>

They are created using the GetNewManager method of the template manager provider. It is recommended that every thread intended to be used for rendering acquires its own instance of the template manager and then uses it for all rendering it performs. The actual rendering is done by calling the RenderTemplate method. This method returns a text reader which should be used to retrieve the result of the rendering. The path is used to identify the template and is passed through to the template loader, and the context is a dictionary of the context variables to be used during rendering.

Sample Implementation

The following is the integration code from bistro, which illustrates all of the components previously mentioned

Note: This code has some differences from versions 0.9.0.x <source lang="csharp"> /// <summary> /// Integration point for django into the bistro rendering framework /// </summary> public class DjangoEngine : TemplateEngine {

   readonly string errorTemplate =

@" <head>

   <title>Exception Processing Request</title>

</head> <body>

Exception processing {0}

<p />

{1}

<p />
{2}

</body> </html> ";

   public DjangoEngine(IHttpHandler handler)
   {
       manager = Provider.GetNewManager();
   }
   private static TemplateManagerProvider provider;
   private static object lockObj = new object();
   private static TemplateManagerProvider Provider
   {
       get
       {
           lock (lockObj)
           {
               if (provider == null)
               {
                   provider = new TemplateManagerProvider().WithLoader(new IntegrationTemplateLoader()).WithTag("url", new BistroUrlTag(HttpRuntime.AppDomainAppVirtualPath));
                   provider = NDjango.FiltersCS.FilterManager.Initialize(provider);
               }
           }
           return provider;
       }
   }
   private NDjango.Interfaces.ITemplateManager manager;
   public override void Render(HttpContextBase httpContext, IContext requestContext)
   {
       if (httpContext.Session != null)
           foreach (object key in httpContext.Session.Keys)
           {
               if (requestContext.Contains(key))
                   throw new ApplicationException(String.Format("{0} is present on both the Session and the Request.", key));
               requestContext.Add(key.ToString(), httpContext.Session[key.ToString()]);
           }
       try
       {
           TextReader reader = manager.RenderTemplate(requestContext.Response.RenderTarget, (IDictionary<string, object>)requestContext);
           char[] buffer = new char[4096];
           int count = 0;
           while ((count = reader.ReadBlock(buffer, 0, 4096)) > 0)
               httpContext.Response.Write(buffer, 0, count);
       }
       catch (Exception ex)
       {
           httpContext.Response.StatusCode = 500;
           httpContext.Response.Write(RenderException(requestContext.Response.RenderTarget, ex, true));
       }
   }
   public string RenderException(string request, Exception ex, bool showTrace)
   {
       return String.Format(errorTemplate, request, ex.Message, showTrace ? x.ToString() : String.Empty);
   }

}


/// <summary> /// Standalone class for Template loader. /// </summary> internal class IntegrationTemplateLoader : NDjango.Interfaces.ITemplateLoader {

   internal IntegrationTemplateLoader()
   {
       rootDir = HttpRuntime.AppDomainAppPath;
   }
   string rootDir;
   #region ITemplateLoader Members
   public TextReader GetTemplate(string name)
   {
       return File.OpenText(Path.Combine(rootDir, name));
   }
   public bool IsUpdated(string name, System.DateTime timestamp)
   {
       return File.GetLastWriteTime(Path.Combine(rootDir, name)) > timestamp;
   }
   #endregion

} </source>

NDjango Integration (v0.9.0.x)

Externally, NDjango is completely encapsulated into the manager class, which provides all of the necessary operations. Additionally, the loader, IFilter and ISimpleFilter interfaces can be used to extend the library.

Manager and ITemplateManager

The manager class and ITemplateManager interface define the classes necessary to interact with the NDjango engine.

The following methods are used to register new Tags and Filters with the runtime.

Something to remember
NDjango template managers are not singletons. Each instance of a template manager is immutable, meaning that operations such as those listed below, that do (or may) modify the state of the manager, return a new instance. That means that if you registered a filter on an instance of a template manager, that specific instance will not know of the new filter, but the instance returned by the "Register" method, will. Each time a new instance of template manager is returned, it is guaranteed to be the newest and most up-to-date instance at the time of invocation.

<source lang="csharp"> /// Registers a new filter that takes a single parameter static Template.Manager RegisterFilter(string name, Interfaces.ISimpleFilter filter);

/// Registers a new filter that takes a single parameter, and has a built-in default value for the parameter static Template.Manager RegisterLoader(Interfaces.ITemplateLoader loader);

/// Registers a new tag static Template.Manager RegisterTag(string name, Interfaces.ITag tag); </source>

additionally, the following method is then used to interact with the newly configured engine

<source lang="csharp"> /// Retrieves a pair of an updated template manager, and text reader containing the contents /// of the rendered template Tuple<Interfaces.ITemplateManager, TextReader> RenderTemplate(string templateName, IDictionary<string, object> context); </source>

Something to remember
The Tuple<T1, T2> types you see being returned by the methods are the C# representation of the native F# tuple type. For the purposes of consuming the data returned by the engine, accessing the Item1 and Item2 properties will suffice.
More to remember
Did I mention that the template managers are immutable? Yes, they are. Among other things it means that if the instance of the Template Manager that you have does not have the template you want, it never will. However, the RenderTemplate method will obidiently locate such a template, parse it, render it and return the text reader for you to read the content. It also returns a new instance of the Template Manager which will have the template you just requested ready to use. So the next time you want a template rendered, make sure you use the latest instance of the Template Manager, otherwise your templates will be read from the disk and parsed anew every time you request them
Note to the curious
It seems to be much simpler to have a global dictionary of all templates retrieved so far and every time a tempalte is requested look it up in the dictionary. The problem with this solution is that global dictionary would require a global lock to synchronize all access to the dictionary from ASP.NET worker threads processing HttpRequests in parallel. To prevent contention created by such global lock we decided to give every thread its own instance of the manager.
Now every thread has its own manager and can access the dictionary of templates without locking at a price of being responsible for keeping a reference to the thread's personal manager instance. Adding new templates is still a global operation and requires global locks for synchronization, but it only happens when a template is retrieved for the first time in the application lifetime

Sample Implementation

The following is the integration code from bistro, which illustrates all of the components previously mentioned

<source lang="csharp"> public class DjangoEngine : TemplateEngine, NDjango.Interfaces.ITemplateLoader {

   readonly string errorTemplate =

@" <head> <title>Exception Processing Request</title> </head> <body>

Exception processing {0}

<p />

{1}

<p />
{2}

</body> </html> ";

   public DjangoEngine(IHttpHandler handler)
   {
       NDjango.FiltersCS.FilterManager.Instance.Initialize();
       manager = NDjango.Template.Manager.RegisterLoader(this);
       rootDir = HttpRuntime.AppDomainAppPath;
   }
   NDjango.Interfaces.ITemplateManager manager;
   string rootDir;
   public override void Render(HttpContextBase httpContext, IContext requestContext)
   {
       if (httpContext.Session != null)
           foreach (object key in httpContext.Session.Keys)
           {
               if (requestContext.Contains(key))
                   throw new ApplicationException(String.Format("{0} is present on both the Session and the Request.", key));
               requestContext.Add(key.ToString(), httpContext.Session[key.ToString()]);
           }
       try
       {
           var templateTuple = 
               manager.RenderTemplate(requestContext.Response.RenderTarget, (IDictionary<string, object>)requestContext);
           manager = templateTuple.Item1;
           TextReader reader = templateTuple.Item2;
           char[] buffer = new char[4096];
           int count = 0;
           while ((count = reader.ReadBlock(buffer, 0, 4096)) > 0)
               httpContext.Response.Write(buffer, 0, count);
       }
       catch (Exception ex)
       {
           httpContext.Response.Write(RenderException(requestContext.Response.RenderTarget, ex, true));
       }
   }
   public string RenderException(string request, Exception ex, bool showTrace)
   {
       return String.Format(errorTemplate, request, ex.Message, showTrace ? ex.ToString() : String.Empty);
   }
   public TextReader GetTemplate(string name)
   {
       return File.OpenText(Path.Combine(rootDir, name));
   }
   public bool IsUpdated(string name, System.DateTime timestamp)
   {
       return File.GetLastWriteTime(Path.Combine(rootDir, name)) > timestamp;
   }

} </source>

ITemplateLoader

The Manager class is file-system-agnostic, meaning that it delegates template source code retrieval to loader class. The simplest of these is a file loader.

The ITemplateLoader interface defines just two methods:

<source lang="csharp"> public interface ITemplateLoader { /// Retrieve the source code of the tempalte identified by "templateName" TextReader GetTemplate(string templateName);

/// Determine if the template stored in the repository has changed since "asOf" bool IsUpdated(string templateName, DateTime asOf); } </source>

One to retrieve template contents, and one to determine whether the currently available (and compiled) version is up-to-date. Keep in mind that if the RELOAD_IF_UPDATED flag is set to false the IsUpdated method will never be called.