Archive for the 'CAB' Category

How To: Pass parameters to Commands Handlers in Composite UI Application Block (CAB) / SCSF

This week there were some questions in the SCSF Codeplex Forum asking for a way to pass custom parameters to a method marked as a Command Handler. As you may know, there is no out-of-the-box way to do it because they are raised internally by the Command class and, since you usually bind a command to a control event via the AddInvoker method, you cannot manage when the command is executed.

Additionally, the signature of a command handler is fixed and should always be like the following:

[CommandHandler(CommandNames.MyCommand)]
public void OnMyCommandHandler(object sender, EventArgs args)
{
    // The sender parameter is an instance of the Command class.
    Command cmd = sender as Command;

    // The args parameter is empty.
    bool f = (args == EventArgs.Empty);
} 

The instance of the Command class received in the sender parameter could be useful, for example, if you want to change the status of the command to Enabled, Disabled or Unavailable.

cmd.Status = CommandStatus.Disabled;

But, what if you want to share the same command handler for different command invokers (like several menu items) and perform some operations based on which item was the invoker? You will somehow need to receive a parameter describing which was the item that raised the command.

Workaround

A possible workaround to receive parameters using a command could be by not adding your items as invokers of the command and instead execute the command programmatically. If you do this, you could use the State collection of the RootWorkItem to pass parameters to the command.

To implement this workaround follow these steps (the code use the ToolStripMenuItem control but could be used with other toolstrip controls):

  1. Add the parameter that you want to receive in the command handler to the Tag property of your menu item control:
    ToolStripMenuItem item = new ToolStripMenuItem() { Text = “My Menu Item”, Tag = “My Tag”};

  2. Add a handler for the Click event to your menu item:
    item.Click += new EventHandler(Item_Click);
  3. In the Item_Click method, add the Tag of your item to the State collection of the RootWorkItem and execute the command programmatically:
    WorkItem.RootWorkItem.State["LastTag"] = item.Tag;
    WorkItem.Commands[CommandNames.CommandWithParameters].Execute();
  4. In the command handler, retrieve the Tag from the State collection:
    string tag = WorkItem.RootWorkItem.State["LastTag"] as string;

The following code shows a ModuleController class that demonstrates how the approach above could be implemented:

public class ModuleController : WorkItemController
{
    public override void Run()
    {
        AddServices();
        ExtendMenu();
        AddViews();
    }

    private void ExtendMenu()
    {
        AddTaggedMenuItem(“Menu Item 1″, “Tag 1″, UIExtensionSiteNames.ModulesMenu);
        AddTaggedMenuItem(“Menu Item 2″, “Tag 2″, UIExtensionSiteNames.ModulesMenu);
        AddTaggedMenuItem(“Menu Item 3″, “Tag 3″, UIExtensionSiteNames.ModulesMenu);
        AddTaggedMenuItem(“Menu Item 4″, “Tag 4″, UIExtensionSiteNames.ModulesMenu);
    }

    private void AddTaggedMenuItem(string menuText, string menuTag, string extensionSiteName)
    {
        ToolStripMenuItem menuItem = new ToolStripMenuItem() { Text = menuText, ToolTipText = menuText, Tag = menuTag };

        menuItem.Click += new EventHandler(MenuItem_Click);

        WorkItem.UIExtensionSites[extensionSiteName].Add(menuItem);
    }

    private void MenuItem_Click(object sender, EventArgs e)
    {
        ToolStripMenuItem item = sender as ToolStripMenuItem;

        if (item != null)
        {
            WorkItem.RootWorkItem.State["LastTag"] = item.Tag;
            WorkItem.Commands[CommandNames.CommandWithParameters].Execute();
        }
    }

    [CommandHandler(CommandNames.CommandWithParameters)]
    public void OnCommandWithParameters(object sender, EventArgs args)
    {
        string tag = WorkItem.RootWorkItem.State["LastTag"] as string;

        if (tag != null)
        {
            MessageBox.Show(“The tag received in the command handler is: ” + tag, “Tags”);
        }
    }

    // …
}

In this way, the State collection is used as a container for your custom command parameters.

 

Enjoy.

How To: Use the Ngen tool to improve the performance in CAB / SCSF applications

There are some scenarios, like applications with a very big number of modules and views, that could present performance issues at startup due to the just-in-time (JIT) compilation process. In these cases it could be useful to precompile the assemblies using the Ngen tool.

The Native Image Generator (Ngen.exe) is a tool that improves the performance of managed applications by creating native images, which are files containing compiled processor-specific machine code. This allows the runtime to use native images instead of using the just-in-time (JIT) compiler to compile the original assembly code in runtime. One disadvantage of native images is that you cannot use the Assembly.LoadFrom method to load them.

If you are a user of CAB / SCSF, you may be aware of the Module Loader Service. This service allows you to load modules’ assemblies at run time when the application starts. The default implementation of this service uses the Assembly.LoadFrom method to load the assemblies enumerated by the Module Enumerator Service. So, if you try to “Ngen a CAB based application”, the native images of all your modules are not going to be used (the modules will be JIT compiled like always).

Workaround: Change the Module Loader Service class to use Assembly.Load method

This workaround enables the usage of the Assembly.Load method instead of the Assembly.LoadFrom method in the Module Loader Service class. The only limitation is that the modules’ assemblies must be physically located in the application’s working directory. To apply the workaround follow these steps:

  1. If you have an SCSF solution, open the DependentModuleLoaderService.cs file located in the Services folder of the Infrastructure.Library project. If you are using CAB without SCSF, open the ModuleLoaderService.cs file located in the Services folder of the CompositeUI project.
  2. Replace the implementation of the Load(WorkItem workItem, params Assembly[] assemblies) method for the following one:
    public void Load(WorkItem workItem, params Assembly[] assemblies)
    {
        Guard.ArgumentNotNull(workItem, “workItem”);
        Guard.ArgumentNotNull(assemblies, “assemblies”);
    
        List<IModuleInfo> modules = new List<IModuleInfo>();
    
        foreach (Assembly assembly in assemblies)
        {
            ModuleInfo mi = new ModuleInfo(assembly);
            // Use Assembly’s Full Name instead of the Assembly’s File Name.
            mi.SetAssemblyFile(assembly.FullName);
            modules.Add(mi);
        }
    
        InnerLoad(workItem, modules.ToArray());
    }

  3. Replace the implementation of the LoadAssembly(string assemblyFile) method for the following one:
    private Assembly LoadAssembly(string assemblyName)
    {
        Guard.ArgumentNotNullOrEmptyString(assemblyName, “assemblyName”);
        Assembly assembly = null;
    
        try
        {
            // Use Assembly.Load instead of Assembly.LoadFrom
            assembly = Assembly.Load(assemblyName);
        }
        catch (Exception ex)
        {
            throw new ModuleLoadException(assemblyName, ex.Message, ex);
        }
    
        if (traceSource != null)
            traceSource.TraceInformation(Properties.Resources.LogModuleAssemblyLoaded, assemblyName);
    
        return assembly;
    }

  4. Replace the implementation of the GuardLegalAssemblyFile(IModuleInfo modInfo) method for the following one:
    private void GuardLegalAssemblyFile(IModuleInfo modInfo)
    {
        Guard.ArgumentNotNull(modInfo, “modInfo”);
        Guard.ArgumentNotNull(modInfo.AssemblyFile, “modInfo.AssemblyFile”);
    }

  5. Open your ProfileCatalog.xml file.
  6. Change the value of the AssemblyFile attribute of each of your ModuleInfo tags to the assembly’s Full Name:
    <ModuleInfo AssemblyFile=Module1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null />
    <ModuleInfo AssemblyFile=Module2, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null />

Running Ngen on CAB based applications

When you run Ngen.exe on an assembly, it also generates native images for the all its dependencies (dependencies are determined from references in the assembly manifest). The only scenario in which you need to install a dependency separately is when the application loads it using reflection, for example by calling the Assembly.Load method.

This is the case for CAB / SCSF applications, so you must run Ngen for the your application executable (exe file) and all its modules. To do this follow these steps:

  1. Open a Visual Studio Command Prompt.
    Note: If you are running on Vista with the User Account Control (UAC) activated, you must open it with the option Run as administrator.
  2. Navigate to the directory where all your applications binaries are located and run the following command:
    ngen install YourApplication.exe

  3. Run the same command for all of your business and foundational modules.
    ngen install ModuleN.dll

Results

After applying the workaround on the BankTeller Quickstart and running Ngen to its binaries, the JIT compilation time was eliminated. To profile the application I used the ANTS Profiler.

  • Before generation the native images, the application presented some peaks in the JIT compilation time.
    Before generating the native images
  • After generating the native images, the JIT compilation time was eliminated.
    After generating the native images
Note: In order to perform profiling to native images, you must install them using the /Profile scenario as follows:

ngen install YourApplication.exe /Profile

 

Feedback is appreciated!

Mariano

Technorati Tags: ,,
kick it on DotNetKicks.com

Smart Client Software Factory (SCSF) GP source code with Installer that works on Visual Studio 2008 + SP1

A couple of months ago I announced in my blog that the P&P Sustained Engineering team had published an article in the SCSF Knowledge Base that describes the Known Issues and Fixes for the SCSF - April 2008 release running with Microsoft Visual Studio 2008 Service Pack 1.

The fixes for these issues included modifying the guidance package source code and register a custom one. So I decided to post the source code with the fixes including a setup project in order to create an installer for it.

Download it from here.

Disclaimer: This is not an official Microsoft release. Use it at your own risk.

Registering the Guidance Package using the installer

To register the guidance package using the custom installer performs the following steps:

  1. Open and build the GuidancePackage.sln solution to generate the installer.
  2. Navigate to the SmartClientFactorySetup\Debug folder.
  3. Close all instances of Visual Studio.
  4. Run the SmartClientFactoryPackageSetup.msi installer.
    Note: If you are running on Vista with the User Account Control (UAC) activated, you must run the installer with the option Run as administrator.

Manually registering the Guidance Package

To manually register the guidance package perform the following steps:

  1. Open the GuidancePackage.sln solution.
    Note: If you are running on Vista with the User Account Control (UAC) activated, you must open Visual Studio with the option Run as administrator.
  2. On the Tools menu of Visual Studio, click Guidance Package Manager.
  3. In the Guidance Package Manager dialog box, click Enable / Disable Packages.
  4. In the Enable and Disable Packages dialog box, select the Guidance Package Development check box.
  5. Click OK.
    Enabling the Guidance Package Development allows you to register a guidance package.
  6. Close all other instances of Visual Studio.
  7. Right-click the SmartClientFactoryPackage project, and then click Register Guidance Package.

For more information you can check the following article:

Using the Fixed Guidance Package

Once you have installed/registered the guidance package, you will be able to use the Smart Client Development for SP1 package (see the image below).

SmartClientDevelopmentForSP1

Enjoy.

Workaround for DeckWorkspace issue: The smartpart is not present in the workspace

There are several questions in the SCSF forums asking about this excepction: The smartpart is not present in the workspace, when using the DeckWorkspace.

Symptom

When you show several views in the same DeckWorkspace and then close the application or terminate the parent WorkItem, you get the following exception:

Exception

Cause

Every time a Smartpart is closed in a DeckWorkspace it is removed from its Controls collection. Then the ActivateTopmost method is called to show the previously shown Smartpart. That method takes the first element in the Controls collection of the DeckWorkspace and activates it (this.Controls[0]).

When the Shell is being closed (by closing the application), the DeckWorkspace is disposed and starts to dispose all of its child elements. Therefore, because the DeckWorkspace is being disposed, the Controls collection cannot be modified.

Consequently, when the Smarparts in the DeckWorkspace begin to get closed, they cannot be removed from the Controls collection (because it is being disposed). The ActivateTopmost method then receives a Smartpart that is no longer present in the DeckWorkspace and tries to activate it. This causes the “The SmartPart is not present in workspace” exception.

Workaround

Modify the Deckworkspace source code (you will need to have the SCSF source code installed) to avoid activating the previous Smartpart when the workspace is being disposed.

You can perform the following steps to get this done:

  1. Open the CompositeUI-CS.sln solution.
  2. Right-click in the DeckWorkspace.cs file located in the Workspaces folder in the CompositeUI.WinForms project and select the View Code option.
  3. Replace the OnClose method for the following one:
    protected virtual void OnClose(Control smartPart)
    {
        this.Controls.Remove(smartPart);
    
        smartPart.Disposed -= ControlDisposed;
    
        if (!Disposing)
        {
            ActivateTopmost();
        }
    }

  4. Build the solution.
  5. Copy the recompiled CAB assemblies to the Lib folder of your SCSF solution.
Technorati Tags: ,

How To: Get the active view across multiple workspaces in a SCSF application

Last week I saw a question in the SCSF forum about getting the active view in an application with several types of workspaces. In his post CAB: Solving The Active View Problem, Chris Holmes tackles the scenario within the context of a single workspace. I found a way to apply another solution that gets the Active View by monitoring all workspaces. I created the ActiveViewMonitorService service that is in charge of doing this.

I made a sample application that shows how this service works. Download from here.

Change the application’s active view by clicking in the TextBox of each view. Then click in the Active View button in the main menu bar to verify the active view’s name.

Implementation Details

The ActiveViewMonitorService service monitors the SmartPartActivated and Enter events for each workspace in the application. The SmartPartActivated allows to know when the user is changing the active view in the context of the same workspace and the Enter event allows to know when the user is changing to a view in another workspace. The following is the service implementation:

public interface IActiveViewMonitorService
{
    void AddWorkspaceToMonitor(IWorkspace workspace);
    object ActiveView { get; }
}

public class ActiveViewMonitorService : IActiveViewMonitorService
{
    #region IActiveViewMonitorService Members

    public void AddWorkspaceToMonitor(IWorkspace workspace)
    {
        workspace.SmartPartActivated += new EventHandler<WorkspaceEventArgs>(OnSmartPartActivated);
        ActiveView = workspace.ActiveSmartPart;

        Control wk = workspace as Control;
        if (wk != null)
        {
            wk.Enter += new EventHandler(OnEnter);
        }
    }

    public object ActiveView { get; private set; }

    #endregion

    private void OnSmartPartActivated(object sender, WorkspaceEventArgs e)
    {
        ActiveView = e.SmartPart;
    }

    private void OnEnter(object sender, EventArgs e)
    {
        IWorkspace wk = sender as IWorkspace;

        if (wk != null)
        {
            ActiveView = wk.ActiveSmartPart;
        }
    }
}

Steps

  1. Register it as a service in the RootWorkItem.
  2. Add the workspaces you want to monitor using the AddWorkspaceToMonitor method (you can do this by adding an event handler to the Initialized event of the RootWorkItem in the ShellApplication class).
    // ShellApplication class
    protected override void AfterShellCreated()
    {
        RootWorkItem.Initialized += new EventHandler(RootWorkItem_Initialized);
    }
    
    private void RootWorkItem_Initialized(object sender, EventArgs args)
    {
        IActiveViewMonitorService activeViewMonitorService = RootWorkItem.Services.Get<IActiveViewMonitorService>();
    
        foreach (KeyValuePair<string, IWorkspace> key in RootWorkItem.Workspaces)
        {
            activeViewMonitorService.AddWorkspaceToMonitor(key.Value);
        }
    }

  3. Get the active view using the ActiveView property of the service.
Note: The ActiveViewMonitorService service accepts Workspaces that inherit from the Control class.

Enjoy

Technorati Tags: ,

Memory Leak Fix for DeckWorkspace in CAB Extensions for WPF

The P&P Sustained Engineering Team has found a fix for the DeckWorkspace memory leak issue of the Composite UI Application Block Extensions for WPF.

Content

Symptom

When you use a DeckWorkspace workspace to show a WPF View, the view will remain in memory even if you call its Dispose method from the presenter. This causes an increase in the memory usage.

Cause of issue

Summary: Some objects (like instances of LayoutEventArgs class) still reference to the View after it is disposed not allowing it to be swept from memory.

Full description: Using the ANTS profiler tool, we found out that there are some references to the WPF View that do not allow the View to be released from memory after it is shown in a DeckWorkspace workspace. The following figure shows the View2 still in memory even after it had been closed in our application. Should have been swept but the references shown in the bottom left do not allow that.

ANTS profiler

If you look at the Control class (DeckWorkspace inherits this class) using Reflector, you can see that there is a cachedLayoutEventArgs field that sometimes holds a reference to LayoutEventArgs, which ultimately holds a reference the view in this scenario. Once you call PerformLayout on the control, this reference gets released.

Fix

Summary: Call the PerformLayout method when a View is being closed in the DeckWorkspace, which releases all remaining references to the View.

Step by step: You will need to have installed the SCSF source code and perform the following steps to fix the issue:

  1. Open the CompositeUI-WPFExtensions.sln solution.
  2. Right-click in the DeckWorkspace.cs file located in the Workspaces folder on the CompositeUI.WPF project and then select View Code.
  3. Locate the Close method of the DeckWorkspace class and add the following bold line:
    public void Close(object smartPart)
    {
        composer.Close(smartPart);
    
        // Add this line.
        this.PerformLayout();
    }

  4. Locate the OnClose method of the DeckWorkspace class and add the following bold line:
    protected virtual void OnClose(Control smartPart)
    {
        this.Controls.Remove(smartPart);
    
        smartPart.Disposed -= ControlDisposed;
    
        ActivateTopmost();
    
        // Add this line
        this.PerformLayout();
    }

Download

Enjoy.

Technorati Tags: ,

Introducción a Composite UI Application Block (CAB) VI - Commands

Introducción

Las aplicaciones de Windows a menudo presentan situaciones donde más de un control realiza la misma acción. Por ejemplo un ítem de un menú y un botón de la barra de herramientas pueden ambos ejecutar el método SaveFile para guardar un archivo. Composite UI Application Block resuelve estas situaciones utilizando el concepto de comandos (Commands). Los comandos permiten, entre otras cosas, implementar un solo handler y asociarlo con más de un UIElement y/o asociar un UIElement con más de un handler. La Figura 1 muestra un escenario posible.

UsosDeLosComandos

Figura 1
Dos botones diferentes que ejecutan la misma acción para el evento Click.

Los comandos de CAB son una implementación del patrón de diseño Command. Este patrón permite desacoplar el código que ejecuta el comando, del código que lo invoca. Para entender el diseño e implementacón de este patrón en CAB, hay que tener en cuenta a las siguientes clases:

Clase Características
Command Es la clase que representa a un comando. Utiliza CommandAdapters para poder manejar distintos tipos de invocadores de comandos. La clase WorkItem contiene la colección Commands donde se van guardando todos los comandos creados en la aplicación. Cada comando está identificado en esta colección con un string como ID.
CommandAdapter Es una clase abstracta que define la lógica e interfaz necesaria para que un objeto pueda ser registrado como un invocador de un comando.
EventCommandAdapter Clase genérica que extiende a CommandAdapter. Esta implementación dispara un evento cuando el comando es lanzado.
CommandAdapterMapService Servicio que implementa la interfaz ICommandAdapterMapService usado para realizar el mapeo entre un CommandAdapter y un determinado tipo de invoker.

Uso de los Comandos

Como se pudo ver en previos post, cuando un módulo se carga puede agregar ítems al Shell utilizando la colección UIExtensionSites del WorkItem. La clase WorkItem además posee la colección Commands donde se almacenan todos los comandos identificándolos con un string de ID. El siguiente código muestra cómo agregar un botón a la barra de herramientas del Shell y registrarlo como invocador del comando cuyo ID es “SaveCommand“.

[C#]

private void ExtendToolStrip()

{

ToolStripButton saveButton = new ToolStripButton(”Save”);

RootWorkItem.UIExtensionSites["MainToolStrip"].Add(saveButton);

RootWorkItem.Commands["SaveCommand"].AddInvoker(saveButton, “Click”);

}

[Visual Basic]

Private Sub ExtendToolStrip()

Dim saveButton As ToolStripButton = New ToolStripButton(”Save”)

RootWorkItem.UIExtensionSites(”MainToolStrip”).Add(saveButton)

RootWorkItem.Commands(”SaveCommand”).Add(saveButton, “Click”)

End Sub

La registración de invocadores de comandos se logra mediante el método AddInvoker de la clase Command. A continuación se muestra la declaración del método de esta clase.

[C#]

public virtual void AddInvoker(object invoker, string eventName);

[Visual Basic]

Public Overridable Sub AddInvoker(ByVal invoker As Object, ByVal eventName As String)

El parámetro invoker es el UIElement que será invocador del comando y el parámetro eventName es el nombre del evento del invoker que disparará el comando (por ejemplo el evento “Click”). El objeto invoker debe contener entre sus atributos al evento especificado en eventName pues sino se lanzará una excepción.

Nota:
CAB provee soporte de CommandAdapters solo para las clases ToolStripItem y Control y sus descendientes (ver las clases ToolStripItemCommandAdapter y ControlCommandAdapter). Para poder registrar otros tipos de objetos como invocadores de comandos, será necesario implementar los correspondientes CommandAdapter y registrarlos en el servicio CommandAdapterMapService.

Hasta ahora vimos cómo se registran los comandos. Pero ¿cómo se identifica a los métodos que queremos que se ejecuten cuando un comando es lanzado?

Para identificar a los métodos que se ejecutarán cuando se dispare el comando se los debe marcar con el atributo [CommandHandler] especificando el ID del comando con el que cual se lo quiere asociar. El siguiente código registra al método SaveButton_Click como handler del comando “SaveCommand”.

[C#]

[CommandHandler("SaveCommand")]

public void SaveButton_Click(object sender, EventArgs e)

{

// TODO: Save application’s state.

}

[Visual Basic]

<CommandHandler(”SaveCommand”)> _

Public Sub SaveButton_Click(ByVal sender As Object, ByVal e As EventArgs)

‘ TODO: Save application’s state.

End Sub

Como se puede observar en el código anterior, el identificador de visibilidad del método debe ser public.

Para lograr el correcto funcionamiento de los comandos, la clase en donde se agregan los objetos invocadores de comandos debe ser construida a partir de un WorkItem descendiente (o el mismo) del que construyó la clase que contiene a los métodos marcados con el atributo [CommandHandler]. De lo contrario no se encontrarán los comandos requeridos en la colección Commands.

Una última característica a tener en cuenta es que no se puede decidir qué parámetros pasar a los handlers de los comandos debido a que no se tiene control desde nuestro código de cómo y cuando estos se lanzarán. Si lo que se necesita es pasar parámetros al handler entonces es recomendable usar la funcionalidad Event Broker en lugar de comandos (este sistema se verá en más detalle en próximos post).

¿Cómo funciona el atributo [CommandHandler]?

Para responder esta pregunta primero debemos introducirnos a ObjectBuilder.

ObjectBuilder es una fábrica abstracta de objetos. CAB utiliza este framework como base para construir a todos los objetos e implementar el patrón Dependency Injection. Es por eso que para crear objetos en una aplicación CAB no se debe utilizar new, sino que se debe utilizar, por ejemplo, el método genérico AddNew.

ObjectBuilder posee un un pipeline por donde pasan los objetos durante su etapa de construcción. Este pipeline está dividido en distintas etapas donde se van ejecutando distintas Strategies (clases que realizan una parte del proceso de construcción) y Policies (objetos usados por las Strategies para customizar sus acciones y para pasar a la siguiente en la cadena).

ObjectBuilder define las siguientes etapas para la construcción de un objeto, las cuales suceden en el siguiente orden:

  • PreCreation
  • Creation
  • Initialization
  • PostInitialization

CAB agrega a la cadena de Strategies la clase CommandStrategy la cual se ejecuta durante la etapa de Initialization. Esta strategy busca por reflection dentro del objeto que se está construyendo a todos los métodos marcados con el atributo [CommandHandler] y los registra en la colección Commands del WorkItem (con el ID correspondiente).

Para más información acerca de ObjectBuilder recomiendo la lectura de los siguientes artículos (en inglés):

Alcance de los Comandos

Hay casos donde la disponibilidad de un comando depende del estado de la aplicación. Por ejemplo el botón Copiar de la barra de herramientas en un editor de textos estará deshabilitado si no hay ningún texto seleccionado. En una aplicación normal esto se logra modificando acordemente la propiedad Visible en cada ítem de la barra de herramientas. CAB provee una forma de realizar esta tarea indirectamente usando los comandos. Esto permite que si hay más de un UIElement que invocan al mismo comando, entonces todos se actualizarán acordemente.

Cada comando presenta la propiedad Status a la cual se le puede asignar cualquiera de los siguientes valores:

  • Enable: El CommandAdapter deberá mostrar el UIElement.
  • Disable: El CommandAdapter deberá deshabilitar el UIElements (o una operación equivalente disponible). En general el elemento estará visible pero no disponible para su uso.
  • Unavailable: El CommandAdapter deberá ocultar el UIElements (o una operación equivalente disponible). En general el elemento no estará visible y obviamente tampoco disponible para su uso.

Cuando el valor de esta propiedad cambia, cada CommandAdapter relacionado con el su UIElement aplica los cambios correspondientes de la manera apropiada.

El siguiente código muestra cómo se usa esta propiedad en el comando cuyo ID es “SaveCommand”:

[C#]

// CommandStatus es un tipo enumerado con los posibles estados

// de un comando.

// Habilita al comando.

WorkItem.Commands["SaveCommand"].Status = CommandStatus.Enabled;

// Deshabilita el comando.

WorkItem.Commands["SaveCommand"].Status = CommandStatus.Disabled;

// Oculta y deshabilita al comando.

WorkItem.Commands["SaveCommand"].Status = CommandStatus.Unavailable;

[Visual Basic]

‘ CommandStatus es un tipo enumerado con los posibles estados

de un comando.

‘ Habilita al comando.

WorkItem.Commands(”SaveCommand”).Status = CommandStatus.Enabled

‘ Deshabilita el comando.

WorkItem.Commands(”SaveCommand”).Status = CommandStatus.Disabled

‘ Oculta y deshabilita al comando.

WorkItem.Commands(”SaveCommand”).Status = CommandStatus.Unavailable

La Figura 2 muestra una sucesión de screenshots de una aplicación donde se han aplicado los tres estados posibles al comando Save.

500x155.aspx

Figura 2
La secuencia corresponde primero al comando habilitado, luego al comando deshabilitado y por último al comando oculto.

En el próximo post explicaré en detalle cómo se implementan los servicios en CAB.

Introducción a Composite UI Application Block (CAB) V - UI Extension Sites

Descripción

La mayoría de las aplicaciones basadas en Windows tienen elementos comunes en la interfaz de usuario, como son los menús, las barras de estado, las barras de herramientas, etc. que poseen la funcionalidad necesaria para realizar gran variedad de acciones que pueden afectar a distintos sectores de la aplicación. Esto generalmente provoca acoplamiento entre los módulos que necesiten usar o modificar estos elementos. Composite UI Application Block permite que los UIElements puedan ser accedidos por todos los módulos de una manera desacoplada.

Antes de continuar voy a introducir el concepto de UIElement. Los UIElements son controles que están hosteados en el Shell los cuales presentan propiedades y funcionalidades requeridas por la mayoría de los módulos de la aplicación.

Durante el proceso de inicialización de la aplicación, cada módulo que se carga tiene la posibilidad de agregar elementos propios a los UIElements registrados. La Figura 1 muestra el proceso de la carga de un ítem a la barra de menú del shell durante la carga de un módulo.

UIExtensionSites

Figura 1

Proceso de carga de un item a la barra de menú durante la carga de un módulo.

El siguiente diagrama de clases muestra las relaciones entre las clases involucradas con el proceso mostrado en la Figura 1 y sus principales métodos.

Diag clases - UIElement

Figura 2

Clases involucradas con los UIElement y sus adapters.

Para poder entender mejor el diagrama anterior, la siguiente tabla muestra una breve descripción de las clases que lo componen.

Clase

Características

IUIElementAdapter Interfaz implementada por los objetos que van a proveer soporte para un determinado UIElement.
UIElementAdapter Es una clase genérica que implementa la interfaz IUIElementAdapter. Se usa como soporte para facilitar la creación de adapters, ya que trae implementación genérica para los métodos de la interfaz .
UIExtensionSite Esta clase permite que los UIElements sean extendidos por los todos los módulos. Es una especie de “gancho” donde los módulos pueden colgar elementos a un determinado elemento de la interfaz de usuario.
UIExtensionSiteCollection Representa una colección compartida por todos los módulos donde se guardan los IUIElementAdapter y UIExtensionSite para cada UIElement. La colección los identifica con un string como ID.
IUIElementAdapterFactory Provee la lógica necesaria para crear una instancia apropiada de un IUIElementAdapter para un determinado UIElement.
UIElementAdapterFactoryCatalog Clase que implementa la Interfaz IUIElementAdapterFactoryCatalog. La clase contiene un catálogo donde se guardan todas las factories de los adapters para cada UIElement soportado.

Tabla 1

Interfaces y Clases responsables de registrar y utilizar los UIElement.

La clase WorkItem posee la propiedad UIExtensionSites que expone una colección de tipo UIExtensionSitesCollection. El usuario puede registrar todos sus UIElements dentro de esa colección mediante el método RegisterSite. Este método posee dos sobrecargas. La primera versión del método es la siguiente:

[C#]

public void RegisterSite(string siteName, IUIElementAdapter adapter)

[Visual Basic]

Public Sub RegisterSite(ByVal siteName As String, ByVal adapter As IUIElementAdapter)

Este método crea una instancia de la clase UIExtensionSite para el adapter pasado y lo agrega a la colección asociándolo con el ID recibido en el string siteName.

La otra versión del método RegisterSite es:

[C#]

public void RegisterSite(string siteName, object uiElement)

[Visual Basic]

Public Sub RegisterSite(ByVal siteName As String, ByVal uiElement As Object)

Este método se encarga de crear un nuevo IUIElementAdapter para el UIElement pasado utilizando una IUIElementAdapterFactory la cual tuvo que ser registrada anteriormente para ese tipo de UIElement en particular. Una vez echo esto invoca a la version anterior del método con el adapter recién creado.

Luego invocando al siguiente método de la colección UIExtensionSites del WorkItem:

[C#]

public UIExtensionSite this[string siteName]

[Visual Basic]

Default Public ReadOnly Property Item(ByVal siteName As String) As UIExtensionSite

Se accede, mediante el siteName, al UIExtensionSite encargado de manejar al IUIElementAdapter creado para el UIElement que se agregó.

Es muy importante notar que la colección UIExtensionSites no permite acceder directamente a los UIElements que se registraron sino que sólo se obtiene una referencia a un objeto de tipo UIExtensionSite el cual es el responsable de utilizar al adapter creado para manejar el objeto.

Registrar un UIElement

Generalmente los UIElements se registran en el método AfterShellCreated de la clase principal de la aplicación (la que hereda de FormShellApplication) como se muestra en el siguiente código.

[C#]

// MainMenuStrip expone una instancia de un MenuStrip.

// MainStatusStrip expone una instancia de un StatusStrip.

// MainToolbarStrip expone una instancia de un ToolStrip.

protected override void AfterShellCreated()

{

base.AfterShellCreated();

RootWorkItem.UIExtensionSites.RegisterSite(”MainMenu”, this.Shell.MainMenuStrip)

RootWorkItem.UIExtensionSites.RegisterSite(“MainStatus”, this.Shell.MainStatusStrip)

RootWorkItem.UIExtensionSites.RegisterSite(“MainToolbar”, this.Shell.MainToolbarStrip)

}

[Visual Basic]

‘ MainMenuStrip expone una instancia de un MenuStrip.

‘ MainStatusStrip expone una instancia de un StatusStrip.

‘ MainToolbarStrip expone una instancia de un ToolStrip.

Protected Overrides Sub AfterShellCreated()

MyBase.AfterShellCreated()

RootWorkItem.UIExtensionSites.RegisterSite(“MainMenu”, Me.Shell.MainMenuStrip)

RootWorkItem.UIExtensionSites.RegisterSite(“MainStatus”, Me.Shell.MainStatusStrip)

RootWorkItem.UIExtensionSites.RegisterSite(“MainToolbar”, Me.Shell.MainToolbarStrip)

End Sub

Es buena práctica utilizar constantes para guardar los ID de los UIElements. Estas constantes se deberían ubicar en clases accesibles por todos los módulos. Para ello se puede crear un proyecto donde se albergue todo lo que se considere de uso común entre los módulos. Esto permite un mayor desacople.

Nota: Smart Client Software Factory provee una estructura generada por la Guiance Package la cual establece un proyecto (Infrastructure.Interface) donde se definen todas las constantes y clases comunes que son usadas por todos los módulos. Además registra por defecto al menú principal, la barra de estado y la barra de herramientas dentro de la colección UIExtensionSites.

Utilizar los UIExtensionSite para agregar ítems al Shell

En general los ítems particulares de cada módulo se cargan en el método OnRunStarted del WorkItem del módulo como se muestra en el siguiente código de ejemplo.

[C#]

// En este ejemplo se agregará un menú con dos botones a la barra de menu

// del shell.

protected override void OnRunStarted()

{

ExtendMenu();

AddViews();

base.OnRunStarted();

}

private void ExtendMenu()

{

ToolStripMenuItem menu = new ToolStripMenuItem(”Menu del Modulo”);

ToolStripMenuItem button1 = new ToolStripMenuItem(”Button1″);

ToolStripMenuItem button2 = new ToolStripMenuItem(”Button2″);

menu.DropDownItems.Add(button1);

menu.DropDownItems.Add(button2);

UIExtensionSites["MainMenu"].Add(menu);

}

[Visual Basic]

‘ En este ejemplo se agregará un menú con dos botones a la barra de menu

‘ del shell.

Public Overrides Sub OnRunStarted()

ExtendMenu()

AddViews()

MyBase.OnRunStarted()

End Sub

Private Sub ExtendMenu()

Dim menu As ToolStripMenuItem

Dim button1 As ToolStripMenuItem

Dim button2 As ToolStripMenuItem

menu = New ToolStripMenuItem(”Menu del Modulo”)

button1 = New ToolStripMenuItem(”Button1″)

button2 = New ToolStripMenuItem(”Button2″)

menu.DropDownItems.Add(button1)

menu.DropDownItems.Add(button2)

UIExtensionSites(”MainMenu”).Add(menu)

End Sub

Si se remueve el nombre del módulo en el archivo ProfileCatalog.xml o el ModuleLoaderService verifica que el usuario actual no puede tener acceso a ese módulo, entonces no se cargará el módulo anterior y por lo tanto no aparecerán los ítems en la barra de menús. La aplicación podrá continuar normalmente.

Crear Adapters para UIElements

Cada tipo de UIElement requiere su propio adapter para que sea correctamente manejado. CAB provee la interfaz IUElementAdapter que define los miembros que debe soportar un adapter y además la template class UIElementAdapter la cual provee implementación genérica de los métodos de la interfaz.

CAB trae soporte para MenuStrips y ToolBarStrips (las clases ToolStrip, ToolStripItem, ToolStripItemCollection, MenuStrip y todas las clases derivadas de estas). Utiliza las clases ToolStripItemCollectionUIAdapter, ToolStripItemOwnerCollectionUIAdapter como adapters y la clase ToolStripUIAdapterFactory como factory para crear los adapters. Sin embargo CAB permite incluir soporte para clases customizadas si se construyen los adapters correspondientes. Para que tenga sentido crear un adapter para un determinado UIElement, este deberá poseer una colección de objetos que permita agregar y quitar elementos.

El siguiente ejemplo muestra como se implementa un adapter para la clase ListBox del assembly System.Windows.Forms.

[C#]

using Microsoft.Practices.CompositeUI.UIElements;

using System.Windows.Forms;

using Microsoft.Practices.CompositeUI.Utility;

public class ListBoxAdapter : IUIElementAdapter

{

private ListBox _listBox;

public ListBoxAdapter(ListBox listBox)

{

Guard.ArgumentNotNull(listBox, “listBox”);

_listBox = listBox;

}

#region IUIElementAdapter Members

public object Add(object uiElement)

{

if (_listBox != null)

{

if (!_listBox.Items.Contains(uiElement))

{

_listBox.Items.Add(uiElement);

}

}

return uiElement;

}

public void Remove(object uiElement)

{

if (_listBox != null)

{

if (_listBox.Items.Contains(uiElement))

{

_listBox.Items.Remove(uiElement);

}

}

}

#endregion

}

[Visual Basic]

Imports Microsoft.Practices.CompositeUI.UIElements

Imports System.Windows.Forms

Imports Microsoft.Practices.CompositeUI.Utility

Public Class ListBoxAdapter

Implements IUIElementAdapter

Private _listBox As ListBox = Nothing

Public Sub New(ByVal listBox As ListBox)

Guard.ArgumentNotNull(listBox, “listBox”)

_listBox = listBox

End Sub

Public Function Add(ByVal uiElement As Object) As Object Implements IUIElementAdapter.Add

If Not _listBox Is Nothing Then

If Not _listBox.Items.Contains(uiElement) Then

_listBox.Items.Add(uiElement)

End If

End If

Return uiElement

End Function

Public Sub Remove(ByVal uiElement As Object) Implements IUIElementAdapter.Remove

If Not _listBox Is Nothing Then

If _listBox.Items.Contains(uiElement) Then

_listBox.Items.Remove(uiElement)

End If

End If

End Sub

End Class

Como se puede ver la clase ListBoxAdapter implementa la interfaz IUIElementAdapter.

Con la clase implementada anteriormente ya se pueden registrar objetos de tipo ListBox en la colección UIExtensionSites del WorkItem de la siguiente forma:

[C#]

// La propiedad ListBox expone una instancia de la clase ListBox.

RootWorkItem.UIExtensionSites.RegisterSite(”ListBox”, new ListBoxAdapter (this.Shell.ListBox));

[Visual Basic]

‘ La propiedad ListBox expone una instancia de la clase ListBox.

RootWorkItem.UIExtensionSites.RegisterSite(”ListBox”, New ListBoxAdapter (Me.Shell.ListBox))

Si se prefiere utilizar la otra sobrecarga del método RegisterSite donde se pasa directamente el UIElement en el segundo parámetro, se deberá registrar una factory que se encargue de crear los adapters. El siguiente código muestra como implementarla continuando el ejemplo anterior.

[C#]

using Microsoft.Practices.CompositeUI.UIElements;

using System.Windows.Forms;

public class ListBoxAdapterFactory : IUIElementAdapterFactory

{

#region IUIElementAdapterFactory Members

public IUIElementAdapter GetAdapter(object uiElement)

{

if (Supports(uiElement))

{

return new ListBoxAdapter((ListBox)uiElement);

}

return null;

}

public bool Supports(object uiElement)

{

if (uiElement is ListBox)

return true;

return false;

}

#endregion

}

[Visual Basic]

Imports Microsoft.Practices.CompositeUI.UIElements

Imports System.Windows.Forms

Public Class ListBoxAdapterFactory

Implements IUIElementAdapterFactory

Public Function GetAdapter(ByVal uiElement As Object) As IUIElementAdapter Implements IUIElementAdapterFactory.GetAdapter

If Supports(uiElement) Then

Return New ListBoxAdapter(CType(uiElement, ListBox))

End If

Return Nothing

End Function

Public Function Supports(ByVal uiElement As Object) As Boolean Implements IUIElementAdapterFactory.Supports

If TypeOf uiElement Is ListBox Then

Return True

End If

Return False

End Function

End Class

Como se observa en el código anterior, la factory implementa la interfaz IUIElementAdapterFactory.

El método Support de la clase ListBoxAdapterFactory devuelve true si el objeto pasado como parámetro es soportado por la factory. En caso contrario devuelve false. Este método es utilizado por la clase UIElementAdapterFactoryCatalog en el método GetFactory. Dentro de ese método se recorre todo el catálogo de factories hasta encontrar una que soporte el objeto requerido.

En cuanto al método GetAdapter devuelve una instancia del adapter correcto para el tipo de objeto pasado como parámetro. Si el objeto no es soportado por la factory devuelve null/Nothing.

Por ultimo se deberá agregar la factory creada anteriormente al catálogo de fábricas del WorkItem (el catálogo está registrado como un servicio que implementa la interfaz IUIElementAdpterFactoryCatalog). Esto generalmente se realiza en el método AfterShellCreated de la clase principal de la aplicación (la que hereda de FormShellApplication) como se muestra en el siguiente código.

[C#]

protected override void AfterShellCreated()

{

base.AfterShellCreated();

IUIElementAdapterFactoryCatalog catalogFactory = RootWorkItem.Services.Get();

catalogFactory.RegisterFactory(new ListBoxAdapterFactory());

// TODO: Agregar los UIElements a la coleccion UIExtensionSites.

}

[Visual Basic]

Protected Overrides Sub AfterShellCreated()

MyBase.AfterShellCreated()

Dim catalogFactory As IUIElementAdapterFactoryCatalog = RootWorkItem.Services.Get(Of IUIElementAdapterFactoryCatalog)()

catalogFactory.RegisterFactory(New ListBoxAdapterFactory())

‘ TODO: Agregar los UIElements a la coleccion UIExtensionSites.

End Sub

En el siguiente post se explicarán los principales rasgos de los Commands.

Introducción a Composite UI Application Block (CAB) IV

Model-View-Presenter

En general los formularios siempre están formados por varios controles, manejan eventos y contienen lógica para responder a estos eventos. Si se escribe este código dentro de la clase del formulario, se hace demasiado compleja y difícil de probar. Una posible solución a este problema es separar las responsabilidades de mostrar los aspectos visuales y el manejo de los eventos en diferentes clases. Una clase representará a la vista, la cual controlará el formulario, y la otra, el presenter, se encargará del manejo de los eventos y de actualizar correctamente a la vista.

El patrón Model-View-Presenter (MVP) separa el modelo del dominio, la presentación y las acciones basadas en la interacción con el usuario en tres clases separadas. La vista le delega a su presenter toda la responsabilidad del manejo de los eventos del usuario. El presenter se encarga de actualizar el modelo cuando surge un evento en la vista, pero también es responsable de actualizar a la vista cuando el modelo le indica que ha cambiado. El modelo no conoce la existencia del presenter. Por lo tanto, si el modelo cambia por acción de algún otro componente que no sea el presenter, debe disparar un evento para que el presenter se entere.

A la hora de implementar este patrón, se identifican los siguientes componentes:

· IView: es la interfaz con la que el presenter se comunica con la vista.

· View: vista que implementa la interfaz IView y se encarga de manejar los aspectos visuales. Mantiene una referencia a su presenter al cual le delega la responsabilidad del manejo de los eventos.

· Presenter: contiene la lógica para responder a los eventos y manipula el estado de la vista mediante una referencia a la interfaz IView. El presenter utiliza el modelo para saber cómo responder a los eventos.

· Model: Esta compuesto por los objetos que conocen y manejan los datos dentro de la aplicación. Por ejemplo, pueden ser las clases que conforman el modelo del negocio (business entities).

La Figura1 ilustra la lógica del patrón Model-View-Presenter (MVP).

MVP(expandido)

Figura 1
Vista lógica del patrón MVP.

MVP aplicado en CAB

CAB provee una infraestructura que permite aplicar fácilmente el patrón MVP para mejorar el diseño de nuestras aplicaciones. Johnny Halife escribió hace un tiempo el siguiente post: How CAB and TDD helps doing better designs, donde explica cómo CAB permite mejorar nuestros diseños.

A continuación se mostrarán implementaciones de ejemplo de una vista con su presenter utilizando las herramientas proporcionas por Composite UI Application Block.

Vista

Gracias a la inyección de dependenicias (Dependency Injection) la vista puede inyectar una nueva instancia de su presenter cuando esta se crea. El siguiente código muestra una implementación de una vista usando el patrón MVP:

[C#]

[SmartPart]

public partial class View : UserControl, IView

{

private Presenter _presenter = null;

[CreateNew]

public Presenter Presenter

{

set

{

_presenter = value;

_presenter.View = this;

}

}

public View()

{

InitializeComponent();

}

protected override void OnLoad(EventArgs e)

{

_presenter.OnViewReady();

base.OnLoad(e);

}

#region IView Members

// TODO: Implementar los metodos de la interfaz IView.

#endregion

// TODO: Agregar metodos que controlen los controles de

// la vista.

}

[Visual Basic]

<SmartPart()> _

Partial Public Class View

Inherits UserControl

Implements IView

Private _presenter As Presenter = Nothing

_

Public WriteOnly Property Presenter() As Presenter

Set(ByVal value As Presenter)

_presenter = value

_presenter.View = Me

End Set

End Property

Public Sub New()

InitializeComponent()

End Sub

Protected Overrides Sub OnLoad(ByVal e As EventArgs)

_presenter.OnViewReady()

MyBase.OnLoad(e)

End Sub

‘ TODO: Implementar los metodos de la interfaz IView.

‘ TODO: Agregar metodos que controlen los controles de

‘ la vista.

End Class

Analizando el código anterior, lo primero que se observa es el atributo SmartPart sobre la definición de la clase. Este atributo es requerido para soportar la funcionalidad Inversión de Control (Inversion of Control - IoC) e Inyección de dependencias (Dependency Injection) provista por CAB.

Otro dato curioso es el operador partial en el encabezado de la clase. Ese operador indica que la clase está definida en más de un archivo.

También se observa el atributo CreateNew encima de la propiedad Presenter. Este atributo le indica al ObjectBuilder que cuando instancie la clase (mediante algún WorkItem) se encargue también de crear una nueva instancia de su presenter e inyectarla. Como se puede ver, la vista además vincula al presenter con ella misma mediante la propiedad View.

Por último se sobrescribe el método OnLoad() de la clase UserControl para poder avisarle al presenter cuando la vista se está cargando.

Presenter

CAB provee la clase Controller la cual puede ser usada como base para crear los presenters o controladores (si se usa el patrón MVC). Esta clase incluye una referencia al WorkItem padre y una propiedad para acceder a su estado (State). Sin embargo no es necesario usar la clase Controller para poder implementer a un presenter. A continuación se muestra una implementación de ejemplo de esta clase:

[C#]

public class Presenter

{

private IService _service;

private IView _view;

[ServiceDependency]

public IService Service

{

set { service = value; }

}

public IView View

{

get { return _view; }

set { _view = value; }

}

public override void OnViewReady()

{

// TODO: Agregar el codigo necesario para que la

// vista se cargue con datos validos.

}

// TODO: Agregar los metodos necesarios para manejar

// los eventos del usuario.

}

[Visual Basic]

Public Class Presenter

Private _service As IService

Private _view As IView

_

Public WriteOnly Property Service() As IService

Set(ByVal value As IService)

_service = value

End Set

End Property

Public Property View() As TView

Get

Return _view

End Get

Set(ByVal value As TView)

_view = value

End Set

End Property

Public Overrides Sub OnViewReady()

‘ TODO: Agregar el codigo necesario para que la

‘ vista se cargue con datos validos.

End Sub

‘ TODO: Agregar los metodos necesarios para manejar

‘ los eventos del usuario.

End Class

Como se vio anteriormente, el presenter posee una referencia a la vista mediante la interfaz IView. La encargada de cargar el valor en esta propiedad es la vista cuando ObjectBuilder le inyecta la nueva instancia de su presenter.

El presenter también es el responsable de acceder a los servicios necesarios y/o entidades del modelo de negocios (Business Entities). En el ejemplo anterior, cuando el ObjectBuilder cree una instancia de la clase Presenter, se encargará también de inyectar el servicio que implementa la interfaz IService (en los próximos post se profundizará más sobre este tema).

Dentro del método OnViewReady() se deberá colocar la lógica necesaria para cargar por primera vez los datos en la vista, en caso de que fueran necesarios, llamando a los métodos de la IView que se encargan de modificar cargar los datos que se muestran.

Además, es el presenter el que se encarga de publicar y/o suscribirse a todos los eventos necesarios. Para ello, CAB proporciona la herramienta EventBroker para facilitar esta tarea (este tema se explicará en más detalle en los próximos post).

Nota: Smar Client Software Factory trae una recipe llamada Add View (with presenter) la cual se encarga de crear las clases las clases con la estructura necesaria para implementar el patrón Model-View-Presenter. Además provee una clase abstracta y genérica llamada Presenter que usa como base para construir los presenter de cada vista.

TDD con MVP

Usando el patrón MVP se puede realizar fácilmente Test-Driven-Development (TDD). Por ejemplo se puede testear el presenter, independientemente de la implementación de la vista. Aislar al presenter en los tests es útil porque se puede enfocar toda la atención en probar sólo esta clase y no sus dependencias. Estas dependencias (como por ejemplo la vista y los servicios) son reemplazadas por implementaciones falsas (mock-implementations) lo cual es posible gracias a que el presenter tiene referencias sólo a interfaces y no a implementaciones concretas.

Para obtener una mejor visión de cómo aplicar TDD con el patrón MVP, recomiendo la lectura del post de Johnny Halife: How-To: Write MVP using TDD.

En el siguiente post se verá cómo utilizar UI Extension Sites y Commands.

Introducción a Composite UI Application Block (CAB) III

Módulos

Componentes de cada módulo

En una aplicación CAB un módulo es una unidad de desarrollo y despliegue. CAB provee la posibilidad de cargar los módulos teniendo en cuenta el tipo de usuario que utiliza la aplicación. Esto permite presentar una interfaz y comportamiento distinto dependiendo del rol del usuario.

Los módulos generalmente están compuestos por:

  • WorkItems: Cada módulo debe poseer uno o más WorkItems. Estas clases heredan de la clase WorkItem de CAB y se construyen a partir del RootWorkItem. Son usadas para construir y albergar a todos los objetos que el módulo necesite, agregar servicios y acceder a ellos. El WorkItem es el único que tiene acceso a todos los componentes del módulo.
  • SmartParts: Son las partes visuales que agregará el módulo a la aplicación. Se implementan mediante clases que heredan de UserControl y presentan el atributo SmartPart. Mantienen una referencia a su controlador o presenter.
  • Servicios: El módulo puede agregar sus propios servicios. Dependiendo en qué WorkItem se los agregue estos pueden estar disponibles para todos los módulos o sólo para el que los creó.
  • Business Entities: Son las clases propias del modelo de negocio que la aplicación utilizará para implementar su comportamiento.

ProfileCatalog

Composite UI Application Block provee un servicio para la carga de los módulos durante la etapa de inicialización de la aplicación. Utiliza un archivo XML como catálogo para determinar qué módulos se van cargar. Su nombre por defecto es ProfileCatalog.xml y debe estar ubicado en la carpeta principal de la aplicación (donde está el ejecutable). Dentro de este archivo se especifican todos los módulos de la aplicación y cuales son los usuarios que tendrán acceso a ellos. Si no se especifica ningún rol de usuario todos los módulos presentes en el archivo se cargarán.

A continuación se muestra el formato del archivo ProfileCatalog.xml.

XML

sdfasd

<?xml version=”1.0″ encoding=”utf-8″ ?>

<SolutionProfile xmlns=”http://schemas.microsoft.com/pag/cab-profile>

<Modules>

<ModuleInfo AssemblyFile=”Module1.dll”>

<Roles>

<Role Allow=”Users”/>

</Roles>

</ModuleInfo>

<ModuleInfo AssemblyFile=”Module2.dll”>

<Roles>

<Role Allow=”Administrators”/>

</Roles>

</ModuleInfo>

</Modules>

</SolutionProfile>

En el ejemplo anterior se especifican dos módulos y el rol del usuario necesario para que estos se carguen.

Dependencias

Durante el desarrollo de la aplicación puede darse el caso de tener módulos independientes y dependientes. Cuando un módulo posee una dependencia hacia otro módulo esta se debe indicar a CAB mediante el atributo ModuleDependency. Este atributo afecta la secuencia de carga asegurando que primero se cargarán todos los módulos de los cuales se depende.

La siguiente línea de código muestra como se implementa esto. Se la debe colocar en el archivo AssemblyInfo.cs/vb del módulo que tiene la dependencia (cambiar MyModule por el nombre del módulo del cual depende). Antes de hacer esto asegurarse que el proyecto tenga referencias a los assemblies Microsoft.Practices.CompositeUI.dll y Microsoft.Practices.ObjectBuilder.dll.

[C#]

[assembly: Microsoft.Practices.CompositeUI.ModuleDependency("MyModule")]

[Visual Basic]

<Assembly: Microsoft.Practices.CompositeUI.ModuleDependency(”MyModule”)>

Para que funcione el código anterior se debe agregar en el archivo AssemblyInfo.cs/vb del módulo del cual se depende la siguiente línea:

[C#]

[assembly: Microsoft.Practices.CompositeUI.Module("MyModule")]

[Visual Basic]

<Assembly: Microsoft.Practices.CompositeUI.Module(”MyModule“)>

Esto permite establecerle un nombre al módulo para que los otros lo puedan identificar.

Nota: En Smart Client Software Factory, se pueden especificar las dependencias entre los módulos directamente desde el Profile Catalog.

Proceso de carga

Cuando CAB carga un módulo usa reflection para saber si el assembly contiene una clase que implementa la interfaz IModule. En general en vez de implementar esa interfaz, se hereda de la clase abstracta ModuleInit (que ya implementa la interfaz anterior) y se sobrescriben los métodos Load() y AddServices().

En el método Load() generalmente se inicializa el WorkItem principal del módulo y se lo ejecuta llamando a su método Run(), y en el método AddServices() se agregan al RootWorkItem los servicios del módulo (esto se verá en más detalle en los próximos posts). El primero en ejecutarse es el método AddServices() y luego Load().

A continuación se muestra un ejemplo de cómo construir esta clase:

[C#]

using Microsoft.Practices.ObjectBuilder;

using Microsoft.Practices.CompositeUI;

// namespace donde se encuentran los servicios en el ejemplo.

using ExampleModule.Services;

namespace ExampleModule

{

public class ExampleModuleInit : ModuleInit

{

private WorkItem _rootWorkItem;

[InjectionConstructor]

public ExampleModuleInit([ServiceDependency] WorkItem rootWorkItem)

{

_rootWorkItem = rootWorkItem;

}

public override void AddServices()

{

base.AddServices();

_rootWorkItem.Services.AddNew();

}

public override void Load()

{

ExampleWorkItem workitem = _rootWorkItem.WorkItems.AddNew();

workitem.Run();

}

}

}

[Visual Basic]

Imports Microsoft.Practices.CompositeUI

Imports Microsoft.Practices.ObjectBuilder

‘ namespace donde se encuentran los servicios en el ejemplo.

Imports ExampleModule.Services

Public Class ExampleModuleInit

Inherits ModuleInit

Private _rootWorkItem As WorkItem

<InjectionConstructor()> _

Public Sub New(<ServiceDependency()> ByVal rootWorkItem As WorkItem)

_rootWorkItem = rootWorkItem

End Sub

Public Overrides Sub AddServices()

MyBase.AddServices()

_rootWorkItem.Services.AddNew(Of Service, IService)()

End Sub

Public Overrides Sub Load()

MyBase.Load()

Dim workItem As ExampleWorkItem = _rootWorkItem.WorkItems.AddNew(Of ExampleWorkItem)()

workItem.Run()

End Sub

End Class

La clase ExampleModuleInit posee una referencia al RootWorkItem la cual se inyecta en el constructor. El atributo InjectionConstructor le indica al ObjectBuilder que deberá usar ese constructor en el momento de instanciar la clase y el atributo ServiceDependency le informa que se debe inyectar una dependencia en el parámetro del constructor.

El método AddServices() agrega un servicio que implementa la interfaz IService al contenedor de servicios del RootWorkItem.

En el método Load() se crea una instancia de la clase ExampleWorkItem (que representa al WorkIem principal del módulo) y se ejecuta el método Run(). La clase ExampleWorkItem deberá sobrescribir el método OnRunStarted() para agregar el código que se encargará de crear las vistas del módulo y cargarlas en el Workspace correspondiente. A continuación se muestra un ejemplo de cómo implementar esta clase:

[C#]

using Microsoft.Practices.CompositeUI;

using Microsoft.Practices.CompositeUI.SmartParts;

// namespace donde se encuentran las vistas en el ejemplo.

using ExampleModule.Views;

namespace ExampleModule

{

public class ExampleWorkItem : WorkItem

{

protected override void OnRunStarted()

{

AddViews();

base.OnRunStarted();

}

private void AddViews()

{

IWorkspace workspace = Workspaces["MainWorkspace"];

workspace.Show(Items.AddNew<UserControl1>());

}

}

}

[Visual Basic]

Imports Microsoft.Practices.CompositeUI

Imports Microsoft.Practices.CompositeUI.SmartParts

‘ namespace donde se encuentran las vistas en el ejemplo.

Imports ExampleModule.Views

Public Class ExampleWorkItem

Inherits WorkItem

Protected Overrides Sub OnRunStarted()

AddViews()

MyBase.OnRunStarted()

End Sub

Public Sub AddViews()

Dim workspace As IWorkspace = Workspaces(”MainWorkspace”)

workspace.Show(Items.AddNew(Of UserControl1)())

End Sub

End Class

Al ejecutar el método Run() de la clase ExampleWorkItem, se invocará el método sobrescrito OnRunStarted(). Este método crea una vista de tipo UserControl1 y la muestra en el Workspace del shell identificado con el nombre “MainWorkspace”.

En el próximo post se explicará el patrón Model-View-Presenter (MVP) y se dará un ejemplo de cómo aplicarlo en CAB.

Next Page »