Implementación de un modelo de objetos similar a microsoft Word para la aplicación de .NET Framework

 

Omar AL Zabir
OmarAlZabir@gmail.com

Agosto de 2005

Descargue el código de ejemplo asociado a este artículo, OfficeAutomation.msi.

Resumen: Muestra una manera de implementar un modelo de objetos similar a microsoft Word para su propia aplicación de .NET Framework, siguiendo el patrón de diseño Model-View-Controller. (26 páginas impresas)

Contenido

Información general de la jerarquía del modelo de objetos de Microsoft Word
Ventajas del modelo de objetos con compatibilidad con Automation
Comparación con el patrón Model-View-Controller
La creación de Smart Editor
El modelo de objetos admitido por Automation y el modelo de objetos de la aplicación
Cómo usa la capa de presentación el modelo de objetos
Ejecución de ejemplo de una actividad
Clases de marco
Cómo crear su propia aplicación
Presentación de complementos
Característica de scripting
Conclusión

Todas las aplicaciones de Microsoft Office se basan en un modelo de objetos que admite la automatización. Cualquier desarrollador puede usar el modelo 0bject para impulsar la interfaz de usuario de la aplicación y agregar, editar y eliminar contenido, al igual que un usuario real interactúa con la aplicación. El modelo de objetos enriquecido, junto con la compatibilidad con la automatización, hace que las aplicaciones de Office sean verdaderamente extensibles y conectables. Cualquier persona puede escribir un complemento eficaz en un plazo muy corto para ampliar el comportamiento de Microsoft Word según sus propias necesidades. Como buenos desarrolladores orientados a objetos (OO), desarrollamos nuestras aplicaciones con una arquitectura enriquecida y con un modelo de objetos razonablemente bueno siguiendo el patrón de diseño Model-View-Controller (MVC).

Sin embargo, hasta hace poco, se han desarrollado muy pocas aplicaciones que ofrecen automatización similar a las aplicaciones de Microsoft Office. Como resultado, no podemos ampliar nuestras aplicaciones de la misma manera que podemos ampliar y personalizar aplicaciones de Office mediante .NET Framework y Microsoft Visual Basic para Aplicaciones (VBA). En este artículo se muestra una manera de implementar un modelo de objetos similar a microsoft Word para su propia aplicación .NET. Seguiremos el patrón de diseño Model-View-Controller y también los eventos y delegados de .NET Framework en gran medida. El modelo de objetos que desarrollaremos aquí agregará extensibilidad infinita a nuestra aplicación. Nos dará la oportunidad de agregar la funcionalidad de complemento y scripting a nuestras aplicaciones según lo diseñado, sin escribir código adicional para ellas. La característica de complemento y scripting tendrá la misma potencia que la aplicación principal. El diseño también generará una base de código muy limpia que desacopla realmente la lógica de negocios de la lógica de la interfaz de usuario. Lo mejor de todo es que podremos escribir código para controlar la interfaz de usuario y, por tanto, crear scripts de prueba que no solo prueben la lógica de negocios, sino que también prueben los comportamientos de la interfaz de usuario.

Información general de la jerarquía del modelo de objetos de Microsoft Word

Comencemos con el modelo de objetos de Microsoft Word. Aquellos que ya saben cómo funciona el modelo de objetos de Microsoft Word pueden ir directamente a la sección siguiente.

El modelo de objetos compatible con la automatización de Microsoft Word comienza desde la clase Application. Una instancia de Word tiene un objeto de aplicación singleton que proporciona las barras de herramientas, menús, barra de estado, etc.

Figura 1. El modelo de objetos de Microsoft Word (parcial)

Toda la aplicación se puede controlar mediante la clase Application . Podemos acceder a cualquier documento, manipular el contenido, agregar barras de comandos, cambiar etiquetas de menú e iconos, hacer clic mediante programación en un botón, mostrar mensajes de estado, etc. Por ejemplo, puede hacer que Word mostrar un cuadro de diálogo Abrir archivo llamando a Application.Documents.Open(). Puede hacer que Word salga llamando a Application.Quit(). Escribe código y Microsoft Word actúa según el código. No es necesario, ni para los desarrolladores de Word, encontrar el módulo que muestra un documento o llamar a su método público para indicarle que cambie el nombre del documento. Lo único que haga es obtener la instancia del objeto Document y, a continuación, modificar su propiedad Name . La interfaz de usuario responde inmediatamente. Esta es la belleza de un modelo de objetos que admite la automatización.

Antes de implementar el modelo de objetos compatible con la automatización para mis propias aplicaciones, solía examinar el IDE de Visual Studio con un profundo respeto y maravillarlo. Solía preguntarme cómo los desarrolladores de Visual Studio realizan un seguimiento de tantos aspectos de un evento determinado. Por ejemplo, piense en cuántas cosas se producen cuando se elimina un archivo del árbol de Explorador de soluciones al presionar el botón SUPR. Se quita el nodo de árbol, el proyecto y la solución se establecen en modo de guardado pendiente (sucio), cambia la barra de título de la ventana, cambia la barra de estado, si el documento está abierto, se cierra su pestaña, se actualizan todas las barras de comandos, la barra de menús se deshabilita, etc. Todo el IDE pasa por muchos cambios. Piense en el tipo pobre que implementó esta característica de eliminación de archivos. ¿Alguna vez la persona ha imaginado que todas estas acciones deben realizarse solo para una característica de eliminación de archivos? La verdad es que la persona no necesita preocuparse de todos estos en absoluto porque Visual Studio tiene un maravilloso modelo de objetos compatible con la automatización. Después de aprender sobre estos modelos de objetos, encontré que estas aplicaciones no son realmente tan complicadas.

Ventajas del modelo de objetos con compatibilidad con Automation

Vamos a revisar rápidamente por qué un modelo de objetos compatible con automatización es tan beneficioso. Cuando tenga este modelo de objetos para su propia aplicación, tenga siempre en cuenta tres principios sencillos:

  • No siempre pienses en la interfaz de usuario.
  • Céntrese en realizar pequeñas unidades de trabajo a la vez.
  • Deje que cada módulo realice su propio trabajo.

No piense siempre en la interfaz de usuario

He visto que los programadores suelen centrarse primero en la interfaz de usuario cada vez que se les da una tarea. Si pides a un programador que cargue los archivos en una carpeta y lo muestres en una vista de árbol (como el Explorador de Windows), generalmente la persona pensará en lo siguiente:

Ilustración 2. Explorador de Windows que muestra los archivos y carpetas en una vista de árbol

  • ¿Cómo leer el sistema de archivos?
  • ¿Cómo rellenar los nodos de vista de árbol?
  • ¿Cómo obtener la suspensión de esa vista de árbol de la clase que la rellenará?
  • ¿Cómo pasar la referencia de la vista de árbol a esa clase?
  • ¿Cómo reflejar los cambios en el sistema de archivos en esa vista de árbol?
  • ¿Cómo actualizar el modelo de objetos cada vez que los usuarios cambian los nodos de vista de árbol?

Verá que las preocupaciones de un programador se centran principalmente en reflejar una acción determinada en la interfaz de usuario. Si le dices al programador que rellene una clase de colección con los archivos y carpetas y le digas que tan pronto como agregues un objeto en la colección, por alguna magia se mostrará automáticamente en la vista de árbol, esa persona puede concentrarse libremente en sus propias tareas y no preocuparse por todas las demás partes de la aplicación.

Un modelo de objetos compatible con la automatización libera a los desarrolladores de pensar en la implementación de una tarea de principio a fin, desde la capa de acceso a datos al front-end. Un programador puede centrarse en desarrollar un determinado sin pensar en el resto del sistema.

Centrarse en realizar tareas unitarias pequeñas

Cuando tiene un modelo de objetos compatible con la automatización, el proceso de diseño e implementación se limita a tareas unitarias pequeñas. Por ejemplo, primero piensa cómo rellenar la colección Application.Files con los archivos del sistema de archivos. No es necesario saber si hay alguna vista de árbol en la aplicación que mostrará los archivos en absoluto. Tenga esto siempre en su mente: "No hay ninguna interfaz de usuario, no hay ninguna interfaz de usuario. . . " Puede que no haya una vista de árbol; puede haber tres vistas de árbol y cuatro cuadros de lista que deben rellenarse con la misma lista de archivos. No necesita saber cuántos de ellos están allí, solo tiene que pensar en cómo leer los archivos y, a continuación, llamar a Application.Files.Add( new File( ... ) para rellenar la colección.

Figura 3. Visual Studio .NET Explorador de soluciones

Ahora vamos a ir a la parte de la interfaz de usuario de la tarea. Supongamos que está haciendo una aplicación similar a Visual Studio (sin bromas). El desarrollador responsable de compilar el "Explorador de soluciones" de la aplicación sabe que, cada vez que se agrega un elemento en la colección Application.Files , debe agregarse en el árbol del Explorador de soluciones. La persona no necesita saber de dónde procede el objeto de archivo. Puede provenir del sistema de archivos, puede ser un nuevo archivo agregado por el usuario que aún no está en el sistema de archivos, que puede ser agregado por un complemento de terceros o desde una macro. De nuevo, el desarrollador del explorador de soluciones sabe que si el usuario elimina un elemento de la colección Application.Files , debe eliminarse del sistema de archivos. Pero eso no puede ser todo lo que debe hacerse. El archivo puede ser un archivo lógico que no está en el sistema de archivos. Puede ser algo que haya producido un complemento. La persona solo debe saber que el objeto File debe quitarse cada vez que el usuario lo elimina. La otra persona responsable de agregar ese objeto de archivo en la colección ya está escuchando los cambios en ese objeto y actuará en consecuencia cada vez que se quite el objeto.

Permitir que cada módulo realice su propio trabajo

Cuando tenemos un modelo de objetos compatible con la automatización, podemos distribuir la responsabilidad de implementar algunas características a las personas responsables de diferentes módulos, independientemente de si conocen la existencia de los demás. Esto significa que el tipo del explorador de soluciones no necesita saber que hay un tipo de pestañas de documento que necesita activar una pestaña cuando se hace doble clic en un archivo en el explorador de soluciones. El tipo de ficha del documento no necesita saber que hay un tipo de menú Ventana que necesita mostrar todas las pestañas del documento abierto. Todo lo que necesitan saber es que hay un modelo de objetos y en ese modelo de objetos hay objetos de modelo que les notificarán cada vez que le ocurra algo.

Imagine lo que sucede al eliminar un archivo del árbol del Explorador de soluciones. El nombre de archivo del menú Ventana debe quitarse. Si el archivo está abierto, debe cerrarse la ventana del documento. El archivo debe quitarse del sistema de archivos. El proyecto debe marcarse como "sucio" o guardar pendiente. Si no hay ningún documento abierto, es necesario deshabilitar menús, deshabilitar barras de comandos, etc. Hay tantas cosas que hacer solo cuando el usuario presiona un simple botón SUPR en un archivo. Cuando no era consciente del concepto de modelo de objetos compatible con la automatización, solía escribir código de esta manera:

AskForSave( file );
DeleteTreeNode( file );
CloseOpenDocument( file );
RemoveMenuItem( windowMenu, file );
UpdateRecentFiles( fileMenu, file );
KillFile( file );
MakeProjectDirty( project );

Necesitaba tener una imagen mental de toda la aplicación mientras implementaba cualquier característica, sin embargo pequeña era. Por supuesto, he usado un patrón de comandos adecuado, clases muy bien diseñadas para realizar los trabajos y una aplicación altamente modularizada. Sin embargo, los pensamientos de la implementación completa siempre se ejecutaron en mi cerebro y que crearon una gran presión cuando el número de módulos aumentó día a día.

Cuando tenga un modelo de objetos compatible con automatización, haga lo siguiente:

  1. El Visor de documentos espera alguna notificación de que se quita un archivo y cierra la pestaña que muestra el documento.
  2. El árbol de Explorador de soluciones espera alguna notificación de que se quita un archivo y, a continuación, quita el archivo del árbol.
  3. El menú Archivo recibe una notificación y quita el elemento de archivo.
  4. El menú ventana obtiene una notificación y quita el nombre de archivo de su lista.
  5. El administrador del sistema de archivos recibe notificaciones y elimina el archivo.
  6. Algunos complementos en ejecución que se han asociado con la colección Application.Files obtienen la notificación Remove y actúan en consecuencia.
  7. La barra estado recibe una notificación y muestra un mensaje.

Los distintos módulos de la aplicación se suscriben a diferentes objetos y colecciones expuestos por el objeto Application y escuchan las notificaciones pertinentes para ellos. Cuando se recibe una notificación, solo hacen lo que es relevante para ellos y omiten otras notificaciones. No les importa la existencia de los demás.

Comparación con el patrón Model-View-Controller

Los que conocen el patrón de diseño de MVC deben estar diciendo a sí mismos, esto no es más que MVC. Este tipo de modelo de objetos sigue los principios de Model-View-Controller, que son:

  • Tiene un modelo que contiene los datos. Por ejemplo, Car es un modelo.
  • Tiene controladores para realizar cambios en el modelo. Por ejemplo, Driver es un controlador que conduce un automóvil.
  • Tiene vistas que muestran la salida en la interfaz de usuario. El controlador usa View para mostrar la salida del modelo. Por ejemplo, FuelIndicator puede ser una vista que muestra la propiedad Fuel del objeto car.
  • Los controladores y vistas observan el modelo para los cambios y cada vez que se encuentra un cambio, la vista refleja el cambio en la interfaz de usuario y el controlador actualiza la vista o actualiza el modelo. Por ejemplo, en cuanto la propiedad "Car.Started" cambia a "True", la vista hace que el sonido "Vroom!" y el controlador actualice el modelo de coche estableciendo "Car.Accelerator=1000".
  • La vista recibe la acción del usuario y refleja el cambio en el modelo. Por ejemplo, cuando el controlador apaga el coche, establece "Car.Started = false".

Figura 4. Modelo de objetos de ejemplo de MVC

MVC es un patrón de diseño muy utilizado para diseñar aplicaciones de escritorio. Verá una diferencia significativa en la complejidad del código de un programa antes de implementar MVC y después de implementar MVC. La mejor descripción de MVC se puede encontrar en http://java.sun.com/blueprints/patterns/MVC.html. Aquí compongo dos párrafos de esa página:

Pueden surgir varios problemas cuando las aplicaciones contienen una combinación de código de acceso a datos, código de lógica empresarial y código de presentación. Estas aplicaciones son difíciles de mantener, ya que las interdependencias entre todos los componentes provocan efectos de onda fuertes cada vez que se realiza un cambio en cualquier lugar. El acoplamiento elevado hace que las clases sean difíciles o imposibles de reutilizar porque dependen de tantas otras clases. La adición de vistas de datos nuevas a menudo requiere volver a implementar o cortar y pegar código de lógica de negocios, que luego requiere mantenimiento en varios lugares. El código de acceso a datos sufre del mismo problema, que se corta y pega entre los métodos de lógica de negocios.

El patrón de diseño Model-View-Controller resuelve estos problemas al desacoplar el acceso a datos, la lógica empresarial y la presentación de datos y la interacción del usuario.

En nuestro modelo de objetos, el modelo es la colección Application.Files y el controlador es el módulo "Explorador de soluciones" y la vista es la vista de árbol que muestra los archivos. Sin embargo, la diferencia fundamental entre MVC normal y este modelo es que el controlador no es consciente de la vista. Como mencioné al principio,"No pienses en la interfaz de usuario siempre",el controlador no sabe que hay una vista. Lo que necesite hacer, lo hace en el modelo. El modelo se encuentra entre la vista y el controlador para desacoplarlos por completo y proporcionar una arquitectura en la que los desarrolladores puedan pensar y desarrollar un sistema sin preocuparse nunca por las INTERFACES de usuario.

Figura 5. MVC modificado para nuestro modelo

Además, normalmente no se crea una jerarquía de modelos de objetos que comienza desde una clase singleton "Application" y proporciona un mapa completo de toda la interfaz de usuario. Normalmente, nuestro modelo de objetos contiene objetos de entidad relevantes para nuestros dominios empresariales. Por ejemplo, Person, Account, Transaction, etc. son los objetos habituales que exponemos a través del modelo de objetos. Normalmente, el modelo de objetos no tiene botones, barras de herramientas, menú, barra de estado, etc. Si creamos un modelo de objetos que no solo refleja los objetos empresariales, sino también la estructura de la interfaz de usuario y hace que nuestros módulos de interfaz de usuario respondan a las acciones realizadas en el modelo de objetos, creamos un modelo de objetos que puede proporcionar automatización. Este es el principio de que los productos de Microsoft usan en sus aplicaciones. Como resultado, podemos ampliar sus aplicaciones con un gran control sobre los datos que contienen objetos y sobre la interfaz de usuario. Esta misma idea se usará en nuestra aplicación para crear un modelo de objetos que proporcione compatibilidad con la automatización mediante eventos y delegados de .NET Framework y vea el resultado final mediante la realización de una aplicación de ejemplo.

La creación de un editor inteligente

El proyecto de ejemplo que acompaña a este artículo es un editor de texto denominado "Editor inteligente". Este editor es "inteligente" porque tiene un modelo de objetos muy extensible y conectable que admite la automatización. La aplicación tiene el aspecto del IDE de Visual Studio. El modelo de objetos también se toma en gran medida del propio modelo de objetos de Visual Studio, que también es similar al modelo de objetos de Microsoft Word. Sorprendentemente, casi todos los productos de Microsoft tienen conceptos muy similares en sus modelos de objetos. Después de leer este artículo, también descubrirá por qué todos los productos comparten las mismas ideas de diseño detrás de su modelo de objetos y lo conveniente que realmente es este diseño.

Figura 6. Interfaz de usuario del Editor inteligente

El modelo de objetos Automation-Supported y el modelo de objetos de la aplicación

La aplicación tiene un pequeño modelo de objetos que admite automatización como Microsoft Word. El objeto raíz es Application. No confunda esta clase Application con la clase Application de .NET Framework. System.Windows.Forms.Application es una clase para Windows Forms. Nuestra clase Application es Editor.ObjectModel.Application. Si desea usar nuestra clase "Application" como valor predeterminado en el código en lugar del Windows Forms uno, use esta declaración al principio del archivo:

using Application = Editor.ObjectModel.Application;

A partir de ahora en este artículo, "Application" hará referencia a Editor.ObjectMode.Application, no al del espacio de nombres Windows Forms.

Ilustración 7. Modelo de objetos de Editor inteligente

Cada uno de estos objetos se asigna a elementos de interfaz de usuario concretos. En la siguiente imagen se explica cómo se organiza la interfaz de usuario.

Figura 8. Módulos del Editor inteligente

En la figura 8 se muestra cómo se divide toda la interfaz de usuario en controles de usuario pequeños. Cada área representa realmente un control de usuario. La parte más importante de esta interfaz de usuario a la que me referiré en todo el artículo es el Explorador de documentos en la parte superior derecha. Las pestañas de documento en las que el contenido de un documento se muestra en un cuadro de texto, también se usan con frecuencia.

Application

Esta es la clase singleton y la raíz del modelo de objetos completo. Su constructor inicializa el modelo de objetos:

private Application()
{
   this._Tabs = new TabCollection( this );
   this._ToolBars = new ToolBarCollection( this );
   this._Menus = new MenuCollection( this );
   this._Documents = new DocumentCollection( this );
}

El rol principal de su constructor es inicializar el primer nivel de objetos en la jerarquía del modelo de objetos.

Cada una de estas colecciones se asigna con algunos elementos de la interfaz de usuario. La colección Toolbars y Menus se rellena inmediatamente cuando se carga la interfaz de usuario.

Figura 9. Asignación de elementos de interfaz de usuario con el modelo de objetos

Colección de documentos

La clase Application expone una colección pública denominada "Documents", que es una instancia de DocumentCollection. Se deriva de una clase de colección personalizada denominada SelectableCollectionBase. Expone una propiedad Selected desde donde siempre puede obtener el documento seleccionado actualmente. Es similar a la colección Elementos del control Listbox o Treeview, donde tiene una propiedad Selected que siempre devuelve el elemento seleccionado actualmente. Cómo ocurre esto se explica más adelante.

DocumentCollection tiene una cantidad muy pequeña de código porque la mayoría del trabajo se realiza en SelectableCollectionBase. Solo proporciona algunas funciones para que esté fuertemente tipada en la clase Document :

public class DocumentCollection : SelectableCollectionBase
{
   new public int Add( Document doc )
   {
      return base.Add( doc );
   }

   new Document this[ int index ]
   {
      get { return (Document)base[ index ]; }
   }

   new public Document Selected
   {
      get { return (Document) base.Selected; }
      set { base.Selected = value; }
   }

   public Document New( string name, string path, byte [] data, IDocumentEditor editor )
   {
      return new Document( name, path, data, editor, null );
   }

   public DocumentCollection( object parent ) : base( false, parent ) { }
}

El único requisito que tiene es llamar a la base( IsMultiSelect, ParentObject ) con true/false para indicar si se trata de una colección de selección múltiple e identificar quién es el elemento primario de esta colección. Para mantener la jerarquía, siempre se lleva a cabo una referencia débil al elemento primario con objetos secundarios.

La clase Document también es muy sencilla, ya que se extiende desde ItemBase (se explica más adelante), que expone todas las características necesarias para este modelo de objetos. Lo único que debe hacer es llamar a base(parent) y pasar la referencia del objeto primario:

public class Document : ItemBase
{
   private string _Name;
   private string _Path;
   private byte [] _Data;
   private IDocumentEditor _DocumentEditor;

   public IDocumentEditor DocumentEditor { ... }

   public byte[] Data  { ... }

   public string Name { ... }

   public string Path { ... }

   public Document( 
      string name, string path, byte [] data, 
      IDocumentEditor editor, object parent ) 
      : base( parent )
   {
      ...
   }

}

La colección menu es una colección similar de la clase Menu . Hereda CollectionBase, no SelectableCollectionBase, ya que no estamos interesados en mantener qué menú está seleccionado. Sin embargo, esto se puede hacer fácilmente simplemente heredando de SelectableCollectionBase en lugar de CollectionBase.

Cada vez que se carga la interfaz de usuario, crea el objeto Menu para cada menú de la barra de menús. Por ejemplo, File, Edit, View, Tools all are menus and you will find one object for each menu in the Application.Menus collection.

Cada menú contiene una colección MenuItem . Los elementos de menú se almacenan en esta colección. Por ejemplo,

MenuItem fileNew = Application.Menus[ "File" ].Items["New"];
fileNew.Click();

Esto devolverá una referencia a un objeto representativo para el elemento de menú "Archivo-Nuevo>". A continuación, puede llamar a su método Click para simular un clic de elemento de menú como si el usuario hubiera clic en el elemento de menú.

Tab (colección)

Un documento se muestra como una pestaña. Para cada documento se crea una pestaña que contiene el editor de documentos. Solo las pestañas abiertas están disponibles en la colección Tabs de la clase Application .

Figura 10. Modelo de objetos de pestañas

Cada vez que se abre un archivo para su edición, se crea una pestaña y se agrega en la colección; cuando se cierra el documento, el objeto Tab se quita de la colección.

Puede llamar en cualquier momento al método Show de un objeto Tab para traer esa pestaña en pantalla o llamar al método Close para cerrarla.

Colección toolbar

Para cada barra de herramientas de la interfaz de usuario, una instancia de la clase Toolbar está disponible en la colección Applications.Toolbars . Puede obtener una referencia a una barra de herramientas de Application.Toolbars[ índice ]. También puede agregar una nueva barra de herramientas en tiempo de ejecución mediante el método Add de la colección Toolbar .

La barra de herramientas contiene una colección de elementos de la barra de herramientas, que pueden ser botones, menús desplegables, separadores, etc. Para cada elemento de barra de herramientas, hay disponible una instancia de ToolbarItem en la colección Items de la clase Toolbar . Puede obtener una referencia al botón Nuevo mediante el código siguiente:

// Get the first toolbar in the collection and the first toolbar item
ToolbarItem newButton = Application.Toolbars[ "Standard" ].Items[ "New" ];
newButton.Click();

A continuación, puede llamar al método Click para simular el clic del botón.

Uso de ejemplo del modelo de objetos

Puede llamar a Application.Quit() desde cualquier lugar para finalizar la aplicación. También puede llamar a Application.Save() para guardar el documento abierto actual, si hay alguno. La propiedad Application.ActiveDocument siempre devuelve el documento seleccionado actualmente que se está editando o seleccionado en el Explorador de documentos. También puede llamar a la pestaña actual que tiene el foco desde Application.ActiveTab.

Cómo usa la capa de presentación el modelo de objetos

La aplicación de ejemplo muestra una manera en la que puede diseñar la aplicación cuando tiene un modelo de objetos compatible con la automatización. La implementación es bastante sencilla y he omitido muchos procedimientos recomendados por motivos de simplicidad. No considere esto como la única manera de compilar aplicaciones sobre un modelo de objetos de este tipo.

Formulario principal

Cuando se carga el formulario principal, primero se suscribe a todos los eventos genéricos relacionados con la interfaz de usuario que expone la clase Application . Por ejemplo, Application.OnFileSaveDialog, Application.ShowStatus, etc. Se trata de servicios de interfaz de usuario comunes y genéricos que cualquier persona puede proporcionar. En nuestro caso, es la forma principal. Actúa como proveedor de servicios de interfaz de usuario centralizado para la clase Application . Por supuesto, las aplicaciones complicadas distribuirán una mayor responsabilidad a los módulos más pequeños. Pero en esta aplicación de ejemplo, proporcionaremos toda la compatibilidad con la interfaz de usuario genérica necesaria del formulario principal. Siempre que sea necesario mostrar mensajes en la interfaz de usuario, cambiar la barra de título de la ventana de la aplicación o mostrar un cuadro de diálogo de archivo común, el formulario principal responde a estos eventos generados por la clase Application y actúa en consecuencia.

Área superior

Este control hospeda el menú y las barras de herramientas. Aunque tanto el menú como la barra de herramientas están preparados en tiempo de diseño, en tiempo de ejecución rellena el modelo de objetos con los elementos Menu, Toolbar yToolbarItems para que los elementos de la interfaz de usuario estén disponibles para la automatización.

En primer lugar, crea el objeto Toolbar para la primera barra de herramientas. El código es muy sencillo:

standardToolBar = Application.ToolBars.New( "Standard" );
Application.ToolBars.Add( standardToolBar );

A continuación, para cada botón de la barra de herramientas, crea ToolbarItems:

ToolBarItem itemNew = standardToolBar.Items.New( "New", string.Empty, btnNew );
standardToolBar.Items.Add( itemNew );
itemNew.OnClick += new ToolBarItemClickHandler(itemNew_OnClick);

De forma similar, crea los objetos Menu y MenuItem para cada menú. Por ejemplo, el menú Archivo se crea de esta manera:

Menu fileMenu = Application.Menu.New( "File", string.Empty, this.mnubarStandard, null );
Application.Menu.Add( fileMenu );

El control "Área superior" tiene varias responsabilidades importantes, que son:

  1. Escucha el modelo de objetos para cualquier cambio en Menu, MenuItem, Toolbar y ToolbarItems. Cada vez que se produce un cambio, por ejemplo, si se deshabilita un botón o se cambia un subtítulo, refleja el cambio en la interfaz de usuario. Por ejemplo, el objeto Toolbar proporciona un evento OnChange . Cada vez que se modifica cualquier elemento de la barra de herramientas, se desencadena este evento. El módulo de hospedaje Toolbar recibe este evento y refleja el cambio en la interfaz de usuario.

    private void toolbar_OnChange(ItemBase item, StringArgs s)
    {
       if( item is ToolBarItem )
       {
          ToolBarItem toolBarItem = item as ToolBarItem;
    
          ButtonItem button = toolBarItem.Tag as ButtonItem;
    
          button.Enabled = toolBarItem.Enabled;
          button.Visible = toolBarItem.Visible;
          button.Checked = toolBarItem.Selected;
       }
    }
    
  2. Escucha el modelo de objetos para agregar o quitar Menu, MenuItem, Toolbar y ToolbarItems. Otro módulo puede agregar en cualquier momento una nueva barra de herramientas en el modelo de objetos. En ese caso, la barra de herramientas también debe crearse en la interfaz de usuario con los elementos de diseño adecuados. Del mismo modo, si se quita un objeto MenuItem de cualquier colección Items del objeto Menu, ese elemento de menú de la interfaz de usuario también debe quitarse.

Cada módulo que necesita proporcionar automatización debe proporcionar estos dos servicios: refleje las acciones del modelo de objetos en la interfaz de usuario y refleje las acciones de la interfaz de usuario en el modelo de objetos.

Área central

Este control hospeda las pestañas de los documentos abiertos. Escucha los cambios realizados en Application.Documents y lo más importante de todo, Applications.Tabs. Cada vez que se agrega un nuevo objeto Tab en la colección Application.Tabs , se crea un control tab que contiene el documento y se muestra en la interfaz de usuario.

Application.Tabs.OnItemCollectionAdd += new CollectionAddHandler( Tabs_OnAdd );
...
...
private void Tabs_OnAdd( CollectionBase collection, ItemBase item )
{
   if( item is Tab )
   {
      CreateNewTab( (Tab) item );
   }
}

También escucha la llamada al método Show de cualquier objeto Document porque si alguien llama a doc. Show(), se debe crear una nueva pestaña y agregarla a la colección Tabs, que a su vez muestra el documento en modo de edición.

Árbol del Explorador de documentos

Cada vez que el usuario crea un nuevo archivo o abre un documento, se agrega al árbol del Explorador de documentos. Este árbol representa la colección Application.Documents . Realiza dos tareas básicas:

  1. Escucha los cambios realizados en la colección Application.Documents . Cada vez que se agrega o edita o elimina un elemento, responde al cambio y hace algo en el árbol. Por ejemplo, cada vez que se agrega un nuevo documento en DocumentCollection, se ejecuta el código siguiente:

    private void Documents_OnItemCollectionAdd(CollectionBase collection, ItemBase item)
    {
       if( item is Document )
       {
          Document doc = item as Document;
    
          DocumentNode node = new DocumentNode( doc );
    
          // There's a tree view control named "treSolution" on the UI
          treSolution.Nodes[0].Nodes.Add( node );
       }
    }
    
  2. Cada vez que el usuario realiza alguna acción en el árbol, como eliminar un archivo, los cambios se reflejan en la colección Application.Documents . Por ejemplo, cuando el usuario presiona el botón SUPR, se genera el evento keypress de vista de árbol y se controla de esta manera:

    private void treSolution_KeyUp(object sender, System.Windows.Forms.KeyEventArgs e)
    {
       DocumentNode node = treSolution.SelectedNode as DocumentNode;
       if( null != node )
       {            
          if( e.KeyCode == Keys.Delete )
          {
             Application.Documents.Remove( node.Document );
          }
          else if( e.KeyCode == Keys.Enter )
          {
             node.Document.Show();
          }
       }
    
    }
    

Todo lo que hace es simplemente eliminar el objeto Document del modelo de objetos. Como el control ya está escuchando los cambios realizados en la colección y actualizando el árbol, el nodo del documento se quita en cuanto se quita de la colección.

Ejecución de ejemplo de una actividad

Esta es una ilustración de ejemplo de lo que sucede cuando el usuario hace clic en el botón Abrir de la barra de herramientas.

Figura 11. Flujo de ejecución cuando el usuario hace clic en el botón Abrir

En primer lugar, la barra de herramientas recibe el evento click y llama al método Click del objeto Button que representa el botón Abrir de la barra de herramientas. Un controlador de clic predeterminado se adjunta con el objeto de botón que recibe la notificación. A continuación, llama a Application.Open().. El método no hace nada más que generar el evento OnOpen . El formulario principal captura este evento y muestra el cuadro de diálogo Abrir archivo de .NET Framework. Cuando el usuario selecciona un archivo, se carga el archivo y se crea un nuevo objeto Document para el archivo. En cuanto se agrega el nuevo objeto de documento en la colección Application.Documents , el control Explorador de documentos obtiene la notificación y muestra el archivo en el árbol. A continuación se llama al método Show() del nuevo objeto de documento. Esto genera el evento OnShow del documento. El módulo Pestañas de documentos captura el evento, que es responsable de mostrar las pestañas de los documentos. Cuando recibe la notificación, crea un nuevo objeto Tab para el documento que generó el evento. En cuanto se agrega el objeto Tab en la colección Application.Tabs , se desencadena otro evento que también captura el módulo Fichas de documento. Este evento indica al módulo que hay un nuevo objeto tab en la colección tabs que debe mostrarse. Por lo tanto, el módulo crea un nuevo control de pestaña, hospeda el cuadro de texto y muestra el contenido del documento dentro de él.

Clases de marco

El modelo de objetos compatible con la automatización se basa en tres clases de marco: ItemBase, CollectionBase y SelectableCollectionBase. Estas tres clases proporcionan la característica más importante del modelo de objetos: es un modelo de objetos observable. Si hereda la clase de cualquiera de estas, puede observar los cambios realizados en ellos. Por ejemplo, la clase DocumentCollection hereda de CollectionBase. CollectionBase contiene todo el código para generar eventos cada vez que se agrega, quita, borra, etc. Además, la clase de colección también escucha los cambios realizados en todos los elementos que contiene. Como resultado, puede capturar eventos generados a partir de objetos secundarios y reenviarlos a sus propios observadores.

Ilustración 12. Propagación de eventos de elementos de colección

En la imagen anterior se muestra cómo los eventos se propagan de objetos secundarios a objetos primarios. A medida que se propagan los eventos, puede adjuntar eventos a una colección y recibir notificaciones de cambio de cualquiera de los elementos que contiene, independientemente de lo profundo que se encuentra en la jerarquía.

Ilustración 13. Suscríbase a la colección para recibir notificaciones de elementos secundarios

En la imagen anterior se muestra cómo puede suscribirse a objetos individuales para la notificación y también suscribirse a una colección para recibir notificaciones de cualquier objeto dentro de esa colección.

Los eventos se propagan desde el nivel más bajo hasta el nivel más alto. Considere el elemento de menú Salir en el menú Archivo . Cada menuItem se agrega en MenuItemCollection. Se encuentra en un objeto Menu que, a su vez, se encuentra en una MenuCollection. Por lo tanto, si se suscribe a cualquier objeto de menú para cualquier evento, puede recibir notificaciones de cualquiera de los elementos de menú que contiene. Lo mejor de todo, si se suscribe a Application.Menus , recibirá una notificación desde cualquier elemento de menú de cualquier objeto de menú.

ItemBase

Esta clase es para objetos individuales, como Document, Toolbar, ToolbarItem, etc. Extienda desde este objeto solo cuando esté interesado en recibir notificaciones de cambios de un objeto.

La clase contiene un método muy útil denominado ListenCollection, que toma una colección y se suscribe a todos sus eventos. Necesitará este método para esas clases compuestas como Menu que contengan sus propias colecciones, como MenuItemCollection. Para admitir la propagación de eventos, debe capturar todos los eventos generados desde la clase de colección secundaria para que pueda propagarlos hacia arriba en la jerarquía del modelo de objetos. Por lo tanto, simplemente llame al método ListenCollection y pase la colección a la que desea escuchar y recibirá todos los eventos de él. Lo único que debe hacer es generar eventos de tal manera que el objeto original que generó el evento al principio se lleva a cabo.

CollectionBase

Esta clase útil es para las clases de colección que son colecciones de heredadores de ItemBase . Algunos ejemplos son DocumentCollection, ToolbarCollection y MenuCollection. Proporciona todos los códigos para admitir la propagación de eventos de elementos secundarios. Cada vez que se agrega un objeto a él, se suscribe a los eventos expuestos por ItemBase. Como resultado, cada vez que se genera un evento desde el objeto secundario, primero recibe el evento y, a continuación, genera el evento a través de sus propios eventos. Por ejemplo, si recibe un evento OnChange de un elemento, reenvía el evento generando OnItemChange(item). Esto proporciona una característica muy útil. Puede suscribirse a una colección, en lugar de objetos individuales, y obtener eventos de todos los elementos que contiene.

SelectableCollectionBase

Esta clase extiende CollectionBase y expone la propiedad Selected y una colección SelectedItems . ItemBase contiene la propiedad Selected. Cuando se establece en true, si el elemento está dentro de una colección seleccionable, la propiedad Selected de la colección se establece en ese elemento. El elemento también se agrega en la colección SelectedItems de la clase Collection .

Hay dos modos: selección única y selección múltiple. En el modo de selección única, solo se selecciona un elemento siempre. Tan pronto como recibe un evento de cambio de selección, establece el elemento anterior en modo deselección y establece el nuevo elemento como seleccionado actualmente. El modo multiselección es más complicado. Puede estudiar el código para comprender la lógica. Sin embargo, siempre que solo herede la clase, no es necesario comprender la lógica en absoluto.

Cómo crear su propia aplicación

Veamos cómo empezar a trabajar con su propia aplicación. Esta es una guía paso a paso:

  1. Copie ItemBase, CollectionBase y SelectableCollectionBase en el proyecto.

  2. Cree su propia clase Application . La clase debe ser una clase singleton. Puede copiar la clase de ejemplo y, a continuación, quitar el código no deseado.

  3. Cree clases para los modelos como Person, Contact, File, Relationship, etc. Hereda de ItemBase para todos los objetos únicos.

  4. Cree clases para elementos de la interfaz de usuario, como Ventana, Barra de herramientas, Menú, Menuitem, DockingPane, etc. Todos estos también heredan de ItemBase. Exponga tantas propiedades como pueda para proporcionar un mayor control sobre la interfaz de usuario desde el modelo de objetos.

  5. Cree clases de colección para los modelos. Por ejemplo, PersonCollection, ContactCollection, WindowCollection. Todos estos heredan de CollectionBase o de SelectableCollectionBase.

  6. Exponga los objetos raíz o las colecciones de la clase Application . Por ejemplo, exponga colecciones públicas como Personas, Contactos, Windows y objetos únicos como ActivePerson, CurrentContact y ActiveWindow.

  7. Haga que los elementos de la interfaz de usuario realicen dos tareas básicas:

    a. Escuche el modelo de objetos para ver los cambios y reflejarlos en la interfaz de usuario. Por ejemplo, cuando se desencadena un evento OnClick desde un objeto Button del modelo de objetos, refleje el clic en la interfaz de usuario, haga que el botón se presione, por ejemplo.

    b. Reflejar eventos de la interfaz de usuario al modelo de objetos. Por ejemplo, cuando el usuario hace clic en un botón, obtenga referencia al objeto de botón representativo del modelo de objetos y llame a Click().

Presentación de complementos

Una de las características más importantes del modelo de objetos admitidos por la automatización es que proporciona extensibilidad a tal nivel que puede agregar complementos y capacidad de scripting según lo diseñado. Por ejemplo, en el programa de ejemplo, los módulos externos realizan el control de archivos. Hay dos módulos externos que se cargan en tiempo de ejecución, al igual que una extensión o un complemento. La primera es NewFileHandler, que proporciona las características para crear nuevos archivos y la segunda es TextFileLoader, que proporciona la característica de carga y guardado del archivo de texto. Le sorprenderá saber que no hay código para controlar la característica Nuevo y Abrir en la aplicación principal. El módulo Menú de la aplicación principal solo crea los elementos de menú para Nuevo y Abrir y adjunta un controlador de clic predeterminado para invocar Application.New() y Application.Open() respectivamente. Los complementos en tiempo de ejecución se asocian a los eventos Application.OnNew y Application.OnOpen/OnSave cuando se cargan. Proporcionan la funcionalidad real de crear un nuevo archivo y abrir y guardar contenido de archivo en un archivo. El código siguiente muestra cómo funciona NewFileHandler :

public class NewFileHandler
{
   public NewFileHandler()
   {
      Application.OnNew += new EventHandler(Application_OnNew);
   }

   private void Application_OnNew( object source, EventArgs e )
   {
      // A new file needs to be opened
      
      // Create a blank document with some dummy text
      byte [] data = System.Text.Encoding.UTF8.GetBytes(string.Empty);

      string newName = this.MakeNewDocumentName();
      Document doc = new Document( newName, string.Empty, data, 
new TextDocumentEditor(), null );
      Application.Documents.Add( doc );

      // Show the editor
      doc.Show();

      Application.ShowStatus( this, "New file created." );
   }
}
// When the main form loads, it is initialized this way
new NewFileHandler();

Del mismo modo, el complemento TextFileLoader se suscribe a los eventos Application.OnOpen y Application.OnSave y proporciona la característica de apertura y guardado del archivo de texto. Puede seguir la misma idea y crear BitmapFileLoader, MusicFileLoader, WordFileLoader (etc.) simplemente suscribiéndose a los eventos OnOpen y OnSave de la clase Application .

El modelo de objetos facilita a los desarrolladores reducir la cantidad de código escrito en ensamblados principales y mover la mayoría del código a ensamblados de extensión que se cargan en tiempo de ejecución. La aplicación principal sigue siendo ligera y contiene muy poca lógica de negocios. Como resultado, es más fácil publicar revisiones, correcciones y actualizaciones en las aplicaciones, ya que requiere enviar solo la versión actualizada de esas extensiones.

Característica de scripting

Scripting es la característica de extensibilidad más eficaz de cualquier aplicación de Windows. Los scripts permiten escribir código y ejecutar sobre la marcha, lo que hace que la aplicación haga lo que quiera que haga. El IDE de Visual Studio tiene una ventana de comandos donde puede escribir una línea de código a la vez y ejecutarla al instante.

Figura 14. Ventana de comandos de Visual Studio

Puede escribir código en VBA para controlar el IDE y hacer que haga lo que desee. Esta eficaz capacidad de scripting puede agregar una verdadera extensibilidad a sus propias aplicaciones. Ya ha visto que puede hacer que la interfaz de usuario baile como se indica simplemente llamando a algunos métodos de Application y sus objetos secundarios. Por lo tanto, si puede ejecutar código de C# en tiempo de ejecución, puede proporcionar la característica de scripting. Los usuarios escribirán código con el modelo de objetos y deberá ejecutar el código en tiempo de ejecución.

La aplicación de ejemplo muestra cómo se puede hacer esto. Con el espacio de nombres System.CodeDom.Compiler , tenemos la capacidad de generar código de C#, compilar y, a continuación, ejecutar todo en tiempo de ejecución con nuestro propio código. Así es como se hace:

using( CSharpCodeProvider provider = new CSharpCodeProvider() )
{
   ICodeCompiler compiler = provider.CreateCompiler();
   ...
   string fullSource = this.MakeCode( code );
   CompilerResults results = compiler.CompileAssemblyFromSource(options, fullSource);
   ...
   try
   {
      Assembly assembly = results.CompiledAssembly;
      Type type = assembly.GetType( NAMESPACE_NAME + "." + CLASS_NAME );

      object obj = Activator.CreateInstance( type );
      MethodInfo method = type.GetMethod(METHOD_NAME);
      method.Invoke( obj, null );
   }
   ...
}

Podemos proporcionar a los usuarios un cuadro de texto y recopilar el código que se va a ejecutar y, a continuación, podemos encapsular el código dentro de un método de una clase y, a continuación, compilar un ensamblado fuera de él. Después, podemos obtener la clase y, a continuación, llamar al método dinámicamente mediante reflexión.

Smart Editor tiene una ventana de comandos similar a la siguiente:

Figura 15. Ventana de comandos del Editor inteligente

Puede escribir código dentro de él y ejecutarlo a la vez. Pruebe a ejecutar el código que ya contiene y verá cómo crea una barra de herramientas con 10 botones y abre 10 pestañas de documento nuevas. Lo que pueda hacer desde sus propios archivos de código de C#, ahora puede hacer lo mismo desde esta ventana de comandos.

Además, puede proporcionar una característica de guardado y reproducción de scripts como puede usar Microsoft Word VBA. Esta capacidad de scripting puede dar a la aplicación la "extensibilidad definitiva" y es una característica muy útil para que los usuarios avanzados realicen trabajos rutinarios muy rápidamente.

Conclusión

Las posibilidades del modelo de objetos admitidos por la automatización son infinitas. Hace que la aplicación sea muy reutilizable, con código realmente desacoplado y lógica de interfaz de usuario, y lo mejor de todo lo que proporciona las características de extensibilidad definitivas: complementos y scripting.

 

Acerca del autor

Omar AL Zabir es un estudiante de Ciencias Informáticas en la Universidad Internacional Americana —Bangladesh, (www.aiub.edu) que trabaja actualmente hacia su B.Sc. Desarrolló el sistema de colaboración y automatización basado en Web para su universidad mediante .NET Framework hace casi tres años, cuando estaba en la fase Beta 1. También ha trabajado durante siete años (comenzando en la escuela secundaria) en Orion Technologies como desarrollador principal, desarrollando soluciones para grandes bancos en el Estados Unidos. Su amor por las tecnologías de Microsoft se puede ver en su sitio web www.oazabir.com.