1

I have a UserControl which is designed to edit a collection of some arbitrary POCO. The POCO is selected at design time, so I can pass a description of the properties within the POCO that need to be displayed and edited but I'm struggling to see the best way to instantiate new POCOs within the control to add to the collection.

At the moment, I'm running with adding a new property to the control that holds an IPocoFactory but this doesn't seem satisfactory for a couple of reasons:

  • The control user has to do quite a bit of leg work creating a class that implements the IPocoFactory interface just to use the control which would otherwise be quite straightforward
  • Controls such as the DataGrid have already solved this problem (although I cannot seem to figure out how, despite poking around for a while with ILSpy!)

Can anyone suggest a decent pattern for this problem? I can't be the only one who's faced it!

It occurs to me that reflection might play a part in a solution, but I'm not quite sure about that either: I could examine the ItemsSource (a non-generic IEnumerable) to see what's in it, but if it's empty, there's nothing to look at.

Bob Sammers
  • 3,080
  • 27
  • 33

1 Answers1

3

You can get the type to be created by calling ItemsSource.GetType().GetInterfaces(), finding the Type object for the IEnumerable<T> interface (which any generic collection will implement), and calling GetGenericArguments() on it. IEnumerable<T> has one type argument, of course, so that's the type you need to create an instance of.

Then you can create an instance fairly easily (see UPDATE below for a static method which wraps this all up into a single method call):

ObjectType instance = (ObjectType)Activator.CreateInstance("AssemblyName",
                                                           "MyNamespace.ObjectType");

You'll need the assembly in which the type is declared, but that's a property of Type. Assembly has a CreateInstance method as well. Here's another way to do the same thing:

Type otype = typeof(ObjectType);
ObjectType instance = (ObjectType)otype.Assembly.CreateInstance(otype.FullName);

If the type to be instantiated doesn't have a default constructor, this gets uglier. You'd have to write explicit code to provide values, and there's no way to guarantee that they make any sense. But at least that's a much lighter burden to impose on the consumer than a mess of IPOCOFactory implementations.

Remember by the way that System.String doesn't have a default constructor. It's natural to test the code below with List<String>, but that's going to fail.

Once you have the type of the objects in ItemsSource, you can further simplify maintenance by programmatically enumerating the names and types of the properties and auto-generating columns. If desired, you could write an Attribute class to control which ones are displayed, provide display names, etc. etc.

UPDATE

Here's a rough implementation that's working for me to create instances of a class declared in a different assembly:

/// <summary>
/// Collection item type must have default constructor
/// </summary>
/// <param name="items"></param>
/// <returns></returns>
public static Object CreateInstanceOfCollectionItem(IEnumerable items)
{
    try
    {
        var itemType = items.GetType()
                            .GetInterfaces()
                            .FirstOrDefault(t => t.Name == "IEnumerable`1")
                            ?.GetGenericArguments()
                            .First();

        //  If it's not generic, we may be able to retrieve an item and get its type. 
        //  System.Windows.Controls.DataGrid will auto-generate columns for items in 
        //  a non-generic collection, based on the properties of the first object in 
        //  the collection (I tried it).
        if (itemType == null)
        {
            itemType = items.Cast<Object>().FirstOrDefault()?.GetType();
        }

        //  If that failed, we can't do anything. 
        if (itemType == null)
        {
            return null;
        }

        return itemType.Assembly.CreateInstance(itemType.FullName);
    }
    catch (Exception ex)
    {
        return null;
    }
}

public static TestCreate()
{
    var e = Enumerable.Empty<Foo.Bar<Foo.Baz>>();

    var result = CreateInstanceOfCollectionItem(e);
}

You could make CreateInstanceOfCollectionItem() an extension method on IEnumerable if you like:

var newItem = ItemsSource?.CreateInstanceOfCollectionItem();

NOTE

This depends on the actual collection being a generic collection, but it doesn't care about the type of your reference to the collection. ItemsControl.ItemsSource is of the type System.Collections.IEnumerable, because any standard generic collection supports that interface, and so can be cast to it. But calling GetType() on that non-generic interface reference will return the actual real runtime type of the object on the other end (so to speak) of the reference:

var ienumref = (new List<String>()) as System.Collections.IEnumerable;
//  fullName will be "System.Collections.Generic.List`1[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]"
//  ...or something like it, for whatever version of .NET is on the host.
var fullName = ienumref.GetType().Name;
Community
  • 1
  • 1
  • Thanks for your detailed answer... but unfortunately, it doesn't quite tick the right boxes. As I said, the collection is a non-generic `IEnumerable` (as is usual in WPF controls because of XAML's difficulties with type parameters), so there isn't a type parameter. The non-generic part of your method only works if the collection isn't empty and creating collections from scratch is a first-class use-case for this control. – Bob Sammers Jun 23 '16 at 10:39
  • @BobSammers If the type of the collections is out of your control, there's nothing that can be done. The information isn't there. Your best bet is to change the collection types. Why aren't they generic? – 15ee8f99-57ff-4f92-890c-b56153 Jun 23 '16 at 11:17
  • @BobSammers You do have one more option: Instead of your IPOCOFactory property idea, expose `Type IPOCOType`. This imposes a much lighter burden on the consumer: just pass the type. From my code above you can steal just one line to instantiate instances from a `Type`. But fixing the collections is the real answer, if that code is under your control. – 15ee8f99-57ff-4f92-890c-b56153 Jun 23 '16 at 12:00
  • Sorry, I've misunderstood. If the collection the control is bound to is generic, this works, even if the ItemsSource in the control itself is not (which is the case). I did copy your code into a test application, but I must have got the wrong answer for a different reason! – Bob Sammers Jun 23 '16 at 12:13
  • @BobSammers Oh right, I'll add that: `GetType()` returns the actual runtime type of the object, regardless of the declared type of the reference to it. Generic collections all support `System.Collections.IEnumerable` and so can be cast to it, but `GetType()` on that `IEnumerable` reference may return all kinds of goodies. Glad to hear it's working for you. – 15ee8f99-57ff-4f92-890c-b56153 Jun 23 '16 at 12:51
  • Thanks for the clarification - this does make sense. For reference, I assumed it didn't work because I naively tried it with `List` (`string` doesn't have a default ctor, of course). Because of the `try` / `catch` block the resulting exception from calling `CreateInstance()` on `string` caused a `null` return. – Bob Sammers Jun 23 '16 at 13:44
  • @BobSammers Oh wow, I didn't think of that case. I'll update. That's the first thing most people would try; sheer luck I happened not to. – 15ee8f99-57ff-4f92-890c-b56153 Jun 23 '16 at 13:46