Tuesday, February 2, 2010

Merging WPF assemblies

Life can be unnecessarily complex, especially if you work with technology!

WPF is great. One of the ten best things Microsoft has ever done. (Er - not that they haven't also done loads of terrible things, but that's another story.)

Unfortunately, WPF windows and user controls are compiled into BAML, every time you rebuild your application, and the compile-to-BAML process is not very efficient.

I have, hmmm, probably 50 to 100 WPF windows and user controls in a particular project, and on a 3.2ghz CPU, app build time is around 12 seconds. And that's with heaps of RAM, the superb Intel X25-M SSD, and other speed features.

Compile the project without WPF windows and user controls, and it takes more like a second.

In other words, compilation to BAML is just plain slow.

Yet every time you change anything in the project's source code - regardless of whether or not you actually modified any XAML files or associated code-behind files - the BAML files are regenerated.

With C#, those BAML files are (it seems) regenerated just when you rebuild the project - which wastes some time.

But with VB.NET, those BAML files are constantly being regenerated as the VB.NET IDE performs ongoing recompilations of the entire project for Intellisense purposes.

This means that, working on a large VB.NET project with lots of XAML windows and user controls, you can face an unresponsive IDE several times a minute as it constantly compiles and recompiles and re-re-compiles, taking more than 10 seconds every time.

Slow, slow, slow, slow, slow, slow.

And for those of us who are into rapid testing and release cycles, slow builds are productivity killers.

So I refactored the solution, putting the windows and user controls in a WPF DLL, leaving most of the application logic in the main project (the WPF EXE that already existed).

There were a few tricks to getting that to work, but now build time is slashed. Unless I actually modify a window or user control in that DLL, the DLL is not rebuilt, and so I save roughly 10 seconds each time. And if you're making a series of minor tweaks and re-running, as is typical during testing cycles, that can easily translate into a 10% to 30% reduction in time spent by the developer. THAT'S good!

Of course, whatever XAML window or user control I happen to be working on at the time, I leave in the main WPF EXE, and only transfer it into the WPF DLL once I've tested it, and the usual flood of change requests for the new screen have died down.

That way, I'm rarely modifying the WPF DLL, and so I'm rarely copping the full 12-ish second build time - sufficiently rarely that the slowness of the full build is largely irrelevant.

All good and well ...

... and I highly recommend it if you need the speed boost ...

... BUT, things fall apart if you try to use a tool that merges assemblies.

I spent hours today digging around, and this is what I found out :

All of the XAML windows and user controls that have "Build Action" set to "Page" (which is the default), are compiled into a single assembly manifest resource named {AssemblyNameMinusFilenameExtension}.g.resources (e.g. MyApp.g.resources).

Within that one assembly manifest resource is a bunch of BAML items, one for each window or user control.

Now, if you change the name of the assembly manifest resource, WPF can no longer locate the BAML files.

So let's for example say you have two assemblies : MyWpfExe and MyWpfDll.

MyWpfExe.exe contains an assembly manifest resource named MyWpfExe.g.resources.

MyWpfDll.dll contains an assembly manifest resource named MyWpfDll.g.resources.

Problem is, if you merge the assemblies together, these two assembly manifest resources do not get merged, and because WPF will ONLY look for assembly manifest resources that match the assembly name, one of the assembly manifest resources (and all associated windows and user controls) will be ignored.

e.g. MyMergedWpfExe.exe contains two assembly manifest resources :
MyWpfExe.g.resources
MyWpfDll.g.resources

... but WPF will only look for one, matching the assembly name. i.e. the MyWpfDll.g.resources will be completely ignored.

Obvious solution? Well, somehow merge the two assembly manifest resources into one. But no easy cigar. I expect it can be done. I'll leave that as an exercise to the reader, if they care. (I found no existing tools that do the job, so it would probably require someone to write a tool especially for this purpose.)

I ended up finding a way that didn't require me to write a special tool. It's cumbersome, it's "clunky", but it does work, and it meets my immediate needs, and I can always improve it later.

So here goes (and warning : it is very technical!) :

There are two parts to my trick.

Part 1) Trick WPF into loading TWO DIFFERENT assembly manifest resources, from the SAME assembly, BOTH of which will be fully consulted when loading windows and user controls. The heart of this trick is to rename one of the assembly manifest resources to include the "en-US" culture. Unfortunately, this trick can only work if there are ONLY two assemblies with BAML stuff being merged, and it gets tricky if you are using localisation for other purposes (although you can still do it). (More details below.)

Part 2) The compiled BAML contains a link to the original assembly name, and of course, post-merging, the original assembly ain't anywhere to be found. So, ridiculous as it might seem, you actually need to temporarily produce a version of the DLL that has the same assembly name as the EXE. (More details below.)

So, step by step, it goes something like this :

Step 1) Alter MyWpfDll.dll's project settings so that its Assembly Name matches the exe's Assembly Name (MyWpfExe).

Step 2) Build just the DLL (MyWpfExe.dll, due to the rename in step #1).

Step 2) Open MyWpfExe.dll in Lutz Roeder's Reflector.

Step 3) Drill down through the tree view until you find the resource named MyWpfExe.g.resources. Right-click it and save it somewhere - e.g. C:\MyWpfExe.g.resources.

Step 4) Revert the name change from step #1. (i.e. reconfigure the MyWpfDll project to use MyWpfDll as its assembly name again instead of MyWpfExe.)

Step 5) Rename MyWpfExe.g.resources to MyWpfExe.g.en-US.resources.

Step 6) "Add existing file" to the MyWpfExe project, and choose MyWpfExe.g.en-US.resources.

Step 7) Ensure that MyWpfExe.g.en-US.resources has Build Action set to "Embedded Resource" (which is the default at least on my computer, so that SHOULD already be fine).

Step 8) Build the "solution" (MyWpfExe.exe and MyWpfDll.dll).

Step 9) Merge assemblies.

Voila! That was very painful and cumbersome, but you now have two WPF assemblies merged together, in a manner that all of the BAML from both assemblies is still accessible.

Alternatives :

* Of course, one option since you have full source code for both projects, is to make a project file that combines all source files into one mega project. Only use that project file when doing your release build. Not ideal, but an option, especially if you can find or make a tool that will automatically combine two VS.NET projects into a single project. (And hey - if you can find or make such a tool, it'll probably be less un-ideal than what I've presently settled for! It's just, when I finally found something that met my immediate purposes, I decided to not waste any further time refining it for the moment.)

No comments: