开发者

Determine the model of a partial view from the controller within MVC

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 project so I will try to outline it here, maybe someone can offer a better way to do this.

I am designing something like the Google iGoogle page. A main page with multiple widgets that are able to move around or be configured as needed. The current system loads the actual widget's data asynchronously view a POST to a controller within my application. That controller will either render a partial view to HTML that can be returned (and then loaded into the page view JQUERY) or just straight HTML/JavaScript that is stored in a database.

This was working fine for me, I had a model for the widgets that holds a dictionary of options that are described via the database, and then used by the partial view. The problem came when I wanted to pass data to a partial view. The best solution I could come up with was having the controller determine which model the partial view in question uses, have some function that will fill the model, and then pass it, along with the partial view, to the function that wil开发者_C百科l render it to HTML within the controller.

I realize this is an odd scenario for MVC (the layers are blending...) and any advice on fundamental design, or implementation of this would be greatly appreciated.

I am currently using MVC3/Razor. Feel free to ask any other questions.


I prototyped a possible solution to this, because it seemed like a fun problem. I hope it's useful to you.

Models

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;
    }
}

Controller

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<ClockModel>(() => new ClockModel(DateTime.Now));
    selector.WhenRendering<NewsModel>(() => 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.

ModelSelector

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<Type, Func<object>> modelLookup = new Dictionary<Type, Func<object>>();

    public void WhenRendering<T>(Func<object> getter)
    {
        modelLookup.Add(typeof(T), getter);
    }

    public object GetModel(Type modelType)
    {
        if (!modelLookup.ContainsKey(modelType))
        {
            throw new KeyNotFoundException(string.Format("A provider for the model type '{0}' was not provided", modelType.FullName));
        }

        return modelLookup[modelType]();
    }
}

The Views - Simple solution

Now, the easiest way to implement a view would be:

@model MvcApplication2.ModelSelector
@using MvcApplication2.Models
@{
    var clock = (ClockModel) Model.GetModel(typeof (ClockModel));
}

<h2>The time is: @clock.Now</h2>

You could end here and use this approach.

The Views - Better solution

That's pretty ugly. I wanted my views to look like this:

@model MvcApplication2.Models.ClockModel
<h2>Clock</h2>
@Model.Now

And

@model MvcApplication2.Models.NewsModel
<h2>News Widget</h2>
@foreach (var headline in Model.Headlines)
{
    <h3>@headline</h3>
}

To make this work, I had to create a custom view engine.

Custom view engine

When a Razor view is compiled, it inherits a ViewPage<T>, 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<T>, 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<object>
                throw new Exception(string.Format("The view '{0}' needs to include the @model directive to specify the model type. Did you forget to include an @model line?", view.ViewPath));                    
            }

            var model = modelSelector.GetModel(modelType);

            // Switch the current model from the ModelSelector to the value of @model
            viewContext.ViewData.Model = model;

            view.Render(viewContext, writer);
        }
    }
}

The view engine is registered by putting this in Global.asax.cs:

ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new ModelSelectorEnabledRazorViewEngine());

Rendering

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" })


One option would be to extend the idea of partial requests in your application. Steve Sanderson has a fantastic example of this, although the post relates to MVC 1 & 2. I think it would still help in you v3, but I haven't investigated v3 to see if the MVC team implemented their own version. In your asynch scenario, you'll need to toy with the implementation a bit, perhaps change the PartialRequest definition to accept different information as needed, but I think this might be a good start. The net result would be better isolation of concerns, allowing individual controllers to manage a particular type of partial, and in turn be better aware of the model Type you want to work with.


I'm not 100% sure that this is what you'd be looking for, but the [ChildActionOnly] attribute can be added to a method within your controller. That requires that the method can only be called from a partial view. Then you can set up your partial view for that method that basically resembles one of your widgets. Check out the MVC Music Store example here:

http://www.asp.net/mvc/tutorials/mvc-music-store-part-10


What about a dynamic view model? Layouts in MVC3 use them, and maybe you can use something similar for your purposes:

  1. Dynamic in C# 4.0: Introducing the ExpandoObject
  2. Fun With Method Missing and C# 4
  3. Dynamic View Page, MVC without a View Model


I blogged about doing exactly this. Please see http://blogs.planetcloud.co.uk/mygreatdiscovery/?tag=/widget

Essentially I built out a similar widget system. The posts also cover how to handle configuration of those widgets. This makes use of the dynamic support in Mvc3 so that any model object can be passed to the view, from a single controller action.

By default all widgets have a collection of KVP properties (I believe this is what the OP has). So for a simple widget we get access to those properties from within the view. I used for a widget that displayed some html (where the html was stored in one of those properties).

However, for more complex widgets we implement IWidgetWithDisplayModel. This tells us that before we pass the loaded widget back to the view, we need to "build" our display model.

Here's the controller action that does that. Check the posts for full details.

[HttpGet]
public ActionResult Get(string name)
{
    var widget = widgetService.GetWidgetBySystemName(name, true);

    if (widget == null)
        return Content(string.Format("Widget [{0}] not found!", name));

    if (!this.ViewExists(widget.WidgetName))
        return Content(string.Format("A template for widget [{0}] was not found.", widget.WidgetName));

    if (widget is IWidgetWithDisplayModel) {
        (widget as IWidgetWithDisplayModel).CreateDisplayModel();
    }

    return PartialView(widget.WidgetName, widget);
}
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜