实现按需加载 Silverlight 组件
Silverlight 客户端加载一个程序集很容易, 关键是如何分析并加载程序集引用的其它程序集, 这些程序集又会引用另外的程序集, 然后再加在这些程序集。
借助于 Mono.Cecil ,可以在客户端很容易的分析出程序集引用的其它程序集。
至于如何加载, 我的实现思路是, 做一个下载队列, 每下载一个程序集, 分析其引用的程序集列表, 找出其没有加载过的程序集, 添加到下载队列, 从下载队列中删除下载过的程序集, 如果队列不为空, 则依次进行递归; 否则,触发下载完成事件。实现代如下:
public class AssemblyDownloader {
private static readonly IDictionary LoadedAssemblies = new Dictionary(StringComparer.OrdinalIgnoreCase);
private readonly ISet _loadingSet = new HashSet();
private static readonly object LoadingSetLock = new object();
private static readonly string[] SilverlightRuntimeAssemblyNames = new[] {
"Microsoft.VisualBasic.dll",
"mscorlib.dll",
"System.Core.dll",
"System.dll",
"System.Net.dll",
"System.Runtime.Serialization.dll",
"System.ServiceModel.dll",
"System.ServiceModel.Web.dll",
"System.Windows.Browser.dll",
"System.Windows.dll",
"System.Windows.RuntimeHost.dll",
"System.Xml.dll"
};
private bool _isbusy;
private string _loadingAssemblyName;
public event EventHandler DownloadAssemblyCommpleted;
public event EventHandler DownloadFailed;
public Assembly GetAssembly(string assemblyName) {
return LoadedAssemblies.ContainsKey(assemblyName) ? LoadedAssemblies[assemblyName] : null;
}
public void OnDownloadFailed(Exception ex) {
var handler = this.DownloadFailed;
if (handler != null) {
handler(this, new AsyncCompletedEventArgs(ex, true, null));
}
}
private void OnDownloadAssemblyCommpleted(DownloadAssemblyCommpletedEventArgs e) {
var handler = this.DownloadAssemblyCommpleted;
if (handler != null) {
handler(this, e);
}
}
public void DownloadAssemblyAsync(string assemblyName) {
if (this._isbusy) {
throw new InvalidOperationException(string.Format("AssemblyDownloader is loading {0}, please waite ...", this._loadingAssemblyName));
}
assemblyName = EnsureEndWdithDll(assemblyName);
this._loadingAssemblyName = assemblyName;
if (IsAssemblyLoaded(assemblyName)) {
this.DownloadCompleted();
}
else {
DownloadAssemblyAsyncCore(assemblyName);
}
}
private void DownloadAssemblyAsyncCore(string assemblyName) {
this._isbusy = true;
assemblyName = EnsureEndWdithDll(assemblyName);
var name = assemblyName;
var webClient = new WebClient();
webClient.OpenReadCompleted += (sender, e) => this.OnReadOneAssembly(name, e);
try {
webClient.OpenReadAsync(new Uri(assemblyName, UriKind.Relative));
}
catch (Exception ex) {
this.OnDownloadFailed(ex);
}
}
private static string EnsureEndWdithDll(string assemblyName) {
if (!assemblyName.EndsWith(".dll", StringComparison.OrdinalIgnoreCase)) {
assemblyName += ".dll";
}
return assemblyName;
}
private void OnReadOneAssembly(string name, OpenReadCompletedEventArgs e) {
if (e.Error != null) {
this.OnDownloadFailed(e.Error);
this._isbusy = false;
return;
}
var assemblyStream = e.Result;
var references = GetReferenceAssemblyNames(assemblyStream);
AddNotLoadedReferenceAssemblyToLoadingSet(references);
assemblyStream.Seek(0, SeekOrigin.Begin);
LoadToAssemblyPart(assemblyStream, name);
if (this._loadingSet.Count > 0) {
var asm = this._loadingSet.First();
lock (LoadingSetLock) {
this._loadingSet.Remove(asm);
}
this.DownloadAssemblyAsyncCore(asm);
}
else {
this.DownloadCompleted();
}
}
private void DownloadCompleted() {
this._isbusy = false;
var assembly = this.GetAssembly(this._loadingAssemblyName);
this.OnDownloadAssemblyCommpleted(new DownloadAssemblyCommpletedEventArgs(assembly));
}
private static void LoadToAssemblyPart(Stream assemblyStream, string name) {
var part = new AssemblyPart {
Source = name
};
var assembly = part.Load(assemblyStream);
LoadedAssemblies.Add(name, assembly);
}
private void AddNotLoadedReferenceAssemblyToLoadingSet(IEnumerable references) {
var referencesNotLoaded = from reference in references
where !(IsAssemblyLoaded(reference))
select reference;
foreach (var @ref in referencesNotLoaded) {
lock (LoadingSetLock) {
if (!this._loadingSet.Contains(@ref)) {
this._loadingSet.Add(@ref);
}
}
}
}
private static bool IsAssemblyLoaded(string assemblyName) {
return SilverlightRuntimeAssemblyNames.Any(asmName => asmName.Equals(assemblyName, StringComparison.OrdinalIgnoreCase))
|| LoadedAssemblies.ContainsKey(assemblyName)
|| Deployment.Current.Parts.Any(ap => ap.Source.Equals(assemblyName, StringComparison.OrdinalIgnoreCase));
}
private static IEnumerable GetReferenceAssemblyNames(Stream assemblyStream) {
var asmDef = AssemblyDefinition.ReadAssembly(assemblyStream);
return asmDef.MainModule.AssemblyReferences.Select(anr => anr.Name + ".dll");
}
}
实现一个自定义的 ContentLoader
实现自定义的 ContentLoader 很容易, 只要实现 INavigationContentLoader 接口即可, 结合上面的AssemblyDownloader, 实现代码如下:
public class MyContentLoader : INavigationContentLoader {
private static AssemblyDownloader _assemblyDownloader;
public IAsyncResult BeginLoad(Uri targetUri, Uri currentUri, AsyncCallback userCallback, object asyncState) {
var typeFullName = targetUri.ToString();
if (string.IsNullOrEmpty(typeFullName)) {
return null;
}
var arr = typeFullName.Split(',');
var typeName = arr[0];
var assemblyName = arr[1];
if (!assemblyName.EndsWith(".dll", StringComparison.OrdinalIgnoreCase)) {
assemblyName += ".dll";
}
var asyncResult = new MyContentLoaderAsyncResult {
AsyncState = asyncState,
TypeName = typeName,
AssemblyName = assemblyName
};
BeginLoadCore(userCallback, asyncResult);
return asyncResult;
}
private static void BeginLoadCore(AsyncCallback userCallback, MyContentLoaderAsyncResult result) {
if (_assemblyDownloader == null) {
_assemblyDownloader = new AssemblyDownloader();
}
var handlers = new EventHandler[1];
handlers[0] = (sender, e) => {
_assemblyDownloader.DownloadAssemblyCommpleted -= handlers[0];
result.Assembly = e.Result;
userCallback(result);
};
_assemblyDownloader.DownloadAssemblyCommpleted += handlers[0];
_assemblyDownloader.DownloadAssemblyAsync(result.AssemblyName);
}
public void CancelLoad(IAsyncResult asyncResult) {
}
public LoadResult EndLoad(IAsyncResult asyncResult) {
var result = asyncResult as MyContentLoaderAsyncResult;
if (result == null) {
throw new InvalidOperationException(string.Format("Wrong kind of {0} passed in. The {0} passed in should only come from {1}.", "IAsyncResult", "MyContentLoader.BeginLoad"));
}
var loadResult = new LoadResult(result.GetResultInstance());
return loadResult;
}
public bool CanLoad(Uri targetUri, Uri currentUri) {
return targetUri.ToString().Split(',').Length == 2;
}
}
使用自定义的 ContentLoader
只要设置 Frame 控件的 ContentLoader 为自定义的 ContentLoader 即可, 比如可以这样使用:
当加载第一个程序集的时候, 会自动加载引用的程序集, 如下图:
当加载第二个程序集的时候, 重复引用的程序集将不会被加载, 如下图:
与 Silverlight 内置的导航机制比较
与内置的导航机制相比, 最大的优点是实现了真正的按需加载, 可以有效地减少主程序的大小, 减少用户的初次等待时间, 而且可以支持 OOB 模式。