MVC - Compartir información contextual entre vistas

8

Por favor, disculpe la larga publicación. Hay una pregunta, solo tengan paciencia conmigo.

Un pequeño contexto

Tenemos un sitio que debe adaptarse considerablemente en función de una variedad de configuraciones de usuario, el grupo al que pertenece el usuario, de dónde provienen y otras cosas. Solíamos incluir los bits relevantes en el modelo para la página, por lo que si la página tenía una tabla que mostrara si el usuario era mayor de cierta edad, entonces en el modelo haríamos algo como:

//model
public PageModel
{
    public bool ShowTable {get;set;}
}

//controller
public PageController
{
    public ActionResult ShowPage()
    {
        var model = new PageModel() {
            ShowTable = User.Age > 21
        };
        return View(model);
    }
}

//view
@if(Model.ShowTable)
{ 
    <table>Some Html here</table>
}

Esto rápidamente se volvió muy complicado para saber qué deberíamos mostrar a qué usuarios. Para tratar de abordar este problema, centralizamos toda la lógica sobre cuándo se debe mostrar u ocultar una cosa en particular. Llamamos a esta clase UserConfigurationy (principalmente) solo contenía una serie de funciones que devuelven booleanos que indican lo que se debe mostrar. Esto nos permitió configurar una serie de especificaciones y pruebas de lo que debería mostrarse a un usuario. Esto UserConfigratuionluego se puso en una clase base, de la cual todos los modelos de página debían heredar, por lo que actualmente tenemos algo como esto:

//UserConfiguration 
public UserConfiguration
{
    private readonly User _user;

    public UserConfiguration(User user) {
        _user = user
    }

    public bool ShowTable() {
        return _user.Age > 21;
    }
}

//model base
public ModelBase
{
    public UserConfiguration {get;set;}
}

//model
public PageModel : ModelBase
{
    // whatever data is needed for the page
}

//controller
public PageController
{
    public ActionResult ShowPage()
    {
        var userConfiguration = new UserConfiguration(User);
        var model = new PageModel {
            UserConfiguration = userConfiguration
        };
        return View(model);
    }
}

//view
@if(Model.UserConfiguration.ShowTable())
{ 
    <table>Some Html here</table>
}

Esto ha ayudado, principalmente porque nos permitió crear una serie de pruebas de lo que un usuario debería y no debería ver, etc. Sin embargo, no es una solución muy limpia, tener que reunir esta clase adicional e incluirla en el modelo. También tiene ramificaciones para representar vistas parciales. Si el modelo tiene una propiedad IEnumerable<Foo> Foos, que queremos representar en un parcial, pero ese parcial también se basa en la configuración del usuario, tenemos un problema. No puede simplemente pasar los foos a lo Parcial como modelo, porque entonces lo parcial no tiene acceso a lo parcial UserConfiguration. Entonces, ¿cuál sería la mejor manera de acceder a esta información? A mi modo de ver, en el contexto de asp.net MVC hay 4 formas disponibles:

1) Tener un nuevo modelo para el parcial, por ejemplo

// parent view
@{
    var foosModel = new foosModel {
        Foos = Model.Foos,
        UserConfiguration = Model.UserConfiguration
    }
}

@Html.RenderPartial("FooList", foosModel)

// child partial view
@if(Model.UserConfiguration.ShowTable) {
    foreach(var foo in Model.Foos) {
        //Some HTML
    }
}

Esta es probablemente la solución "más pura", que se adhiere mejor a los principios de MVC, pero involucra una gran cantidad de modelos (posiblemente innecesarios), lo que causa la hinchazón del proyecto.

2) Exponga la configuración del usuario a través de ViewData. p.ej :

// parent view
@Html.RenderPartial("FooList", Model.Foos, new ViewDataDictionary { { "UserConfiguration", Model.UserConfiguration } })

// child partial view
@{ 
    var userConfig = (UserConfiguration)ViewData["UserConfiguration"];
}
@if(userConfig.ShowTable) {
    foreach(var foo in Model) {
        //Some HTML
    }
}

Realmente no me gusta esto porque no es de tipo seguro y se basa en cadenas mágicas para obtenerlo de ViewData.

3) Coloque la configuración de usuario en la ViewBag. Los mismos problemas que los anteriores realmente

4) Modifique el modelo de página y exponga la configuración de usuario a través de una propiedad de la página en sí, según http://haacked.com/archive/2011/02/21/changing-base-type-of-a-razor-view .aspx /

Creo que dado que la configuración del usuario es información contextual ambiental, tiene sentido exponerla a través de la clase como en la opción 4 anterior. ¿Existe una práctica recomendada generalmente aceptada en MVC para exponer este tipo de datos? ¿Alguien ha intentado algo como la opción 4 en el pasado y hubo algún "problema"?

tl; dr: ¿Cuál es la mejor manera de exponer información contextual a las vistas en su sitio, en MVC en general o asp.net MVC en particular?

Anduril
fuente

Respuestas:

2

Deberías ir con el n. ° 5: Ninguno de los anteriores.

Comencé a crear métodos de extensión para la IPrincipalinterfaz, lo que me da declaraciones fuertemente tipadas de lo que el usuario actual puede hacer. Incluso podría crear un UserConfigurationDTO que rellene en la sesión para que lo utilicen estos métodos de extensión.

Primero, los métodos de extensión:

namespace YourApplication.Helpers
{
    public static class UserConfigurationExtensions
    {
        private HttpContext CurrentContext
        {
            get
            {
                return System.Web.HttpContext.Current;
            }
        }

        private static UserConfiguration Config
        {
            get
            {
                if (CurrentContext == null)
                    return null;

                return CurrentContext.Session["UserConfiguration"] as UserConfiguration;
            }
        }

        public static bool CanViewTable(this IPrincipal user)
        {
            return Config.ShowTable;
        }
    }
}

Ahora, cuando el usuario haya iniciado sesión correctamente, cree la instancia UserConfigurationy guárdela en Session:

public class AccountController : Controller
{
    [HttpPost]
    public ActionResult Login(LoginFormModel model)
    {
        if (ModelState.IsValid)
        {
            // Log in
            Session["UserConfiguration"] = new UserConfiguration(...);
        }

        return RedirectToAction("Index", "Home");
    }
}

A continuación, agregue el espacio de nombres en el que existen los métodos de extensión a sus espacios de nombres predeterminados en las plantillas Razor.

YourApplication / Views / Web.config

<?xml version="1.0"?>

<configuration>
  <!-- ... -->

  <system.web.webPages.razor>
    <namespaces>
      <add namespace="YourApplication.Helpers"/>
    </namespaces>
  </system.web.webPages.razor>

  <!-- ... -->
</configuration>

Ahora cierre y vuelva a abrir la solución de Visual Studio. Entonces sus plantillas Razor tienen nuevos métodos disponibles:

@if (User.CanViewTable())
{
    foreach(var foo in Model)
    {
        //Some HTML
    }
}
Greg Burghardt
fuente
0

Iría con su primera opción, haciendo las modificaciones necesarias a sus clases de modelo. Parece que su opción 4, modificar el modelo de página, sería intercambiar referencias de modelo por referencias de ayuda en sus vistas de afeitar. La opción 1 parece más fácil de mantener que la opción 4 porque aparentemente requeriría menos código y porque más desarrolladores de MVC lo entenderían.

gpersell
fuente
0

Mi opinión es que los datos contextuales con ViewBag / ViewData configurados por el servicio de datos contextuales. Me molestó con poca o muy mala flexibilidad de tener "BaseController" que establece todas las cosas de "GodModel" porque cada vista necesita tenerla a menos que necesite agregar alguna nueva vista delgada donde no necesito todas esas cosas. Donde, por supuesto, "GodModel" también era una clase base para modelos en vistas.

Es difícil saber de antemano si "todas" realmente necesitan algo y hacer muchas cosas obligatorias me hace mucho más difícil que hacer las cosas opcionales y cuando de vez en cuando me olvido de configurarlo porque es dinámico .

Por supuesto, todos los puntos de vista específicos y realmente obligatorios deben ir al modelo y estar fuertemente tipados y tener validación. Pero las cosas generales que pueden empantanar el rendimiento porque alguien pensó que "todo tiene que estar fuertemente tipeado" no es bueno.

Mateusz
fuente
0

Parece que la MAYORÍA de la información que necesita inspeccionar está centrada en el usuario y no está basada en la acción. ¿Por qué no almacenar su UserConfiguration en la sesión? otro enfoque, dependiendo de cómo esté haciendo la autenticación / gestión de usuarios, es mantener toda la información que necesita en ClaimsPrincipal (ejemplo a continuación) ...

    private ClaimsPrincipal CurrentClaimsPrincipal
    {
        get { return System.Security.Claims.ClaimsPrincipal.Current; }
    }

    public string Firstname
    {
        get { return (null != CurrentClaimsPrincipal) ? CurrentClaimsPrincipal.FindFirst(Helpers.Constants.GIVEN_NAME_KEY)?.Value : string.Empty; }
    }

    public string Lastname
    {
        get { return (null != CurrentClaimsPrincipal) ? CurrentClaimsPrincipal.FindFirst(Helpers.Constants.FAMILY_NAME_KEY)?.Value : string.Empty; }
    }

    public string AccessLevel
    {
        get { return (null != CurrentClaimsPrincipal) ? CurrentClaimsPrincipal.FindFirst(Helpers.Constants.ACCESS_LEVEL_KEY)?.Value : string.Empty; }
    }
jklDev
fuente