-
How to: Use Application Library Caching in Prism modules
No CommentsImagine you have a Silverlight application with functionality that is likely to change over time, but that application depends on some possibly heavyweight libraries that aren’t going to change. Every single change in your code would cause users to download the entire XAP file again, including those heavy libraries that didn’t change.
To address this concern, it’s possible to leverage Application Library Caching, which involves separating those libraries in ZIP files that are outside the XAP file, and should remain cached in the users’ browsers regardless of changes in the XAP. Prism supports Application Library Caching by, for example, providing the necessary metadata in the Prism assemblies to set them as external libraries.
However, when using Application Library Caching in Prism Modules, this doesn’t work out of the box. Let’s examine why this happens.
What happens with modules and Application Library Caching?
Agustin Adami and I examined the way Prism loads the content of module’s XAP files and found that the XapModuleTypeLoader reads the AppManifest.xaml file inside the module’s XAP file, searches for the Deployment.Parts elements and then loads the assemblies corresponding to the ones specified in the Source property of the AssemblyPart elements in the aforementioned XML element. However, dependencies that have been placed on a ZIP file using Application Library Caching are referenced in ExtensionPart elements inside the Deployment.ExternalParts element, and the physical file is stored outside the module’s XAP file (which is the only thing downloaded at this point.)
To illustrate this, here’s how the AppManifest for a certain module might look like:
1 <Deployment xmlns="http://schemas.microsoft.com/client/2007/deployment" (...)> 2 <Deployment.Parts> 3 <AssemblyPart x:Name="SomeModule" Source="SomeModule.dll" /> 4 <AssemblyPart x:Name="Infrastructure" Source="Infrastructure.dll" /> 5 </Deployment.Parts> 6 <Deployment.ExternalParts> 7 <ExtensionPart Source="System.Windows.Controls.Data.Input.zip" /> 8 <ExtensionPart Source="System.ComponentModel.DataAnnotations.zip" /> 9 </Deployment.ExternalParts> 10 </Deployment>Now, when a module’s XAP file has been downloaded, the XapModuleTypeLoader.HandleModuleDownloaded method is called, which loads in memory the results obtained by calling the XapModuleTypeLoader.GetParts method. This method ignores the Deployment.ExternalParts element inside the AppManifest.xaml file (as seen in the code snippet above):
1 (...)private static IEnumerable<AssemblyPart> GetParts(Stream stream) 2 { 3 List<AssemblyPart> assemblyParts = new List<AssemblyPart>(); 4 5 var streamReader = new StreamReader(Application.GetResourceStream(new StreamResourceInfo(stream, null), new Uri("AppManifest.xaml", UriKind.Relative)).Stream); 6 using (XmlReader xmlReader = XmlReader.Create(streamReader)) 7 { 8 xmlReader.MoveToContent(); 9 while (xmlReader.Read()) 10 { 11 if (xmlReader.NodeType == XmlNodeType.Element && xmlReader.Name == "Deployment.Parts") 12 { 13 // Deployment.Parts is examined, but Deployment.ExternalParts isn't 14 using (XmlReader xmlReaderAssemblyParts = xmlReader.ReadSubtree()) 15 { 16 while (xmlReaderAssemblyParts.Read()) 17 { 18 if (xmlReaderAssemblyParts.NodeType == XmlNodeType.Element && xmlReaderAssemblyParts.Name == "AssemblyPart") 19 { 20 AssemblyPart assemblyPart = new AssemblyPart(); 21 assemblyPart.Source = xmlReaderAssemblyParts.GetAttribute("Source"); 22 assemblyParts.Add(assemblyPart); 23 } 24 } 25 } 26 27 break; 28 } 29 } 30 } 31 32 return assemblyParts; 33 } 34 (...) 35The Unity Solution
As a possible workaround to avoid this problem from happening, we created a modified version of the XapModuleTypeLoader (named LibraryCachingXapModuleTypeLoader), which loads the extension parts in memory before loading the assembly parts. To do this, we modified the IFileDownloader_DownloadCompleted method to obtain an ExternalPartsLoader from the container and call its LoadExtensionParts method before calling the HandleModuleDownloaded. The ExternalPartsLoader.LoadExtensionParts method basically searches for the AppManifest.xaml inside the XAP file, downloads the ZIP files containing the extension parts and loads the corresponding libraries in memory. Once this operation has finished, the HandleModuleDownloaded (which had been passed as a callback) is called, and the module loading process continues as it would regularly do.
To make this work, we had to create a class that inherits from ModuleManager and overrides its ModuleTypeLoaders property to use the LibraryCachingXapModuleTypeLoaderWith class instead of the regular XapModuleTypeLoader. We then overrode the ConfigureContainer method in the bootstrapper to register this custom module manager (named LibraryCachingModuleManager) in the container, so that it is used instead of the regular one.
You can find a sample that contains all the aforementioned modifications in my Skydrive account under the name LibraryCachingSupport.zip. This sample contains a Shell project and a module; both of which use Application Library Caching.
The MEF Solution
In the MefXapModuleTypeLoader a DeploymentCatalog is used to download the module’s XAP file and then compose all the parts defined in that file. Unlike in the XapModuleTypeLoader, it’s not possible to insert the external parts loading logic in the middle, so for this workaround, we had to opt out of the DeploymentCatalog and implement a similar functionality using the LibraryCachingXapModuleTypeLoader class.
Basically, the main aspect that should be considered for the MEF version is that the DeploymentCatalog, aside from loading the modules in memory, composes the parts discovered in it into the container. So, we added the logic to do so in both the LibraryCachingMefXapModuleTypeLoader and the MefExternalPartsLoader:
1 private static void ComposePartsInContainer(Assembly assembly) 2 { 3 var catalog = new AssemblyCatalog(assembly); 4 5 var batch = new CompositionBatch(catalog.Parts.Select(e => e.CreatePart()), null); 6 7 var container = ServiceLocator.Current.GetInstance<CompositionContainer>(); 8 9 container.Compose(batch); 10 }Additionally, we had to register the CompositionContainer in itself in order to be retrieved by the service locator in that piece of code.
You can find a sample that contains the MEF version of this approach in my Skydrive account under the name MefLibraryCachingSupport.zip.
Please note that this might not be the best approach to achieve this functionality, and it hasn’t been thoroughly tested; the intention of this post is to explain why this happens, and provide a possible workaround that might also serve as an example to implement the approach differently.
-
Leave a comment
Your email address will not be published.
