8

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?

Christos
  • 53,228
  • 8
  • 76
  • 108
mflair2000
  • 315
  • 3
  • 13
  • you could have the `ProvinceDetail` page do the switching instead to treat the different provinces appropriately instead? That would let you keep the logic on that page instead of having to repeat it if you use these elsewhere – DLeh Apr 09 '15 at 15:35
  • 1
    This isn't clear: do you need to instantiate a partial view for each *concrete type* of ProvinceDetail, or each *instantiation*? It just seems odd that you would have multiple partial QuebecDetail views, for example. – Kjata30 Apr 09 '15 at 15:57
  • Sorry, i tried to simplify the example. I need to instantiate a partial view for each province that has some different properties. for example Ontario, Quebec, Manitoba, BC all have a specific and different partial view, everything else gets a standard default generic view ie. ProvinceDetail. The result will be a list of Provinces and their details on the main page. – mflair2000 Apr 09 '15 at 16:19

3 Answers3

3

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)
}
Craig Curtis
  • 853
  • 10
  • 24
1

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)

Community
  • 1
  • 1
haim770
  • 48,394
  • 7
  • 105
  • 133
0

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