Jak ustawić właściwości ViewBag dla wszystkich widoków bez używania klasy bazowej dla kontrolerów?


96

W przeszłości utknąłem typowe właściwości, takie jak bieżący użytkownik, w ViewData / ViewBag w sposób globalny, ponieważ wszystkie kontrolery dziedziczą ze wspólnego kontrolera podstawowego.

Pozwoliło mi to używać IoC na podstawowym kontrolerze, a nie tylko sięgać po globalne udostępnianie takich danych.

Zastanawiam się, czy istnieje alternatywny sposób wstawiania tego rodzaju kodu do potoku MVC?

Odpowiedzi:


22

Niepróbowane przeze mnie, ale możesz spojrzeć na rejestrację swoich widoków, a następnie ustawienie danych widoku podczas procesu aktywacji.

Ponieważ widoki są rejestrowane w locie, składnia rejestracji nie pomaga w łączeniu się z Activatedwydarzeniem, więc musisz skonfigurować je w 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";
            };
        }
    }
}

To może być jedna z moich propozycji typu „jedyne narzędzie to młotek”; mogą istnieć prostsze sposoby z obsługą MVC, aby to osiągnąć.

Edycja: alternatywne, mniej kodujące podejście - po prostu dołącz do kontrolera

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

Edycja 2: Inne podejście, które działa bezpośrednio z kodu rejestracyjnego kontrolera:

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

Dokładnie to, czego potrzebowałem. Zaktualizowano odpowiedź, aby działała po wyjęciu z pudełka
— Scott Weinstein

Świetna rzecz - bazując na Twoim podejściu dodałem kolejne uproszczenie, tym razem bez konieczności stosowania modułu.
— Nicholas Blumhardt

co to za Resolveczęść e.Context.Resolve? Powinienem wspomnieć, że jestem przyzwyczajony do Ninject ...
— drzaus

244

Najlepszym sposobem jest użycie ActionFilterAttribute. Pokażę ci, jak go używać w .Net Core i .Net Framework.

.Net Core 2.1 i 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);
    }
}

Następnie musisz to zarejestrować w swoim 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());
        });
}

Następnie możesz go używać we wszystkich widokach i na wszystkich stronach

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

}

zarejestruj swoją niestandardową klasę w globalnym. asax (Application_Start)

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

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

    }

Następnie możesz go używać we wszystkich widokach

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

Jest też inny sposób

Tworzenie metody rozszerzającej w HtmlHelper

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

Następnie możesz go używać we wszystkich widokach

@Html.MyTest()

9
Nie rozumiem, dlaczego nie zostało to bardziej poparte; jest to podejście znacznie mniej inwazyjne niż inne
— joshcomley

5
8 godzin badań, aby znaleźć ... idealną odpowiedź. Dziękuję bardzo.
— deltree

3
+1 Ładny i czysty sposób na integrację globalnych danych. Użyłem tej techniki do zarejestrowania mojej wersji witryny na wszystkich stronach.
— Will Bickford

4
Genialne, łatwe i dyskretne rozwiązanie.
— Eugen Timm

3
Ale gdzie jest IoC? tj. jak byś się wyłączył MembershipService?
— drzaus

39

Ponieważ właściwości ViewBag są z definicji powiązane z prezentacją widoku i dowolną logiką widoku światła, która może być konieczna, utworzyłbym podstawową WebViewPage i ustawiłbym właściwości podczas inicjalizacji strony. Jest bardzo podobny do koncepcji podstawowego kontrolera dla powtarzalnej logiki i typowych funkcji, ale dla twoich poglądów:

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

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

A następnie \Views\Web.configustaw pageBaseTypewłaściwość:

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

Problem z tą konfiguracją polega na tym, że jeśli ustawiasz wartość na właściwość w ViewBag w jednym widoku, a następnie próbujesz uzyskać do niej dostęp w innym widoku (np. W udostępnionym widoku _Layout), wartość ustawiona w pierwszym widoku będzie utracone w widoku układu.
— Pedro

@Pedro to z pewnością prawda, ale wtedy argumentowałbym, że ViewBag nie ma być trwałym źródłem stanu w aplikacji. Wygląda na to, że chciałbyś, aby te dane były w stanie sesji, a następnie możesz je wyciągnąć na stronie widoku podstawowego i ustawić w ViewBag, jeśli istnieje.
— Brandon Linton

Masz ważny punkt widzenia, ale prawie wszyscy używają zestawu danych w jednym widoku w innych widokach; tak jak wtedy, gdy ustawisz tytuł strony w jednym widoku, a współdzielony widok układu drukuje go w tagach <title> dokumentu HTML. Chciałbym nawet pójść o krok dalej, ustawiając wartości logiczne, takie jak „ViewBag.DataTablesJs” w widoku „podrzędnym”, aby widok układu „nadrzędnego” zawierał odpowiednie odwołania do kodu JS w nagłówku HTML. Jeśli jest to związane z układem, myślę, że można to zrobić.
— Pedro

@Pedro dobrze w sytuacji tagów tytułu, zwykle jest to obsługiwane z każdym widokiem ustawiającym ViewBag.Titlewłaściwość, a jedyną rzeczą we wspólnym układzie jest <title>@ViewBag.Title</title>. To naprawdę nie byłoby odpowiednie dla czegoś takiego jak strona widoku aplikacji podstawowej, ponieważ każdy widok jest inny, a strona widoku podstawowego byłaby przeznaczona dla danych, które są naprawdę wspólne dla wszystkich widoków.
— Brandon Linton

@Pedro Rozumiem, co mówisz, i myślę, że Brandon przegapił ten punkt. Używałem niestandardowej strony WebViewPage i próbowałem przekazać niektóre dane z jednego z widoków do widoku układu przy użyciu niestandardowej właściwości w niestandardowej witrynie WebViewPage. Kiedy ustawię właściwość w widoku, zaktualizuję ViewData na mojej niestandardowej stronie WebViewPage, ale kiedy dotrze do widoku układu, wpis ViewData został już utracony. Obejrzałem to za pomocą ViewContext.Controller.ViewData ["SomeValue"] w niestandardowej WebViewPage. Mam nadzieję, że to komuś pomoże.
— Imran Rashid,

17

Stanowisko Brandona jest tuż przy kasie. W rzeczywistości, chciałbym pójść o krok dalej i powiedzieć, że należy po prostu dodać wspólne obiekty jako właściwości tego WebViewPage bazowej , dzięki czemu nie trzeba przedmiotów odlewanych z ViewBag w każdym pojedynczym widoku. W ten sposób wykonuję konfigurację CurrentUser.


Nie mogłem zmusić tego do pracy z błędem'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 w tej sprawie, właśnie to robię, aby udostępnić instancję niestatycznej klasy narzędziowej, która musi być globalnie dostępna we wszystkich widokach.
— Nick Coad

9

Możesz użyć niestandardowego ActionResult:

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

Lub nawet ActionFilter:

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

        base.OnActionExecuting(filterContext);
    }
}

Miał otwarty projekt MVC 2, ale obie techniki nadal mają zastosowanie z niewielkimi zmianami.


5

Nie musisz majstrować przy akcjach ani zmieniać modelu, po prostu użyj kontrolera podstawowego i rzutuj istniejący kontroler z kontekstu widoku układu.

Utwórz podstawowy kontroler z żądanymi wspólnymi danymi (tytuł / strona / lokalizacja itp.) I inicjalizacją akcji ...

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

    protected override void OnActionExecuting(ActionExecutingContext filterContext) {

        MyCommonValue = 12345;

        base.OnActionExecuting(filterContext);
    }
}

Upewnij się, że każdy kontroler używa kontrolera podstawowego ...

public class UserController:_BaseController {...

Przerzuć istniejący kontroler podstawowy z kontekstu widoku na twojej _Layout.cshmlstronie ...

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

Teraz możesz odwoływać się do wartości w kontrolerze podstawowym ze strony układu.

@myController.MyCommonValue

3

Jeśli chcesz sprawdzać czas kompilacji i inteligencję dla właściwości w swoich widokach, ViewBag nie jest właściwym rozwiązaniem.

Rozważ klasę BaseViewModel i niech inne modele widoków dziedziczą z tej klasy, np .:

Podstawowy model widoku

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

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

Wyświetl określony ViewModel

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

Teraz kod widoku może uzyskać dostęp do właściwości bezpośrednio w widoku

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

2

Uważam, że następujące podejście jest najbardziej wydajne i zapewnia doskonałą kontrolę przy użyciu pliku _ViewStart.chtml i instrukcji warunkowych w razie potrzeby:

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

Widok A :

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

Uwaga :

PageData będzie działać doskonale w Widokach; Jednak w przypadku PartialView będzie musiał zostać przekazany z View do elementu podrzędnego Partial.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.