I'm having a problem with a polymorphic collection of ViewModels in my MVC application. I received this via a web service call and i need to iterate through them and give them their own partial view, based on the object type.
public abstract class ProvinceViewModel
{
public string Code { get; set; }
}
public sealed class OntarioViewModel : ProvinceViewModel { }
public sealed class QuebecViewModel : ProvinceViewModel {}
In my view i am trying to iterate through them and assign a partial view. I have to do a lot of type casting here to make it work. If I try and move this to a controller action and pass in the abstract type, i will get an error that we cannot create an instance of abstract class.
ICollection<ProvinceViewModel> ProvinceList; // collection receive via service
@for (int i = 0, c = ProvinceList.Count; i < c; i++)
{
var currentProvince = this.Model.ElementAt(i);
@switch (additionalRegistry.Code)
{
case "QC":
@Html.Partial("AlbertaDetail", (QuebecViewModel)currentProvince)
break;
case "ON":
@Html.Partial("OntarioDetail", (OntarioViewModel)currentProvince)
break;
default:
@Html.Partial("ProvinceDetail", ProvinceViewModel)
break;
}
}
I have strongly type View, so that i can access the different properties.
How would i go about solving this in a more elegant way? Would I need to create a new surrogate base class for the abstract class to create a instance of it easier?
You can achieve this with display templates. Create a display template for each type in the DisplayTemplates folder within your Controller's Views directory:
+-- Views
+-- Provinces
+-- DisplayTemplates
+-- OntarioViewModel.cshtml
+-- QuebecViewModel.cshtml
Display each model using the DisplayFor helper in your view:
@model ICollection<ProvinceViewModel>
@foreach (var province in Model)
{
@Html.DisplayFor(_ => province)
}
Upon encountering the same problem in the past, I have created the following solution:
First, decorate your (concrete) view-model with ExportMetadata
attribute that denotes the view name to be used. For example:
[ExportMetadata("View", "Ontario")]
public sealed class OntarioViewModel : ProvinceViewModel { }
[ExportMetadata("View", "Quebec")]
public sealed class QuebecViewModel : ProvinceViewModel {}
Then extend your HtmlHelper
with the following Partial
method:
public static MvcHtmlString Partial<T>(this HtmlHelper htmlHelper, T model, string prefix = null)
{
var modelType = typeof (T);
var partialAttr = modelType.GetCustomAttributes<ExportMetadataAttribute>().SingleOrDefault(x => x.Name == "View");
if (partialAttr == null)
throw new Exception(modelType.Name + " doesn't define any view to be used");
var partialName = (prefix ?? String.Empty) + partialAttr.Value;
return htmlHelper.Partial(partialName, model, htmlHelper.ViewData);
}
Then use it:
@Html.Partial(currentProvince);
And in case your partials reside in some sub-directory:
@Html.Partial(currentProvince, "Partials/")
(If you need help registering the custom HTML helper see https://stackoverflow.com/a/5052790 )
I had a similar requirement and this is how I managed to solve this issue. My viewmodel (BusinessEventEmailViewModel ) has a list of interfaces (IBusinessEventEmail) resolved at runtime with unity. A IBusinessEventEmail has an EventCode property.
public class BusinessEventEmailViewModel : MailHeaderViewModel
{
#region members
public List<IBusinessEventEmail> Events { get; set; }
In my view, I render the partial view using a naming convention :
Html.RenderPartial("~/Views/Shared/Email/_" + businessEvent.EventCode + ".cshtml", businessEvent);
Then, I have a XXXEventEmail implementing IBusinessEventEmail with the EventCode XXX and a partial view _XXX.cshtml
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.