Управление доставкой динамического содержимого в Silverlight - Создание постоянного кэша пакетов

ОГЛАВЛЕНИЕ

Создание постоянного кэша пакетов

В статье прошлого месяца я использовал класс-оболочку загрузчика для скрытия части кода шаблона, необходимого для загрузки пакета и извлечения сборок и других ресурсов. Однако класс Downloader не является всего лишь вспомогательным классом. Идеологически он представляет нетривиальную часть логики, которую по ряду причин может потребоваться изолировать от остального кода.

Первая причина, которая приходит на ум, – это пригодность для тестирования. Предоставляя функции компонента загрузчика посредством интерфейса, вы получаете возможность быстро и эффективно имитировать загрузчик для целей тестирования. Кроме этого, интерфейс представляет собой инструмент, используемый для замены простого загрузчика более изощренным, которому, возможно, случается поддерживать кэширование пакетов. На рис. 4 показана архитектура проекта, к которой следует стремиться.

 

Рис. 4 Компонент загрузчика и остальная часть приложения

 

Рис. 5 Выделение интерфейса

В исходном коде, приведенном в прошлом месяце, класс Downloader представлял собой монолитную часть кода. Для повышения гибкости проекта давайте выделим из него интерфейс. Как показано на рис. 5, в Visual Studio имеется контекстное меню, которое, хотя и не такое многофункциональное, как аналогичное меню коммерческих инструментов рефакторинга, тем не менее, помогает при извлечении интерфейса из класса.

Теперь, когда базовая часть вашего приложения Silverlight общается с интерфейсом IDownloader, вся логика для кэширования пакета должна входить только в состав фактического класса загрузчика.

interface IDownloader
{
   void LoadPackage(string xapUrl, string asm, string cls);
   event EventHandler<Samples.XapEventArgs> XapDownloaded;
}

В частности, метод LoadPackage будет переписан для включения логики, которая проверяет наличие в изолированном хранилище указанного пакета XAP и в случае отсутствия загружает его из сети Интернет. На рис. 6 представлена существенная часть кода для класса Downloader. Сначала метод выполняет попытку получить поток для пакета XAP из внутреннего кэша. Если эта попытка не удается, метод продолжает работу и загружает пакет с сервера размещения пакета. (Именно это подробно обсуждалось в прошлом месяце).

Рис. 6. Поддержка кэша для компонента загрузчика

public void LoadPackage(string xap, string asm, string cls)
{
   // Cache data within the class
   Initialize(xap, asm, cls, PackageContent.ClassFromAssembly);

   // Have a look in the cache
   Stream xapStream = LookupCacheForPackage();
   if (xapStream == null)
     StartDownload();
   else
   {
     // Process and extract resources
     FindClassFromAssembly(xapStream);
   }
}

protected Stream LookupCacheForPackage()
{
   // Look up the XAP package for the assembly.
   // Assuming the XAP URL is a file name with no HTTP information
   string xapFile = m_data.XapName;

   return DownloadCache.Load(xapFile);
}

protected void StartDownload()
{
   Uri address = new Uri(m_data.XapName, UriKind.RelativeOrAbsolute);
   WebClient client = new WebClient();

   switch (m_data.ActionRequired)
   {
     case PackageContent.ClassFromAssembly:
       client.OpenReadCompleted += 
         new OpenReadCompletedEventHandler(OnCompleted);
       break;
     default:
       return;
   }
   client.OpenReadAsync(address);
}

private void OnCompleted(object sender, OpenReadCompletedEventArgs e)
{
   // Handler registered at the application level?
   if (XapDownloaded == null)
     return;

   if (e.Error != null)
     return;

   // Save to the cache
   DownloadCache.Add(m_data.XapName, e.Result);

   // Process and extract resources
   FindClassFromAssembly(e.Result);
}

private void FindClassFromAssembly(Stream content)
{
   // Load a particular assembly from XAP
   Assembly a = GetAssemblyFromPackage(m_data.AssemblyName, content);

   // Get an instance of the specified user control class
   object page = a.CreateInstance(m_data.ClassName);

   // Fire the event
   XapEventArgs args = new XapEventArgs();
   args.DownloadedContent = page as UserControl;
   XapDownloaded(this, args);
}

В Silverlight загрузка является асинхронным процессом, поэтому внутренний метод StartDownload создает событие «завершено», когда пакет полностью доступен для клиента. Обработчик события сначала сохраняет в локальном файле содержимое потока пакета XAP, а затем извлекает из него ресурсы. Отмечу, что в примере кода я извлекаю только сборки; в компоненте более общего назначения может потребоваться расширить возможности кэширования для работы с ресурсами других типов, например XAML для анимации, изображений или других вспомогательных файлов.

Метод LoadPackage в классе Downloader используется для загрузки пользовательских элементов управления Silverlight с целью вставки в текущее дерево XAML. Поскольку пакет XAP является многофайловым контейнером, необходимо указать, в какой сборке содержится пользовательский элемент управления и имя класса. В коде, приведенном на рис. 6, указанная сборка просто извлекается из пакета, загружается в текущий домен приложения, а затем создается экземпляр входящего в нее указанного класса.

А что если у сборки имеются зависимости? Как раз этот случай не предусмотрен в коде из рис. 6. В результате, если у сборки, передаваемой в качестве аргумента методу LoadPackage, имеется зависимость от другой сборки (даже из того же пакета XAP), то возникает исключение, как только поток выполнения добирается до класса из сборки, от которой зависит текущая. Дело в том, что все сборки пакета должны быть загружены в память. Для этого необходимо получить доступ к файлу манифеста, получить информацию о развернутых сборках и обработать их. На рис. 7 показано, как загрузить в память все сборки, на которые ссылается файл манифеста.

Рис. 7 Загрузка всех сборок из манифеста

private Assembly GetAssemblyFromPackage(
   string assemblyName, Stream xapStream)
{
   // Local variables
   StreamResourceInfo resPackage = null;
   StreamResourceInfo resAssembly = null;

   // Initialize
   Uri assemblyUri = new Uri(assemblyName, UriKind.Relative);
   resPackage = new StreamResourceInfo(xapStream, null);
   resAssembly = Application.GetResourceStream(
                resPackage, assemblyUri);

   // Extract the primary assembly and load into the AppDomain 
   AssemblyPart part = new AssemblyPart();
   Assembly a = part.Load(resAssembly.Stream);

   // Load other assemblies (dependencies) from manifest
   Uri manifestUri = new Uri("AppManifest.xaml", UriKind.Relative);
   Stream manifestStream = Application.GetResourceStream(
     resPackage, manifestUri).Stream; 
   string manifest = new StreamReader(manifestStream).ReadToEnd();

   // Parse the manifest to get the list of referenced assemblies
   List<AssemblyPart> parts = ManifestHelper.GetDeploymentParts(manifest);

   foreach (AssemblyPart ap in parts)  
   {
     // Skip over primary assembly (already processed) 
     if (!ap.Source.ToLower().Equals(assemblyName))
     {
       StreamResourceInfo sri = null;
       sri = Application.GetResourceStream(
         resPackage, new Uri(ap.Source, UriKind.Relative));
       ap.Load(sri.Stream);
     }
   }

   // Close stream and returns
   xapStream.Close();
   return a;
}

Файл манифеста является файлом XML, как показано ниже.

<Deployment EntryPointAssembly="More" EntryPointType="More.App" 
       RuntimeVersion="2.0.31005.0">
  <Deployment.Parts>
   <AssemblyPart x:Name="More" Source="More.dll" />
   <AssemblyPart x:Name="TestLib" Source="TestLib.dll" />
  </Deployment.Parts>
</Deployment> 

Для анализа этого файла можно использовать запрос LINQ-to-XML. В исходном коде содержится пример класса ManifestHelper, содержащего метод, который возвращает список объектов AssemblyPart (см. рис. 8). Стоит отметить, что в бета-версиях Silverlight 2 для анализа файла манифеста объекта Deployment можно было использовать класс XamlReader.

// This code throws in Silverlight 2 RTM
Deployment deploy = XamlReader.Load(manifest) as Deployment;

Рис. 8. Анализ манифеста с использованием запроса LINQ-to-XML

public class ManifestHelper
{
  public static List<AssemblyPart> GetDeploymentParts(string manifest)
  {
    XElement deploymentRoot = XDocument.Parse(manifest).Root;
    List<AssemblyPart> parts = 
      (from n in deploymentRoot.Descendants().Elements() 
      select new AssemblyPart() { 
         Source = n.Attribute("Source").Value }
      ).ToList();

      return parts;
  }
}

В окончательной версии объект Deployment был преобразован в singleton-класс, и, следовательно, его невозможно использовать для динамической загрузки сборок. Следовательно, XML манифеста необходимо анализировать вручную.