How to resolve collection with filtering parameter?
Asked Answered
S

1

7

Can Castle Windsor resolve a collection filtered by a string parameter?

interface IViewFactory
{
   IView[] GetAllViewsInRegion(string regionName);
}

My application defines regions as groups of IView-derived types. When I display a particular region at runtime, I want to resolve an instance of every IView type within it (a la Prism).

I've tried doing it with the Castle's Typed Factory Facility, ComponentModel Construction Contributors, and Handler Selectors, but I can't figure out how to map multiple types to a string in a way that Castle can access, nor how to extend Castle to check the string when it decides which types to try to resolve and return in the container.

Skimp answered 22/2, 2012 at 12:5 Comment(0)
A
1

Is selection by string strictly necessary? Would it be possible to instead have all IView implementations in the same "region" implement a dedicated interface that derives from IView? Then you could use WindsorContainer.ResolveAll() (passing your region-specific IView as T) to resolve the implementations for the region in question (or you could use one of the Collection Resolvers to perform constructor injection).

In general, when trying to do things like this with Windsor, I make every effort to use the type system (and Windsor's support thereof) before resorting to string-based solutions.

Update: since we confirmed that selection by string is necessary in this case, the best solution I see is to simply inspect the list of handlers in the kernel that satisfy the IView service, then filter for the implementers where the region (defined via attribute) matches what we want, then resolve those implementers. This feels a bit hackish, but if you're okay with having a direct reference to the container in your IViewFactory implementation, this appears to work fine. Below is a passing test case demonstrating the solution.

    [Test]
    public void Test()
    {
        using (var factory = new ViewFactory())
        {

            var regionOneViews = factory.GetAllViewsInRegion("One");
            Assert.That(regionOneViews, Is.Not.Null);
            Assert.That(regionOneViews, Has.Length.EqualTo(2));
            Assert.That(regionOneViews, Has.Some.TypeOf<RegionOneA>());
            Assert.That(regionOneViews, Has.Some.TypeOf<RegionOneB>());

            var regionTwoViews = factory.GetAllViewsInRegion("Two");
            Assert.That(regionTwoViews, Is.Not.Null);
            Assert.That(regionTwoViews, Has.Length.EqualTo(1));
            Assert.That(regionTwoViews, Has.Some.TypeOf<RegionTwoA>());
        }
    }
}

public interface IViewFactory
{
    IView[] GetAllViewsInRegion(string regionName);
}

public class ViewFactory : IViewFactory, IDisposable
{
    private readonly WindsorContainer _container;

    public ViewFactory()
    {
        _container = new WindsorContainer();
        _container.Register(
            Component.For<IView>().ImplementedBy<RegionOneA>(),
            Component.For<IView>().ImplementedBy<RegionOneB>(),
            Component.For<IView>().ImplementedBy<RegionTwoA>()
            );
    }

    public IView[] GetAllViewsInRegion(string regionName)
    {
        return _container.Kernel.GetHandlers(typeof (IView))
            .Where(h => IsInRegion(h.ComponentModel.Implementation, regionName))
            .Select(h => _container.Kernel.Resolve(h.ComponentModel.Name, typeof (IView)) as IView)
            .ToArray();
    }

    private bool IsInRegion(Type implementation,
                            string regionName)
    {
        var attr =
            implementation.GetCustomAttributes(typeof (RegionAttribute), false).SingleOrDefault() as RegionAttribute;
        return attr != null && attr.Name == regionName;
    }

    public void Dispose()
    {
        _container.Dispose();
    }
}

public interface IView {}

[Region("One")]
public class RegionOneA : IView {}

[Region("One")]
public class RegionOneB : IView {}

[Region("Two")]
public class RegionTwoA : IView {}

[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class RegionAttribute : Attribute
{
    private readonly string _name;

    public RegionAttribute(string name)
    {
        _name = name;
    }

    public string Name
    {
        get { return _name; }
    }
}
Anthropomorphism answered 27/2, 2012 at 15:0 Comment(5)
I try to use types instead of string keys, too, but the Prism extension point I'm working with (RegionBehavior) associates regions with strings, so I can't see any way around selection by string. In reality, Prism only expects objects in the region, so even the IView interface is an artificial simplification on my part. In revision 1 I gave a bunch of context for my question, but I think it was scaring people away.Skimp
Some regions are defined in plugins. Things get very complicated and slow if my class has to search loaded assemblies for a marker interface with the short name "I" + regionName + "View". I would prefer decorating classes with a custom attribute like ViewExport[RegionName="MainRegion"] and filter types in something like Windsor's HandlersFilter extension point.Skimp
Okay I understand, I'm not familiar with Prism, so I didn't realize that the string selection was necessary. I think something like this should be possible. I'll try to come up with something in my spare time today :-)Anthropomorphism
Alright, I've got something. I don't love it, as it requires a reference to the container and requires you to inspect the kernel. The logic can probably be encapsulated into a facility to avoid the direct container reference, but this basic approach is the only way I can think up that solves the problem.Anthropomorphism
If you pack your code from GetAllViewsInRegion inside an override of DefaultTypedFactoryComponentSelector.BuildFactoryComponent, then it's a perfectly elegant solution. :-) This is great!Skimp

© 2022 - 2024 — McMap. All rights reserved.