Welcome to ShenZhenJia Knowledge Sharing Community for programmer and developer-Open, Learning and Share
menu search
person
Welcome To Ask or Share your Answers For Others

Categories

Thinking of CMS use cases here. Imagine a view like this:

// /Home/Index.cshtml
@model object
@{
  var str = "My <b>CMS</b> content with razor code: @Html.ActionLink("Click", "Home")"
}
@Html.MyCustomRazorStringRenderer(Model)

Expected output:

My <b>CMS</b> content with razor code: <a href="/Home/Click">Click</a>

What does MyCustomRazorStringRenderer look like? It must somehow do sth. like creating/using the ViewContext and render it (like here: Render a view as a string) but I can't quite get my head around it.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
thumb_up_alt 0 like thumb_down_alt 0 dislike
186 views
Welcome To Ask or Share your Answers For Others

1 Answer

You will have to create a static class containing an extension method. The method must return an instance of MvcHtmlString that contains the safe rendered HTML output. Having said this, getting to the renderedOutput properly means "hijacking" the Razor renderer, which is tricky.

What you're really doing is using the Razor engine outside of its intended environment, which is described here: http://vibrantcode.com/blog/2010/7/22/using-the-razor-parser-outside-of-aspnet.html

There is also a lot of good information here, from which I got a lot of inspiration for the code below: http://www.codemag.com/article/1103081

These classes are the starting point for this: RazorEngineHost, RazorTemplateEngine, CSharpCodeProvider, HtmlHelper.

Working code

I actually got an almost working version of this, but realized this is a really futile thing to do. The Razor engine works by generating code, which must then be compiled using CSharpCodeProvider. This takes time. A lot of time!

The only viable and efficient way of doing this would be to save your template strings somewhere, precompile them, and call those compiled templates when called upon. This makes it basically useless for what you are after, because this would be exactly what ASP.NET MVC with Razor is good at - keeping Views in a good place, precompiling them, and calling upon them when referenced. Update: Well, maybe a heavy dose of caching might help, but I still wouldn't actually recommend this solution.

When generating the code, Razor emits calls to this.Write and this.WriteLiteral. Because this is an object inheriting from a baseclass that you write yourself, it is up to you to provide implementations of Write and WriteLiteral.

If you are using any other HtmlHelper extensions in your template string, you need to include assembly references and namespace imports for all of them. The code below adds the most common ones. Because of the nature of anonymous types, those cannot be used for model classes.

The MyRazorExtensions class

public static class MyRazorExtensions
{
    public static MvcHtmlString RazorEncode(this HtmlHelper helper, string template)
    {
        return RazorEncode(helper, template, (object)null);
    }

    public static MvcHtmlString RazorEncode<TModel>(this HtmlHelper helper, string template, TModel model)
    {
        string output = Render(helper, template, model);
        return new MvcHtmlString(output);
    }

    private static string Render<TModel>(HtmlHelper helper, string template, TModel model)
    {
        // 1. Create a host for the razor engine
        //    TModel CANNOT be an anonymous class!
        var host = new RazorEngineHost(RazorCodeLanguage.GetLanguageByExtension("cshtml");
        host.DefaultNamespace = typeof(MyTemplateBase<TModel>).Namespace;
        host.DefaultBaseClass = nameof(MyTemplateBase<TModel>) + "<" + typeof(TModel).FullName + ">";
        host.NamespaceImports.Add("System.Web.Mvc.Html");

        // 2. Create an instance of the razor engine
        var engine = new RazorTemplateEngine(host);

        // 3. Parse the template into a CodeCompileUnit
        using (var reader = new StringReader(template))
        {
            razorResult = engine.GenerateCode(reader);
        }
        if (razorResult.ParserErrors.Count > 0)
        {
            throw new InvalidOperationException($"{razorResult.ParserErrors.Count} errors when parsing template string!");
        }

        // 4. Compile the produced code into an assembly
        var codeProvider = new CSharpCodeProvider();
        var compilerParameters = new CompilerParameters { GenerateInMemory = true };
        compilerParameters.ReferencedAssemblies.Add(typeof(MyTemplateBase<TModel>).Assembly.Location);
        compilerParameters.ReferencedAssemblies.Add(typeof(TModel).Assembly.Location);
        compilerParameters.ReferencedAssemblies.Add(typeof(HtmlHelper).Assembly.Location);
        var compilerResult = codeProvider.CompileAssemblyFromDom(compilerParameters, razorResult.GeneratedCode);
        if (compilerResult.Errors.HasErrors)
        {
            throw new InvalidOperationException($"{compilerResult.Errors.Count} errors when compiling template string!");
        }

        // 5. Create an instance of the compiled class and run it
        var templateType = compilerResult.CompiledAssembly.GetType($"{host.DefaultNamespace}.{host.DefaultClassName}");
        var templateImplementation = Activator.CreateInstance(templateType) as MyTemplateBase<TModel>;
        templateImplementation.Model = model;
        templateImplementation.Html = helper;
        templateImplementation.Execute();

        // 6. Return the html output
        return templateImplementation.Output.ToString();
    }
}

The MyTemplateBase<> class

public abstract class MyTemplateBase<TModel>
{
    public TModel Model { get; set; }
    public HtmlHelper Html { get; set; }

    public void WriteLiteral(object output)
    {
        Output.Append(output.ToString());
    }

    public void Write(object output)
    {
        Output.Append(Html.Encode(output.ToString()));
    }

    public void Write(MvcHtmlString output)
    {
        Output.Append(output.ToString());
    }

    public abstract void Execute();

    public StringBuilder Output { get; private set; } = new StringBuilder();
}

test.cshtml

@using WebApplication1.Models

<h2>Test</h2>

@Html.RazorEncode("<p>Paragraph output</p>")
@Html.RazorEncode("<p>Using a @Model</p>", "string model" )
@Html.RazorEncode("@for (int i = 0; i < 100; ++i) { <p>@i</p> }")
@Html.RazorEncode("@Html.ActionLink(Model.Text, Model.Action)", new TestModel { Text = "Foo", Action = "Bar" })

Update

Doing this "live" - having Razor compile and run for each page load is obviously too slow if you don't go heavy on the caching, but if you break out pieces of my code and have your CMS request a recompilation automatically whenever the contents of a page changes, you could do something really interesting here.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
thumb_up_alt 0 like thumb_down_alt 0 dislike
Welcome to ShenZhenJia Knowledge Sharing Community for programmer and developer-Open, Learning and Share
...