Skip to content
This repository has been archived by the owner on Dec 14, 2018. It is now read-only.

Is @Html.Raw(...) working? ... or am I just a nOOb? #2392

Closed
guardrex opened this issue Apr 15, 2015 · 25 comments
Closed

Is @Html.Raw(...) working? ... or am I just a nOOb? #2392

guardrex opened this issue Apr 15, 2015 · 25 comments
Assignees
Milestone

Comments

@guardrex
Copy link

Trying to use @Html.Raw(@ViewBag.xxx) in a view to inject some raw HTML into the view, but I'm receiving: RuntimeBinderException: 'Microsoft.AspNet.Mvc.Rendering.IHtmlHelper<object>' does not contain a definition for 'Raw'

I'm new to MVC, so I'm not surprised if I'm just doing something wrong.

@dougbu
Copy link
Member

dougbu commented Apr 16, 2015

should work fine. suspect the issue is the second @. try @Html.Raw(ViewBag.xxx) instead.

@guardrex
Copy link
Author

Thanks. I changed it, but I found out that ...

@Html.Raw("<meta name=\"xx\" content=\"xx\">")

works, but

@Html.Raw(ViewBag.Description)

throws the RuntimeBinderException even with that modification.

The string for that is simply in the top of the rendered body view:

@{
ViewBag.Description = "<meta name=\"description\" content=\"Description here\">";
}

Clearly, I'm learning MVC and not doing this right; however, IHtmlHelper<object> does contain a def for Raw, so getting that exception here seems incorrect regardless of how bad my coding is.

@guardrex
Copy link
Author

Ok, so I get it now that

<meta name="description" content="@ViewBag.Description">

in the shared view with

@{
ViewBag.Description = "Description here";
}

works. It still seems strange that I can't just inject the entire HTML tag into the view. I'll leave this open while my concern about that RuntimeBinderException being thrown for what I was doing is the correct exception ... and even perhaps to find out why @Html.Raw(ViewBag.Description) fails.

@henkmollema
Copy link
Contributor

Does casting ViewBag.Description to a string works when passing it to the @Html.Raw() method? Perhaps something goes wrong with the dynamic type in the Raw method, as the RuntimeBinderException suggests.

Something like:

@Html.Raw(ViewBag.Description as string))

@Bartmax
Copy link

Bartmax commented Apr 16, 2015

This is likely a bug:

@{ 
    ViewBag.Description = "Hey!";
}

<p>@ViewBag.Description</p> // works
<p>Raw: @Html.Raw(ViewBag.Description)</p> //throws modelbinder exception
<p>Raw: @Html.Raw(ViewBag.Description as string)</p> // works

@henkmollema
Copy link
Contributor

This does work as expected with MVC 5.

@dougbu
Copy link
Member

dougbu commented Apr 16, 2015

interesting. there is absolutely nothing special about the @Html.Raw() methods and the ModelBinderException comes from much lower in the stack. could one of you with a test project please try it out with different DNX variants -- x86 / x64 and CLR / Core CLR?

also, which MVC and DNX versions are you testing?

@guardrex
Copy link
Author

I was using CoreCLR: dnx-coreclr-x64-1.0.0-beta5-11556 (and MVC 6)

@guardrex
Copy link
Author

@dougbu I'll be back in the office in three hours (1pm CST). If nobody else checks DNX variants by then, I'll do it and report back.

@Eilon
Copy link
Member

Eilon commented Apr 16, 2015

Html.Raw used to be an instance method but is now an extension method. Extension methods unfortunately cannot be called with dynamic parameters, and everything returned by ViewBag is always dynamic. That's why casting works.

This is technically by design but we could consider a different approach here perhaps, such as getting rid of IHtmlHelper.

@dougbu
Copy link
Member

dougbu commented Apr 16, 2015

@Eilon @Html.Raw() remains an instance method https://github.com/aspnet/Mvc/blob/dev/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/HtmlHelper.cs#L524 casting probably works because the Raw(string) method is called instead of Raw(object) though why those are different remains unclear.

@guardrex
Copy link
Author

The original exception in my app was RuntimeBinderException: 'Microsoft.AspNet.Mvc.Rendering.IHtmlHelper<object>' does not contain a definition for 'Raw', and that app doesn't have a model for the view I was working with.

When I went back and tested in my HelloMVC app, which in my version includes a @model User in the view, it threw: RuntimeBinderException: 'Microsoft.AspNet.Mvc.Rendering.IHtmlHelper<HelloMvc.Models.User>' ...

Sort of made me think that Viewbag (the object) is what @Html.Raw() choked on ... not ViewBag.xxx, which would be a, what?, a dynamic property of the ViewBag object?

... anyway, check this out ... When I try this @Html.Raw(ViewData["xxx"]) ... it works!!

Does this mean that @Html.Raw(Viewbag.xxx) isn't necessarily misinterpreting what it has been given? Is it just not resolving the dynamic object property value of Viewbag.xxx before performing its injection into the markup?

If an object is provided to @Html.Raw(), does it expect the passed object implement its own Raw definition? ... that would explain why it's saying the object doesn't have a Raw definition for ViewBag ... Viewbag the object ... it doesn't and shouldn't.

@Bartmax
Copy link

Bartmax commented Apr 16, 2015

why those are different remains unclear.

@dougbu maybe this is because most cases you call raw with string and you avoid the null check ? I also don't think it's necessary.

I still don't know why the object one is throwing, this worked with dynamics in mvc5 with no problem.

@Eilon
Copy link
Member

Eilon commented Apr 16, 2015

@dougbu oh yeah weird I guess it's the same as it always was... so not sure what's going on here.

@henkmollema
Copy link
Contributor

@dougbu @Eilon
Looks like it goes wrong because the Html property of the view uses the generic interface IHtmlHelper<TModel>. This worked in MVC5 because there is no IHtmlHelper interface.


Repro:
An 'html helper' with a Raw method:

public interface IMyHtmlHelper
{
    HtmlString Raw(object x);
}

public interface IMyHtmlHelper<T> : IMyHtmlHelper { }

public class MyHtmlHelper : IMyHtmlHelper
{
    public HtmlString Raw(object value)
    {
        return new HtmlString(value == null ? null : value.ToString());
    }
}

public class MyHtmlHelper<T> : MyHtmlHelper, IMyHtmlHelper<T> { }

With this view:

@{
    IMyHtmlHelper<object> MyHtml = new MyHtmlHelper<object>();
    ViewBag.Test = "<strong>Hello World!</strong>";
}
<p>
    @MyHtml.Raw(ViewBag.Test)
</p>

Causes the same RuntimeBinderException:

at CallSite.Target(Closure , CallSite , IMyHtmlHelper`1 , Object )
at System.Dynamic.UpdateDelegates.UpdateAndExecute2[T0,T1,TRet](CallSite site, T0 arg0, T1 arg1)
at Asp.ASPV__Views_Home_Index_cshtml.<ExecuteAsync>d__13.MoveNext()

However, when I change this line:

IMyHtmlHelper<object> MyHtml = new MyHtmlHelper<object>();

to this:

MyHtmlHelper<object> MyHtml = new MyHtmlHelper<object>();

(notice that the type of MyHtml is the actual class implementation MyHtmlHelper<object>)

It does work.

Using the non-generic type works too:

IMyHtmlHelper MyHtml = new MyHtmlHelper();

Should this be a new issue?

@danroth27 danroth27 added this to the 6.0.0-beta5 milestone Apr 17, 2015
@danroth27 danroth27 modified the milestones: 6.0.0-beta5, 6.0.0-beta6 May 18, 2015
@dougbu
Copy link
Member

dougbu commented May 26, 2015

@guardrex the behaviour seen here is as-expected when attempting to pass a dynamic value to an interface. The downside of that interface is a few cases involving dynamic such as the one you hit. (The upside is the ability to replace the implementation completely if you wish.)

You have a number of potential workarounds here, including the cast @henkmollema first mentioned. For example:

<p>Raw title: @Html.Raw((object)ViewBag.Title)</p>
<p>Raw title: @(((HtmlHelper)Html).Raw(ViewBag.Title))</p>
<p>Raw title: @Html.Raw(ViewData["Title"])</p>

Since ViewData (Dictionary) look-ups far out-perform ViewBag (dynamic) invocations, the last is probably the best choice.

BTW @Bartmax the specific casts, null checks, and overloads here are not that important. The relevant bit is just using IHtmlHelper versus calling a non-virtual method on the concrete HtmlHelper class in MVC 5.

@yishaigalatzer
Copy link
Contributor

@dougbu so there is a way to fix this and keep both advantages. By just changing IHtmlHelper to an abstract base class HtmlHelper with no implementations. You get the best of both worlds

@dougbu
Copy link
Member

dougbu commented May 27, 2015

@yishaigalatzer that approach might work in some cases but it isn't possible here. DefaultHtmlHelper<TModel> ends up inheriting from two base classes -- DefaultHtmlHelper and HtmlHelper<TModel>. Delegation to avoid that makes things very messy.

@dougbu dougbu removed this from the 6.0.0-beta6 milestone May 27, 2015
@dougbu dougbu removed their assignment May 27, 2015
@dougbu
Copy link
Member

dougbu commented May 27, 2015

Clearing milestone and assignee in case triage wants to take another look at this.

@chmontgomery
Copy link

👍 just ran into this myself. using @Html.Raw((object)Bundles.vendor.styles) worked but it's not intuitive. Here's my sample project for reference: https://github.com/chmontgomery/ExampleMVC6Application

@danroth27 danroth27 added this to the 6.0.0-beta6 milestone Jun 12, 2015
@dougbu
Copy link
Member

dougbu commented Jun 12, 2015

Possible options include:

  1. Add Encode() and Raw() methods to RazorPage.
  2. Push everything not already part of IHtmlGenerator into that interface, reduce HtmlHelper and HtmlHelper<TModel> to delegations to IHtmlGenerator, and remove IHtmlHelper and IHtmlHelper<TModel>.
  3. ...

@dougbu
Copy link
Member

dougbu commented Jun 15, 2015

A couple of new options in addition to avoiding or removing IHtmlHelper:

  1. Move the Encode() and Raw() methods from IHtmlHelper to IHtmlHelper<TModel>. We never inject properties of the lower-level interface but do use IHtmlHelper.Encode() in a couple of the default display and editor templates. Might need to finesse that slightly...
  2. Add new string Encode(string value); and so on to IHtmlHelper<TModel>. My testing shows this works fine.

tl;dr;
A slightly reduced repro is:

    class Program
    {
        public static void Main(string[] args)
        {
            dynamic joe = "fred";
            IMyHtmlHelper2 myHtml = new MyHtmlHelper2();

            Console.WriteLine(myHtml.Raw(joe));
        }

        private interface IMyHtmlHelper
        {
            string Raw(object value);

            string Raw(string value);
        }

        private interface IMyHtmlHelper2 : IMyHtmlHelper
        {
        }

        private class MyHtmlHelper2 : IMyHtmlHelper2
        {
            public string Raw(object value)
            {
                return value == null ? null : value.ToString();
            }

            public string Raw(string value)
            {
                return value;
            }
        }
    }

Note the problems occur after eliminating generics from the scenario and after removing inheritance from the implementing class. I've also run the above in .NET 4.5 and encountered the identical RuntimeBinderException.

@henkmollema
Copy link
Contributor

@dougbu dougbu added bug and removed needs design labels Jun 15, 2015
@dougbu
Copy link
Member

dougbu commented Jun 15, 2015

Removed Design label and marked this as a Bug (because it's a regression compared to MVC 5).

dougbu added a commit that referenced this issue Jun 16, 2015
- #2392
- `dynamic` does not work correctly when inherited from a base `interface`
dougbu added a commit that referenced this issue Jun 17, 2015
- #2392
- `dynamic` does not work correctly when inherited from a base `interface`
@dougbu
Copy link
Member

dougbu commented Jun 17, 2015

8b5931d

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

8 participants