My current problem is that I have a partial view that I want to determine what model is being used by it.
I have had to deal with a few strange scenarios for my pro
I prototyped a possible solution to this, because it seemed like a fun problem. I hope it's useful to you.
First, the models. I decided to create two 'widgets', one for news, and one for a clock.
public class NewsModel
{
public string[] Headlines { get; set; }
public NewsModel(params string[] headlines)
{
Headlines = headlines;
}
}
public class ClockModel
{
public DateTime Now { get; set; }
public ClockModel(DateTime now)
{
Now = now;
}
}
My controller doesn't know anything about the views. What it does is returns a single model, but that model has the ability to dynamically fetch the right model as required by the view.
public ActionResult Show(string widgetName)
{
var selector = new ModelSelector();
selector.WhenRendering(() => new ClockModel(DateTime.Now));
selector.WhenRendering(() => new NewsModel("Headline 1", "Headline 2", "Headline 3"));
return PartialView(widgetName, selector);
}
Delegates are used so that the correct model is only created/fetched if it is actually used.
The ModelSelector that the controller uses is pretty simple - it just keeps a bag of delegates to create each model type:
public class ModelSelector
{
private readonly Dictionary> modelLookup = new Dictionary>();
public void WhenRendering(Func
Now, the easiest way to implement a view would be:
@model MvcApplication2.ModelSelector
@using MvcApplication2.Models
@{
var clock = (ClockModel) Model.GetModel(typeof (ClockModel));
}
The time is: @clock.Now
You could end here and use this approach.
That's pretty ugly. I wanted my views to look like this:
@model MvcApplication2.Models.ClockModel
Clock
@Model.Now
And
@model MvcApplication2.Models.NewsModel
News Widget
@foreach (var headline in Model.Headlines)
{
@headline
}
To make this work, I had to create a custom view engine.
When a Razor view is compiled, it inherits a ViewPage, where T is the @model. So we can use reflection to figure out what type the view wanted, and select it.
public class ModelSelectorEnabledRazorViewEngine : RazorViewEngine
{
protected override IView CreateView(ControllerContext controllerContext, string viewPath, string masterPath)
{
var result = base.CreateView(controllerContext, viewPath, masterPath);
if (result == null)
return null;
return new CustomRazorView((RazorView) result);
}
protected override IView CreatePartialView(ControllerContext controllerContext, string partialPath)
{
var result = base.CreatePartialView(controllerContext, partialPath);
if (result == null)
return null;
return new CustomRazorView((RazorView)result);
}
public class CustomRazorView : IView
{
private readonly RazorView view;
public CustomRazorView(RazorView view)
{
this.view = view;
}
public void Render(ViewContext viewContext, TextWriter writer)
{
var modelSelector = viewContext.ViewData.Model as ModelSelector;
if (modelSelector == null)
{
// This is not a widget, so fall back to stock-standard MVC/Razor rendering
view.Render(viewContext, writer);
return;
}
// We need to work out what @model is on the view, so that we can pass the correct model to it.
// We can do this by using reflection over the compiled views, since Razor views implement a
// ViewPage, where T is the @model value.
var compiledViewType = BuildManager.GetCompiledType(view.ViewPath);
var baseType = compiledViewType.BaseType;
if (baseType == null || !baseType.IsGenericType)
{
throw new Exception(string.Format("When the view '{0}' was compiled, the resulting type was '{1}', with base type '{2}'. I expected a base type with a single generic argument; I don't know how to handle this type.", view.ViewPath, compiledViewType, baseType));
}
// This will be the value of @model
var modelType = baseType.GetGenericArguments()[0];
if (modelType == typeof(object))
{
// When no @model is set, the result is a ViewPage
The view engine is registered by putting this in Global.asax.cs:
ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new ModelSelectorEnabledRazorViewEngine());
My home view includes the following lines to test it all out:
@Html.Action("Show", "Widget", new { widgetName = "Clock" })
@Html.Action("Show", "Widget", new { widgetName = "News" })