Property injection into custom WebViewPage using A

2019-05-24 06:22发布

问题:

I'm creating an internal application framework that other dev teams in our organisation will use to build MVC applications from. As part of that, I'm creating the menu structure for all views, that is read from configuration and modified based on the current user's permissions. To create the menu as part of the framework, I've created a custom WebViewPage implementation with a custom HTML Helper class that needs to take a dependency on an ApplicationDataReader to construct the menu.

I've read various posts that explain that MVC needs the WebViewPage to have a paramterless constructor, so I would need to use property injection. I've configured Autofac MVC3 Integration, including registering a ViewRegistrationSource. Trouble is, when the dependent property is accessed, it's always null.

Here's the custom view page and helper with the call I'm trying to make:

public abstract class OuBaseViewPage<TModel> : WebViewPage<TModel> where TModel : class
{
    public OuHelper<TModel> Ou { get; set; }

    public override void InitHelpers()
    {
        base.InitHelpers();
        Ou = new OuHelper<TModel>(ViewContext, this);
    }
}

public class OuHelper<TModel> where TModel : class
{
    public OuHelper(ViewContext viewContext, IViewDataContainer viewDataContainer)
        : this(viewContext, viewDataContainer, RouteTable.Routes)
    {
    }

    public OuHelper(ViewContext viewContext, IViewDataContainer viewDataContainer, RouteCollection routeCollection)
    {
        ViewContext = viewContext;
        ViewData = new ViewDataDictionary<TModel>(viewDataContainer.ViewData);
    }

    public ViewDataDictionary<TModel> ViewData { get; private set; }

    public ViewContext ViewContext { get; private set; }

    public IList<BusinessFocusArea> ReadFocusAreas()
    {
        // this is null - yes, service location, but an isolated instance of...
        return DependencyResolver.Current.GetService<IDataReader>().Read();
    }
}

The problem stems from the fact that InitHelpers is being called (via Layout.Execute()) BEFORE Application_Start is called, so none of the dependencies have been registered. I know that it's not good practice to inject logic into views and that views should simply be dumb, but this is an application framework and it needs to perform certain setup steps that the developers using the framework mustn't have visibility of.

Is there a better approach I could follow?

There's a similar issue here: Can Autofac inject dependencies into layout view files?

回答1:

The problem stems from the fact that InitHelpers is being called (via Layout.Execute()) BEFORE Application_Start is called

I don't think that something is called before Application_Start. I cannot reproduce your problem.

Here are the steps I did and which worked perfectly fine:

  1. Create a new ASP.NET MVC 3 application using the Internet Template
  2. Install the Autofac.Mvc3 NuGet
  3. Define a dummy interface:

    public interface IDataReader
    {
    
    }
    
  4. And a dummy implementation:

    public class DataReader : IDataReader
    {
    
    }
    
  5. Define a custom helper:

    public class OuHelper<TModel> where TModel : class
    {
        private readonly IDataReader dataReader;
        public OuHelper(ViewContext viewContext, IViewDataContainer viewDataContainer, IDataReader dataReader)
            : this(viewContext, viewDataContainer, RouteTable.Routes, dataReader)
        {
        }
    
        public OuHelper(ViewContext viewContext, IViewDataContainer viewDataContainer, RouteCollection routeCollection, IDataReader dataReader)
        {
            ViewContext = viewContext;
            ViewData = new ViewDataDictionary<TModel>(viewDataContainer.ViewData);
            this.dataReader = dataReader;
        }
    
        public ViewDataDictionary<TModel> ViewData { get; private set; }
    
        public ViewContext ViewContext { get; private set; }
    
        public IDataReader DataReader
        {
            get { return this.dataReader; }
        }
    
    }
    
  6. Define a custom WebViewPage using this helper:

    public abstract class OuBaseViewPage<TModel> : WebViewPage<TModel> where TModel : class
    {
        public OuHelper<TModel> Ou { get; set; }
    
        public override void InitHelpers()
        {
            base.InitHelpers();
            var dataReader = DependencyResolver.Current.GetService<IDataReader>();
            Ou = new OuHelper<TModel>(ViewContext, this, dataReader);
        }
    }
    
  7. Replace the default view page with the custom one in ~/Views/web.config:

    <pages pageBaseType="MvcApplication1.OuBaseViewPage">
    
  8. Configure your container in Application_Start:

    protected void Application_Start()
    {
        AreaRegistration.RegisterAllAreas();
        RegisterGlobalFilters(GlobalFilters.Filters);
        RegisterRoutes(RouteTable.Routes);
    
        var builder = new ContainerBuilder();
        builder.RegisterControllers(typeof(MvcApplication).Assembly);
        builder.RegisterType<DataReader>().As<IDataReader>();
        var container = builder.Build();
        DependencyResolver.SetResolver(new AutofacDependencyResolver(container));
    }
    
  9. Now you could happily use the custom helper in all views including the _Layout without any problems:

    @Ou.DataReader.GetType()
    

Of course in this example I have just exposed the IDataReader dependency as a public property to illustrate you that it will always be injected and it will never be null. In your particular code you could of course use only the private readonly field inside the helper to achieve your task.