8

I've encountered a strange anomaly while learning/tinkering with asp.net.

I'm trying to show a partial view like this:

@Html.Partial("_PartialView", new { Action = "Foo" })

When I'm trying to access Action with

// Throws Microsoft.Csharp.RuntimeBinder.RuntimeBinderException
string throwsException = Model.Action; 

a RuntimeBinderExceptionis with the message

'object' does not contain a definition for 'Action'

is thrown.
The strange thing is that this line works fine:

// This line works fine
string works = ((Type)Model.GetType()).GetProperty("Action").GetValue(Model);

This behavior puzzles me quite a bit and I'd rather avoid using this workaround. Also I don't think the problem is anonymous types being internal because the MVC template for ASP.NET Project in VS2013 does this successfully:

enter image description here

So what happened here?

Community
  • 1
  • 1
Kabbalah
  • 471
  • 1
  • 5
  • 16
  • This happened to me; it was working - and then it just stopped, for no good reason. I cannot figure out why. Like you mention in another comment: weakly typed views with anonymous types *do* work in other places, such as the Visual Studio templates. The question is what makes them suddenly stop working here. – joshcomley Feb 07 '14 at 13:53

3 Answers3

10

The answer to this question can be found here: http://www.heartysoft.com/ashic/blog/2010/5/anonymous-types-c-sharp-4-dynamic

Pulling from the excellent blog post:

Anonymous Types are Internal

The reason the call to Model.Action fails is that the type information of Model is not available at runtime. The reason it's not available is because anonymous types are not public. When the method is returning an instance of that anonymous type, it's returning a System.Object which references an instance of an anonymous type - a type who's info isn't available to the main program. The dynamic runtime tries to find a property called Action on the object, but can't resolve it from the type information it has. As such, it throws an exception.

Paul
  • 12,392
  • 4
  • 48
  • 58
3

So what happened here?

Your partial view is weakly typed. You do not have a @model definition for it. So by default it is object which obviously doesn't have an Action property.

The correct way to solve this is to define a view model:

public class MyViewModel
{
    public string Action { get; set; }
}

that your partial view will be strongly typed to:

@model MyViewModel
@{
    string throwsException = Model.Action; 
}

and which will be passed by the main view:

@Html.Partial("_PartialView", new MyViewModel { Action = "Foo" })

Another possibility (which personally I don't like as it relies on runtime binding) is to use a dynamic model in the partial view:

@model dynamic
@{
    string throwsException = Model.Action; 
}

and then you will be able to pass an anonymous object when calling it:

@Html.Partial("_PartialView", new { Action = "Foo" })
Darin Dimitrov
  • 1,023,142
  • 271
  • 3,287
  • 2,928
  • 1
    Thanks for your answer. Creating a view model does solve this issue (dynamic model did not; Model is already dynamic). I'll accept this answer in a few days if it's still the best/only answer until then. But one thing still bothers me: Why does the MVC template for ASP.NET Project in VS2013 work? Its partial view is also weakly typed. In fact my partial view is just a tweaked version of it. If you can add an explanation to your answer I'll accept it right away. – Kabbalah Nov 13 '13 at 11:25
  • While using a VM avoids/solves the problem, it does not explain the error. The default case normally accepts dynamic/anonymous objects (not Object). Anonymous types normally work in partial views, but under some (as-yet unknown circumstance) it simply stops working. I have just hit this same problem and have had to change it to use strongly typed objects. As Kabbalah says, `@model dynamic` fails the same way as the original (as it is already expecting a dynamic). – iCollect.it Ltd Dec 17 '13 at 16:11
  • You could also use reflection but that would be the wrong thing to do. You could add an extension method on Object with T GetData(this object model,String key)... return model.GetType().GetProperty(key).GetValue(model) as T. As a static utility method it would be better. Let's be honest the performance hit isn't that bad. Helper.Data(Model, key). – jwize May 03 '14 at 02:00
1

Here are some options using reflection. The performance should be negligible in most scenarios.

Utility Class

public static class ModelHelper
{
    public static T Data<T>( String key)  
    {
        var html = ((System.Web.Mvc.WebViewPage)WebPageContext.Current.Page).Html;
        var model = html.ViewData.Model;
        return (T)model.GetType().GetProperty(key).GetValue(model) ;
    }
}

view.cshtml

@(Html.Partial("partial",new { Id="InstructorId"}))

paritial.cshtml file

@model dynamic 
@{ 
    var id = ModelHelper.Data<String>("Id");
}

With a more compact parital call in view.cshtml

@{
  var instructorId = "InstructorId";
  var windowTitle = "Window Title";
  var editorPageUrl = "~/View/Editors/Instructors.chstml";
}

@(Html.Partial("partial",new { instructorId, windowTitle, editorPageUrl }))

With variables adjusted to get inferred names in paritial.cshtml file

@model dynamic 
@{ 
    var id = ModelHelper.Data<String>("instructorId");
    var id = ModelHelper.Data<String>("windowTitle");
    var id = ModelHelper.Data<String>("editorPageUrl");
}

It is not hard to create a simple view-model class but I don't like classes that will only ever be used a single time for passing data, so this works for me.

You could also extend the default view base

namespace SafetyPlus.Shell.Code
{
    public abstract class ExtendedPageBaseClass<TModel> : WebViewPage<TModel> where TModel : class
    {
        public T Data<T>(String key) 
        {
            return (T)Model.GetType().GetProperty(key).GetValue(Model);
        }

        public Object Data(String key) 
        {
            return Data<Object>(key);
        }
    }
}

Register the base class in the /Views/web.config

<pages pageBaseType="SafetyPlus.Shell.Code.ExtendedPageBaseClass">
  ...
</pages>

Get the data in your partial view like this

@{ 
    var id = Data("Id");
    var idTyped = Data<String>("Id");
}

Or using an extension method which I suggested against.

namespace NotYourDefaultNamespace
{
    public static class ModelExtensions
    {
        public static T Data<T>( this Object model, String key)  
        {
            return (T)model.GetType().GetProperty(key).GetValue(model) ;
        }
    }
} 

This options really doesn't buy you anything since we are finding the model from within the previous utility method. The call would become.

@{
    var id = Model.Data("Id");
}
jwize
  • 4,230
  • 1
  • 33
  • 51