突破 Silverlight 自身限制, 做更好的动态加载导航机制(二)

上一篇文章中, 动态导航的思路已经比较完善了, 现在来实现动态导航机制。

实现按需加载 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 即可, 比如可以这样使用:




   
      
   </SDK:FRAME.CONTENTLOADER />

当加载第一个程序集的时候, 会自动加载引用的程序集, 如下图:

image

当加载第二个程序集的时候, 重复引用的程序集将不会被加载, 如下图:

image

与 Silverlight 内置的导航机制比较

与内置的导航机制相比, 最大的优点是实现了真正的按需加载, 可以有效地减少主程序的大小, 减少用户的初次等待时间, 而且可以支持 OOB 模式。

posted @ 2011-08-11 13:32  张志敏  阅读(928)  评论(10编辑  收藏  举报