In short I would like to be able to pass a generic ViewModel into my views
Here is some simplified code of the gist of what I am trying to achieve
public interface IPerson
{
string FirstName {get;}
string LastName {get;}
}
public class FakePerson : IPerson
{
public FakePerson()
{
FirstName = "Foo";
LastName = "Bar";
}
public string FirstName {get; private set;}
public string LastName {get; private set;}
}
public class HomeViewModel<T>
where T : IPerson, new()
{
public string SomeOtherProperty{ get; set;}
public T Person { get; private set; }
public HomeViewModel()
{
Person = new T();
}
}
public class HomeController : Controller {
public ViewResult Index() {
return View(new HomeViewModel<FakePerson>());
}
}
If I create my view as follows all works as expected
<%@ Page Language="C#"
MasterPageFile="~/Views/Shared/Site.Master"
Inherits="System.Web.Mvc.ViewPage<HomeViewModel<FakePerson>>" %>
<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
<%: Html.DisplayFor(m=>m.Person.FirstName) %>
<%: Html.DisplayFor(m=>m.Person.LastName) %>
</asp:Content>
However I do not want to depend directly on FakePerson in the view in the event that I want to pass some other IPerson implementation, so I tried to change the page directive to
<%@ Page Language="C#"
MasterPageFile="~/Views/Shared/Site.Master"
Inherits="System.Web.Mvc.ViewPage<HomeViewModel<IPerson>>" %>
But of course that does not work, so, after a whole day of mucking about, I have more grey hair and no clue what to do next.
Can anybody help please.
[UPDATE]
Some advise has sugested that I should use a covariant interface; define a non-generic interface and use it in the View. Unfortuanetly I have tried this but there is one aditional implication. I would like the HtmlHelper function to be able to access any data annotation attributes that may be defined in the IPerson derived class
public class FakePerson : IPerson
{
public FakePerson()
{
FirstName = "Foo";
LastName = "Bar";
}
[DisplayName("First Name")]
public string FirstName {get; private set;}
[DisplayName("Last Name")]
public string LastName {get; private set;}
}
So while using a covariant interface this way does work, partly, to access the derived type through the ViewModel; since the view is typed to the interface, it appears the attributes are not accessible.
Is there perhaps a way, in the view, to get access to these attributes, maybe with reflection.
Or could there be anotherway to type the View to the generic.
I have successfully got covariant to work, that is, binding the View to an abstract base class. In fact, what I have is a binding to a List<MyBaseClass>. Then I create a specific View strongly typed to each subclass. That takes care of the binding.
But rebinding will fail because the DefaultModelBinder only knows about abstract base class and you will get an exception like, 'Cannot create abstract class'. The solution is to have a property on your base class like this:
public virtual string BindingType
{
get
{
return this.GetType().AssemblyQualifiedName;
}
}
Bind that to a hidden input in your view. Then your replace your default ModelBinder with a custom one in Global.asax:
// Replace default model binder with one that can deal with BaseParameter, etc.
ModelBinders.Binders.DefaultBinder = new CustomModelBinder();
And in your custom model binder you intercept the bind. If it is for one of your known abstract types, you parse the BindingType property and replace the model type so you get an instance of the subclass:
public class CustomModelBinder : DefaultModelBinder
{
private static readonly ILog logger = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, System.Type modelType)
{
if (modelType.IsInterface || modelType.IsAbstract)
{
// This is our convention for specifying the actual type of a base type or interface.
string key = string.Format("{0}.{1}", bindingContext.ModelName, Constants.UIKeys.BindingTypeProperty);
var boundValue = bindingContext.ValueProvider.GetValue(key);
if (boundValue != null && boundValue.RawValue != null)
{
string newTypeName = ((string[])boundValue.RawValue)[0].ToString();
logger.DebugFormat("Found type override {0} for Abstract/Interface type {1}.", modelType.Name, newTypeName);
try
{
modelType = System.Type.GetType(newTypeName);
}
catch (Exception ex)
{
logger.ErrorFormat("Error trying to create new binding type {0} to replace original type {1}. Error: {2}", newTypeName, modelType.Name, ex.ToString());
throw;
}
}
}
return base.CreateModel(controllerContext, bindingContext, modelType);
}
protected override object GetPropertyValue(ControllerContext controllerContext, ModelBindingContext bindingContext, System.ComponentModel.PropertyDescriptor propertyDescriptor, IModelBinder propertyBinder)
{
if (propertyDescriptor.ComponentType == typeof(BaseParameter))
{
string match = ".StringValue";
if (bindingContext.ModelName.EndsWith(match))
{
logger.DebugFormat("Try override for BaseParameter StringValue - looking for real type's Value instead.");
string pattern = match.Replace(".", @"\.");
string key = Regex.Replace(bindingContext.ModelName, pattern, ".Value");
var boundValue = bindingContext.ValueProvider.GetValue(key);
if (boundValue != null && boundValue.RawValue != null)
{
// Do some work here to replace the base value with a subclass value...
return value;
}
}
}
return base.GetPropertyValue(controllerContext, bindingContext, propertyDescriptor, propertyBinder);
}
}
Here, my abstract class is BaseParameter and I am replacing the StringValue property with a different value from the subclass (not shown).
Note that although you can rebind to the correct type, the form values associated only with the subclass will not be automatically roundtripped because the modelbinder only see the properties on the base class. In my case, I only had to replace one value in GetValue and get it instead from the subclass, so it was easy. If you need to bind lots of subclass properties you will need to do a bit more work and fetch them out the form (ValueProvider[0]) and populate the instance yourself.
Note that you can add a new model binder for a specific type so you could avoid the generic type checking.
Make your ViewModel class implement a non-generic (or generic covariant) interface, then modify the view to take that interface instead of the concrete class.
Your code is almost correct; you just need to pass a HomeViewModel<IPerson>
(not FakePerson
) in the controller.
You could also use a covariant interface for the Model in the view, but that's probably overkill.
I have created an interface
public interface ITestModel
{
string FirstName { get; set; }
string LastName { get; set; }
string Address { get; set; }
int Age { get; set; }
}
Create class and inherited from this interface
class TestModel : ITestModel
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string Address { get; set; }
public int Age { get; set; }
}
created instance of the class in controller
public ActionResult TestMethod()
{
ITestModel testModel;
testModel = new TestModel();
testModel.FirstName = "joginder";
testModel.Address = "Lovely Home";
testModel.LastName = "singh";
testModel.Age = 36;
return View("testMethod", testModel);
}
View created like this way
<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<TestProject.ITestModel>" %>
<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
TestMethod
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
<h2>TestMethod</h2>
<fieldset>
<legend>Fields</legend>
<div class="display-label">FirstName</div>
<div class="display-field"><%: Model.FirstName %></div>
<div class="display-label">LastName</div>
<div class="display-field"><%: Model.LastName %></div>
<div class="display-label">Address</div>
<div class="display-field"><%: Model.Address %></div>
<div class="display-label">Age</div>
<div class="display-field"><%: Model.Age %></div>
</fieldset>
<p>
<%: Html.ActionLink("Edit", "Edit", new { /* id=Model.PrimaryKey */ }) %> |
<%: Html.ActionLink("Back to List", "Index") %>
</p>
</asp:Content>
Now I can pass any object of any similar interface
I hope it will help :)