How to set ViewBag properties for all Views without using a base class for Controllers?
Asked Answered
G

9

107

In the past I've stuck common properties, such as the current user, onto ViewData/ViewBag in a global fashion by having all Controllers inherit from a common base controller.

This allowed my to use IoC on the base controller and not just reach out into global shared for such data.

I'm wondering if there is an alternate way of inserting this kind of code into the MVC pipeline?

Gemagemara answered 27/3, 2011 at 23:50 Comment(0)
S
21

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>();
    });
Stumpage answered 28/3, 2011 at 9:43 Comment(3)
Exactly what I needed. Updated the answer to work out of the boxGemagemara
Great stuff - based on your approach I've added another simplification, this time without the need for a module.Stumpage
what's the Resolve part of e.Context.Resolve? I should mention I'm used to Ninject...Niece
W
266

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()
Warfeld answered 15/1, 2014 at 6:52 Comment(7)
But where's the IoC? i.e. how would you switch out MembershipService?Niece
One problem I have noticed with this approach is that the Controllers will no longer pass ViewBag data. I had to modify my controllers to pass data using TempData insteadl.Baroda
The OnResultExecuting executes after the action and before the view. To be able to set data in your action, change the event to OnActionExecutingNecolenecro
Not efficient since UserProfilePictureActionFilter will get hit multiple times. Imagine you are making a costly db call.Cacie
For a costly db call? ok, create an extension method on System.Web.Mvc.HtmlHelper then use @Html.YourMethodNameWarfeld
But if the web application is hosted on servers (load balancer). You lose the data saved in the TempData.Ity
@Mirak Problem is, when user updates his Avatar how can I update ViewBag valuesKinsfolk
S
39

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>
Saharanpur answered 28/3, 2011 at 17:12 Comment(5)
The problem with this setup is that if you're setting the value to a property in the ViewBag on one view and then trying to access it on another view (like your shared _Layout view), the value value set on the first view will be lost on the layout view.Vixen
@Vixen that's definitely true, but then I'd argue that ViewBag isn't meant to be a persistent source of state in the application. Sounds like you would want that data in session state and then you could pull that out in your base view page and set it in the ViewBag if it exists.Saharanpur
You have a valid point but pretty much everyone uses data set in one view in other views; like when you set the title of the page in one view and your shared layout view then prints it out in the <title> tags of the html document. I even like to take this a step further by setting booleans like "ViewBag.DataTablesJs" in a "child" view to make the "master" layout view include the proper JS references on the html's head. As long as it is layout related, I think it is ok to do this.Vixen
@Vixen well in the title tags situation, usually that's handled with each view setting a ViewBag.Title property and then the only thing in the shared layout is <title>@ViewBag.Title</title>. It wouldn't really be appropriate for something like a base application view page since each view is distinct, and the base view page would be for data that is truly common across all views.Saharanpur
@Vixen I understand what you are saying and I think Brandon missed the point there. I was using a custom WebViewPage and I tried to pass some data from one of the views to the layout view using a custom property in the custom WebViewPage. When I set the property in the view, it would update the ViewData in my custom WebViewPage but when it got to the layout view, the ViewData entry was already lost. I got around it by using ViewContext.Controller.ViewData["SomeValue"] in the custom WebViewPage. I hope it helps someone.Milka
S
21

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>();
    });
Stumpage answered 28/3, 2011 at 9:43 Comment(3)
Exactly what I needed. Updated the answer to work out of the boxGemagemara
Great stuff - based on your approach I've added another simplification, this time without the need for a module.Stumpage
what's the Resolve part of e.Context.Resolve? I should mention I'm used to Ninject...Niece
H
17

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.

Hoodlum answered 7/4, 2011 at 4:29 Comment(2)
I couldn't get this to work with the error 'ASP._Page_Views_Shared__Layout_cshtml' does not contain a definition for 'MyProp' and no extension method 'MyProp' accepting a first argument of type 'ASP._Page_Views_Shared__Layout_cshtml' could be found (are you missing a using directive or an assembly reference?)Miliaria
+1 on this, this is exactly what I'm doing for sharing an instance of a non-static utility class that needs to be globally available in all the views.Bannockburn
K
9

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.

Kila answered 28/3, 2011 at 0:42 Comment(0)
M
5

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
Munificent answered 25/7, 2017 at 15:49 Comment(1)
I believe that ViewContext lost the Controller property. There doesn't seem to be an easy way now to access the Controller from the View. I get that this might be considered a code smell, but it sure is a good way to pass peripheral (global) data to the view.Laraelaraine
L
3

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>
Lowly answered 22/1, 2015 at 2:35 Comment(0)
C
2

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.

Cacie answered 29/8, 2017 at 19:58 Comment(0)
T
0

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));
    });
}
Triage answered 16/8, 2022 at 14:2 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.