简体   繁体   中英

How should I instantiate a class of unknown type?

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.

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;

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM