从 MVC 中的控制器确定局部视图的模型

Posted

技术标签:

【中文标题】从 MVC 中的控制器确定局部视图的模型【英文标题】:Determine the model of a partial view from the controller within MVC 【发布时间】:2011-06-22 18:45:37 【问题描述】:

我目前的问题是我有一个局部视图,我想确定它正在使用什么模型。

我不得不为我的项目处理一些奇怪的场景,所以我将尝试在这里概述它,也许有人可以提供更好的方法来做到这一点。

我正在设计类似于 Google iGoogle 页面的东西。具有多个小部件的主页,这些小部件可以移动或根据需要进行配置。当前系统异步加载实际小部件的数据,查看我的应用程序中的控制器的 POST。该控制器要么将部分视图呈现为可以返回的 html(然后加载到 JQUERY 页面视图中),要么只呈现存储在数据库中的直接 HTML/javascript

这对我来说很好用,我有一个小部件模型,其中包含通过数据库描述的选项字典,然后由局部视图使用。当我想将数据传递给局部视图时,问题就来了。我能想出的最佳解决方案是让控制器确定所讨论的局部视图使用哪个模型,具有一些填充模型的函数,然后将它与局部视图一起传递给将其呈现给的函数控制器中的 HTML。

我意识到这对于 MVC 来说是一个奇怪的场景(层正在混合......),任何关于基本设计或实现的建议将不胜感激。

我目前正在使用 MVC3/Razor。如有任何其他问题,请随时提出。

【问题讨论】:

【参考方案1】:

一种选择是在您的应用程序中扩展部分请求的概念。 Steve Sanderson 有a fantastic example of this,虽然这篇文章与 MVC 1 和 2 相关。我认为它仍然对你 v3 有帮助,但我没有调查 v3 以查看 MVC 团队是否实现了他们自己的版本。在您的异步场景中,您需要稍微尝试一下实现,也许更改 PartialRequest 定义以根据需要接受不同的信息,但我认为这可能是一个好的开始。最终结果将是更好地隔离关注点,允许单个控制器管理特定类型的部分,进而更好地了解您要使用的模型类型。

【讨论】:

我正在查看这个,但我不确定它是否完全适用于我的场景。我的设计目前是针对不同小部件的数据库驱动的。这意味着我没有任何特定小部件的单独控制器。有很多小部件甚至不需要局部视图,只需要存储在服务器端的 HTML。它还使用 JSON 来返回呈现的 HTML,这是异步方面所需要的。我将继续查看它,看看是否有任何帮助。谢谢。 我理解,并且不认为直接应用会起作用,但认为底层机制(特别是对 RenderPartial 的修改)可能会为其他想法播下种子。所以,如果我理解正确,你有一个来自客户端的 javascript 类型调用,假设它将从控制器接收 HTML。控制器直接从数据库或部分视图中检索 html。无论哪种情况,控制器都会通过 JSON 返回纯 html。问题是当您专门使用需要模型的局部视图时,不知道要使用哪个模型,对吗? 正确。现在我有一个全局“小部件”模型,它有几个 ID 和一个适用于任何模型的首选项字典(并且是从数据库构建的)。这工作得很好而且非常灵活,当我引入想要直接访问数据库的小部件时,问题就来了。我最终不得不为每个小部件创建一个新模型,本质上是一个案例语句,用于确定小部件需要传递给它的模型(基于小部件的数据库定义)。我的想法是有一些方法来确定小部件的模型,然后为每个模型编写通用代码。【参考方案2】:

我不能 100% 确定这就是您要查找的内容,但是可以将 [ChildActionOnly] 属性添加到控制器中的方法中。这要求该方法只能从partial view 调用。然后,您可以为该方法设置部分视图,该方法基本上类似于您的一个小部件。在此处查看 MVC 音乐商店示例:

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

【讨论】:

【参考方案3】:

动态视图模型呢? MVC3 中的布局使用它们,也许您可​​以为您的目的使用类似的东西:

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

【讨论】:

【参考方案4】:

我为此设计了一个可能的解决方案原型,因为这似乎是一个有趣的问题。我希望它对你有用。

型号

首先,模型。我决定创建两个“小部件”,一个用于新闻,一个用于时钟。

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;
    

控制器

我的控制器对视图一无所知。它所做的是返回单个模型,但该模型能够根据视图的要求动态获取正确的模型。

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

使用委托以便仅在实际使用时创建/获取正确的模型。

模型选择器

控制器使用的 ModelSelector 非常简单 - 它只保留一袋代表来创建每种模型类型:

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]();
    

视图 - 简单的解决方案

现在,实现视图的最简单方法是:

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


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

你可以在这里结束并使用这种方法。

观点 - 更好的解决方案

这太丑了。我希望我的视图看起来像这样:

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

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

    <h3>@headline</h3>

为了完成这项工作,我必须创建一个自定义视图引擎。

自定义视图引擎

编译 Razor 视图时,它会继承 ViewPage&lt;T&gt;,其中 T@model。所以我们可以使用反射来确定视图想要什么类型,然后选择它。

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

通过将其放入 Global.asax.cs 来注册视图引擎:

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

渲染

我的主页视图包括以下几行来测试它:

@Html.Action("Show", "Widget", new  widgetName = "Clock" )
@Html.Action("Show", "Widget", new  widgetName = "News" )

【讨论】:

【参考方案5】:

我在博客上写了关于这样做的内容。请看http://blogs.planetcloud.co.uk/mygreatdiscovery/?tag=/widget

基本上我构建了一个类似的小部件系统。这些帖子还介绍了如何处理这些小部件的配置。这利用了 Mvc3 中的动态支持,因此任何模型对象都可以从单个控制器操作传递到视图。

默认情况下,所有小部件都有一组 KVP 属性(我相信这就是 OP 所拥有的)。因此,对于一个简单的小部件,我们可以从视图中访问这些属性。我用于显示一些 html 的小部件(其中 html 存储在这些属性之一中)。

但是,对于更复杂的小部件,我们实现了IWidgetWithDisplayModel。这告诉我们,在将加载的小部件传回视图之前,我们需要“构建”我们的显示模型。

这是执行此操作的控制器操作。查看帖子了解完整详情。

[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);

【讨论】:

仅作为博文的一部分(见上面的链接)

以上是关于从 MVC 中的控制器确定局部视图的模型的主要内容,如果未能解决你的问题,请参考以下文章

在部分视图 MVC5 之间传递视图模型

局部视图中的模型未返回任何数据

需要从 MVC 5 中的控制器操作关闭 Jquery UI 对话框

控制器中的 Asp.Net MVC 5 ModelState 具有索引、添加、编辑

将数据从 MVC 控制器传递到 PHP 中的视图

从 jsp 访问 Spring MVC DI bean