将数据传递到所有页面共有的布局


124

我有一个网站,其中有一个布局页面。但是,此布局页面具有所有页面模型都必须提供的数据,例如页面标题,页面名称以及我为执行某些操作的HTML帮助器所实际位于的位置。每个页面也都有自己的视图模型属性。

我怎样才能做到这一点?键入布局似乎不是一个好主意,但是如何传递这些信息呢?


10
对于在这里阅读答复的任何人,请访问stackoverflow.com/a/21130867/706346,在这里您会看到一个更简单,更整洁的解决方案,所有内容都张贴在这里。
Avrohom Yisroel 2015年

5
@AvrohomYisroel好建议。但是我更喜欢@Colin Bacon的方法,因为它是强类型的,而不是in ViewBag。也许是偏好问题。尽管支持您的评论
JP赫勒蒙斯

对于mvc 5,请参见以下答案:stackoverflow.com/a/46783375/5519026
Laz Ziya

Answers:


142

如果需要将相同的属性传递给每个页面,那么明智的做法是创建一个供所有视图模型使用的基本视图模型。然后,您的布局页面可以采用此基本模型。

如果此数据后面需要逻辑,则应将其放入所有控制器使用的基本控制器中。

您可以做很多事情,重要的方法是不要在多个地方重复相同的代码。

编辑:从下面的评论更新

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

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

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>

最后,在action方法中设置数据。

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

12
但是数据在布局中使用。如何将数据传递到布局?
Rushino

2
完善!我看到了我的错误。我忘了将模型传递给视图..真是la脚的错误。谢谢!
Rushino '11

7
这种方法的问题是,有时并非每个视图都具有ViewModel,因此在这种情况下将不起作用:O /
Cacho Santa

16
但这是否不要求每个控制器和每个动作都包含{Name =“ Bacon”}代码?而且,如果我想向ViewModelBase添加另一个属性,则必须转到每个控制器和每个动作并添加代码以填充该属性?您提到了“如果需要逻辑,则应将其放入基本控制器中。” 如何消除每个控制器和每个动作中的重复代码?

5
@Lee如果它是所有页面上的通用数据,则可以在其中放置基本控制器。然后,您的控制器将从该基本控制器继承。例如public class HomeController : BaseController。这样,通用代码只需要编写一次就可以应用于所有控制器。
科林·培根

73

我在布局中使用RenderAction html helper作为剃刀。

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

我需要简单的字符串。因此,我的操作返回字符串并将其写下来很容易。但是,如果您需要复杂的数据,则可以返回PartialViewResult和模型。

 public PartialViewResult Action()
    {
        var model = someList;
        return PartialView("~/Views/Shared/_maPartialView.cshtml", model);
    }

您只需要将模型放在您创建的部分视图'_maPartialView.cshtml'的开头

@model List<WhatEverYourObjeIs>

然后,您可以在带有html的部分视图中使用模型中的数据。


18
这是迄今为止最好的答案!
gingerbreadboy

@gingerbreadboy同意它可以促进良好的封装和分离关注点。
A-Dubb

35

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


1
这实际上听起来像是最不痛苦的解决方案,是否有缺点?+1
formatc

2
绝对是最好的解决方案,我看不到任何缺点。
Wiktor Zychla 2015年

7
我看不到这给你什么。如果您拥有一个包含布局所需的所有属性的类,那么为什么不费心地将其添加到ViewBag中,而又不得不将其重新投射回去呢?在布局视图中使用模型,您仍然可以在中填充模型OnActionExecuting。使用ViewBag还意味着您在控制器中失去了类型安全性,这从来都不是一件好事。
科林·培根

3
这给我带来了添加模型的能力,而不必在已经存在的项目中重构所有模型以从所有控制器的所有方法的单个“超级”模型继承而来。如果您是从头开始的,则可以选择从公共根导出所有模型。
DenNukem

5
@ColinBacon此选项的另一个优点是您的操作不需要始终具有视图模型。另外,我认为开发人员需要知道他们必须始终从基础继承视图模型是不利的。
乔什·诺

28

大概,这的主要用例是为所有(或大多数)控制器动作获取基本模型视图。

鉴于此,我将这些答案中的几个结合使用,主要是支持Colin Bacon的答案。

正确的是,这仍然是控制器逻辑,因为我们正在填充视图模型以返回视图。因此,将其放置在控制器中的正确位置。

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

我们还仍然希望强类型ViewModel的附加好处

因此,我创建了一个BaseViewModel和BaseController。所有ViewModels控制器将分别继承自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!";
    }
}

注意从本SO帖子中获取的OnActionExecuted的使用

家庭控制器

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

        // populate HomeIndexModel ...

        return View(model);
    }
}

BaseViewModel

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 if(model!= null){model.AwesomeModelProperty =“ Awesome Property Value”; }
Tom Gerken

2
很好的答案,我比其他所有人都喜欢这个。
Jynn

1
好的答案,但是我有一个问题。“如果我有一些没有ViewModels的视图怎么办?”
伊斯玛·哈罗

尝试了此操作,但在索引操作上,OnActionExecuted填充了FooterModel,然后使用空的FooterModel创建了新的HomeIndexModel :(
SteveCav

1
@drizzie:在您的基本控制器中,模型是Filter方法中的局部变量:var model = filterContext.Controller.ViewData.Model作为BaseViewModel。我不明白MVC如何理解此局部变量与HomeController发送给视图的模型相同。
Hooman Bahreini

9

您不必弄乱动作或更改模型,只需使用基本控制器,然后从布局viewcontext投射现有的控制器即可。

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

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

.net核心中的等效项是什么?由于ViewContext.Controller不存在,并且继承链中有一些更改
Jayanth Thyagarajan

4

如果要传递整个模型,请在布局中进行以下操作:

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

4

我认为对于大型企业级应用程序,这些答案都不够灵活。我不喜欢过度使用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方法呢?
拉蒙德·伯加

1
嗯,您仍然需要base.OnActionExecuting(filterContext)您的OnActionExecuting方法!!!
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

我想这对于ScopedASP..Net Core中的布局模型对象也是个好主意。
詹姆斯·威尔金斯

我不会去获取依赖项。那绝对不是“ MVC”。服务定位器是一种反模式
Jiveman

与流行的看法相反,Service Locator不是反模式,实际上这与MVC无关,您只是在用流行语@Jiveman扔东西吗?blog.gauffin.org/2012/09/service-locator-is-not-an-anti-pattern
hyankov

Jgauffin在该文章中的要点似乎是,不应将术语“反模式”应用于Service Locator,因为至少可以有效使用SL。一个公平的观点。但是,从他自己的一些讨论评论中可以明显看出,他建议,尽管SL在构建库和框架时可能是一种有效的方法,但在构建应用程序时并不一定推荐使用SL(我认为这是OP的问题,这里的讨论是旋转)。
Jiveman

3

其他答案几乎涵盖了有关如何将模型传递到布局页面的所有内容。但是我找到了一种方法,可以在不使用任何模型或局部视图的情况下,将变量动态地传递到布局页面。假设您有这个模型-

public class SubLocationsViewModel
{
    public string city { get; set; }
    public string state { get; set; }
}

您想动态获取城市和州。例如

在index.cshtml中,您可以将这两个变量放在ViewBag中

@model  MyProject.Models.ViewModel.SubLocationsViewModel
@{
    ViewBag.City = Model.city;
    ViewBag.State = Model.state;
}

然后在您的layout.cshtml中,您可以访问这些viewbag变量

<div class="text-wrap">
    <div class="heading">@ViewBag.City @ViewBag.State</div>
</div>

效果很好,@ stun_Gravy使用ViewBag传递角色或用户访问级别等数据时是否会失败?
3not3

3

还有另一种方法可以解决这个问题。从体系结构的角度看,这也许不是最干净的方法,但是它避免了其他答案带来的很多痛苦。只需在Razor布局中插入服务,然后调用获取必要数据的方法即可:

@inject IService myService

然后在布局视图中:

@if (await myService.GetBoolValue()) {
   // Good to go...
}

同样,就体系结构而言,这不是很干净(显然,不应将服务直接注入视图中),但是它可以完成工作。


不是最干净的方法吗?我不同意。我认为这很干净:将对象从创建对象的位置直接传递到您想要的位置,而无需用不需要查看的项目“污染”其他控制器。@inject我认为使用是最好的解决方案。
dasblinkenlight

1
想更多的也许你是对的。这种方法避免了太多痛苦的事实表明,这也许是最干净的方法。我一直在开发一个非常大的ASP.NET Core应用程序,并且正在将这种模式用于导航导航逻辑,大多数页面上的标头数据之类的事情。通过这样做,我避免了太多痛苦。
安德鲁

2

您还可以使用RenderSection,它可以帮助您将Model数据注入_Layout视图中。

你可以注入View Model数据,JsonScriptCSSHTML

在此示例中,我JsonIndex视图中注入Layout视图。

Index.chtml

@section commonLayoutData{

    <script>

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

    </script>

    }

_Layout.cshtml

@RenderSection("commonLayoutData", false)

这样就无需创建单独的Base View Model

希望可以帮助某人。


1
当您需要渲染一些视图专用的完美解决方案。
库纳尔

1

我所做的很简单,很有效

在任何控制器中声明“静态”属性,或者您可以按照以下方式使用静态值创建数据类:

public static username = "Admin";
public static UserType = "Administrator";

这些值可以由控制器基于操作来更新。以后您可以在_Layout中使用它们

在_layout.cshtml中

@project_name.Controllers.HomeController.username
@project_name.Controllers.HomeController.UserType

1
它总是有帮助的,可以在答案中添加一些解释,以使其更清晰易懂。请阅读stackoverflow.com/help/how-to-answer
6

0

为什么没有人建议在ViewData上使用扩展方法?

选项1

在我看来,到目前为止,该问题的介入最少,最简单。没有硬编码的字符串。没有强加的限制。没有魔术编码。没有复杂的代码。

public static class ViewDataExtensions
{
    private const string TitleData = "Title";
    public static void SetTitle<T>(this ViewDataDictionary<T> viewData, string value) => viewData[TitleData] = value;
    public static string GetTitle<T>(this ViewDataDictionary<T> viewData) => (string)viewData[TitleData] ?? "";
}

在页面中设置数据

ViewData.SetTitle("abc");

选项#2

另一种选择,使字段声明更容易。

public static class ViewDataExtensions
{
    public static ViewDataField<string, V> Title<V>(this ViewDataDictionary<V> viewData) => new ViewDataField<string, V>(viewData, "Title", "");
}

public class ViewDataField<T,V>
{
    private readonly ViewDataDictionary<V> _viewData;
    private readonly string _field;
    private readonly T _defaultValue;

    public ViewDataField(ViewDataDictionary<V> viewData, string field, T defaultValue)
    {
        _viewData = viewData;
        _field = field;
        _defaultValue = defaultValue;
    }

    public T Value {
        get => (T)(_viewData[_field] ?? _defaultValue);
        set => _viewData[_field] = value;
    }
}

在页面中设置数据。声明比第一个选项容易,但是用法语法稍长一些。

ViewData.Title().Value = "abc";

选项#3

然后可以将其与返回包含所有与布局相关的字段及其默认值的单个对象结合起来。

public static class ViewDataExtensions
{
    private const string LayoutField = "Layout";
    public static LayoutData Layout<T>(this ViewDataDictionary<T> viewData) => 
        (LayoutData)(viewData[LayoutField] ?? (viewData[LayoutField] = new LayoutData()));
}

public class LayoutData
{
    public string Title { get; set; } = "";
}

在页面中设置数据

var layout = ViewData.Layout();
layout.Title = "abc";

第三种选择有很多好处,我认为在大多数情况下是最好的选择:

  • 最简单的字段和默认值声明。

  • 设置多个字段时最简单的用法语法。

  • 允许在ViewData中设置各种数据(例如,Layout,Header,Navigation)。

  • 允许LayoutData类中的其他代码和逻辑。

PS不要忘记在_ViewImports.cshtml中添加ViewDataExtensions的命名空间


0

您可以在App_Code文件夹中创建一个剃刀文件,然后从视图页面访问它。

项目>存储库/IdentityRepository.cs

namespace Infrastructure.Repository
{
    public class IdentityRepository : IIdentityRepository
    {
        private readonly ISystemSettings _systemSettings;
        private readonly ISessionDataManager _sessionDataManager;

        public IdentityRepository(
            ISystemSettings systemSettings
            )
        {
            _systemSettings = systemSettings;
        }

        public string GetCurrentUserName()
        {
            return HttpContext.Current.User.Identity.Name;
        }
    }
}

项目> App_Code / IdentityRepositoryViewFunctions.cshtml:

@using System.Web.Mvc
@using Infrastructure.Repository
@functions
{
    public static IIdentityRepository IdentityRepositoryInstance
    {
        get { return DependencyResolver.Current.GetService<IIdentityRepository>(); }
    }

    public static string GetCurrentUserName
    {
        get
        {
            var identityRepo = IdentityRepositoryInstance;
            if (identityRepo != null)
            {
                return identityRepo.GetCurrentUserName();
            }
            return null;
        }
    }
}

项目>视图/共享/_Layout.cshtml(或任何其他.cshtml文件)

<div>
    @IdentityRepositoryViewFunctions.GetCurrentUserName
</div>

-1

而不是通过这种方式,您总是可以使用另一种方法,这种方法也很快

在共享目录中创建一个新的局部视图,并在布局中调用局部视图为

@Html.Partial("MyPartialView")

在局部视图中,您可以调用数据库并执行您想执行的操作

@{
    IEnumerable<HOXAT.Models.CourseCategory> categories = new HOXAT.Models.HOXATEntities().CourseCategories;
}

<div>
//do what ever here
</div>

假设您已经添加了实体框架数据库


1
向下转换,因为视图永远不应该获得其自己的模型。
Oxonhammer

-1

令人惊讶的是,没有人在这里说过这一点。通过基本控制器传递视图模型是一团糟。我们使用用户声明将信息传递到布局页面(例如,用于在导航栏上显示用户数据)。还有一个优势。数据是通过cookie存储的,因此无需通过局部检索每个请求中的数据。只需执行一些谷歌搜索“ asp网络身份声明”即可。


@CodeSmith是什么?我正在提供解决方案。
makore

我不好,认为这是一个问题,但现在看到的是答复,已删除。
CodeSmith

如果试图回答这个问题,则应予以澄清。评注是可以容忍的,但它不应成为整个答案的主导。此外,有关Google的提示也不构成有效的答案。
Tripleee

-6

您可以这样使用:

 @{ 
    ApplicationDbContext db = new ApplicationDbContext();
    IEnumerable<YourModel> bd_recent = db.YourModel.Where(m => m.Pin == true).OrderByDescending(m=>m.ID).Select(m => m);
}
<div class="col-md-12">
    <div class="panel panel-default">
        <div class="panel-body">
            <div class="baner1">
                <h3 class="bb-hred">Recent Posts</h3>
                @foreach(var item in bd_recent)
                {
                    <a href="/BaiDangs/BaiDangChiTiet/@item.ID">@item.Name</a>
                }
            </div>
        </div>
    </div>
</div>

7
在视图中连接数据库确实不是一个好主意。
1_bug
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.