¿Cómo configurar las propiedades de ViewBag para todas las vistas sin usar una clase base para controladores?

96

En el pasado, pegué propiedades comunes, como el usuario actual, en ViewData / ViewBag de manera global al hacer que todos los controladores hereden de un controlador base común.

Esto me permitió usar IoC en el controlador base y no solo llegar a los compartidos globales para tales datos.

Me pregunto si hay una forma alternativa de insertar este tipo de código en la canalización MVC.

Scott Weinstein
fuente

Respuestas:

22

No lo he probado, pero es posible que desee registrar sus vistas y luego configurar los datos de la vista durante el proceso de activación.

Debido a que las vistas se registran sobre la marcha, la sintaxis de registro no lo ayuda a conectarse al Activatedevento, por lo que deberá configurarlo en un 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";
            };
        }
    }
}

Esta podría ser una de esas sugerencias mías del tipo "la única herramienta es un martillo"; puede haber formas más simples habilitadas para MVC para hacerlo.

Editar: enfoque alternativo, menos código, solo conéctelo al controlador

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

Edición 2: otro enfoque que funciona directamente desde el código de registro del controlador:

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
fuente
Exactamente lo que necesitaba. Se actualizó la respuesta para que funcione de inmediato
Scott Weinstein
Cosas geniales: según su enfoque, agregué otra simplificación, esta vez sin la necesidad de un módulo.
Nicholas Blumhardt
cual es la Resolveparte e.Context.Resolve? Debo mencionar que estoy acostumbrado a Ninject ...
drzaus
243

La mejor manera es usar ActionFilterAttribute y registrar su clase personalizada en su global. asax (Application_Start)

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

}

registre su clase personalizada en su global. asax (Application_Start)

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

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

    }

Entonces puedes usarlo en todas las vistas

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

También hay otra forma

Creando un método de extensión en HtmlHelper

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

Entonces puedes usarlo en todas las vistas

@Html.MyTest()
Mohammad Karimi
fuente
9
No entiendo por qué esto no ha sido votado más; es un enfoque mucho menos invasivo que los demás
joshcomley
5
8 horas de investigación para encontrar esta ... la respuesta perfecta. Muchas gracias.
Deltree
3
+1 Manera agradable y limpia de integrar datos globales. Usé esta técnica para registrar la versión de mi sitio en todas las páginas.
Will Bickford
4
Solución brillante, fácil y discreta.
Eugen Timm
3
Pero, ¿dónde está el IoC? es decir, ¿cómo cambiarías MembershipService?
drzaus
39

Dado que las propiedades de ViewBag están, por definición, vinculadas a la presentación de la vista y a cualquier lógica de vista ligera que pueda ser necesaria, crearía una WebViewPage base y establecería las propiedades en la inicialización de la página. Es muy similar al concepto de un controlador base para lógica repetida y funcionalidad común, pero para sus puntos de vista:

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

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

Y luego \Views\Web.config, establezca la pageBaseTypepropiedad:

<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
fuente
El problema con esta configuración es que si establece el valor de una propiedad en ViewBag en una vista y luego intenta acceder a ella en otra vista (como su vista _Layout compartida), el valor establecido en la primera vista será perdido en la vista de diseño.
Pedro
@Pedro eso es definitivamente cierto, pero luego diría que ViewBag no está destinado a ser una fuente persistente de estado en la aplicación. Parece que querría esos datos en el estado de sesión y luego podría sacarlos en su página de vista base y configurarlos en ViewBag si existe.
Brandon Linton
Tiene un punto válido, pero casi todo el mundo usa el conjunto de datos en una vista en otras vistas; como cuando establece el título de la página en una vista y su vista de diseño compartido luego lo imprime en las etiquetas <title> del documento html. Incluso me gusta llevar esto un paso más allá configurando valores booleanos como "ViewBag.DataTablesJs" en una vista "secundaria" para que la vista de diseño "maestra" incluya las referencias JS adecuadas en el encabezado del html. Siempre que esté relacionado con el diseño, creo que está bien hacer esto.
Pedro
@Pedro bien en la situación de las etiquetas de título, generalmente se maneja con cada vista configurando una ViewBag.Titlepropiedad y luego lo único en el diseño compartido es <title>@ViewBag.Title</title>. Realmente no sería apropiado para algo como una página de vista de aplicación base, ya que cada vista es distinta, y la página de vista base sería para datos que son realmente comunes en todas las vistas.
Brandon Linton
@Pedro Entiendo lo que estás diciendo y creo que Brandon no entendió. Estaba usando una WebViewPage personalizada e intenté pasar algunos datos de una de las vistas a la vista de diseño usando una propiedad personalizada en la WebViewPage personalizada. Cuando configuré la propiedad en la vista, actualizaría ViewData en mi WebViewPage personalizada, pero cuando llegó a la vista de diseño, la entrada ViewData ya se había perdido. Lo solucioné usando ViewContext.Controller.ViewData ["SomeValue"] en la WebViewPage personalizada. Espero que esto ayude a alguien.
Imran Rashid
17

La publicación de Brandon está en lo cierto. De hecho, llevaría esto un paso más allá y diría que simplemente debe agregar sus objetos comunes como propiedades de la WebViewPage base para que no tenga que lanzar elementos desde ViewBag en cada Vista. Hago mi configuración de CurrentUser de esta manera.

Michael Gagne
fuente
No pude hacer que esto funcione con el 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?)
Sprintstar
+1 en esto, esto es exactamente lo que estoy haciendo para compartir una instancia de una clase de utilidad no estática que debe estar disponible globalmente en todas las vistas.
Nick Coad
9

Podría usar un ActionResult personalizado:

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

O incluso un ActionFilter:

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

        base.OnActionExecuting(filterContext);
    }
}

Tenía un proyecto MVC 2 abierto, pero ambas técnicas aún se aplican con cambios menores.

John Farrell
fuente
5

No tiene que meterse con acciones o cambiar el modelo, solo use un controlador base y transmita el controlador existente desde el contexto de vista de diseño.

Cree un controlador base con los datos comunes deseados (título / página / ubicación, etc.) y la inicialización de la acción ...

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

    protected override void OnActionExecuting(ActionExecutingContext filterContext) {

        MyCommonValue = 12345;

        base.OnActionExecuting(filterContext);
    }
}

Asegúrese de que cada controlador use el controlador base ...

public class UserController:_BaseController {...

Transmita el controlador base existente desde el contexto de vista en su _Layout.cshmlpágina ...

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

Ahora puede consultar los valores en su controlador base desde su página de diseño.

@myController.MyCommonValue
Carter Medlin
fuente
3

Si desea verificar el tiempo de compilación e intellisense para las propiedades en sus vistas, ViewBag no es el camino a seguir.

Considere una clase BaseViewModel y haga que sus otros modelos de vista hereden de esta clase, por ejemplo:

Modelo de vista base

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

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

Ver ViewModel específico

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

Ahora ver código puede acceder a la propiedad directamente en la vista

<p>Is Admin: @Model.IsAdmin</p>
Steven Quick
fuente
2

He encontrado que el siguiente enfoque es el más eficiente y brinda un excelente control utilizando el archivo _ViewStart.chtml y las declaraciones condicionales cuando es necesario:

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

VistaA :

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

Nota :

PageData funcionará perfectamente en Views; sin embargo, en el caso de un PartialView, será necesario pasarlo de View al Partial secundario.

útilAbeja
fuente