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?

Scott Weinstein
źródło

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>();
    });
Nicholas Blumhardt
źródło
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()
Mohammad Karimi
źródło
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>
Brandon Linton
źródło
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.

Michael Gagne
źródło
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.

John Farrell
źródło
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
Carter Medlin
źródło
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>
Steven Quick
źródło
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.

przydatne
źródło