Как я могу использовать привязку модели Razor из строки ресурса?

Обычно мы имеем:

<div>
  <div>Some property from a model: @Model.Property</div>
</div>

Но допустим, у меня есть точная строка в виде полной строки html в файле ресурсов, и я попытаюсь сослаться на нее:

<div>
  @Resource.MyResourceLine
</div>

Привязка модели не работает. Это делает строку необработанной без привязки.

Как я могу заставить Razor связываться в этом сценарии?

РЕДАКТИРОВАТЬ: есть альтернативный способ, который работает, изменяя содержимое строки ресурса на строку. Формат заполнителя:

<div>Some property from a model:{0}</div>

а потом:

<div>
  @string.Format(@Resource.MyResourceLine,@Model.Property)
</div>

Но это затрудняет ведение больших текстов со многими ссылками на свойства. Было бы идеально, если бы имена свойств можно было увидеть в файле ресурсов. Есть ли более элегантный способ?

1 ответ

Я немного копался в исходном коде Asp.Net Mvc (последняя версия 5.2.3), взятом из официального кодекса: https://aspnetwebstack.codeplex.com/

Краткий ответ: для этого нет простого способа, поскольку страница уже скомпилирована, и любая передаваемая вами строка в вашей модели обрабатывается как строка - либо MvcHtmlString, либо String. Вы можете использовать пакет RazorEngine, чтобы сделать это быстро и без особых проблем: ( https://github.com/Antaris/RazorEngine)

Длинный ответ:

Когда вы открываете маршрут, и контроллер обслуживает его представление, вы должны взять проанализированный и скомпилированный код для этого представления (который может быть сгенерирован во время запуска или лениво прямо перед тем, как вы фактически используете это представление), а затем визуализировать страницу, объединяющую скомпилированный Просмотр и данные вашей модели (что делается, когда вы звоните View() метод в контроллере).

Как ASP.NET анализирует и компилирует представление, генерируя для него исполняемый код:

// https://aspnetwebstack.codeplex.com/SourceControl/latest#src/System.Web.Razor/RazorTemplateEngine.cs
// Line 152
protected internal virtual GeneratorResults GenerateCodeCore(ITextDocument input, string className, string rootNamespace, string sourceFileName, CancellationToken? cancelToken) {
    //...
    // Run the parser
    RazorParser parser = CreateParser();
    Debug.Assert(parser != null);
    ParserResults results = parser.Parse(input);

    // Generate code
    RazorCodeGenerator generator = CreateCodeGenerator(className, rootNamespace, sourceFileName);
    generator.DesignTimeMode = Host.DesignTimeMode;
    generator.Visit(results);
    //...
}

Как asp.net отображает страницу, комбинируя исходный код для просмотра и данные из модели

// https://aspnetwebstack.codeplex.com/SourceControl/latest#src/System.Web.WebPages/WebPageBase.cs
// Line 215
// public override void ExecutePageHierarchy() {
// ...
    try
    {
        // Execute the developer-written code of the WebPage
        Execute(); //**you can see example of the code it executes right below in the code block**
    }
    finally
    {
        TemplateStack.Pop(Context);
    }
}

После компиляции представления оно превращается в простой класс C#, который генерирует строку, которая затем отображается пользователю в браузере. Давайте создадим простой Controller, View и ViewModel:

Вот некоторый код:

Класс ViewModel:

namespace Stackru.Models
{
    public class TestViewModel
    {
        public int IntProperty { get; set; }
        public string StringProperty { get; set; }
    }
}

Код контроллера:

public ActionResult Test()
{       
    var viewModel = new TestViewModel
    {
        IntProperty = 5,
        StringProperty = "@DateTime.UtcNow.ToString()"
    };

    return View(viewModel);
}

Посмотреть:

@model Stackru.Models.TestViewModel

@{
    ViewBag.Title = "just a test";
    Layout = null;
}

@Model.IntProperty

@Html.Raw(@Model.StringProperty)

Пример страницы с использованием приведенного выше кода генерирует следующее скомпилированное представление:

//------------------------------------------------------------------------------
// <auto-generated>
//     This code was generated by a tool.
//     Runtime Version:4.0.30319.42000
//
//     Changes to this file may cause incorrect behavior and will be lost if
//     the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------

namespace ASP {
    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
    using System.Net;
    using System.Web;
    using System.Web.Helpers;
    using System.Web.Security;
    using System.Web.UI;
    using System.Web.WebPages;
    using System.Web.Mvc;
    using System.Web.Mvc.Ajax;
    using System.Web.Mvc.Html;
    using System.Web.Optimization;
    using System.Web.Routing;
    using Stackru;


    public class _Page_Views_Test_Index_cshtml : System.Web.Mvc.WebViewPage<Stackru.Models.TestViewModel> {

        #line hidden

        public _Page_Views_Test_Index_cshtml() {
        }

        protected ASP.global_asax ApplicationInstance {
            get {
                return ((ASP.global_asax)(Context.ApplicationInstance));
            }
        }

        public override void Execute() {

            #line 3 "XXX\Views\Test\Index.cshtml"

            ViewBag.Title = "just a test";
            Layout = null;


            #line default
            #line hidden
            BeginContext("~/Views/Test/Index.cshtml", 100, 4, true);

            WriteLiteral("\r\n\r\n");

            EndContext("~/Views/Test/Index.cshtml", 100, 4, true);

            BeginContext("~/Views/Test/Index.cshtml", 105, 17, false);


            #line 8 "XXX\Views\Test\Index.cshtml"
            Write(Model.IntProperty);


            #line default
            #line hidden
            EndContext("~/Views/Test/Index.cshtml", 105, 17, false);

            BeginContext("~/Views/Test/Index.cshtml", 122, 4, true);

            WriteLiteral("\r\n\r\n");

            EndContext("~/Views/Test/Index.cshtml", 122, 4, true);

            BeginContext("~/Views/Test/Index.cshtml", 127, 31, false);


            #line 10 "XXX\Views\Test\Index.cshtml"
            Write(Html.Raw(@Model.StringProperty));


            #line default
            #line hidden
            EndContext("~/Views/Test/Index.cshtml", 127, 31, false);

        }
    }
}

Как вы видите, код вашей страницы просто записывается в выходной раздел за разделом, проверяя Write Метод приводит к следующим деталям реализации:

public override void Write(object value)
{
    WriteTo(Output, value);
}

public static void WriteTo(TextWriter writer, object content)
{
    writer.Write(HttpUtility.HtmlEncode(content)); //writer - instance of TextWriter
}

Таким образом, все, что вы помещаете в свое строковое поле в viewmodel, просто кодируется методом HtmlEncode и помещается на страницу, и оно не может быть скомпилировано во время выполнения с использованием функций mvc по умолчанию.

Я действительно уверен, что вы можете сделать это с Mvc и Razor, копающимися глубоко в источниках, но это потребует намного больше времени и, вероятно, много старых добрых хаков. Для быстрого и простого решения вы можете использовать пакет https://github.com/Antaris/RazorEngine. Вы также можете проверить его исходный код, чтобы узнать, как они это сделали.

Вот код контроллера, который будет отображать шаблон с помощью пакета RazorEngine:

public ActionResult Test()
{
    var stringTemplate = @"
        @model Stackru.Models.TestViewModel  

        <br/>
        >>COMPILED
        <br/>

        @DateTime.UtcNow.ToString() 
        <br/>
        Compiled model int property value:
        <br/>
        @Model.IntProperty
    ";

    var viewModel = new TestViewModel
    {
        IntProperty = 5,
        StringProperty = null
    };

    viewModel.StringProperty = Engine.Razor.RunCompile(stringTemplate, viewModel.GetType().ToString(), null, viewModel);

    return View("Index", viewModel);
}

Основная идея здесь довольно проста - передать рендеринг компоненту, сделать это в контроллере и передать полученную строку в ViewModel, а затем использовать @HtmlHelper.Raw для рендеринга HTML, полученного из движка.

Это может работать для многих сценариев, но я настоятельно рекомендую вам не делать этого, если вам это действительно не нужно, и нет жизнеспособных альтернатив. Динамические шаблоны бритвы очень сложно поддерживать.

Другие вопросы по тегам