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

前端 未结 5 1772
长情又很酷
长情又很酷 2020-12-13 10:12

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

5条回答
  •  暖寄归人
    2020-12-13 10:55

    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(() => 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.

    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> modelLookup = new Dictionary>();
    
        public void WhenRendering(Func 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));
    }
    
    

    The time is: @clock.Now

    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
    

    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.

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

    提交回复
    热议问题