RazorEngine @Html helpers work, but escape the Htm

2019-03-02 01:23发布

问题:

I am using RazorEngine to parse templates from html snippets on a web page. (This is a legacy system that switching to Mvc Razor views isn't possible, so we are switching small sections over to using RazorEngine where it makes sense). There are many of questions on SO and the internet trying to get Mvc's Html and Url helpers to work with Razor engine. To get @Html syntax to work, I've modified some code found here to add Html to the base template:

public HtmlHelper<t> Html
{
    get
    {
        if (helper == null) 
        {           
            var writer = this.CurrentWriter; //TemplateBase.CurrentWriter
            var vcontext = new ViewContext() { Writer = writer, ViewData = this.ViewData};

            helper = new HtmlHelper<t>(vcontext, this);
        }
        return helper;
    }
}
public ViewDataDictionary ViewData
{
    get
    {
        if (viewdata == null)
        {

            viewdata = new ViewDataDictionary();
            viewdata.TemplateInfo = new TemplateInfo() { HtmlFieldPrefix = string.Empty };

            if (this.Model != null)
            {
                viewdata.Model = Model;
            }

        }

    return viewdata;
    }
    set
    {
        viewdata = value;
    }
}

After a lot of debugging into the Html source code I think I've managed to instantiate everything that the Html helper needs, and it runs successfully for @Html.Label... The problem is that the resulting html is:

&lt;label for=&quot;MyNumber&quot;&gt;MyNumber&lt;/label&gt;

When it obviously should be:

<label for="MyNumber">MyNumber</label>

I am stumped as to how to fix this. I was not able to find how the encoding happens when looking through the RazorEngine source. My initial thought was that the TextWriter must be encoding the value but I have not been able to confirm this. How can I get @Html.BlahFor() to render un-escaped html?

回答1:

I have found a workaround for the problem I was facing. RazorEngine's base Template will automatically encode a string (or object) if it doesn't cast to an IEncodedString. In my case, I solved this issue by overriding the WriteTo method in my template class:

public override void WriteTo(TextWriter writer, object value)
{
    if (writer == null)
        throw new ArgumentNullException("writer");

    if (value == null) return;

    var encodedString = value as IEncodedString;
    if (encodedString != null)
    {
        writer.Write(encodedString);
    }
    else
    {
        var htmlString = value as IHtmlString;
        if(htmlString != null) 
            writer.Write(htmlString.ToHtmlString());
        else
        {
            //This was the base template's implementation:
            encodedString = TemplateService.EncodedStringFactory.CreateEncodedString(value);
            writer.Write(encodedString);
        }
    }
}

I have yet to see if this change will cause any errors, but now that I found the method it should be relatively easy to change in the future.

EDIT: Checked to see Html helpers return an MvcHtmlString, which implements IHtmlString, so by adding a cast to this interface, we can avoid encoding the html returned by the helpers, while still having a safety in place for any other callers of this method.



回答2:

Thanks for the inspiration, I had my data stored in a xml and created a different workaround without overwriting the WriteTo method. I used a class that inherited from the TemplateBase and created 2 new methods.

    public IEncodedString HtmlOf(NBrightInfo info, String xpath)
    {
        var strOut = info.GetXmlProperty(xpath);
        strOut = System.Web.HttpUtility.HtmlDecode(strOut);
        return new RawString(strOut);
    }

    public IEncodedString BreakOf(NBrightInfo info, String xpath)
    {
        var strOut = info.GetXmlProperty(xpath);
        strOut = System.Web.HttpUtility.HtmlEncode(strOut);
        strOut = strOut.Replace(Environment.NewLine, "<br/>");
        strOut = strOut.Replace("\t", "&nbsp;&nbsp;&nbsp;");
        strOut = strOut.Replace("'", "&apos;");
        return new RawString(strOut);
    }


回答3:

Antaris, author of RazorEngine, has supplied another interesting way for handling this issue, and explained by the way why it is not done by default (for reducing as much as possible RazorEngine dependencies).

Here is for your case the relevant part of his answer on the corresponding github issue:

I think the easiest thing to do here, would be to implement a custom IEncodedStringFactory which handles MvcHtmlString. Something like:

public class MvcHtmlStringFactory : IEncodedStringFactory
{
  public IEncodedString CreateEncodedString(string rawString)
  {
    return new HtmlEncodedString(rawString);
  }

  public IEncodedString CreateEncodedString(object obj)
  {
    if (obj == null)
      return new HtmlEncodedString(string.Empty);

    var htmlString = obj as HtmlEncodedString;
    if (htmlString != null)
      return htmlString;

    var mvcHtmlString = obj as MvcHtmlString;
    if (mvcHtmlString != null)
      return new MvcHtmlStringWrapper(mvcHtmlString);

     return new HtmlEncodedString(obj.Tostring());
  }
}

public MvcHtmlStringWrapper : IEncodedString
{
  private readonly MvcHtmlString _value;

  public MvcHtmlStringWrapper(MvcHtmlString value)
  {
    _value = value;
  }

  public string ToEncodedString()
  {
    return _value.ToString();
  }

  public override string ToString()
  {
    return ToEncodedString();
  }
}

This would enable existing MvcHtmlString instances to bypass RazorEngine's built in encoding mechanism. This would not be part of the base library because we don't take any dependencies on System.Web, or System.Web.Mvc.

You would need to wire this up via your configuration:

var config = new TemplateServiceConfiguration()
{
  BaseTemplateType = typeof(MvcTemplateBase<>),
  EncodedStringFactory = new MvcHtmlStringFactory()
};

Personally, I have switched MvcHtmlString for IHtmlString before using his code. As of MVC 5, MvcHtmlString does implement it too. (Beware, it was not the case in some older MVC version.)