Como definir as propriedades de ViewBag para todas as visualizações sem usar uma classe base para controladores?

96

No passado, colei propriedades comuns, como o usuário atual, em ViewData / ViewBag de maneira global, fazendo com que todos os controladores herdassem de um controlador de base comum.

Isso me permitiu usar IoC no controlador de base e não apenas alcançar o compartilhamento global para esses dados.

Estou me perguntando se existe uma maneira alternativa de inserir esse tipo de código no pipeline MVC?

Scott Weinstein
fonte

Respostas:

22

Não experimentado por mim, mas você pode tentar registrar suas visualizações e, em seguida, definir os dados de visualização durante o processo de ativação.

Como as visualizações são registradas instantaneamente, a sintaxe de registro não ajuda você a se conectar ao Activatedevento, então você precisa configurá-la em 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 pode ser uma daquelas sugestões do tipo "única ferramenta é um martelo"; pode haver maneiras mais simples habilitadas para MVC de fazer isso.

Editar: alternativa, abordagem menos código - basta anexar ao 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>();
            };
        }
    }
}

Edição 2: Outra abordagem que funciona diretamente a partir do código de registro do 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
fonte
Exatamente o que eu precisava. Atualizado a resposta para trabalhar fora da caixa
Scott Weinstein
Grande coisa - com base na sua abordagem, adicionei outra simplificação, desta vez sem a necessidade de um módulo.
Nicholas Blumhardt
qual é a Resolveparte e.Context.Resolve? Devo mencionar que estou acostumado com Ninject ...
drzaus
243

A melhor maneira é usar ActionFilterAttribute e registrar sua classe customizada em seu 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 sua classe customizada em seu global. asax (Application_Start)

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

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

    }

Então você pode usá-lo em todas as visualizações

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

Também há outra maneira

Criação de um método de extensão em HtmlHelper

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

Então você pode usá-lo em todas as visualizações

@Html.MyTest()
Mohammad Karimi
fonte
9
Eu não entendo por que isso não foi mais votado; é uma abordagem muito menos invasiva do que as outras
joshcomley
5
8 horas de pesquisa para encontrar esta ... a resposta perfeita. Muito obrigado.
deltree
3
+1 maneira agradável e limpa de integrar dados globais. Usei essa técnica para registrar a versão do meu site em todas as páginas.
Will Bickford de
4
Solução brilhante, fácil e discreta.
Eugen Timm
3
Mas onde está o IoC? ou seja, como você mudaria MembershipService?
drzaus
39

Como as propriedades do ViewBag são, por definição, vinculadas à apresentação da visualização e a qualquer lógica de visualização de luz que possa ser necessária, eu criaria uma WebViewPage base e definiria as propriedades na inicialização da página. É muito semelhante ao conceito de um controlador de base para lógica repetida e funcionalidade comum, mas para suas visualizações:

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

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

Em seguida \Views\Web.config, defina a pageBaseTypepropriedade:

<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
fonte
O problema com esta configuração é que se você definir o valor para uma propriedade no ViewBag em uma vista da propriedade e, em seguida, tentar acessá-la em outra vista da propriedade (como a sua vista _Layout compartilhada), o valor do valor definido na primeira vista será perdido na visualização do layout.
Pedro
@Pedro isso é definitivamente verdade, mas eu argumentaria que ViewBag não foi feito para ser uma fonte persistente de estado no aplicativo. Parece que você gostaria que os dados estivessem no estado de sessão e, em seguida, poderia puxá-los em sua página de visualização de base e defini-los no ViewBag, se existir.
Brandon Linton
Você tem um ponto válido, mas quase todo mundo usa o conjunto de dados em uma visualização em outras visualizações; como quando você define o título da página em uma visualização e sua visualização de layout compartilhada, em seguida, imprime-o nas tags <title> do documento html. Eu até gosto de dar um passo adiante, definindo booleanos como "ViewBag.DataTablesJs" em uma visualização "filha" para fazer com que a visualização de layout "mestre" inclua as referências JS adequadas no cabeçalho do html. Contanto que seja relacionado ao layout, acho que não há problema em fazer isso.
Pedro
@Pedro bem na situação das tags de título, geralmente isso é tratado com cada visualização configurando uma ViewBag.Titlepropriedade e então a única coisa no layout compartilhado é <title>@ViewBag.Title</title>. Não seria realmente apropriado para algo como uma página de visualização de aplicativo base, pois cada visualização é distinta, e a página de visualização básica seria para dados que são verdadeiramente comuns em todas as visualizações.
Brandon Linton
@Pedro Eu entendo o que você está dizendo e acho que Brandon não entendeu. Eu estava usando uma WebViewPage personalizada e tentei passar alguns dados de uma das exibições para a exibição de layout usando uma propriedade personalizada na WebViewPage personalizada. Quando eu definia a propriedade na visualização, ele atualizava o ViewData em minha WebViewPage personalizada, mas quando chegava à visualização do layout, a entrada ViewData já estava perdida. Eu consegui contornar isso usando ViewContext.Controller.ViewData ["SomeValue"] na WebViewPage personalizada. Eu espero que isso ajude alguém.
Imran Rashid,
17

A postagem de Brandon está certa sobre o dinheiro. Na verdade, eu levaria isso um passo adiante e diria que você deve apenas adicionar seus objetos comuns como propriedades da WebViewPage base para que você não precise lançar itens do ViewBag em cada View. Eu faço minha configuração do CurrentUser desta forma.

Michael Gagne
fonte
Não consegui fazer isso funcionar com o erro'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 sobre isso, isso é exatamente o que estou fazendo para compartilhar uma instância de uma classe de utilitário não estática que precisa estar globalmente disponível em todas as visualizações.
Nick Coad
9

Você pode usar um ActionResult personalizado:

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

Ou mesmo um ActionFilter:

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

        base.OnActionExecuting(filterContext);
    }
}

Tinha um projeto MVC 2 aberto, mas ambas as técnicas ainda se aplicam com pequenas alterações.

John Farrell
fonte
5

Você não precisa mexer com as ações ou alterar o modelo, apenas use um controlador de base e lance o controlador existente a partir do contexto de visualização do layout.

Crie um controlador de base com os dados comuns desejados (título / página / localização etc) e inicialização de ação ...

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

    protected override void OnActionExecuting(ActionExecutingContext filterContext) {

        MyCommonValue = 12345;

        base.OnActionExecuting(filterContext);
    }
}

Certifique-se de que cada controlador usa o controlador de base ...

public class UserController:_BaseController {...

Transmita o controlador de base existente do contexto de visualização em sua _Layout.cshmlpágina ...

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

Agora você pode consultar os valores em seu controlador de base na página de layout.

@myController.MyCommonValue
Carter Medlin
fonte
3

Se você deseja compilar a verificação do tempo e o intellisense para as propriedades em suas visualizações, o ViewBag não é o caminho certo.

Considere uma classe BaseViewModel e faça com que seus outros modelos de visualização herdem desta classe, por exemplo:

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

Agora o código de visualização pode acessar a propriedade diretamente na visualização

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

Eu descobri que a seguinte abordagem é a mais eficiente e oferece excelente controle utilizando o arquivo _ViewStart.chtml e instruções condicionais quando necessário:

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

ViewA :

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

Nota :

PageData funcionará perfeitamente em Views; entretanto, no caso de uma PartialView, ela precisará ser passada da View para a Partial filha.

útilBee
fonte