MSDN Magazine > Inicio > Todos los números > 2009 > MSDN Magazine Julio 2009 >  Silverlight: Aplicaciones web compuestas con Pr...
Silverlight
Aplicaciones web compuestas con Prism
Shawn Wildermuth
Descarga de código disponible en la Galería de códigos de MSDN
Examinar el código en línea

En este artículo se analizan los siguientes temas:
  • Silverlight 2
  • Creación de aplicaciones
  • Inserción de dependencia
En este artículo se usan las siguientes tecnologías:
Silverlight 2, Prism
Su primera experiencia con Silverlight probablemente fue algo pequeño: un reproductor de vídeo, una aplicación de gráficos simple o incluso un menú. Estos tipos de aplicaciones son simples y sencillas de diseñar, y segmentarlas en capas rigurosas con responsabilidades separadas es innecesario.
Sin embargo, los problemas surgen cuando intenta aplicar un estilo de acoplamiento ajustado a aplicaciones grandes. A medida que aumenta la cantidad de partes móviles, el estilo simple del desarrollo de aplicaciones se desmorona. Parte de la solución es la creación de capas (consulte mi artículo "Model-View-ViewModel In Silverlight 2 Apps"), pero una arquitectura de acoplamiento ajustado es sólo uno de los diversos problemas que es necesario resolver en proyectos de Silverlight grandes.
En este artículo, muestro cómo crear una aplicación mediante el uso de las técnicas de composición de la Biblioteca de aplicaciones compuestas del proyecto Prism. El ejemplo que desarrollo es un editor simple de datos de base de datos.

A medida que los requisitos cambian y que un proyecto madura, es útil que pueda cambiar las partes de la aplicación sin tener que realizar estos cambios en cascada en todo el sistema. El diseño modular de una aplicación le permite crear componentes de aplicación de forma separada (y de acoplamiento flexible) y cambiar todas las partes de la aplicación sin afectar el resto del código.
Además, es posible que no desee cargar todos los elementos de su aplicación al mismo tiempo. Imagine una aplicación de administración de clientes en que los usuarios inician sesión y después pueden administrar la canalización de sus clientes potenciales así como también pueden revisar el correo electrónico de cualquiera de sus clientes potenciales. Si un usuario revisa el correo electrónico varias veces al día, pero administra la canalización sólo una vez al día o cada dos días, ¿por qué cargar el código para administrar la canalización hasta que sea necesario? Sería estupendo si una aplicación admitiera la carga a petición de las partes de la aplicación, situación que se podría solucionar mediante el diseño modular de la aplicación.
El equipo Microsoft patterns & practices creó un proyecto denominado Prism (o CompositeWPF), cuyo propósito es solucionar problemas como éstos para las aplicaciones de Windows Presentation Foundation (WPF), y Prism se ha actualizado para ser compatible con Silverlight. El paquete Prism es una combinación de marco e instrucciones para crear aplicaciones. El marco, denominado Biblioteca de aplicaciones de componentes (CAL), permite lo siguiente:
  • Modularidad de aplicaciones: crear aplicaciones a partir de componentes con particiones.
  • Composición de interfaz de usuario: permite que componentes de acoplamiento flexible formen interfaces de usuario sin conocimiento discreto del resto de la aplicación.
  • Ubicación de servicios: servicios horizontales separados (por ejemplo, registro y autenticación) de servicios verticales (lógica empresarial) con el propósito de promover la creación de capas limpias de una aplicación.
La CAL se escribe teniendo en cuenta estos mismos principios de diseño y para los programadores de aplicaciones es un marco estilo buffet, es decir, tomar lo necesario y dejar el resto. En la Figura 1 se muestra el diseño básico de la CAL en relación con su propia aplicación.
Figura 1 Biblioteca de aplicaciones compuestas
La CAL admite estos servicios para ayudarle a crear su aplicación a partir de partes más pequeñas. Esto significa que la CAL controla qué elementos se cargan (y cuándo) y también proporciona la funcionalidad base. Usted puede decidir cuáles de estas capacidades le ayudan a realizar su trabajo y cuáles podrían poner obstáculos.
Mi ejemplo en este artículo usa la mayor cantidad posible de la CAL. Es una aplicación de shell que usa la CAL para cargar varios módulos en tiempo de ejecución, colocar las vistas en las regiones (como se muestra en la Figura 2) y ofrecer servicios de soporte técnico. Sin embargo, antes de que lleguemos a ese código, es necesario que comprenda algunos conceptos básicos acerca de la inserción de dependencia (también denominada Inversión de control o IoC). Varias de las características de la CAL dependen de la inserción de dependencia, de modo que comprender los aspectos básicos le ayudará a desarrollar la arquitectura de su proyecto de Silverlight con Prism.
Figura 2 Arquitectura de aplicaciones compuestas

Presentación de inserción de dependencia
En un desarrollo típico, un proyecto comienza con un punto de entrada (un ejecutable, una página default.aspx, etc.). Podría desarrollar su aplicación como un proyecto de gran magnitud, pero en la mayoría de los casos existe algún nivel de modularidad porque su aplicación carga varios ensamblados que forman parte del proyecto. El ensamblado principal sabe qué ensamblados necesita y crea referencias fuertes a esos elementos. En el momento de la compilación, el proyecto principal está en conocimiento de todos los ensamblados a los que se hace referencia y la interfaz de usuario consiste en controles estáticos. La aplicación está en control de qué código necesita y normalmente conoce todo el código que podría usar. Sin embargo, esto se convierte en un problema, porque el desarrollo se lleva a cabo dentro del proyecto de aplicación principal. A medida que crece una aplicación monolítica, el tiempo de creación y los cambios conflictivos pueden ralentizar el desarrollo.
El propósito de la inserción de dependencia es revertir esta situación mediante la entrega de instrucciones que configuren las dependencias en tiempo de ejecución. En lugar de que el proyecto controle estas dependencias, un código denominado contenedor es responsable de insertarlas.
Pero, ¿por qué esto es importante? Por un motivo, realizar un diseño modular de su código debería hacer más fácil probarlo. La posibilidad de intercambiar las dependencias de un proyecto permite realizar pruebas más limpias, de modo que sólo el código que será probado puede ser el origen de una prueba que presente error, en lugar de un código en algún lugar de la cadena anidada de dependencias. He aquí un ejemplo concreto. Suponga que tiene un componente que otros programadores usan para consultar direcciones para empresas determinadas. El componente depende de un componente de acceso a datos que recupere los datos por usted. Cuando prueba el componente, empieza a probarlo con respecto a la base de datos y algunas de las pruebas presentan error. Sin embargo, puesto que los esquemas y las compilaciones de la base de datos están en constante cambio, usted no sabe si las pruebas presentan error debido a su propio código o al código de acceso a los datos. Con la dependencia fuerte de su componente en el componente de acceso a los datos, las pruebas de la aplicación se tornan no confiables y ocasionan la renovación mientras usted encuentra los errores en su código o en el código de terceros.
El componente podría presentar este aspecto:
public class AddressComponent
{
  DataAccessComponent data = new DataAccessComponent();

  public AddressComponent()
  {
  }

  ...
}
En lugar de un componente conectado, podría aceptar una interfaz que represente el acceso a los datos, como se muestra a continuación:
public interface IDataAccess
{
  ...
}

public class AddressComponent
{
  IDataAccess data;

  public AddressComponent(IDataAccess da)
  {
    data = da;
  }

  ...
}
En general, una interfaz se usa de modo que se pueda crear una versión que permita ajustar el código. Este enfoque con frecuencia se denomina "ficticio". "Ficticio" se refiere a crear una implementación de la dependencia que en realidad no representa la versión real. Literalmente, se está creando una implementación ficticia.
Este enfoque es mejor porque la dependencia (IDataAccess) se puede insertar en el proyecto durante la construcción de éste. La implementación del componente IDataAccess dependerá de los requisitos (pruebas o real).
Así es cómo funciona esencialmente la inserción de dependencia, pero ¿cómo se controla la inserción? El trabajo del contenedor es controlar la creación de los tipos, lo que hace al permitir que el usuario registre tipos y después los resuelva. Por ejemplo, suponga que tiene una clase concreta que implementa la interfaz IDataAccess. Durante el inicio de la aplicación, puede indicar al contenedor que registre el tipo. En cualquier parte de la aplicación donde necesite el tipo, puede solicitar al contenedor que resuelva el tipo, como se muestra a continuación:
public void App_Startup()
{
  container.RegisterType<IDataAccess, DbDataAccess>();
}

...

public void GetData()
{
  IDataAccess acc = container.Resolve<IDataAccess>();
}
Según la situación (pruebas o producción), puede intercambiar la implementación de IDataAccess simplemente cambiando el registro. Además, el contenedor puede controlar la inserción de dependencia de construcción. Si un objeto que es necesario que sea creado por el constructor del contenedor toma una interfaz que el contenedor puede resolver, resuelve el tipo y lo transfiere al constructor, como se muestra en la Figura 3.
public class AddressComponent : IAddressComponent
{
  IDataAccess data;

  public AddressComponent(IDataAccess da)
  {
    data = da;
  }
}

...

public void App_Startup()
{
  container.RegisterType<IAddressComponent, AddressComponent>();
  container.RegisterType<IDataAccess, DbDataAccess>();
}

public void GetAddresses()
{
  // When we ask the container to create the AddressComponent,
  // it sees that a constructor takes a IDataAccess object
  // so it automatically resolves that dependency
  IAddressComponent addr = container.Resolve<IAddressComponent>();
}
Observe que el constructor de AddressComponent toma una implementación de IDataAccess. Cuando el constructor crea la clase AddressComponent durante la resolución, automáticamente crea la instancia de IDataAccess y la transfiere a AddressComponent.
Cuando se registran los tipos con el contenedor, también se indica al contenedor que aborde la duración del tipo de formas especiales. Por ejemplo, si se está trabajando con un componente de registro, podría tratarlo como singleton, de modo que cada parte de la aplicación que necesite registro no obtenga su propia copia (que es el comportamiento predeterminado). Para hacer esto, puede suministrar una implementación de la clase LifetimeManager abstracta. Se admiten varios administradores de duración. ContainerControlledLifetimeManager es un singleton por proceso y PerThreadLifetimeManager es un singleton por subproceso. Para ExternallyControlledLifetimeManager, el contenedor incluye a referencia débil al singleton. Si el objeto se publica externamente, el contenedor crea una nueva instancia; de otro modo, devuelve el objeto activo contenido en la referencia débil.
Use la clase LifetimeManager especificándola cuando registre un tipo. Aquí se muestra un ejemplo:
container.RegisterType<IAddressComponent, AddressComponent>(  new ContainerControlledLifetimeManager());
En la CAL, el contenedor IoC se basa en el marco Unity del grupo patterns & practices. Usaré el contenedor Unity en los siguientes ejemplos, pero también existen varias alternativas de código abierto al contenedor Unity IoC, tales como Ninject, Spring.NET, Castle y StructureMap. Si está familiarizado con un contenedor IoC que no sea Unity y ya lo está utilizando, puede suministrar su propio contenedor (aunque requiere un poco más de esfuerzo).

Comportamiento de inicio
En general, en una aplicación de Silverlight, el comportamiento de inicio es simplemente crear la clase de la página XAML principal y asignarla a la propiedad RootVisual de la aplicación. En una aplicación compuesta, este trabajo aún es necesario, pero en lugar de crear la clase de página XAML, una aplicación compuesta normalmente usa una clase de secuencia de inicio para controlar el comportamiento de inicio.
Para empezar, necesita una nueva clase que se derive de la clase UnityBootstrapper. Esta clase está en el ensamblado Microsoft.Practices.Composite.UnityExtensions. El programa previo contiene métodos reemplazables que controlan distintas partes del comportamiento de inicio. Con frecuencia, no reemplazará cada uno de los métodos de inicio, sólo los necesarios. Los dos métodos que debe reemplazar son CreateShell y GetModuleCatalog.
El método CreateShell es donde se crea la clase XAML principal. Esto normalmente se denomina shell porque es el contenedor visual para los componentes de la aplicación. Mi ejemplo incluye un programa previo que crea una nueva instancia de la clase Shell y la asigna a RootVisual antes de devolver esta nueva clase Shell, como se muestra a continuación:
public class Bootstrapper : UnityBootstrapper
{
  protected override DependencyObject CreateShell()
  {
    Shell theShell = new Shell();
    App.Current.RootVisual = theShell;
    return theShell;
  }

  protected override IModuleCatalog GetModuleCatalog()
  {
    ...
  }
}
El método GetModuleCatalog, que explicaré en la siguiente sección, devuelve la lista de módulos que se cargarán.
Ahora que tiene una clase de programa previo, puede usarla en el método de inicio de la aplicación de Silverlight. Normalmente, crea una nueva instancia de la clase de programa previo y llama a su método Run, como se muestra en la Figura 4.
public partial class App : Application
{

  public App()
  {
    this.Startup += this.Application_Startup;
    this.Exit += this.Application_Exit;
    this.UnhandledException += this.Application_UnhandledException;

    InitializeComponent();
  }

  private void Application_Startup(object sender, StartupEventArgs e)
  {
    Bootstrapper boot = new Bootstrapper();
    boot.Run();
  }

  ...
}
El programa previo también participa en el registro de tipos con el contenedor que necesitan las distintas partes de la aplicación. Para realizar esto, reemplace el método ConfigureContainer del programa previo. Esto le proporciona una posibilidad de registrar cualquier tipo que vaya a utilizar el resto de la aplicación. En la Figura 5 se muestra el código.
public class Bootstrapper : UnityBootstrapper
{
  protected override void ConfigureContainer()
  {
    Container.RegisterType<IShellProvider, Shell>();
    base.ConfigureContainer();
  }

  protected override DependencyObject CreateShell()
  {
    // Get the provider for the shell
    IShellProvider shellProvider = Container.Resolve<IShellProvider>();

    // Tell the provider to create the shell
    UIElement theShell = shellProvider.CreateShell();

    // Assign the shell to the root visual of our App
    App.Current.RootVisual = theShell;

    // Return the Shell
    return theShell;
  }

  protected override IModuleCatalog GetModuleCatalog()
  {
    ...
  }
}
Aquí, el código registra una interfaz para una clase que implementa la interfaz IShellProvider, que se crea en nuestro ejemplo y que no forma parte del marco CAL. De ese modo podemos usarla en nuestra implementación del método CreateShell. Podemos resolver la interfaz y después usarla para crear una instancia de shell, de modo que podamos asignarla a RootVisual y devolverla. Esta metodología podría parecer trabajo adicional, pero a medida que profundice en cómo la CAL le ayuda a crear la aplicación, quedará claro de qué forma este programa previo le está ayudando.

Modularidad
En un entorno .NET típico, el ensamblado es la unidad de trabajo principal. Esta designación permite que los programadores trabajen en su código de forma independiente de cada uno de los otros. En la CAL, cada una de estas unidades de trabajo conforma un módulo, y para que la CAL use un módulo, necesita una clase que pueda comunicar el comportamiento de inicio del módulo. Esta clase también necesita ser compatible con la interfaz IModule. La interfaz IModule necesita un solo método, denominado Initialize, que permite que el módulo se configure él mismo para su uso en el resto de la aplicación. El ejemplo incluye un módulo ServerLogger que contiene las capacidades de registro para nuestra aplicación. La clase ServerLoggingModule admite la interfaz IModule como se muestra a continuación:
public class ServerLoggerModule : IModule
{
  public void Initialize()
  {
    ...
  }
}
El problema reside en que no sabemos qué deseamos inicializar en nuestro módulo. Puesto que es un módulo ServerLogging, parece lógico que deseemos registrar un tipo que realice el registro por nosotros. Deseamos usar el contenedor para registrar el tipo, de modo que cualquier usuario que necesite la función de registro pueda simplemente usar nuestra implementación sin saber el tipo exacto de registro que realiza.
Obtenemos el contenedor al crear un constructor que toma la interfaz IUnityContainer. Si recuerda el análisis de inserción de dependencia, el contenedor usar la inserción del constructor para agregar tipos que conoce. IUnityContainer representa el contenedor en nuestra aplicación, de modo que si agregamos ese constructor, podemos guardarlo y usarlo en nuestra inicialización de la siguiente manera:
public class ServerLoggerModule : IModule
{
  IUnityContainer theContainer;

  public ServerLoggerModule(IUnityContainer container)
  {
    theContainer = container;
  }

  public void Initialize()
  {
    theContainer.RegisterType<ILoggerFacade, ServerBasedLogger>(
      new ContainerControlledLifetimeManager());
  }
}
Este módulo, una vez inicializado, es responsable de la implementación de registro para la aplicación. Pero, ¿cómo se carga este módulo?
Al usar la CAL para crear una aplicación, es necesario que cree ModuleCatalog que contenga todos los módulos para la aplicación. Este catálogo se crea al reemplazar la llamada GetModuleCatalog del programa previo. En Silverlight, puede rellenar este catálogo con código o con XAML.
Con código, crea una nueva instancia de la clase ModuleCatalog y la rellena con los módulos. Por ejemplo, observe lo siguiente:
protected override IModuleCatalog GetModuleCatalog()
{
  var logModule = new ModuleInfo()
  {
    ModuleName = "ServerLogger",
    ModuleType =       "ServerLogger.ServerLoggerModule, ServerLogger, Version = 1.0.0.0"
  };

  var catalog = new ModuleCatalog();
  catalog.AddModule(logModule);

  return catalog;
}
Aquí, simplemente agrego un solo módulo denominado ServerLogger, el tipo definido en la propiedad ModuleType de ModuleInfo. Además, puede especificar dependencias entre módulos. Puesto que algunos módulos podrían depender de otros módulos, el uso de dependencias permite que el catálogo sepa en qué orden mostrar las dependencias. Con la propiedad ModuleInfo.DependsOn, puede especificar qué módulos con nombre son necesarios para cargar otro módulo.
Puede cargar el catálogo directamente desde un archivo XAML, como se muestra a continuación:
protected override IModuleCatalog GetModuleCatalog()
{
  var catalog = ModuleCatalog.CreateFromXaml(new Uri("catalog.xaml", 
                                                     UriKind.Relative));
  return catalog;
}
El archivo XAML contiene el mismo tipo de información que se puede crear con código. El beneficio de usar XAML es que se puede cambiar sobre la marcha. (Imagine recuperar el archivo XAML desde un servidor o desde otra ubicación según qué usuario inició sesión.) Un ejemplo de un archivo catalog.xaml se muestra en la Figura 6.
<m:ModuleCatalog 
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:sys="clr-namespace:System;assembly=mscorlib"
  xmlns:m="clr-namespace:Microsoft.Practices.Composite.Modularity; 
    assembly=Microsoft.Practices.Composite">
  <m:ModuleInfoGroup InitializationMode="WhenAvailable">
    <m:ModuleInfo ModuleName="GameEditor.Client.Data"
        ModuleType="GameEditor.Client.Data.GameEditorDataModule, 
          GameEditor.Client.Data, Version=1.0.0.0"/>
    <m:ModuleInfo ModuleName="GameEditor.GameList"
      ModuleType="GameEditor.GameList.GameListModule,
      GameEditor.GameList, Version=1.0.0.0"
      InitializationMode="WhenAvailable">
      <m:ModuleInfo.DependsOn>
        <sys:String>GameEditor.Client.Data</sys:String>
      </m:ModuleInfo.DependsOn>
    </m:ModuleInfo>
  </m:ModuleInfoGroup>
</m:ModuleCatalog>
En este catálogo XAML, el grupo incluye dos módulos y el segundo módulo depende del primero. Podría usar un catálogo XAML específico basándose en las funciones o los permisos, como podría hacerlo con código.
Una vez que el programa previo carga el catálogo, intenta crear instancias de las clases de módulo y les permite inicializarse. En los siguientes ejemplos de código, la aplicación tiene que hacer referencia a los tipos (por lo tanto, ya están cargados en memoria) para que funcione este catálogo.
Aquí es donde esta función se vuelve indispensable para Silverlight. Aunque la unidad de trabajo es el ensamblado, puede especificar un archivo .xap que contiene el o los módulos. Para hacerlo, especifique un valor Ref en ModuleInfo. El valor Ref es una ruta al archivo .xap que contiene el módulo:
protected override IModuleCatalog GetModuleCatalog()
{
  var logModule = new ModuleInfo()
  {
    ModuleName = "ServerLogger",
    ModuleType =
      "ServerLogger.ServerLoggerModule, ServerLogger, Version= 1.0.0.0",
    Ref = "ServerLogger.xap"
  };

  var catalog = new ModuleCatalog();
  catalog.AddModule(logModule);

  return catalog;
}
Cuando se especifica un archivo .xap, el programa previo sabe que el ensamblado no está disponible y sale del servidor y recupera el archivo .xap de forma asincrónica. Una vez cargado el archivo .xap, Prism carga el ensamblado y crea el tipo de módulo y lo inicializa.
Para los archivos .xap que contienen varios módulos, puede crear ModuleGroup que contiene un conjunto de objetos ModuleInfo y establecer el valor Ref de ModuleGroup para cargar todos esos módulos desde un solo archivo .xap:
var modGroup = new ModuleInfoGroup();
modGroup.Ref = "MyMods.xap";
modGroup.Add(logModule);
modGroup.Add(dataModule);
modGroup.Add(viewModule);

var catalog = new ModuleCatalog();
catalog.AddGroup(modGroup);
Para las aplicaciones de Silverlight, esta es una forma de crear las aplicaciones desde varios archivos .xap, lo que permite controlar las versiones de las distintas secciones de la aplicación creada de forma separada.
Al crear los módulos de Silverlight que se almacenarán en un archivo .xap, crea una Aplicación de Silverlight (no una Biblioteca de Silverlight). Haga referencia a todos los proyectos de módulo que desee poner en el archivo .xap. Es necesario que quite los archivos app.xaml y page.xaml porque este archivo .xap no se cargará ni se ejecutará igual que un archivo .xap típico. El archivo .xap es solo un contenedor (podría ser un archivo .zip, pero no importa). Además, si hace referencia a proyectos a los que ya se ha hecho referencia en el proyecto principal, puede cambiar esas referencias a Copy Local=false en las propiedades porque no necesita los ensamblados en el archivo .xap (la aplicación principal ya los cargó, de modo que el catálogo no intentará cargarlos por segunda vez).
Sin embargo, cargar una aplicación de gran tamaño con varias llamadas por cable no parece ser útil para el rendimiento. Aquí es donde entra en juego la propiedad InitializationMode de ModuleInfo. InitializationMode admite dos modos: WhenAvailable, en que el archivo .xap se carga de forma asincrónica y después se inicializa (este es el comportamiento predeterminado) y OnDemand, en que el archivo .xap se carga cuando es solicitado explícitamente. Puesto que el catálogo de módulos no sabe cuáles son los tipos en los módulos hasta la inicialización, no se podrán resolver los tipos que se inicializan con OnDemand.
La compatibilidad a petición para los módulos y los grupos le permite cargar determinada funcionalidad en una aplicación grande, según sea necesario. El tiempo de inicio se acelera y otro código necesario se puede cargar cuando los usuarios interactúan con una aplicación. Esta es una excelente característica que se puede usar cuando tiene autorización para separar las partes de una aplicación. Los usuarios que necesitan sólo algunas partes de la aplicación no tienen que cargar código que nunca usarán.
Para cargar un módulo a petición, necesita acceso a una interfaz IModuleManager. Por lo general, esto se solicita en el constructor de la clase que necesita para cargar un módulo a petición. Use IModuleManager para cargar el módulo mediante la llamada a LoadModule, como se muestra en la Figura 7.
public class GameListViewModel : IGameListViewModel
{
  IModuleManager theModuleManager = null;

  public GameListViewModel(IModuleManager modMgr)
  {
    theModuleManager = modMgr;
  }

  void theModel_LoadGamesComplete(object sender, 
                                  LoadEntityCompleteEventArgs<Game> e)
  {
    ...

    // Since we now have games, let's load the detail pane
    theModuleManager.LoadModule("GameEditor.GameDetails");
  }
}
Los módulos son simplemente la unidad de diseño modular en las aplicaciones. En Silverlight, al tratar un módulo igual como trataría un proyecto de biblioteca, pero con el trabajo adicional de inicialización de módulo, puede desacoplar los módulos del proyecto principal.

Composición de interfaz de usuario
En una aplicación Explorer típica, el panel izquierdo muestra una lista o un árbol de información y el lado derecho contiene detalles acerca del elemento seleccionado en el panel izquierdo. En la CAL, estas áreas se denominan regiones.
La CAL admite la definición de regiones directamente en XAML mediante el uso de una propiedad adjunta en la clase RegionManager. Esta propiedad permite especificar regiones en shell e indicar qué vistas se deben hospedar en la región. Por ejemplo, nuestro shell tiene dos regiones, LookupRegion y DetailRegion, como se muestra a continuación:
<UserControl 
  ...
  xmlns:rg=
    "clr-namespace:Microsoft.Practices.Composite.Presentation.Regions;
    assembly=Microsoft.Practices.Composite.Presentation">
  ...
  <ScrollViewer rg:RegionManager.RegionName="LookupRegion" />
  <ScrollViewer rg:RegionManager.RegionName="DetailRegion" />
</UserControl>
RegionName se puede aplicar a ItemsControl y sus controles derivados (por ejemplo, ListBox); Selector y sus controles derivados (por ejemplo, TabControl); y ContentControl y sus controles derivados (por ejemplo, ScrollViewer).
Una vez que haya definido las regiones, puede dirigir los módulos para que carguen sus vistas en las regiones mediante el uso de la interfaz IRegionManager, como se muestra a continuación:
public class GameListModule : IModule
{
  IRegionManager regionManager = null;

  public GameListModule(IRegionManager mgr)
  {
    regionManager = mgr;
  }

  public void Initialize()
  {
    // Build the View
    var view = new GameListView();

    // Show it in the region
    regionManager.AddToRegion("LookupRegion", view);
  }
}
Esta función permite definir regiones en la aplicación en que pueden aparecer vistas y después hacer que los módulos definan cómo colocar las vistas en la región, lo que hace que shell ignore la existencia de las vistas.
El comportamiento de las regiones podría ser diferente según el tipo de control hospedado. El ejemplo usa ScrollViewer, de modo que sólo una vista se puede agregar a la región. En contraste, las regiones ItemControl permiten varias vistas. Cuando se agrega cada vista, aparece como un nuevo elemento en ItemsControl. Esa función hace más fácil crear una funcionalidad como un panel de información.
Si está usando un patrón MVVM para definir las vistas, puede combinar las regiones y los aspectos de ubicación de servicios del contenedor para dejar que las vistas y los modelos de las vistas ignoren la existencia de los otros y dejar que el módulo los combine en tiempo de ejecución. Por ejemplo, si cambio GameListModule, puedo registrar las vistas y los modelos de las vistas con el contenedor y después combinarlos antes de aplicar la vista a la región, como se muestra en la Figura 8.
public class GameListModule : IModule
{
  IRegionManager regionManager = null;
  IUnityContainer container = null;

  public GameListModule(IUnityContainer con, IRegionManager mgr)
  {
    regionManager = mgr;
    container = con;
  }

  public void Initialize()
  {
    RegisterServices();

    // Build the View
    var view = container.Resolve<IGameListView>();

    // Get an Implemenation of IViewModel
    var viewModel = container.Resolve<IGameListViewModel>();

    // Marry Them
    view.ApplyModel(viewModel);

    // Show it in the region
    regionManager.AddToRegion("LookupRegion", view);
  }

  void RegisterServices()
  {
    container.RegisterType<IGameListView, GameListView>();
    container.RegisterType<IGameListViewModel, GameListViewModel>();
  }
}
Este enfoque permite usar la composición de interfaz de usuario y, al mismo tiempo, mantener la estricta separación de MVVM.

Agregación de eventos
Después de que tenga varias vistas en las aplicaciones mediante composición de interfaz de usuario, enfrenta un problema común. Aunque haya creado vistas independientes para admitir mejores pruebas y desarrollo, con frecuencia existen puntos de conexión en que las vistas no se pueden aislar completamente. Están acopladas de forma lógica porque necesitan comunicarse, pero desea mantenerlas en lo posible con acoplamiento flexible sin importar el acoplamiento lógico.
Para permitir el acoplamiento flexible y la comunicación entre las vistas, la CAL admite un servicio denominado agregación de eventos. La agregación de eventos permite el acceso a diferentes partes del código a los publicadores y a los consumidores de eventos internacionales. Dicho acceso proporciona una forma directa de comunicarse sin estar con acoplamiento ajustado y se logra mediante el uso de la interfaz IEventAggregator de la CAL. IEventAggregator permite publicar y suscribirse a eventos en los diferentes módulos de la aplicación.
Antes de que pueda comunicarse, necesita una clase que se derive de EventBase. Normalmente, cree un evento simple que se derive de la clase CompositePresentationEvent<T>. Esta clase genérica le permite especificar la carga del evento que va a publicar. En este caso, GameListViewModel va a publicar un evento después de que se seleccione un juego, de modo que los otros controles que deseen cambiar su contexto cuando el usuario selecciona un juego puedan suscribirse a ese evento. Nuestra clase de evento tiene el siguiente aspecto:
public class GameSelectedEvent : CompositePresentationEvent<Game>
{
}
Una vez definido el evento, el agregador de eventos puede publicar el evento llamando a su método GetEvent. Esto recupera el evento singleton que se va a agregar. El primer usuario que llama a este método crea el singleton. Desde el evento, puede llamar al método Publish para crear el evento. Publicar el evento es como activar un evento. No es necesario que publique el evento hasta que necesite enviar información. Por ejemplo, cuando un juego se selecciona en GameList, nuestro ejemplo publica el juego seleccionado usando el nuevo evento:
// Fire Selection Changed with Global Event
theEventAggregator.GetEvent<GameSelectedEvent>().Publish(o as Game);
En otras partes de la aplicación creada, puede suscribirse al evento que se llamará después de publicado el evento. El método Subscribe del evento permite especificar el método que se llamará cuando se publique el evento, una opción que permite solicitar una semántica de subprocesos para llamar al evento (por ejemplo, comúnmente se usa el subproceso de la interfaz de usuario) y si hacer que el agregador mantenga una referencia a la información transmitida, de modo que no esté sujeto a la recolección de elementos no utilizados:
// Register for the aggregated event
aggregator.GetEvent<GameSelectedEvent>().Subscribe(SetGame, 
                                                  ThreadOption.UIThread, 
                                                  false);
Como suscriptor, también puede especificar filtros que se llaman sólo en situaciones específicas. Imagine un evento que devuelve el estado de una aplicación y un filtro que se llama sólo durante estados de datos determinados.
El agregador de eventos le permite tener comunicación entre los módulos que causan un acoplamiento ajustado. Si se suscribe a un evento que nunca se publicó o si publica un evento al que nunca se suscribió, su código nunca presentará error.

Comandos de delegación
En Silverlight (a diferencia de WPF), no existe una verdadera infraestructura de comandos. Esto fuerza el uso de código subyacente en las vistas para realizar tareas que se realizarían sin problemas directamente en XAML mediante el uso de la infraestructura de comandos. Hasta que Silverlight admita esta función, la CAL admitirá una clase que ayuda a resolver este problema: DelegateCommand.
Para comenzar con DelegateCommand, es necesario que defina DelegateCommand en ViewModel, de modo que pueda enlazar los datos a éste. En ViewModel, debe crear una nueva clase DelegateCommand. DelegateCommand espera el tipo de datos que le enviarán (con frecuencia Object si no se usan datos) y uno o dos métodos de devolución de llamada (o funciones lambda). El primero de estos métodos es la acción que se ejecutará cuando se active el comando. De forma opcional, puede especificar que se llame a una segunda devolución de llamada para probar si se puede activar el comando. La idea es deshabilitar los objetos en la interfaz de usuario (por ejemplo, los botones) cuando no sea válido activar el comando. Por ejemplo, GameDetailsViewModel contiene un comando que admite guardar datos:
// Create the DelegateCommand
SaveCommand = new DelegateCommand<object>(c => Save(), c => CanSave());
Cuando se ejecuta SaveCommand, llama al método Save en ViewModel. El CanSave se llama después para asegurarse de que el comando sea válido. Esto permite que DelegateCommand deshabilite la interfaz de usuario si es necesario. Cuando cambie el estado de la vista, puede llamar al método DelegateCommand.RaiseCanExecuteChanged para forzar una nueva inspección del método CanSave para habilitar o deshabilitar la interfaz de usuario si es necesario.
Para enlazar esto a XAML, use la propiedad Click.Command adjunta que se encuentra en el espacio de nombres Microsoft.Practices.Composite.Presentation.Commands. Después enlace el valor del comando, que será el comando que tendrá en ViewModel, de la siguiente manera:
<Button Content="Save"
        cmd:Click.Command="{Binding SaveCommand}"
        Style="{StaticResource ourButton}"
        Grid.Column="1" />
Ahora cuando se activa el evento Click, se ejecuta nuestro comando. Si lo desea, puede especificar que un parámetro de comando se envíe al comando, de modo que pueda reutilizarlo.
Es posible que se esté preguntando si el único comando que existe en la CAL es el evento Click para un botón (o cualquier otro selector). Sin embargo, las clases que puede usar para escribir sus propios comandos son bastante sencillas. El código muestra incluye un comando para SelectionChanged en ListBox/ComboBox. Este comando se denomina SelectorCommandBehavior y se deriva de la clase CommandBehaviorBase<T>. Observar la implementación de comportamientos de comando personalizada le proporcionará un punto de inicio para escribir sus propios comportamientos de comando.

Conclusión
Existen problemas bien definidos cuando se desarrollan aplicaciones de Silverlight grandes. Al crear sus aplicaciones con acoplamiento flexible y modularidad, obtiene beneficios y puede tener agilidad para responder al cambio. El proyecto Prism de Microsoft proporciona las herramientas e instrucciones para que esa agilidad surja de su proyecto. Aunque Prism no es un enfoque que se ajusta a todo, la modularidad de la CAL significa que usted puede usar lo que se ajuste a sus escenarios específicos y desechar el resto.

Shawn Wildemuth es un profesional destacado de Microsoft (C#) y fundador de Wildermuth Consulting Services. Es autor de varios libros y artículos. Además, Shawn actualmente dirige el Paseo por Silverlight, y enseña Silverlight 2 en todo el país. Puede ponerse en contacto con Shawn en shawn@wildermuthconsulting.com.

Page view tracker