如何在不使用控制器的基类的情况下为所有视图设置ViewBag属性?

106

过去,我将常见属性(例如当前用户)通过让所有控制器从一个共同的基本控制器继承来全局地放入ViewData/ViewBag中。

这使我能够在基本控制器上使用IoC,而不仅仅是在全局共享数据中获取它们。

我想知道是否有其他方法将此类代码插入MVC管道?

9个回答

266

最佳方式是使用ActionFilterAttribute。我将向您展示如何在.Net Core和.Net Framework中使用它。

.Net Core 2.1和3.1

public class ViewBagActionFilter : ActionFilterAttribute
{

    public ViewBagActionFilter(IOptions<Settings> settings){
        //DI will inject what you need here
    }

    public override void OnResultExecuting(ResultExecutingContext context)
    {
        // for razor pages
        if (context.Controller is PageModel)
        {
            var controller = context.Controller as PageModel;
            controller.ViewData.Add("Avatar", $"~/avatar/empty.png");
            // or
            controller.ViewBag.Avatar = $"~/avatar/empty.png";

            //also you have access to the httpcontext & route in controller.HttpContext & controller.RouteData
        }

        // for Razor Views
        if (context.Controller is Controller)
        {
            var controller = context.Controller as Controller;
            controller.ViewData.Add("Avatar", $"~/avatar/empty.png");
            // or
            controller.ViewBag.Avatar = $"~/avatar/empty.png";

            //also you have access to the httpcontext & route in controller.HttpContext & controller.RouteData
        }

        base.OnResultExecuting(context);
    }
}

接下来您需要在startup.cs文件中进行注册。

.Net Core 3.1

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews(options => { 
        options.Filters.Add<Components.ViewBagActionFilter>();
    });
}

.Net Core 2.1

->

.Net Core 2.1

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc(options =>
        {
            options.Filters.Add<Configs.ViewBagActionFilter>();
        });
}

然后您可以在所有视图和页面中使用它

@ViewData["Avatar"]
@ViewBag.Avatar

.Net框架 (ASP.NET MVC .Net框架)

public class UserProfilePictureActionFilter : ActionFilterAttribute
{

    public override void OnResultExecuting(ResultExecutingContext filterContext)
    {
        filterContext.Controller.ViewBag.IsAuthenticated = MembershipService.IsAuthenticated;
        filterContext.Controller.ViewBag.IsAdmin = MembershipService.IsAdmin;

        var userProfile = MembershipService.GetCurrentUserProfile();
        if (userProfile != null)
        {
            filterContext.Controller.ViewBag.Avatar = userProfile.Picture;
        }
    }

}

在global.asax文件的Application_Start方法中注册您的自定义类。

protected void Application_Start()
    {
        AreaRegistration.RegisterAllAreas();

        GlobalFilters.Filters.Add(new UserProfilePictureActionFilter(), 0);

    }
然后您可以在所有视图中使用它。
@ViewBag.IsAdmin
@ViewBag.IsAuthenticated
@ViewBag.Avatar

还有另一种方式

创建一个HtmlHelper的扩展方法

[Extension()]
public string MyTest(System.Web.Mvc.HtmlHelper htmlHelper)
{
    return "This is a test";
}

然后您可以在所有视图中使用它

@Html.MyTest()

10
我不明白为什么这个还没有得到更多的点赞;它是一种比其他方法更少侵入性的方法。 - joshcomley
6
8个小时的研究,终于找到了完美的答案。非常感谢你。 - deltree
3
一个很好、简洁的方法来整合全局数据。我使用这种技术在所有页面上注册了我的网站版本。+1 - Will Bickford
4
出色、简单且不显眼的解决方案。 - Eugen Timm
5
但是 IoC 在哪里?也就是说,你如何替换“MembershipService”? - drzaus
显示剩余10条评论

39

根据定义,ViewBag属性与视图呈现和可能需要的任何轻量级视图逻辑相关联,我会创建一个基础WebViewPage并在页面初始化时设置这些属性。这非常类似于为重复的逻辑和常见功能创建基础控制器的概念,但是针对你的视图:

    public abstract class ApplicationViewPage<T> : WebViewPage<T>
    {
        protected override void InitializePage()
        {
            SetViewBagDefaultProperties();
            base.InitializePage();
        }

        private void SetViewBagDefaultProperties()
        {
            ViewBag.GlobalProperty = "MyValue";
        }
    }

然后,在\Views\Web.config文件中,设置pageBaseType属性:

<system.web.webPages.razor>
    <host factoryType="System.Web.Mvc.MvcWebRazorHostFactory, System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
    <pages pageBaseType="MyNamespace.ApplicationViewPage">
      <namespaces>
        <add namespace="System.Web.Mvc" />
        <add namespace="System.Web.Mvc.Ajax" />
        <add namespace="System.Web.Mvc.Html" />
        <add namespace="System.Web.Routing" />
      </namespaces>
    </pages>
  </system.web.webPages.razor>

这种设置的问题在于,如果您在一个视图中将值设置为ViewBag中的属性,然后尝试在另一个视图(例如共享的_Layout视图)中访问它,则在第一个视图上设置的值将在布局视图上丢失。 - Pedro
@Pedro,这绝对是正确的,但我认为ViewBag并不意味着在应用程序中成为一个持久化状态源。听起来你想要将数据存储在会话状态中,然后在基本视图页面中提取它,并在存在时将其设置在ViewBag中。 - Brandon Linton
你说得有道理,但几乎每个人都会在一个视图中使用数据集,并在其他视图中使用它;例如,在一个视图中设置页面标题,然后在共享布局视图中将其打印到HTML文档的<title>标签中。我甚至喜欢更进一步地设置布尔值,如“ViewBag.DataTablesJs”在“子”视图中,以使“主”布局视图在html的头部包含适当的JS引用。只要与布局相关,我认为这样做是可以的。 - Pedro
@Pedro,对于标题标签的情况,通常是通过每个视图设置ViewBag.Title属性来处理,然后共享布局中唯一的内容就是<title>@ViewBag.Title</title>。这对于像基础应用程序视图页面这样的东西并不合适,因为每个视图都是独立的,而基础视图页面将用于在所有视图之间真正共同的数据。 - Brandon Linton
@Pedro 我明白你的意思,我认为Brandon错过了重点。我正在使用自定义的WebViewPage,并尝试使用自定义WebViewPage中的自定义属性将一些数据从一个视图传递到布局视图。当我在视图中设置属性时,它会更新我的自定义WebViewPage中的ViewData,但是当它到达布局视图时,ViewData条目已经丢失了。我通过在自定义WebViewPage中使用ViewContext.Controller.ViewData["SomeValue"]来解决这个问题。希望能对某人有所帮助。 - Imran Rashid

21

我没有亲自尝试过,但你可以查看注册视图,然后在激活过程中设置视图数据。

由于视图是动态注册的,注册语法无法帮助你连接到Activated事件,所以你需要在一个Module中设置它:

class SetViewBagItemsModule : Module
{
    protected override void AttachToComponentRegistration(
        IComponentRegistration registration,
        IComponentRegistry registry)
    {
        if (typeof(WebViewPage).IsAssignableFrom(registration.Activator.LimitType))
        {
            registration.Activated += (s, e) => {
                ((WebViewPage)e.Instance).ViewBag.Global = "global";
            };
        }
    }
}

这可能是我那种“只有锤子的工具”类型建议之一;也许有更简单的MVC启用方法来解决它。

编辑:备选方案,更少的代码方法 - 只需附加到控制器

public class SetViewBagItemsModule: Module
{
    protected override void AttachToComponentRegistration(IComponentRegistry cr,
                                                      IComponentRegistration reg)
    {
        Type limitType = reg.Activator.LimitType;
        if (typeof(Controller).IsAssignableFrom(limitType))
        {
            registration.Activated += (s, e) =>
            {
                dynamic viewBag = ((Controller)e.Instance).ViewBag;
                viewBag.Config = e.Context.Resolve<Config>();
                viewBag.Identity = e.Context.Resolve<IIdentity>();
            };
        }
    }
}

编辑2: 另一种直接从控制器注册代码工作的方法:

builder.RegisterControllers(asm)
    .OnActivated(e => {
        dynamic viewBag = ((Controller)e.Instance).ViewBag;
        viewBag.Config = e.Context.Resolve<Config>();
        viewBag.Identity = e.Context.Resolve<IIdentity>();
    });

正是我所需要的。已更新答案,可直接使用。 - Scott Weinstein
非常棒 - 基于您的方法,我添加了另一种简化,这次不需要模块。 - Nicholas Blumhardt
e.Context.Resolve 中的 Resolve 是什么?我应该提一下我习惯使用 Ninject... - drzaus

17

Brandon的帖子说得太对了。事实上,我想再进一步说一下,你应该把常用的对象作为基本WebViewPage属性添加进去,这样你就不必在每个视图中都从ViewBag中强制转换项目了。我会用这种方式设置我的CurrentUser。


我无法让它正常工作,出现了错误'ASP._Page_Views_Shared__Layout_cshtml'不包含'MyProp'的定义,并且没有接受类型为'ASP._Page_Views_Shared__Layout_cshtml'的第一个参数的扩展方法'MyProp'可以找到(您是否缺少使用指令或程序集引用?) - Sprintstar
在这个问题上我赞同,这正是我正在做的事情——共享一个非静态实用类的实例,以便在所有视图中全局使用。 - Nick Coad

9
您可以使用自定义的ActionResult:
public class  GlobalView : ActionResult 
{
    public override void ExecuteResult(ControllerContext context)
    {
        context.Controller.ViewData["Global"] = "global";
    }
}

甚至还可以使用ActionFilter:

public class  GlobalView : ActionFilterAttribute 
{
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        filterContext.Result = new ViewResult() {ViewData = new ViewDataDictionary()};

        base.OnActionExecuting(filterContext);
    }
}

我有一个MVC 2项目打开,但两种技术都适用,只需进行微小的更改。


5
你不需要修改操作或更改模型,只需使用基础控制器并将来自布局视图上下文的现有控制器强制转换即可。
创建一个带有所需公共数据(标题/页面/位置等)和操作初始化的基础控制器...
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

我认为ViewContext丢失了Controller属性。现在似乎没有简单的方法可以从View中访问Controller。我知道这可能被视为代码异味,但这确实是将外围(全局)数据传递到视图的好方法。 - Ben Mills

3
如果您想要在编译时检查视图中的属性并获得智能感知,那么 ViewBag 并不是一个好去处。考虑使用 BaseViewModel 类,并让其他视图模型从此类继承,例如:

基本视图模型

public class BaseViewModel
{
    public bool IsAdmin { get; set; }

    public BaseViewModel(IUserService userService)
    {
        IsAdmin = userService.IsAdmin;
    }
}

查看特定的ViewModel

public class WidgetViewModel : BaseViewModel
{
    public string WidgetName { get; set;}
}

现在,查看代码可以直接访问视图中的属性。
<p>Is Admin: @Model.IsAdmin</p>

2

我发现下面这种方法最高效,利用_ViewStart.chtml文件和必要时的条件语句可以获得出色的控制:

_ViewStart:

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

 var CurrentView = ViewContext.Controller.ValueProvider.GetValue("controller").RawValue.ToString();

 if (CurrentView == "ViewA" || CurrentView == "ViewB" || CurrentView == "ViewC")
    {
      PageData["Profile"] = db.GetUserAccessProfile();
    }
}

ViewA:

@{
   var UserProfile= PageData["Profile"] as List<string>;
 }

注意:

在视图中,PageData将完美地工作;但是,在部分视图的情况下,它需要从视图传递到子部分视图。


0

我实现了@Mohammad Karimi的ActionFilterAttribute解决方案。它在我的情况下很有效,因为我需要向每个视图添加数据。该操作过滤器属性对于每个Razor页面请求都会执行,但也会针对每个Web API控制器请求进行调用。

Razor Pages提供了页面过滤器属性,以避免在进行Web API控制器请求时不必要地执行操作过滤器。

Razor Page过滤器IPageFilter和IAsyncPageFilter允许在运行Razor Page处理程序之前和之后运行代码。

using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Configuration;

namespace MyProject
{
// learn.microsoft.com/en-us/aspnet/core/razor-pages/filter?view=aspnetcore-6.0
// "The following code implements the synchronous IPageFilter"
// Enable the page filter using 'services.AddRazorPages().AddMvcOptions( ... )
// in the 'ConfigureServices()' startup method.

public class ViewDataPageFilter : IPageFilter
{
    private readonly IConfiguration _config;

    public ViewDataPageFilter(IConfiguration config)
    {
        _config = config;
    }

    // "Called after a handler method has been selected,
    // but before model binding occurs."
    public void OnPageHandlerSelected(PageHandlerSelectedContext context)
    {
    }

    // "Called before the handler method executes,
    // after model binding is complete."
    public void OnPageHandlerExecuting(PageHandlerExecutingContext context)
    {
        PageModel page = context.HandlerInstance as PageModel;
        if (page == null) { return; }
        page.ViewData["cdn"] = _config["cdn:url"];
    }

    // "Called after the handler method executes,
    // before the action result."
    public void OnPageHandlerExecuted(PageHandlerExecutedContext context)
    {
    }
}
}

根据 Razor 页面文档中的筛选方法示例,启用页面筛选器的方法为:


public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages()
    .AddMvcOptions(options =>
    {
        options.Filters.Add(new ViewDataPageFilter(Configuration));
    });
}

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