Reputation: 3270
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:
IPocoFactory
interface just to use the control which would otherwise be quite straightforwardCan 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.
Upvotes: 1
Views: 1758
Reputation: 37059
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.
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();
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;
Upvotes: 3