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);
}
}
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)
{
var disposable = renderer as IDisposable;
if (disposable != null)
{
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 { return 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)
{
if (container == null)
throw new ArgumentNullException("container");
this.container = 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);