Last time in this series we discussed projects and how to setup your large application. One point that dictates project layout in our application is the fact that we load XAP files as needed. Recently I've had some interesting insights from our functional specialist. He not only wanted multiple modules on one screen, but he also wanted users, or at least administrators, to be able to configure which modules would be shown on which screen.
The first alarm bells started ringing at this point. It could all end up in a performance nightmare. In the initial faze of the project we figured we would load modules on a single screen from a single XAP file, all at once. This would greatly reduce any overhead involved in loading XAP files dynamically. But now we would have to load multiple modules from multiple XAP files, up to fifteen at a time. So at that moment in time I told the functional specialist, "No, we can't do that." I explained why this was likely to give problems and his response was, "Well can't you build these XAP files dynamically as you need them?" Well, wouldn't that be a heroic solution and a hell of a blog post :-).
I told him that would complicate things a great deal and we decided that for the most part we would decide which modules would end up on which screens, but users would be able to configure extra screens, with the knowledge that these screens could end up being slower than others.
In the two weeks since, I've come up with some alternative solutions to our problem. First of all, you don't have to put an assembly in a XAP file in order to load it, BUT... be warned! If you decide to load an assembly on it's own, you won't load anything referenced from the assembly, not other assemblies you might need and no resource files either. You would have to resolve these references yourself or have some kind of common set of references that every module is restricted to. In our case, this would be troublesome at best, so I discarded this.
I did figure out we would need some kind of meta database that would contain the settings of all these modules and who wants to see what where. Also this database would have to include security information on all the modules.
Then my colleague came up with the following solution. We do build everything we need to dynamically place any module on any screen as this is configured, but we don't solve the performance issue in the code. As customers experience issues, we simply build a new XAP file with the required modules and deploy it. We add it to our meta database and Bob's your uncle. This does mean we have to build some algorithm that can figure out the most optimal set of XAP files possible with what's available, but that is actually doable if you design a decent entity structure. After this was sorted I concluded that we didn't even have code yet that would load multiple modules from a single XAP file. So on I went and build that. It's basically a variation on the code I showed you all earlier in this post. Unfortunately I won't be able to upload all the code for this, but I can show you some highlights.
public void LoadModules(Collection<ModuleInfo> modules)
{
_modules = modules;
foreach (ModuleInfo module in modules)
{
GenerateTab(module);
}
var result = from ModuleInfo module in modules
group module by module.XapFileName;
foreach (IGrouping<string, ModuleInfo> xapFileGroup in result)
{
string xapFileName = xapFileGroup.Key;
WebClient client = new WebClient();
client.OpenReadCompleted += new OpenReadCompletedEventHandler(client_OpenReadCompleted);
client.OpenReadAsync(new Uri(xapFileName, UriKind.Relative), xapFileGroup);
}
}
void client_OpenReadCompleted(object sender, OpenReadCompletedEventArgs e)
{
if (e.Error != null)
{
return;
}
IGrouping<string, ModuleInfo> xapFileGroup = e.UserState as IGrouping<string, ModuleInfo>;
if (xapFileGroup == null)
{
return;
}
var assemblyNames = (from ModuleInfo module in xapFileGroup
select module.AssemblyFileName).Distinct();
string[] stringAssemblyNames = assemblyNames.ToArray();
var classNames = (from ModuleInfo module in xapFileGroup
select module.ClassName).Distinct();
string[] stringClassNames = classNames.ToArray();
Collection<UIElement> modules = XapHelper.LoadUIElementsFromXap(e.Result,
stringAssemblyNames, stringClassNames);
foreach (UIElement module in modules)
{
PlaceModule(module);
}
}
These are basically the loading of the modules and receiving a stream. In the LoadModules method I get a collection of ModuleInfo structs, that tell me what XAP file the module is in, what assembly and which class. In a linq query I group the modules by XAP file and then I kick off a load for each of these.
In the completed event I pick up the grouping from the linq query and use it to get to the right assembly and class names. I then use an adjusted method from the XapHelper class I wrote earlier to load the UIElements I need from the XAP file:
public static Collection<uielement> LoadUIElementsFromXap(Stream xapStream,
string[] assemblyNames, string[] classNames)
{
Collection<uielement> elements = new Collection<uielement>();
string applicationManifest = ReadApplicationManifest(xapStream);
XElement deploymentElement = XDocument.Parse(applicationManifest).Root;
IEnumerable<xelement> deploymentParts =
from assemblyParts in deploymentElement.Elements().Elements()
select assemblyParts;
Collection<assembly> assemblies = LoadAssemblies(xapStream, assemblyNames,
deploymentParts);
foreach (Assembly assembly in assemblies)
{
foreach (string className in classNames)
{
UIElement element = assembly.CreateInstance(className) as UIElement;
if (element != null)
{
elements.Add(element);
}
}
}
return elements;
}
As you may notice, the only thing I changed is what I do with each assembly and class as I load them.
I hope this post was helpful for you and once again, if you have any questions or comments, please let me know.