RazorEngine string layouts and sections?
Asked Answered
C

3

19

I use razor engine like this:

public class EmailService : IService
{
    private readonly ITemplateService templateService;

    public EmailService(ITemplateService templateService)
    {
        if (templateService == null)
        {
            throw new ArgumentNullException("templateService");
        }
        this.templateService = templateService;
    }

    public string GetEmailTemplate(string templateName)
    {
        if (templateName == null)
        {
            throw new ArgumentNullException("templateName");
        }
        Assembly assembly = Assembly.GetAssembly(typeof(EmailTemplate));
        Stream stream = assembly.GetManifestResourceStream(typeof(EmailTemplate), "{0}.cshtml".FormatWith(templateName));
        string template = stream.ReadFully();
        return template;
    }

    public string GetEmailBody(string templateName, object model = null)
    {
        if (templateName == null)
        {
            throw new ArgumentNullException("templateName");
        }
        string template = GetEmailTemplate(templateName);
        string emailBody = templateService.Parse(template, model, null, null);
        return emailBody;
    }
}

The templating service I use is injected although it's just a default implementation:

    internal ITemplateService InstanceDefaultTemplateService()
    {
        ITemplateServiceConfiguration configuration = new TemplateServiceConfiguration();
        ITemplateService service = new TemplateService(configuration);
        return service;
    }

Since in this case in particular I will be building emails from these templates. I want to be able to use @sections for the email'a subject, and different sections of the email body, while using a layout where I specify the styles that are common to the whole email structure (which will look like one of MailChimp's probably).

The question is then twofold:

  • How can I specify layouts in RazorEngine?
  • How can I specify these layouts from strings (or a stream)? since as you can see, I use embedded resources to store the razor email templates.

Update

Maybe I wasn't clear, but I'm referring to the RazorEngine library.

Cyrstalcyrus answered 29/4, 2012 at 3:44 Comment(4)
Though not a direct answer to the question, these posts seem helpful: - west-wind.com/weblog/posts/2010/Dec/27/… - vibrantcode.com/blog/2010/11/16/… - razorengine.codeplex.comTorrent
By saying "RazorEngine library" you mean not ASP.NET MVC3?Unreal
No. That's why I linked to the project, it's a templating library based on ASP.NET MVC3's RazorCyrstalcyrus
Great work! Did you figure out how to render the subject separately using @section? i.e Is there a way to render just a section using something like Razor.Render(sectionName, model) and get an string returned?Cran
C
17

Turns out after some digging that layouts are supported, we just have to declare them with _Layout instead of Layout

As for the embedded resource issue, I implemented the following ITemplateResolver

using System;
using System.IO;
using System.Reflection;
using Bruttissimo.Common;
using RazorEngine.Templating;

namespace Website.Extensions.RazorEngine
{
    /// <summary>
    /// Resolves templates embedded as resources in a target assembly.
    /// </summary>
    public class EmbeddedTemplateResolver : ITemplateResolver
    {
        private readonly Assembly assembly;
        private readonly Type type;
        private readonly string templateNamespace;

        /// <summary>
        /// Specify an assembly and the template namespace manually.
        /// </summary>
        /// <param name="assembly">The assembly where the templates are embedded.</param>
        /// <param name="templateNamespace"></param>
        public EmbeddedTemplateResolver(Assembly assembly, string templateNamespace)
        {
            if (assembly == null)
            {
                throw new ArgumentNullException("assembly");
            }
            if (templateNamespace == null)
            {
                throw new ArgumentNullException("templateNamespace");
            }
            this.assembly = assembly;
            this.templateNamespace = templateNamespace;
        }

        /// <summary>
        /// Uses a type reference to resolve the assembly and namespace where the template resources are embedded.
        /// </summary>
        /// <param name="type">The type whose namespace is used to scope the manifest resource name.</param>
        public EmbeddedTemplateResolver(Type type)
        {
            if (type == null)
            {
                throw new ArgumentNullException("type");
            }
            this.assembly = Assembly.GetAssembly(type);
            this.type = type;
        }

        public string Resolve(string name)
        {
            if (name == null)
            {
                throw new ArgumentNullException("name");
            }
            Stream stream;
            if (templateNamespace == null)
            {
                stream = assembly.GetManifestResourceStream(type, "{0}.cshtml".FormatWith(name));
            }
            else
            {
                stream = assembly.GetManifestResourceStream("{0}.{1}.cshtml".FormatWith(templateNamespace, name));
            }
            if (stream == null)
            {
                throw new ArgumentException("EmbeddedResourceNotFound");
            }
            string template = stream.ReadFully();
            return template;
        }
    }
}

Then you just wire it like this:

    internal static ITemplateService InstanceTemplateService()
    {
        TemplateServiceConfiguration configuration = new TemplateServiceConfiguration
        {
            Resolver = new EmbeddedTemplateResolver(typeof(EmailTemplate))
        };
        ITemplateService service = new TemplateService(configuration);
        return service;
    }

The type you pass is just for referencing the assembly and namespace where the resources are embedded.

namespace Website.Domain.Logic.Email.Template
{
    /// <summary>
    /// The purpose of this class is to expose the namespace of razor engine templates in order to
    /// avoid having to hard-code it when retrieving the templates embedded as resources.
    /// </summary>
    public sealed class EmailTemplate
    {
    }
}

One last thing, in order to have the templates resolved with our resolver we have to resolve them like this:

ITemplate template = templateService.Resolve(templateName, model);
string body = template.Run();
return body;

.Run is just a simple extension method since I can't find any use for a ViewBag.

public static class ITemplateExtensions
{
    public static string Run(this ITemplate template)
    {
        ExecuteContext context = new ExecuteContext();
        string result = template.Run(context);
        return result;
    }
}

UPDATE

Here are the missing extensions

    public static string FormatWith(this string text, params object[] args)
    {
        return string.Format(text, args);
    }

    public static string ReadFully(this Stream stream)
    {
        using (StreamReader reader = new StreamReader(stream))
        {
            return reader.ReadToEnd();
        }
    }
Cyrstalcyrus answered 12/5, 2012 at 18:13 Comment(5)
It's ridiculous that we have to write all this code.. I thought there was a reason we were using a third party library...........Garrity
@TheMuffinMan You're using a third-party library to render information to the web. I think that writing a little bit of code to create an email (not what it was intended for) is not unexpected.Hydrogenous
It's worth noting that ITemplateResolver has been replaced by ITemplateManager in version 3.5.Lorylose
@KyleW Just seen this post and am trying to do the same thing from a class library but am struggling on where to place the templateresolver. Do you have a full working solution you can share?Helenhelena
@TheMuffinMan You can always make a pull request!Lethargic
H
6

I needed to supply my own layout as either a string or a file name. Here is how I solved this (based on this blog post)

public static class RazorEngineConfigurator
{
    public static void Configure()
    {
        var templateConfig = new TemplateServiceConfiguration
            {
                Resolver = new DelegateTemplateResolver(name =>
                    {
                        //no caching cause RazorEngine handles that itself
                        var emailsTemplatesFolder = HttpContext.Current.Server.MapPath(Properties.Settings.Default.EmailTemplatesLocation);
                        var templatePath = Path.Combine(emailsTemplatesFolder, name);
                        using (var reader = new StreamReader(templatePath)) // let it throw if doesn't exist
                        {
                            return reader.ReadToEnd();
                        }
                    })
            };
        RazorEngine.Razor.SetTemplateService(new TemplateService(templateConfig));
    }
}

Then I call RazorEngineConfigurator.Configure() in Global.asax.cs and it's ready.

The path to my templates is in Properties.Settings.Default.EmailTemplatesLocation

In my view I have this:

@{ Layout = "_layout.html";}

_layout.html is in emailsTemplatesFolder

It's a pretty standard HTML with a @RenderBody() call in the middle.

As far as I understand, RazorEngine uses the template name ("_layout.html" in this case) as a key to its cache so the delegate in my configurator is called only once per template.

I think it uses that resolver for every template name it doesn't know (yet).

Hakeem answered 7/5, 2013 at 7:57 Comment(1)
The blog has now been archived. The new link can be found by following: learn.microsoft.com/en-gb/archive/blogs/hongyes/…Mattins
T
3

It looks like someone else solved it for you.

https://github.com/aqueduct/Appia/blob/master/src/Aqueduct.Appia.Razor/RazorViewEngine.cs

The code you want is in the second ExecuteView method. Though they're creating their own view engine, you can instead create your own custom templating solution and use something similar. Basically they are looking for the Layout property of the Template, and if it exists doing a search and replace for the content from the layout.

Here is a link to RazorEngine's custom templating:

http://razorengine.codeplex.com/wikipage?title=Building%20Custom%20Base%20Templates&referringTitle=Documentation

Here is where I found your solution:

.NET Razor engine - Implementing layouts

Toluate answered 8/5, 2012 at 19:46 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.