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

在过去,我通过让所有控制器从一个公共基础控制器继承,以全局方式将公共属性(如当前用户)粘贴到 ViewData/ViewBag 上。

这使我能够在基础控制器上使用 IoC,而不仅仅是在全局共享这些数据。

我想知道是否有一种替代方法可以将这种代码插入到 MVC 管道中?

81708 次浏览

You could use a custom ActionResult:

public class  GlobalView : ActionResult
{
public override void ExecuteResult(ControllerContext context)
{
context.Controller.ViewData["Global"] = "global";
}
}

Or even a ActionFilter:

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


base.OnActionExecuting(filterContext);
}
}

Had an MVC 2 project open but both techniques still apply with minor changes.

Un-tried by me, but you might look at registering your views and then setting the view data during the activation process.

Because views are registered on-the-fly, the registration syntax doesn't help you with connecting to the Activated event, so you'd need to set it up in a 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";
};
}
}
}

This might be one of those "only tool's a hammer"-type suggestions from me; there may be simpler MVC-enabled ways to get at it.

Edit: Alternate, less code approach - just attach to the Controller

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

Edit 2: Another approach that works directly from the controller registration code:

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

Since ViewBag properties are, by definition, tied to the view presentation and any light view logic that may be necessary, I'd create a base WebViewPage and set the properties on page initialization. It's very similar to the concept of a base controller for repeated logic and common functionality, but for your views:

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


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

And then in \Views\Web.config, set the pageBaseType property:

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

Brandon's post is right on the money. As a matter of fact, I would take this a step further and say that you should just add your common objects as properties of the base WebViewPage so you don't have to cast items from the ViewBag in every single View. I do my CurrentUser setup this way.

The best way is using the ActionFilterAttribute. I'll show you how to use it in .Net Core and .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);
}
}

Then you need to register this in your startup.cs.

.Net Core 3.1

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

.Net Core 2.1

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

Then you can use it in all views and pages

@ViewData["Avatar"]
@ViewBag.Avatar

.Net Framework (ASP.NET MVC .Net Framework)

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


}

register your custom class in the global. asax (Application_Start)

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


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


}

Then you can use it in all views

@ViewBag.IsAdmin
@ViewBag.IsAuthenticated
@ViewBag.Avatar

Also there is another way

Creating an extension method on HtmlHelper

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

Then you can use it in all views

@Html.MyTest()

If you want compile time checking and intellisense for the properties in your views then the ViewBag isn't the way to go.

Consider a BaseViewModel class and have your other view models inherit from this class, eg:

Base ViewModel

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


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

View specific ViewModel

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

Now view code can access the property directly in the view

<p>Is Admin: @Model.IsAdmin</p>

You don't have to mess with actions or change the model, just use a base controller and cast the existing controller from the layout viewcontext.

Create a base controller with the desired common data (title/page/location etc) and action initialization...

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


protected override void OnActionExecuting(ActionExecutingContext filterContext) {


MyCommonValue = 12345;


base.OnActionExecuting(filterContext);
}
}

Make sure every controller uses the base controller...

public class UserController:_BaseController {...

Cast the existing base controller from the view context in your _Layout.cshml page...

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

Now you can refer to values in your base controller from your layout page.

@myController.MyCommonValue

I have found the following approach to be the most efficient and gives excellent control utilizing the _ViewStart.chtml file and conditional statements when necessary:

_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>;
}

Note:

PageData will work perfectly in Views; however, in the case of a PartialView, it will need to be passed from the View to the child Partial.

I implemented the ActionFilterAttribute solution from @Mohammad Karimi. It worked well as I had the same scenario as the OP. I needed to add data to every view. The action filter attribute was executed for every Razor page request, but it was also called for every web API controller request.

Razor Pages offers a page filter attribute to avoid unnecessary execution of the action filter when a web API controller request is made.

Razor Page filters IPageFilter and IAsyncPageFilter allow Razor Pages to run code before and after a Razor Page handler is run.

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)
{
}
}
}

As per the sample in the filter methods for Razor Pages documentation, the page filter is enabled by:

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