Как установить свойства ViewBag для всех представлений без использования базового класса для контроллеров?


96

Раньше я прикреплял общие свойства, такие как текущий пользователь, к ViewData / ViewBag в глобальном порядке, унаследовав все контроллеры от общего базового контроллера.

Это позволило мне использовать IoC на базовом контроллере, а не просто обращаться к глобальному общему доступу для таких данных.

Мне интересно, есть ли альтернативный способ вставки такого кода в конвейер MVC?

Ответы:


22

Я не пробовал, но вы можете зарегистрировать свои представления, а затем установить данные представления во время процесса активации.

Поскольку представления регистрируются на лету, синтаксис регистрации не поможет вам подключиться к 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>();
    });

Именно то, что мне нужно. Обновлен ответ, чтобы он работал из коробки
Скотт Вайнштейн

Отличный материал - исходя из вашего подхода, я добавил еще одно упрощение, на этот раз без модуля.
Николас Блюмхардт

какая Resolveчасть e.Context.Resolve? Я должен упомянуть, что привык к Ninject ...
drzaus

244

Лучший способ - использовать ActionFilterAttribute. Я покажу вам, как использовать его в .Net Core и .Net Framework.

.Net Core 2.1 и 3.1

public class ViewBagActionFilter : ActionFilterAttribute
{
    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(new Components.ViewBagActionFilter()); });
}

.Net Core 2.1

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

Затем вы можете использовать его во всех представлениях и страницах

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

}

зарегистрируйте свой собственный класс в файле 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()

9
Я не понимаю, почему за это не проголосовали больше; это гораздо менее инвазивный подход, чем другие
Джошкомли

5
8 часов поиска, чтобы найти это ... идеальный ответ. Огромное спасибо.
deltree

3
+1 Хороший и чистый способ интеграции глобальных данных. Я использовал этот метод для регистрации своей версии сайта на всех страницах.
Уилл Бикфорд

4
Гениальное, простое и ненавязчивое решение.
Eugen Timm

3
Но где же IoC? т.е. как бы вы переключились MembershipService?
drzaus

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, это определенно правда, но я бы сказал, что ViewBag не предназначен для постоянного источника состояния в приложении. Похоже, вы хотели бы, чтобы эти данные были в состоянии сеанса, а затем вы могли бы вытащить их на своей базовой странице просмотра и установить их в ViewBag, если он существует.
Брэндон Линтон,

У вас есть верная точка зрения, но почти все используют набор данных в одном представлении в других представлениях; например, когда вы устанавливаете заголовок страницы в одном представлении, а ваше общее представление макета затем распечатывает его в тегах <title> html-документа. Мне даже нравится делать еще один шаг вперед, устанавливая логические значения вроде «ViewBag.DataTablesJs» в «дочернем» представлении, чтобы «главное» представление макета включало правильные ссылки JS в заголовке html. Если это связано с макетом, я думаю, что это нормально.
Педро

@Pedro хорошо в ситуации тегов заголовков, обычно это обрабатывается с каждым представлением, устанавливающим ViewBag.Titleсвойство, а затем единственное, что в общем макете <title>@ViewBag.Title</title>. Это не совсем подходит для чего-то вроде базовой страницы представления приложения, поскольку каждое представление индивидуально, а страница базового представления будет предназначена для данных, которые действительно являются общими для всех представлений.
Брэндон Линтон,

@Pedro Я понимаю, о чем вы говорите, и думаю, что Брэндон упустил суть. Я использовал настраиваемый WebViewPage и пытался передать некоторые данные из одного из представлений в представление макета, используя настраиваемое свойство в настраиваемом WebViewPage. Когда я устанавливаю свойство в представлении, оно обновляет ViewData в моем пользовательском WebViewPage, но когда он попадает в представление макета, запись ViewData уже была потеряна. Я обошел это, используя ViewContext.Controller.ViewData ["SomeValue"] в настраиваемом WebViewPage. Надеюсь, это кому-то поможет.
Имран Рашид

17

Пост Брэндона идет прямо по деньгам. В самом деле, я бы этот шаг еще дальше и сказать , что вы должны просто добавить свои общие объекты как свойства в базовой WebViewPage , поэтому вам не придется литых деталей из ViewBag в каждом View. Я делаю настройку CurrentUser таким образом.


Я не смог заставить это работать из-за ошибки'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?)
Sprintstar

+1 по этому поводу, это именно то, что я делаю для совместного использования экземпляра нестатического служебного класса, который должен быть глобально доступен во всех представлениях.
Ник Коад,

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

3

Если вам нужна проверка времени компиляции и intellisense для свойств в ваших представлениях, 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 отлично работает в представлениях; однако в случае PartialView его нужно будет передать из View дочернему Partial.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.