Comment définir les propriétés ViewBag pour toutes les vues sans utiliser de classe de base pour les contrôleurs?

96

Dans le passé, j'ai collé des propriétés communes, telles que l'utilisateur actuel, sur ViewData / ViewBag de manière globale en faisant hériter tous les contrôleurs d'un contrôleur de base commun.

Cela m'a permis d'utiliser IoC sur le contrôleur de base et pas seulement d'atteindre le partage global pour ces données.

Je me demande s'il existe une autre façon d'insérer ce type de code dans le pipeline MVC?

Scott Weinstein
la source

Réponses:

22

Je n'ai pas essayé, mais vous pouvez envisager d' enregistrer vos vues , puis de définir les données de vue pendant le processus d'activation.

Étant donné que les vues sont enregistrées à la volée, la syntaxe d'enregistrement ne vous aide pas à vous connecter à l' Activatedévénement, vous devez donc le configurer dans 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";
            };
        }
    }
}

Cela pourrait être une de ces suggestions de type "seul outil est un marteau" de ma part; il peut y avoir des moyens plus simples activés par MVC pour y parvenir.

Edit: Autre approche, moins de code - il suffit de l'attacher au contrôleur

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

Edit 2: Une autre approche qui fonctionne directement à partir du code d'enregistrement du contrôleur:

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
la source
Exactement ce dont j'avais besoin. Mise à jour de la réponse pour travailler hors de la boîte
Scott Weinstein
Super truc - basé sur votre approche, j'ai ajouté une autre simplification, cette fois sans avoir besoin d'un module.
Nicholas Blumhardt
quelle est la Resolvepartie de e.Context.Resolve? Je devrais mentionner que je suis habitué à Ninject ...
drzaus
243

Le meilleur moyen consiste à utiliser ActionFilterAttribute et à enregistrer votre classe personnalisée dans votre fichier 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;
        }
    }

}

enregistrez votre classe personnalisée dans votre global. asax (Application_Start)

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

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

    }

Ensuite, vous pouvez l'utiliser dans toutes les vues

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

Il y a aussi une autre façon

Création d'une méthode d'extension sur HtmlHelper

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

Ensuite, vous pouvez l'utiliser dans toutes les vues

@Html.MyTest()
Mohammad Karimi
la source
9
Je ne comprends pas pourquoi cela n'a pas été plus voté; c'est une approche beaucoup moins invasive que les autres
joshcomley
5
8 heures de recherche pour trouver cela ... la réponse parfaite. Merci beaucoup.
deltree
3
+1 Une manière agréable et propre d'intégrer des données mondiales. J'ai utilisé cette technique pour enregistrer la version de mon site sur toutes les pages.
Will Bickford
4
Solution brillante, simple et discrète.
Eugen Timm
3
Mais où est l'IoC? c'est-à-dire comment changeriez-vous MembershipService?
drzaus
39

Étant donné que les propriétés ViewBag sont, par définition, liées à la présentation de la vue et à toute logique de vue légère qui peut être nécessaire, je créerais une WebViewPage de base et définirais les propriétés lors de l'initialisation de la page. C'est très similaire au concept d'un contrôleur de base pour une logique répétée et des fonctionnalités communes, mais pour vos vues:

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

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

Et puis dans \Views\Web.config, définissez la pageBaseTypepropriété:

<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
la source
Le problème avec cette configuration est que si vous définissez la valeur d'une propriété dans le ViewBag sur une vue, puis que vous essayez d'y accéder sur une autre vue (comme votre vue _Layout partagée), la valeur définie sur la première vue sera perdu sur la vue de mise en page.
Pedro
@Pedro c'est certainement vrai, mais je dirais que ViewBag n'est pas censé être une source d'état persistante dans l'application. On dirait que vous voudriez que les données soient en état de session, puis vous pouvez les extraire dans votre page de vue de base et les définir dans le ViewBag s'il existe.
Brandon Linton
Vous avez un point valide, mais à peu près tout le monde utilise un ensemble de données dans une vue dans d'autres vues; comme lorsque vous définissez le titre de la page dans une vue et que votre mise en page partagée l'imprime ensuite dans les balises <title> du document html. J'aime même aller plus loin en définissant des booléens comme "ViewBag.DataTablesJs" dans une vue "enfant" pour que la vue de mise en page "maître" inclue les références JS appropriées sur la tête du html. Tant que c'est lié à la mise en page, je pense que c'est correct de le faire.
Pedro
@Pedro bien dans la situation des balises de titre, généralement cela est géré avec chaque vue définissant une ViewBag.Titlepropriété, puis la seule chose dans la mise en page partagée est <title>@ViewBag.Title</title>. Cela ne serait pas vraiment approprié pour quelque chose comme une page de vue d'application de base, car chaque vue est distincte et la page de vue de base serait pour des données qui sont vraiment communes à toutes les vues.
Brandon Linton
@Pedro Je comprends ce que vous dites et je pense que Brandon a raté le point. J'utilisais une WebViewPage personnalisée et j'ai essayé de transmettre des données de l'une des vues à la vue de mise en page à l'aide d'une propriété personnalisée dans la WebViewPage personnalisée. Lorsque je définissais la propriété dans la vue, cela mettait à jour ViewData dans ma page WebViewPage personnalisée, mais lorsqu'il accédait à la vue de mise en page, l'entrée ViewData était déjà perdue. Je l'ai contourné en utilisant ViewContext.Controller.ViewData ["SomeValue"] dans le WebViewPage personnalisé. J'espère que cela aide quelqu'un.
Imran Rashid
17

Le message de Brandon est juste sur l'argent. En fait, je voudrais aller plus loin et dire que vous devez simplement ajouter vos objets communs comme propriétés de la WebViewPage de base afin que vous n'ayez pas à lancer des éléments du ViewBag dans chaque vue. Je fais ma configuration CurrentUser de cette façon.

Michael Gagné
la source
Je n'ai pas pu faire fonctionner cela avec l'erreur'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 à ce sujet, c'est exactement ce que je fais pour partager une instance d'une classe utilitaire non statique qui doit être globalement disponible dans toutes les vues.
Nick Coad
9

Vous pouvez utiliser un ActionResult personnalisé:

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

Ou même un ActionFilter:

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

        base.OnActionExecuting(filterContext);
    }
}

Avait un projet MVC 2 ouvert mais les deux techniques s'appliquent toujours avec des modifications mineures.

John Farrell
la source
5

Vous n'avez pas à vous soucier des actions ou à modifier le modèle, utilisez simplement un contrôleur de base et lancez le contrôleur existant à partir du contexte de la vue de disposition.

Créez un contrôleur de base avec les données communes souhaitées (titre / page / emplacement, etc.) et l'initialisation de l'action ...

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

    protected override void OnActionExecuting(ActionExecutingContext filterContext) {

        MyCommonValue = 12345;

        base.OnActionExecuting(filterContext);
    }
}

Assurez-vous que chaque contrôleur utilise le contrôleur de base ...

public class UserController:_BaseController {...

Diffusez le contrôleur de base existant à partir du contexte de vue de votre _Layout.cshmlpage ...

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

Vous pouvez maintenant vous référer aux valeurs de votre contrôleur de base à partir de votre page de mise en page.

@myController.MyCommonValue
Carter Medlin
la source
3

Si vous voulez vérifier le temps de compilation et intellisense pour les propriétés de vos vues, le ViewBag n'est pas la voie à suivre.

Considérez une classe BaseViewModel et faites hériter vos autres modèles de vue de cette classe, par exemple:

Vue de base

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

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

Afficher un ViewModel spécifique

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

Le code de vue peut maintenant accéder à la propriété directement dans la vue

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

J'ai trouvé que l'approche suivante est la plus efficace et donne un excellent contrôle en utilisant le fichier _ViewStart.chtml et des instructions conditionnelles si nécessaire:

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

VueA :

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

Remarque :

PageData fonctionnera parfaitement dans Views; cependant, dans le cas d'un PartialView, il devra être passé de View au Partial enfant.

utileBee
la source