ASP.NET MVC Razor如何将模型传递给布局视图

113

我看到的是一个字符串布局属性。但是我如何显式地将模型传递给布局?


我有几个页面,它们使用不同的模型但是相同的布局。 - SiberianGuy
2
这个 stackoverflow 的问题似乎回答了你的疑问:https://dev59.com/gWYs5IYBdhLWcg3wAfEi - Paul
我没有遇到这个问题。Model 已经在 _Layout 中可用了。我使用的是 MVC5。 - toddmo
12个回答

90
  1. 在您的控制器(或基本控制器)中添加名为MainLayoutViewModel(或其他名称)的属性,并使用您想要使用的任何类型。
  2. 在控制器(或基本控制器)的构造函数中,实例化该类型并将其设置为属性。
  3. 将其设置为ViewData字段(或ViewBag)
  4. 在布局页面中,将该属性转换为您的类型。

示例: 控制器:

public class MyController : Controller
{
    public MainLayoutViewModel MainLayoutViewModel { get; set; }

    public MyController()
    {
        this.MainLayoutViewModel = new MainLayoutViewModel();//has property PageTitle
        this.MainLayoutViewModel.PageTitle = "my title";

        this.ViewData["MainLayoutViewModel"] = this.MainLayoutViewModel;
    }

}

布局页面的顶部示例

@{
var viewModel = (MainLayoutViewModel)ViewBag.MainLayoutViewModel;
}
现在你可以在布局页面中引用变量“viewModel”,并完全访问已键入的对象。
我喜欢这种方法,因为控制器控制布局,而各个页面的视图模型保持不受布局影响。
MVC Core的注意事项
Mvc Core似乎会在第一次调用每个操作时清除ViewData / ViewBag的内容。 这意味着在构造函数中分配ViewData是行不通的。 然而,可以使用IActionFilter,并在OnActionExecuting中完成完全相同的工作。 在MyController上放置MyActionFilter
public class MyActionFilter: Attribute, IActionFilter
    {
        public void OnActionExecuted(ActionExecutedContext context)
        {
        }

        public void OnActionExecuting(ActionExecutingContext context)
        {
            var myController= context.Controller as MyController;

            if (myController!= null)
            {
                myController.Layout = new MainLayoutViewModel
                {

                };

                myController.ViewBag.MainLayoutViewModel= myController.Layout;
            }
        }
    }

1
我明白你的意思...但动态类型和强制类型转换在 Razor Pages 中非常重要。你可以在 MainLayoutViewModel 中添加一个静态方法,为你执行强制类型转换(例如:MainLayoutViewModel.FromViewBag(this.ViewBag)),这样至少强制类型转换只会发生在一个地方,你可以更好地处理异常情况。 - BlackjacketMack
2
@用户 - TempData 使用 Session,总让我感觉有点笨拙。我的理解是它是“一次性读取”的,也就是说,一旦你读取它,它就会从 Session 中删除(或者可能是在请求结束时)。如果你将 Session 存储在 Sql Server(或 Dynamo Db)中,那么考虑到你需要序列化 MasterLayoutViewModel...这很可能不是你想要的。因此,基本上将其设置为 ViewData 将其存储在内存中的一个小型灵活字典中,非常适合。 - BlackjacketMack
足够简单,我使用了你的解决方案,但是我对MVC比较新,所以我想知道这是否被认为是一种良好的实践?或者至少不是坏的实践? - Karim AG
1
嗨,Karim AG,我认为这是两者兼而有之。我倾向于认为将东西存储在ViewData中是一种不好的做法(它很难跟踪,基于字典,不是真正的类型化)...但是...在强类型对象中键入所有布局属性是一个很好的做法。所以我妥协了,说,好吧,让我们在那里存储一件事情,但是其余部分都锁定为良好的强类型ViewModel。 - BlackjacketMack
你有关于MVC Core在第一次调用每个操作时清除ViewData/ViewBag的任何信息吗?也许现在已经修复了? - niico
显示剩余4条评论

75

如果你遇到这个问题,那么你的视图模型可能有一些错误。个人而言,我不会为布局页编写代码。但是,如果你想这样做,你应该有一个基础视图模型,其他视图模型都继承自它,并将布局页编写为基础视图模型,将特定页面编写为各自的视图模型。


12
个人而言,我从不编写布局页面。为什么?我的意思是,你如何处理出现在所有页面中的侧面动态内容?你会跳过视图中的控制器吗?也许你的意思是从布局中使用RenderAction?(我现在只是看看而已) - eglasius
54
@eglasius,我使用的解决方案取决于我们谈论的内容类型。但是一个常见的解决方案是使用RenderAction来呈现需要自己的数据的部分在布局页面中。我不喜欢在布局页面中输入代码的原因是这会迫使你始终在所有特定视图模型中继承一个“基本”视图模型。根据我的经验,这通常不是一个很好的想法,很多时候当你需要更改设计时就会遇到问题(或者需要花费很长时间)。 - Mattias Jakobsson
2
如果我想通过聚合而不是继承来包含基础模型怎么办?从设计角度来看,这是一个完全合法的方式。那么我该如何处理布局呢? - Fyodor Soikin
4
我有两种解决方案:一种是为布局创建通用模型,这样我就可以使用"MyLayoutModel<MyViewModel>"来呈现视图模型,并使用"MyViewModel"在布局中进行"RenderPartial"。另一种是使用"RenderAction"部分呈现页面的静态缓存部分,并使用ajax调用动态部分。但我更喜欢第一种方案,因为它更加友好于搜索引擎,并且很容易与ajax更新结合使用。 - Softlion
4
在处理旧有代码时,如果出现这种情况,就会变成噩梦。请不要手动输入您的布局...拜托了! - user338195
显示剩余7条评论

47

一种常见的解决方案是创建一个基础视图模型,其中包含布局文件中使用的属性,然后从基础模型继承到各自页面使用的模型。

这种方法的问题在于,现在您已经将自己锁定为模型只能从另一个类继承,而且也许您的解决方案本身就不能在所需的模型上使用继承。

我的解决方案也是从一个基础视图模型开始:

public class LayoutModel
{
    public LayoutModel(string title)
    {
        Title = title;
    }

    public string Title { get;}
}

我通常使用继承自LayoutModel的通用版本的LayoutModel,如下所示:
``` class GenericLayoutModel : LayoutModel { // implementation details } ```
请注意,保留了HTML标签。
public class LayoutModel<T> : LayoutModel
{
    public LayoutModel(T pageModel, string title) : base(title)
    {
        PageModel = pageModel;
    }

    public T PageModel { get; }
}

通过这个解决方案,我已经消除了在布局模型和模型之间需要继承的需求。

现在我可以像这样在Layout.cshtml中使用LayoutModel:

@model LayoutModel
<!doctype html>
<html>
<head>
<title>@Model.Title</title>
</head>
<body>
@RenderBody()
</body>
</html>

在页面中,您可以像这样使用通用的LayoutModel:

@model LayoutModel<Customer>
@{
    var customer = Model.PageModel;
}

<p>Customer name: @customer.Name</p>

在你的控制器中,你只需要返回一个类型为LayoutModel的模型:

public ActionResult Page()
{
    return View(new LayoutModel<Customer>(new Customer() { Name = "Test" }, "Title");
}

1
指出多重继承问题及其处理方法将获得额外奖励!这是更好的可扩展性答案。 - Brett Spencer
1
我认为这是最佳解决方案。从架构角度看,它具有可扩展性和可维护性。这是正确的做法。我从未喜欢过ViewBag或ViewData.....它们两个在我看来都很不专业。 - Jonathan Alfaro

32

这是非常基础的东西,你只需要创建一个基本视图模型,并确保所有使用该布局的视图都接收使用该基本模型的视图!

public class SomeViewModel : ViewModelBase
{
    public bool ImNotEmpty = true;
}

public class EmptyViewModel : ViewModelBase
{
}

public abstract class ViewModelBase
{
}
在_Layout.cshtml文件中:
@model Models.ViewModelBase
<!DOCTYPE html>
  <html>
  and so on...

在HomeController中的Index(例如)方法中:

    public ActionResult Index()
    {
        var model = new SomeViewModel()
        {
        };
        return View(model);
    }

Index.cshtml页面:

@model Models.SomeViewModel

@{
  ViewBag.Title = "Title";
  Layout = "~/Views/Shared/_Layout.cshtml";
}

<div class="row">

我不同意将模型传递给_layout是一个错误,一些用户信息可以被传递,并且数据可以在控制器的继承链中填充,因此只需要一个实现。

显然,对于更高级的目的,您应该考虑使用注入创建自定义静态上下文,并将该模型命名空间包含在_Layout.cshtml中。

但对于普通用户来说,这就足够了。


我同意你的观点。谢谢。 - Sebastián Guerrero
1
只是想提一下,使用接口而不是基类也可以正常工作。 - VladL

11

为什么不使用一个具有特定控制器的新部分视图,将所需的模型传递给该部分视图,最后使用 RenderPartial 或 RenderAction 在 Layout.cshtml 上呈现所提到的部分视图?

我使用这种方法来显示已登录用户的信息,例如姓名、个人资料图片等。


3
可以请您详细说明吗?我会尽量让内容更加通俗易懂,但不改变原意。如果您需要了解更多信息,可以提供一篇博客文章的链接来阐述这个技巧。 - J86
1
这样做是可行的,但为什么要影响性能呢?你必须等待控制器完成所有处理,返回视图,只为了让用户的浏览器发起另一个请求来获取所需的数据。而且如果你的布局依赖于数据以适当地呈现,那该怎么办?在我看来,这不是对这个问题的答案。 - Brett Spencer

5

虽然这是一个老问题,但是为了回答 MVC5 开发者的疑问,你可以像在视图中一样使用 Model 属性。

在视图和布局中,Model 属性都与相同的 ViewDataDictionary 对象相关联,因此您不需要额外的工作来将模型传递给布局页,并且您不需要在布局中声明 @model MyModelName

但请注意,在布局中使用 @Model.XXX 时,智能感知上下文菜单不会出现,因为这里的 Model 是一个动态对象,就像 ViewBag 一样。


4
也许从技术角度来看,这并不是正确的处理方式,但对我来说最简单和最合理的解决方案就是创建一个类并在布局中实例化它。这是一种犯错误的特例。如果在布局中频繁使用此方法,则需要认真反思你的做法,并在项目进一步进行之前阅读更多教程。
public class MyLayoutModel {
    public User CurrentUser {
        get {
            .. get the current user ..
        }
    }
}

然后在视图中。
@{
    // Or get if from your DI container
    var myLayoutModel = new MyLayoutModel();
}

在 .NET Core 中,您甚至可以跳过这一步骤,直接使用依赖注入。
@inject My.Namespace.IMyLayoutModel myLayoutModel

这是一个有点模糊的领域。但鉴于我在这里看到的极度复杂的替代方案,我认为它是一种为实用性而做出的完全可以接受的例外。特别是如果您确保保持简单,并确保任何繁重的逻辑(我认为真的不应该有,但需求不同)在另一个类/层中,即其所属的位置。这肯定比因为基本上只有一个视图而污染您所有的控制器或模型要好得多。


2

仅从上述内容来看,但实现简单。

索引页

@model CMS.Models.IndexViewModel 

@{
    ViewBag.PageModel = Model;    
}

布局页面

@{
    var Model = (CMS.Models.IndexViewModel)ViewBag.PageModel;        
}

2
太棒了!一个ViewBag统治它们所有!我曾经在布局中有大约6个ViewBag项。我只是想知道强制转换的问题 - 我想这是不可避免的。对我来说,这是最简单的方法。 - Andy

2

还有一种方法可以实现它。

  1. 只需为所有控制器实现BaseController类

  2. BaseController类中创建一个返回Model类的方法,例如:

public MenuPageModel GetTopMenu() 
{    

var m = new MenuPageModel();    
// populate your model here    
return m; 

}
布局(Layout)页面中,您可以调用该方法GetTopMenu()
@using GJob.Controllers

<header class="header-wrapper border-bottom border-secondary">
  <div class="sticky-header" id="appTopMenu">
    @{
       var menuPageModel = ((BaseController)this.ViewContext.Controller).GetTopMenu();
     }
     @Html.Partial("_TopMainMenu", menuPageModel)
  </div>
</header>

0
假设您的模型是一个对象集合(或者可能是单个对象)。对于模型中的每个对象,请执行以下操作。
1)将要显示的对象放入ViewBag中。例如:
  ViewBag.YourObject = yourObject;

2) 在_Layout.cshtml文件的顶部添加一个using语句,其中包含您对象的类定义。例如:

@using YourApplication.YourClasses;

3) 当您在_Layout中引用yourObject时,请进行强制类型转换。您可以进行强制类型转换是因为您在步骤(2)中所做的操作。


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