Итак, вчера Microsoft выпустила ASP.NET MVC3 RTM, который включает в себя новый движок представлений Razor. Как вы наверняка уже знаете, Razor не содержит каких-то компонентов, специфичных для web, а значит, его можно использовать и в других приложениях. Ну, а если вы этого еще не знаете – то самое время узнать!
В этом посте я покажу, как использовать Razor в качестве движка шаблонов для ваших нужд. Источником для него послужил блог-пост Andrew Nurse «Hosting Razor outside of ASP.Net», но это не прямой перевод.
Для примера я создам текст письма, который содержит информацию о сделанном заказе.
Заказ описывается двумя классами, OrderModel и OrderItemModel:
Шаблон, который я буду использовать для создания e-mail, показан ниже. Более подробную информацию о синтаксисе Razor вы можете получить, например, в блог-посте Андрея Тарицына «Краткий справочник по синтаксису Razor [Перевод]».
Мне потребуется класс, который будет использоваться как базовый для моего шаблона:
Свойство Output задает TextWriter, который получает результат выполнения шаблона.
Свойство Model используется для передачи в шаблон параметров.
Метод Execute() в унаследованном классе будет содержать код шаблона.
Методы Write() и WriteLiteral() используются для вывода, соответственно, результатов вычисления выражений и текстовых строк. Т.к. мне не нужна дополнительная обработка ни для результатов вычислений, ни для строк, код этих методов совпадает.
Примечание: имена Execute(), Write() и WriteLiteral() используются по умолчанию, при необходимости вы можете указать другие имена используя свойство GeneratedClassContext экземпляра класса RazorEngineHost.
Теперь я создам хост для Razor, указав при этом, что в моем шаблоне используется C# (Razor поддерживает C# и VB, при необходимости вы можете использовать VB, передав к конструктор RazorEngineHost экземпляр VBRazorCodeLanguage):
Далее я указываю имя базового класса, пространство имен, в котором будет находиться код шаблона, и имя класса-шаблона:
Добавляю набор пространств имен, которые будут доступны в коде класса-шаблона:
И, наконец, создаю движок шаблонов:
Теперь я попробую разобрать свой шаблон:
Свойство Success класса GeneratorResults показывает, насколько успешно прошел разбор. Если при разборе возникли проблемы, я показываю список ошибок:
Если разбор завершился успешно, свойство GeneratedCode содержит DOM tree, который далее можно использовать для генерации кода:
Теперь мне нужно скомпилировать шаблон:
Приведенный выше метод возвращает имя сборки, которая содержит скомпилированный шаблон.
Код, заключенный в #if DEBUG … #endif используется для отладки и позволяет посмотреть, во что же превратился шаблон после всех проведенных над ним манипуляций.
Все, что теперь мне нужно сделать, это загрузить сборку, создать экземпляр класса-шаблона и «выполнить» его:
Метод GetModel() определяется следующим образом:
Теперь в файле «out.txt» содержится результат «выполнения» шаблона:
Вот и все!
Код примера:
StandaloneRazorDemo.zip
В этом посте я покажу, как использовать 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