Pull to refresh

Использование Razor за пределами ASP.NET

Reading time5 min
Views5.5K
Итак, вчера Microsoft выпустила ASP.NET MVC3 RTM, который включает в себя новый движок представлений Razor. Как вы наверняка уже знаете, Razor не содержит каких-то компонентов, специфичных для web, а значит, его можно использовать и в других приложениях. Ну, а если вы этого еще не знаете – то самое время узнать!

В этом посте я покажу, как использовать Razor в качестве движка шаблонов для ваших нужд. Источником для него послужил блог-пост Andrew Nurse «Hosting Razor outside of ASP.Net», но это не прямой перевод.



Для примера я создам текст письма, который содержит информацию о сделанном заказе.

Заказ описывается двумя классами, OrderModel и OrderItemModel:

public class OrderModel
{
    public string FirstName { get; set; }

    public string LastName { get; set; }

    public List<OrderItemModel> Items { get; set; }
}

public class OrderItemModel
{
    public string ProductName { get; set; }

    public Decimal Price { get; set; }

    public int Qty { get; set; }
}


Шаблон, который я буду использовать для создания e-mail, показан ниже. Более подробную информацию о синтаксисе Razor вы можете получить, например, в блог-посте Андрея Тарицына «Краткий справочник по синтаксису Razor [Перевод]».

Здравствуйте, @Model.FirstName @Model.LastName!

# Продукт    Цена   Кол-во Итог
- ---------- ------ ------ ------
@for (var i = 0; i < Model.Items.Count; i++)
{
    var item = Model.Items[i];
    @String.Format(
        "{0} {1,-10} {2,6} {3,6} {4,6}\r\n",
        i + 1,
        @item.ProductName,
        @item.Price,
        @item.Qty,
        item.Price * item.Qty)
}

Всего: @(((OrderModel)Model).Items.Sum(x => x.Price * x.Qty))

WBR,
ACME team


Мне потребуется класс, который будет использоваться как базовый для моего шаблона:

public abstract class TemplateBase
{
    public TextWriter Output { get; set; }

    public dynamic Model { get; set; }

    public abstract void Execute();

    public virtual void Write(object value)
    {
        this.Output.Write(value);
    }

    public virtual void WriteLiteral(object value)
    {
        this.Output.Write(value);
    }
}


Свойство Output задает TextWriter, который получает результат выполнения шаблона.

Свойство Model используется для передачи в шаблон параметров.

Метод Execute() в унаследованном классе будет содержать код шаблона.

Методы Write() и WriteLiteral() используются для вывода, соответственно, результатов вычисления выражений и текстовых строк. Т.к. мне не нужна дополнительная обработка ни для результатов вычислений, ни для строк, код этих методов совпадает.

Примечание: имена Execute(), Write() и WriteLiteral() используются по умолчанию, при необходимости вы можете указать другие имена используя свойство GeneratedClassContext экземпляра класса RazorEngineHost.

Теперь я создам хост для Razor, указав при этом, что в моем шаблоне используется C# (Razor поддерживает C# и VB, при необходимости вы можете использовать VB, передав к конструктор RazorEngineHost экземпляр VBRazorCodeLanguage):

var razorHost = new RazorEngineHost(new CSharpRazorCodeLanguage());


Далее я указываю имя базового класса, пространство имен, в котором будет находиться код шаблона, и имя класса-шаблона:

razorHost.DefaultBaseClass = typeof(TemplateBase).FullName;
razorHost.DefaultNamespace = "StandaloneRazorDemo";
razorHost.DefaultClassName = "Template";


Добавляю набор пространств имен, которые будут доступны в коде класса-шаблона:

razorHost.NamespaceImports.Add("System");
razorHost.NamespaceImports.Add("System.Collections.Generic");
razorHost.NamespaceImports.Add("System.Linq");
razorHost.NamespaceImports.Add("System.Text");


И, наконец, создаю движок шаблонов:

var razorEngine = new RazorTemplateEngine(razorHost);


Теперь я попробую разобрать свой шаблон:


var templateText = File.ReadAllText("template.txt");

GeneratorResults generatorResults = null;
using (var reader = new StringReader(templateText))
{
    generatorResults = razorEngine.GenerateCode(reader);
}


Свойство Success класса GeneratorResults показывает, насколько успешно прошел разбор. Если при разборе возникли проблемы, я показываю список ошибок:

if (!generatorResults.Success)
{
    foreach (var error in generatorResults.ParserErrors)
    {
        Console.WriteLine(
            "Razor error: ({0}, {1}) {2}",
            error.Location.LineIndex + 1,
            error.Location.CharacterIndex + 1,
            error.Message);
    }

    throw new ApplicationException();
}


Если разбор завершился успешно, свойство GeneratedCode содержит DOM tree, который далее можно использовать для генерации кода:

return generatorResults.GeneratedCode;


Теперь мне нужно скомпилировать шаблон:

private static string CompileTemplate(CodeCompileUnit generatedCode)
{
    var codeProvider = new CSharpCodeProvider();

#if DEBUG
    using (var writer = new StreamWriter("out.cs", false, Encoding.UTF8))
    {
        codeProvider.GenerateCodeFromCompileUnit(
            generatedCode, writer, new CodeGeneratorOptions());
    }
#endif

    var outDirectory = "temp";
    Directory.CreateDirectory(outDirectory);

    var outAssemblyName = Path.Combine(outDirectory,
        String.Format("{0}.dll", Guid.NewGuid().ToString("N")));

    var refAssemblyNames = new List<string>();
    refAssemblyNames.Add(new Uri(typeof(TemplateBase).Assembly.CodeBase).AbsolutePath);
    refAssemblyNames.Add("System.Core.dll");
    refAssemblyNames.Add("Microsoft.CSharp.dll");

    var compilerResults = codeProvider.CompileAssemblyFromDom(
        new CompilerParameters(refAssemblyNames.ToArray(), outAssemblyName),
        generatedCode);

    if (compilerResults.Errors.HasErrors)
    {
        var errors = compilerResults
            .Errors
            .OfType<CompilerError>()
            .Where(x => !x.IsWarning);

        foreach (var error in errors)
        {
            Console.WriteLine("Compiler error: ({0}, {1}) {2}",
                error.Line, error.Column, error.ErrorText);
        }

        throw new ApplicationException();
    }

    return outAssemblyName;
}


Приведенный выше метод возвращает имя сборки, которая содержит скомпилированный шаблон.

Код, заключенный в #if DEBUG … #endif используется для отладки и позволяет посмотреть, во что же превратился шаблон после всех проведенных над ним манипуляций.

Все, что теперь мне нужно сделать, это загрузить сборку, создать экземпляр класса-шаблона и «выполнить» его:

var assembly = Assembly.LoadFrom(outAssemblyName);
var type = assembly.GetType("StandaloneRazorDemo.Template", true);
var template = Activator.CreateInstance(type) as TemplateBase;

using (var writer = new StringWriter())
{
    template.Output = writer;
    template.Model = GetModel();

    template.Execute();

    File.WriteAllText("out.txt", writer.ToString(), Encoding.UTF8);
}


Метод GetModel() определяется следующим образом:

private static OrderModel GetModel()
{
    var model = new OrderModel { FirstName = "Джеймс", LastName = "Бонд" };

    model.Items = new List<OrderItemModel>();
    model.Items.Add(new OrderItemModel { ProductName = "Apple", Price = 4.95m, Qty = 1 });
    model.Items.Add(new OrderItemModel { ProductName = "Kiwi", Price = 9.95m, Qty = 2 });

    return model;
}


Теперь в файле «out.txt» содержится результат «выполнения» шаблона:

Здравствуйте, Джеймс Бонд!

# Продукт    Цена   Кол-во Итог
- ---------- ------ ------ ------
1 Apple        4,95      1   4,95
2 Kiwi         9,95      2  19,90

Всего: 24,85

WBR,
ACME team


Вот и все!

Код примера:
StandaloneRazorDemo.zip
Tags:
Hubs:
+44
Comments20

Articles