将通用于所有页面的数据传递到布局

146

我有一个网站,其中有一个布局页面。然而,这个布局页面包含了所有页面模型必须提供的数据,例如页面标题、页面名称以及我们实际所在的位置,用于执行一些操作的HTML Helper。此外,每个页面都有其自己的视图模型属性。

我该怎么做呢?手动输入布局似乎不是一个好主意,但我应该如何传递这些信息呢?


12
阅读这里的回复的任何人,请查看https://dev59.com/gm035IYBdhLWcg3wYe8F#21130867,在那里您将看到比此处发布的任何内容都更简单和更整洁的解决方案。 - Avrohom Yisroel
5
@AvrohomYisroel 的建议不错。但我更喜欢 @Colin Bacon 的方法,因为它是强类型的,而且不在 ViewBag 中。可能只是个人偏好问题。不过我还是给你的评论点了赞。 - JP Hellemons
对于 MVC 5,请参见此答案:https://dev59.com/KW855IYBdhLWcg3wxHYq#46783375 - LazZiya
19个回答

157

如果您需要将相同的属性传递给每个页面,则创建一个基础视图模型并由所有视图模型使用会很明智。然后,您的布局页面可以采用此基础模型。

如果需要处理这些数据背后的逻辑,则应将其放入基本控制器中,并由所有控制器使用。

有许多事情可以做,重要的方法是不要在多个位置重复相同的代码。

编辑:以下来自评论的更新

这里有一个简单的示例来演示该概念。

创建一个所有视图模型将继承的基础视图模型。

public abstract class ViewModelBase
{
    public string Name { get; set; }
}

public class HomeViewModel : ViewModelBase
{
}

您的布局页面可以将此作为其模板。

@model ViewModelBase
<!DOCTYPE html>
<html>
    <head>
        <meta name="viewport" content="width=device-width" />
        <title>Test</title>
    </head>
    <body>
        <header>
            Hello @Model.Name
        </header>
        <div>
            @this.RenderBody()
        </div>
    </body>
</html>

最后在操作方法中设置数据。

public class HomeController
{
    public ActionResult Index()
    {
        return this.View(new HomeViewModel { Name = "Bacon" });
    }
}

13
数据被用在布局中,我如何将数据传递给布局? - Rushino
2
太好了!我看到了我的错误。我忘记将模型传递给视图了...多么愚蠢的错误。谢谢! - Rushino
7
这种方法的问题在于有时并非每个视图都有一个ViewModel,所以在这种情况下它将无法工作 :O/ - Cacho Santa
22
但是这是否需要每个控制器和操作都包括 { Name = "Bacon" } 的代码呢?如果我想要向 ViewModelBase 添加另一个属性,那么我就必须去每个控制器和操作并添加代码来填充该属性吗?您提到“如果需要逻辑[...],应将其放入基础控制器中[...]”。如何通过这种方法消除每个控制器和操作中重复的代码呢? - Lee
6
如果某些数据在所有页面上都是共通的,那么你应该将这些数据放在一个基础控制器中。然后让你的控制器继承自这个基础控制器。例如:public class HomeController : BaseController。这样,公共代码只需要编写一次,就可以应用到所有控制器上。 - Colin Bacon
显示剩余6条评论

82

我在布局中使用了Razor的RenderAction HTML助手。

@{
   Html.RenderAction("Action", "Controller");
 }

我需要它来处理简单的字符串。因此,我的动作返回字符串并在视图中轻松地将其写下。但是如果您需要复杂的数据,则可以返回PartialViewResult和model。

 public PartialViewResult Action()
    {
        var model = someList;
        return PartialView("~/Views/Shared/_maPartialView.cshtml", model);
    }
你只需要将你的模型放在你创建的局部视图 '_maPartialView.cshtml' 的开头处即可。
@model List<WhatEverYourObjeIs>

然后您可以在该部分视图中使用模型中的数据与HTML一起使用。


22
这绝对是最佳答案! - gingerbreadboy
@gingerbreadboy同意,它促进了良好的封装和责任分离。 - A-Dubb

37
另一种选择是创建一个名为LayoutModel的单独类,包含您在布局中所需的所有属性,然后将此类的实例放入ViewBag中。我使用Controller.OnActionExecuting方法来填充它。 然后,在布局开始时,您可以从ViewBag中取回此对象,并继续访问此强类型对象。

2
肯定是最好的解决方案,我看不到任何缺点。 - Wiktor Zychla
1
我真诚地认为这是一个更好的解决方案,当你使用集合时,被接受的版本会变得混乱不堪。 - Bobby Tables
7
我不明白这个做法对你有什么好处。如果你已经有一个包含所有布局所需属性的类,为什么要将它添加到ViewBag中,然后再进行强制类型转换呢?直接在布局视图中使用模型,你仍然可以在 OnActionExecuting 中填充模型。使用ViewBag也意味着你在控制器中失去了类型安全性,这是不好的事情。 - Colin Bacon
4
这让我能够为布局添加一个模型,而无需重新构建所有模型以从单个“超级”模型继承所有控制器的所有方法,在已经存在的项目中。 如果你从头开始,可以选择将所有模型都派生自共同的根模型。 - DenNukem
5
@ColinBacon 这个选项的另一个优点是你的动作不必总是有视图模型。此外,我认为开发人员需要知道他们必须始终从基类继承视图模型是一个缺点。 - Josh Noe
显示剩余6条评论

36

大概来说,这个主要用例是为所有(或大部分)控制器操作获取一个基本模型以供查看。

鉴于此,我使用了几个答案的组合,主要是借鉴了 Colin Bacon 的答案。

这是控制器逻辑,因为我们正在填充一个视图模型以返回到视图。因此,放置它的正确位置是在控制器中。

我们希望所有控制器上都发生这种情况,因为我们将其用于布局页面。我将其用于在布局页中呈现的部分视图。

我们还希望获得强类型视图模型的附加好处

因此,我创建了一个 BaseViewModel 和 BaseController。所有 ViewModel 控制器都将分别继承自 BaseViewModel 和 BaseController。

代码:

BaseController

public class BaseController : Controller
{
    protected override void OnActionExecuted(ActionExecutedContext filterContext)
    {
        base.OnActionExecuted(filterContext);

        var model = filterContext.Controller.ViewData.Model as BaseViewModel;

        model.AwesomeModelProperty = "Awesome Property Value";
        model.FooterModel = this.getFooterModel();
    }

    protected FooterModel getFooterModel()
    {
        FooterModel model = new FooterModel();
        model.FooterModelProperty = "OMG Becky!!! Another Awesome Property!";
    }
}

请注意使用 OnActionExecuted,该方法取自此 SO 帖子

HomeController

public class HomeController : BaseController
{
    public ActionResult Index(string id)
    {
        HomeIndexModel model = new HomeIndexModel();

        // populate HomeIndexModel ...

        return View(model);
    }
}

基础视图模型

public class BaseViewModel
{
    public string AwesomeModelProperty { get; set; }
    public FooterModel FooterModel { get; set; }
}

HomeViewModel

public class HomeIndexModel : BaseViewModel
{

    public string FirstName { get; set; }

    // other awesome properties
}

页脚模型

public class FooterModel
{
    public string FooterModelProperty { get; set; }
}

Layout.cshtml

@model WebSite.Models.BaseViewModel
<!DOCTYPE html>
<html>
<head>
    < ... meta tags and styles and whatnot ... >
</head>
<body>
    <header>
        @{ Html.RenderPartial("_Nav", Model.FooterModel.FooterModelProperty);}
    </header>

    <main>
        <div class="container">
            @RenderBody()
        </div>

        @{ Html.RenderPartial("_AnotherPartial", Model); }
        @{ Html.RenderPartial("_Contact"); }
    </main>

    <footer>
        @{ Html.RenderPartial("_Footer", Model.FooterModel); }
    </footer>

    < ... render scripts ... >

    @RenderSection("scripts", required: false)
</body>
</html>

_Nav.cshtml

@model string
<nav>
    <ul>
        <li>
            <a href="@Model" target="_blank">Mind Blown!</a>
        </li>
    </ul>
</nav>

希望这可以帮助。


2
我使用了这种方法,但更喜欢从接口继承而不是从基类继承。所以我做了以下操作: var model = filterContext.Controller.ViewData.Model as IBaseViewModel如果(model != null) { model.AwesomeModelProperty = "Awesome Property Value"; } - Tom Gerken
2
非常好的答案,我更喜欢这个答案胜过其他所有答案。 - Jynn
1
很好的回答,但我有一个问题。“如果我有一些没有ViewModels的视图怎么办...?” - Isma Haro
1
尝试过这个方法,但在Index Action上,OnActionExecuted填充了FooterModel,然后创建了一个具有空FooterModel的新HomeIndexModel :( - SteveCav
1
@drizzie:在你的基础控制器中,模型是Filter方法中的一个局部变量:var model = filterContext.Controller.ViewData.Model as BaseViewModel。我不明白MVC如何理解这个局部变量与HomeController发送到视图的模型相同。 - Hooman Bahreini

11
如果项目使用.Net Core或.Net,还有另一种处理方式。从架构角度来看可能不是最干净的方式,但它避免了其他答案中涉及的许多痛苦。只需在Razor布局中注入一个服务,然后调用一个获取必要数据的方法即可。
@inject IService myService

然后在布局视图中稍后:
@if (await myService.GetBoolValue()) {
   // Good to go...
}

再次强调,从架构的角度来看并不完美(显然服务不应该直接注入到视图中),但它能完成任务。

1
不是最简洁的方式?我不同意。我认为这是最简洁的方式:对象从创建它的地方直接传递到您想要它的地方,而不会“污染”其他控制器不需要看到的项目。在我看来,使用@inject是最好的解决方案。 - Sergey Kalinichenko
3
仔细想了一下,也许你是对的。这种方法避免了很多痛苦,这表明它可能是最干净的方式。我正在开发一个非常大的ASP.NET Core应用程序,并且在处理导航面包屑逻辑、大部分页面上的页眉数据等方面使用了这种模式。通过这种方式,我已经避免了很多痛苦。 - Andrew
这种技术也被_LoginPartial.cshtml使用,当您启用Identity时,它是一个自动生成的视图,用于脚手架生成Asp.Net Core Razor Page项目。 - Bochen Lin

9

您不必修改操作或更改模型,只需使用基本控制器并将现有控制器从布局视图上下文转换。

创建一个带有所需公共数据(标题/页面/位置等)和操作初始化的基本控制器...

public abstract class _BaseController:Controller {
    public Int32 MyCommonValue { get; private set; }

    protected override void OnActionExecuting(ActionExecutingContext filterContext) {

        MyCommonValue = 12345;

        base.OnActionExecuting(filterContext);
    }
}

确保每个控制器都使用基础控制器...

public class UserController:_BaseController {...

在您的_Layout.cshml页面中,将现有的基础控制器从视图上下文转换为...
@{
    var myController = (_BaseController)ViewContext.Controller;
}

现在,您可以从布局页面引用基础控制器中的值。
@myController.MyCommonValue

更新

您也可以创建一个页面扩展,以允许您使用this

//Allows typed "this.Controller()." in cshtml files
public static class MyPageExtensions {
    public static _BaseController Controller(this WebViewPage page) => Controller<_BaseController>(page);
    public static T Controller<T>(this WebViewPage page) where T : _BaseController => (T)page.ViewContext.Controller;
}

当你需要使用控制器时,只需记住使用this.Controller()

@{
    var myController = this.Controller(); //_BaseController
}

或者继承自_BaseController的特定控制器...
@{
    var myController = this.Controller<MyControllerType>();
}

2
在 .NET Core 中的相应等价物是什么?由于 ViewContext.Controller 不存在且继承链中存在一些变化,因此需要注意。 - Jayanth Thyagarajan

5
如果您想传递整个模型,请在布局中按以下格式进行操作:
@model ViewAsModelBase
<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta charset="utf-8"/>
    <link href="/img/phytech_icon.ico" rel="shortcut icon" type="image/x-icon" />
    <title>@ViewBag.Title</title>
    @RenderSection("styles", required: false)    
    <script type="text/javascript" src="http://code.jquery.com/jquery-1.8.3.min.js"></script>
    @RenderSection("scripts", required: false)
    @RenderSection("head", required: false)
</head>
<body>
    @Html.Action("_Header","Controller", new {model = Model})
    <section id="content">
        @RenderBody()
    </section>      
    @RenderSection("footer", required: false)
</body>
</html>

并将以下代码添加到控制器中:

public ActionResult _Header(ViewAsModelBase model)

5

我觉得这些答案都不够灵活,无法适用于大型企业级应用。虽然我不是ViewBag的忠实拥趸,但在这种情况下,为了提高灵活性,我会做个例外。以下是我的处理方法...

你应该在所有控制器上都有一个基本控制器。在基本控制器的OnActionExecuting中添加布局数据(如果你想推迟,则使用OnActionExecuted)...

public class BaseController : Controller
{
    protected override void OnActionExecuting(ActionExecutingContext     
        filterContext)
    {
        ViewBag.LayoutViewModel = MyLayoutViewModel;
    }
}

public class HomeController : BaseController
{
    public ActionResult Index()
    {
        return View(homeModel);
    }
}

然后在您的_Layout.cshtml中,从ViewBag中提取您的ViewModel...
@{
  LayoutViewModel model = (LayoutViewModel)ViewBag.LayoutViewModel;
}

<h1>@model.Title</h1>

或者...
<h1>@ViewBag.LayoutViewModel.Title</h1>

这样做不会干扰您页面的控制器或视图模型的编码。


我喜欢你的想法,但是如果你动态创建了一个 MyLayoutViewModel,我该如何向 OnActionExecuting 方法传递一些参数呢? - Rey
1
哎呀,你的 OnActionExecuting 方法中还需要 base.OnActionExecuting(filterContext) 呢! - ErikE

4
创建一个代表布局视图模型的基础视图是一个糟糕的方法。想象一下,你想要一个代表在布局中定义的导航的模型。你会做 CustomersViewModel : LayoutNavigationViewModel 吗?为什么?为什么应该通过每个解决方案中的单个视图模型传递导航模型数据?
布局视图模型应该是专用的,独立的,并且不应强制其余的视图模型依赖它。
相反,你可以在你的 _Layout.cshtml 文件中这样做:
@{ var model = DependencyResolver.Current.GetService<MyNamespace.LayoutViewModel>(); }

最重要的是,我们不需要new LayoutViewModel(),我们将获得LayoutViewModel所有的依赖项,并为我们解决它们。
例如:
public class LayoutViewModel
{
    private readonly DataContext dataContext;
    private readonly ApplicationUserManager userManager;

    public LayoutViewModel(DataContext dataContext, ApplicationUserManager userManager)
    {
    }
}

你在哪里填写这个模型?也是在BaseController中吗? - ndberg
我认为这对于ASP.Net Core中的Scoped布局模型对象也是一个不错的想法。 - James Wilkins
我不会让视图去获取依赖项。那绝对不是“MVC”。服务定位器是一种反模式 - Jiveman
与普遍观点相反,服务定位器不是一种反模式,实际上这与MVC无关,@Jiveman,你只是在扔噱头吗?http://blog.gauffin.org/2012/09/service-locator-is-not-an-anti-pattern/ - hyankov
Jgauffin在那篇文章中的主要观点似乎是“反模式”这个术语不应该应用于服务定位器,因为SL至少可以有一些有效的用途。这是一个公正的观点。然而,正如他自己在一些讨论评论中所体现的那样,他建议在构建库和框架时使用SL可能是一个有效的方法,但在构建应用程序时并不一定推荐(我认为OP的问题和这里的讨论都是围绕着这个问题)。 - Jiveman

3
您还可以使用RenderSection,它可以帮助您将Model数据注入到_Layout视图中。
您可以注入View Model数据、JsonScriptCSSHTML等内容。
在此示例中,我将Index视图中的Json注入到Layout视图中。 Index.chtml
@section commonLayoutData{

    <script>

        var products = @Html.Raw(Json.Encode(Model.ToList()));

    </script>

    }

_Layout.cshtml

@RenderSection("commonLayoutData", false)

这样可以避免创建单独的基本“视图模型”需求。希望能对某些人有所帮助。

1
完美的解决方案,当你只需要为少数视图渲染特定内容时。 - Kunal

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接