LINQ with Reflection
I was toying last night with my work-in-progress WPF version of Comicster, specifically looking at file formats. I want this version to easily support opening and saving collections to multiple file formats, and the logical way to do that was via some sort of plug-in system.
Eventually (and by that I mean probably when .NET 4.0 is released) I'll look at using the Managed Extensibility Framework to power the file-format plug-in system. I found a great post by Brad Abrams which walks through a simple MEF tutorial, and it looks trivially easy to, say, scan a folder for plug-in assemblies and grab the relevant types from them.
In the meantime, though, I've settled on defining my various "core" file formats in a single assembly and using reflection to load them on demand. The idea is that I've created two simple interfaces: ICollectionReader and ICollectionWriter. I've separated the read and write functionality because some file formats will only support opening and not saving. Then in the same assembly I've defined a few classes that implement those interfaces. For example, XmlCollectionReader (for XML files - duh) and PackageCollectionReader (for CMXX files, see this post for more info).
Once those classes were defined, I needed a way, when the user clicks the "Open" button, to find which file formats were available to read. I settled upon this nifty LINQ statement:
var readers = ( from t in Assembly.GetAssembly(typeof(ICollectionReader)).GetExportedTypes() where t.IsClass && typeof(ICollectionReader).IsAssignableFrom(t) && t.GetConstructor(new Type[0]) != null select Activator.CreateInstance(t) as ICollectionReader ).ToList();
So what's happening here? I'm asking for a list of all the public classes that live in the same assembly as ICollectionReader and implement that interface, and I make sure that they have a constructor which takes no parameters, because otherwise the call to Activator.CreateInstance would throw an exception. I end up with a list of instances of these "reader" objects.
Each reader object has two properties: "Extension" which is the file extension for the file format supported by that reader (eg ".xml") and "Filter" which is the friendly name for that file format to display in a file dialog (eg "XML Files (*.xml)|*.xml"). Using those properties I can construct an OpenFileDialog based on all of the known reader objects:
var ofd = new OpenFileDialog { Title = "Open Collection", Filter = string.Join("|", readers.Select(r => r.Filter).ToArray()), DefaultExt = readers[0].Extension, };
Once I show that dialog I can check that the filename the user selected has a supported reader:
var reader = readers.FirstOrDefault(r => string.Compare(r.Extension, Path.GetExtension(ofd.FileName), StringComparison.OrdinalIgnoreCase) == 0);
if (reader == null) { MessageBox.Show("No readers available for this file extension"); return; }
And lastly, use the ICollectionReader's "ReadCollection" method to open the file:
this.DataContext = reader.ReadCollection(ofd.OpenFile());
There's a little bit more code in my "Open" command's Execute handler to do some sanity checking, but this is the meat-and-potatoes of it. The thing I'm most proud of is the initial LINQ statement, but ironically that'd be the first thing to go if I were to move to MEF. Ah well - if you want to make a MEF omelette you have to break a few Reflection eggs.
Comments
# TimothyP
6/11/2008 7:35 PM
Been doing something very similar lately,
although I didn't think directly creating the instance in the LINQ query, neat :-)
When .NET 4 and C# 4 is released,
all this reflection stuff will be
simplified considerably thanks to the dynamic type.
# mabster
6/11/2008 7:55 PM
You said it, Timothy! The new dynamic stuff looks really exciting!
Be sure to check out my next post where I take this a step further!