Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
494 views
in Technique[技术] by (71.8m points)

asp.net mvc - Dependency Injection in HtmlHelper extension method?

I want to implement property renderers as handlers. I am using Autofac as a DI container in the app. How can I get objects implementing IPropertyHandler in HtmlHelper extension without using globally accessible container (service location)? Is it a way to register own HtmlHelper in Autofac? Maybe MVC framework provide another way?

public static class HtmlHelperExtensions {
    public static MvcHtmlString Editor(this HtmlHelper html, object model) {
        return new Renderer(new List<IPropertyHandler>() /*Where to get these objects?*/ ).Render(html, model);
    }
}

public class Renderer {
    private readonly ICollection<IPropertyHandler> _propertyRenderers;

    public Renderer(ICollection<IPropertyHandler> propertyRenderers) {
        _propertyRenderers = propertyRenderers;
    }

    public MvcHtmlString Render(HtmlHelper html, object model) {
        var result = "";
        foreach(var prop in model.GetType().GetProperties()) {
            var renderers = _propertyRenderers.OrderBy(b => b.Order);
            //impl
        }
        return new MvcHtmlString(result);
    }
}
See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

AFAIK, MVC 5 doesn't provide a way to do this. But that doesn't mean you can't wire up your own solution.

MVC Core now uses view components that are DI friendly, so you don't have to jump through so many hoops.

As per the article DI Friendly Framework by Mark Seemann, you can make a factory interface for your HTML helper that can be used to instantiate it with its dependencies.

DefaultRendererFactory

First there is a default factory that provides the logical default behavior (whatever that is).

public interface IRendererFactory
{
    IRenderer Create();
    void Release(IRenderer renderer);
}

public class DefaultRendererFactory : IRendererFactory
{
    public virtual IRenderer Create()
    {
        return new Renderer(new IPropertyHandler[] { new DefaultPropertyHandler1(), DefaultPropertyHandler2() });
    }
    
    public virtual void Release(IRenderer renderer)
    {
        if (renderer is IDisposable disposable)
        {
            disposable.Dispose();
        }
    }
}

You may wish to make this default factory smarter or even use a fluent builder to supply its dependencies as per the other article DI Friendly Library so it is more flexible without using a DI container.

IRenderer

Then we use an abstraction for Renderer, IRenderer so it can be swapped easily and/or provided via DI.

public interface IRenderer
{
    MvcHtmlString Render(HtmlHelper html, object model);
}

public class Renderer : IRenderer
{
    private readonly ICollection<IPropertyHandler> _propertyRenderers;

    public Renderer(ICollection<IPropertyHandler> propertyRenderers) 
    {
        _propertyRenderers = propertyRenderers;
    }

    public MvcHtmlString Render(HtmlHelper html, object model) 
    {
        var result = "";
        foreach(var prop in model.GetType().GetProperties()) 
        {
            var renderers = _propertyRenderers.OrderBy(b => b.Order);
            //impl
        }
        return new MvcHtmlString(result);
    }
}

Factory Registration

Next, we provide a hook to register the factory. Since the HTML helper is a static extension method, the only option is to make a static field with a static property or method to set it. Its always good practice to make a getter as well in case there is a need to use a decorator pattern on the factory.

public interface IRendererFactory
{
    IRenderer Create();
    void Release(IRenderer renderer);
}

public static class HtmlHelperExtensions {
    private static IRendererFactory rendererFactory = new DefaultRendererFactory();
    
    public static IRendererFactory RendererFactory
    {
        get => rendererFactory;
        set => rendererFactory = value;
    }

    public static MvcHtmlString Editor(this HtmlHelper html, object model) {
        var renderer = rendererFactory.Create();
        try
        {
            return renderer.Render(html, model);
        }
        finally
        {
            rendererFactory.Release(renderer);
        }
    }
}

You could provide some logical place to register all of your factories statically, if that makes more sense for the app. But there will basically need to be a factory per HTML helper to adhere to the SRP. If you try to generalize, you are basically back to a service locator.

AutofacRendererFactory

Now that all of the pieces are in place, this is how you would slip Autofac into the equation. You will need a custom IRendererFactory that you will make part of your composition root that is specific to Autofac.

public class AutofacRendererFactory : IRendererFactory
{
    private readonly Autofac.IContainer container;

    public AutofacRendererFactory(Autofac.IContainer container)
    {
        this.container = container ?? new ArgumentNullException(nameof(container));
    }

    public IRenderer Create()
    {
        return this.container.Resolve(typeof(IRenderer));
    }
    
    public void Release(IRenderer renderer)
    {
        // allow autofac to release dependencies using lifetime management
    }
}

Next, you need to add the type mappings for IRenderer and its dependencies to Autofac.

Last but not least, you will need to add a line to your application startup after creating the Autofac container to resolve the renderer when it is needed by the application.

// Register all of your types with the builder
// ...
// ...
Autofac.IContainer container = builder.Build();
HtmlHelperExtensions.RendererFactory = new AutofacRendererFactory(container);

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...