Export (0) Print
Expand All
Abortable Thread Pool
The Analytic Hierarchy Process
API Test Automation in .NET
Asynchronous HttpWebRequests, Interface Implementation, and More
Bad Code? FxCop to the Rescue
Basics of .NET Internationalization
Behind the Scenes: Discover the Design Patterns You're Already Using in the .NET Framework
BigInteger, GetFiles, and More
Binary Serialization of DataSets
Building Voice User Interfaces
Can't Commit?: Volatile Resource Managers in .NET Bring Transactions to the Common Type
CLR Inside Out: Base Class Library Performance Tips and Tricks
CLR Inside Out: Ensuring .NET Framework 2.0 Compatibility
CLR Inside Out: Extending System.Diagnostics
CLR Profiler: No Code Can Hide from the Profiling API in the .NET Framework 2.0
Concurrent Affairs: Build a Richer Thread Synchronization Lock
Custom Cultures: Extend Your Code's Global Reach With New Features In The .NET Framework 2.0
Cutting Edge: Collections and Data Binding
Const in C#, Exception Filters, IWin32Window, and More
Creating a Custom Metrics Tool
DataGridView
DataSets vs. Collections
Determining .NET Assembly and Method References
Experimenting with F#
File Copy Progress, Custom Thread Pools
Finalizers, Assembly Names, MethodInfo, and More
Got Directory Services?: New Ways to Manage Active Directory using the .NET Framework 2.0
High Availability: Keep Your Code Running with the Reliability Features of the .NET Framework
How Microsoft Uses Reflection
ICustomTypeDescriptor, Part 2
ICustomTypeDescriptor, Part 1
Iterating NTFS Streams
JIT and Run: Drill Into .NET Framework Internals to See How the CLR Creates Runtime Objects
Lightweight UI Test Automation with .NET
Low-Level UI Test Automation
Make Your Apps Fly with the New Enterprise Performance Tool
Managed Spy: Deliver The Power Of Spy++ To Windows Forms With Our New Tool
Memory Models: Understand the Impact of Low-Lock Techniques in Multithreaded Apps
Microsoft Java Virtual Machine Update
Microsoft .NET Framework Delivers the Platform for an Integrated, Service-Oriented Web, Part 2
Mini Dump Snapshots and the New SOS
Mutant Power: Create A Simple Mutation Testing System With The .NET Framework
NamedGZipStream, Covariance and Contravariance
.NET Internationalization Utilities
.NET Profiling: Write Profilers With Ease Using High-Level Wrapper Classes
No More Hangs: Advanced Techniques To Avoid And Detect Deadlocks In .NET Apps
The Perfect Host: Create and Host Custom Designers with the .NET Framework 2.0
Phoenix Rising
Scheme Is Love
Security Enhancements in the .NET Framework 2.0
Sepia Tone, StringLogicalComparer, and More
Software Testing Paradoxes
Stay Alert: Use Managed Code To Generate A Secure Audit Trail
Stream Decorator, Single-Instance Apps
StringStream, Methods with Timeouts
SUPERASSERT Goes .NET
Tailor Your Application by Building a Custom Forms Designer with .NET
Test Harness Design Patterns
ThreadPoolPriority, and MethodImplAttribute
ThreadPoolWait and HandleLeakTracker
Three Vital FXCop Rules
A Tidal Wave of Change
To Confirm is Useless, to Undo Divine
Touch All the Bases: Give Your .NET App Brains and Brawn with the Intelligence of Neural Networks
Transactions for Memory
Trustworthy Software
Tune in to Channel 9
UDP Delivers: Take Total Control Of Your Networking With .NET and UDP
UI on the Fly: Use the .NET Framework to Generate and Execute Custom Controls at Run Time
Unexpected Errors in Managed Applications
Unhandled Exceptions and Tracing in the .NET Framework 2.0
Using Combinations to Improve Your Software Test Case Generation
Wandering Code: Write Mobile Agents In .NET To Roam And Interact On Your Network
What Makes Good Code Good?
XML Comments, Late-bound COM, and More
Expand Minimize

Implement a Microsoft Word-like Object Model for Your .NET Framework Application

 

Omar AL Zabir
OmarAlZabir@gmail.com

August 2005

Download the sample code associated with this article, OfficeAutomation.msi.

Summary: Shows you a way to implement a Microsoft Word-like object model for your own .NET Framework application, following the Model-View-Controller design pattern. (26 printed pages)

Contents

Overview of the Microsoft Word Object Model Hierarchy
Benefits of the Object Model with Automation Support
Comparison with the Model-View-Controller Pattern
The Making of Smart Editor
The Automation Supported Object Model and Object Model of the Application
How the Presentation Layer Uses the Object Model
Sample Execution of an Activity
Framework Classes
How to Make Your Own Application
Introducing Plug-ins
Scripting Feature
Conclusion

All Microsoft Office applications are built on top of an object model that supports automation. Any developer can use the 0bject model to drive the application UI and add, edit, and delete content, just as a real user interacts with the application. The rich object model, together with automation support, makes Office applications truly extensible and pluggable. Anyone can write a powerful add-in within a very short time in order to extend the behavior of Microsoft Word according to their own need. As good object-oriented (OO) developers, we develop our applications with rich architecture and with a reasonably good object model following the Model-View-Controller (MVC) design pattern.

However, until recently, very few applications have been developed that offer automation similar to Microsoft Office applications. As a result, we cannot extend our applications the same way we can extend and customize Office applications using the .NET Framework and Microsoft Visual Basic for Applications (VBA). This article shows you a way to implement a Microsoft Word-like object model for your own .NET application. We will be following the Model-View-Controller design pattern and also .NET Framework events and delegates heavily. The object model we will develop here will add infinite extensibility to our application. It will give us the opportunity to add plug-in and scripting capability to our applications as designed, without writing additional code for them. The plug-in and scripting feature will have the same power as the core application. The design will also produce a very clean code base that truly decouples business logic from the UI logic. Best of all, we will be able to write code to drive the UI and thus create test scripts that not only test business logic but also test the UI behaviors.

Overview of the Microsoft Word Object Model Hierarchy

Let's start with the Microsoft Word object model. Those who already know how the Microsoft Word object model works can skip to the next section.

Microsoft Word's automation-supported object model starts from the Application class. An instance of Word has a singleton application object that provides the toolbars, menus, status bar, and so on.

Figure 1. The Microsoft Word object model (partial)

The entire application can be driven by the Application class. We can access any documents, manipulate the content, add command bars, change menu labels and icons, programmatically click a button, show status messages, and so on. For example, you can make Word show a File Open dialog box by calling Application.Documents.Open(). You can make Word quit by calling Application.Quit(). You write code and Microsoft Word acts according your code. There is no need for you, nor for the Word developers, to find the module that is showing a document or call its public method to tell it to change the document name. All you do is get the instance of the Document object and then modify its Name property. The UI responds to it immediately. This is the beauty of an object model that supports automation.

Before I implemented the automation-supported object model for my own applications, I used to look at the Visual Studio IDE with deep respect and marvel at it. I used to wonder how Visual Studio's developers keep track of so many aspects of a particular event. For example, think how many things happen when a file is deleted from the Solution Explorer tree when you press the DEL button. The tree node is removed, the project and solution is set to pending save mode (dirty), the window title bar changes, the status bar changes, if the document is open then its tab is closed, all the command bars get updated, the menu bar gets disabled, and so on. The entire IDE goes through a lot of changes. Think about the poor guy who implemented this file delete feature. Did the person ever imagine that all of these actions need to be performed just for a mere file delete feature? The truth is that the person need not worry about all of these at all because the Visual Studio has a marvelous automation-supported object model. After learning about such object models I found that such applications are actually not that complicated.

Benefits of the Object Model with Automation Support

Let's have a quick review why an automation supported object model is so much beneficial. When you have such object model for your own application, always keep in mind three easy principles:

  • Don't always think about the UI.
  • Focus on doing small units of work at a time.
  • Let each module do its own job.

Don't Always Think About the UI

I have seen programmers often focus first on the UI whenever they are given a task. If you ask a programmer to load the files in a folder and show it in a tree view (like Windows Explorer), generally the person will think of the following:

Figure 2. Windows Explorer showing the files and folders in a tree view

  • How to read the file system?
  • How to populate tree view nodes?
  • How to get hold of that tree view from the class that will populate it?
  • How to pass reference of the tree view to that class?
  • How to reflect changes in the file system to that tree view?
  • How to update the object model whenever the tree view nodes are changed by users?

You see, a programmer's worries are mostly about reflecting a particular action on the UI. If you tell the programmer just to populate a collection class with the files and folders and tell him that as soon as you add an object in the collection, by some magic it will be automatically displayed on the tree view, that person can freely concentrate more on his own tasks and not worry about all the other parts of the application at all.

An automation-supported object model frees developers from thinking about the implementation of a task from beginning to end, from the data access layer to the front end. A programmer can focus on developing a particular without thinking about the rest of the system.

Focus on Doing Small Unit Tasks

When you have an automation-supported object model, your design and implementation process is narrowed down to small unit tasks. For example, first you think how to populate the Application.Files collection with the files from the file system. You need not know whether there is any tree view in the application that will show the files at all. Always have this running in your mind: "There's no UI, there's no UI . . . " There may not be one tree view; there can be three tree views and four list boxes that need to be populated with the same file list. You need not know how many of them are there, you just need to think how to read the files and then call Application.Files.Add( new File( ... ) ) in order to populate the collection.

Figure 3. Visual Studio .NET Solution Explorer

Now let's come to the UI part of the task. Let's say you are making a Visual Studio-like application (no kidding!). The developer who is responsible for building the "solution explorer" of your application knows that, whenever an item is added in the Application.Files collection, it needs to be added in the solution explorer tree. The person need not know where the file object is coming from. It may be coming from the file system, it may be a new file added by the user that is not yet in the file system, it may be added by a third-party plug-in or from a macro. Again, the solution explorer developer knows that if the user deletes an item from the Application.Files collection, it needs to be deleted from the file system. But that may not be all that needs to be done. The file may be a logical file that is not in the file system. It may be something that a plug-in has produced. The person needs to know only that the File object must be removed whenever the user deletes it. The other person who is responsible for adding that file object in the collection is already listening for changes on that object and will act accordingly whenever the object is removed.

Let Each Module Do Its Own Job

When we have an automation-supported object model, we can distribute the responsibility of implementing some features to the persons who are responsible for different modules, regardless of whether they know about the others' existence. This means that the solution explorer guy need not know that there's a document tabs guy who needs to activate a tab when a file is double-clicked on the solution explorer. The document tab guy need not know that there's a Window menu guy who needs to show all the open document tabs. All they need to know is there's an object model and in that object model there are model objects that will notify them whenever something happens to them.

Imagine what happens when you delete a file from the solution explorer tree. The file name from the Window menu needs to be removed. If the file is open, the document window needs to be closed. The file needs to be removed from the file system. The project needs to be marked as "dirty" or save pending. If there is no open document, we need to disable menus, disable command bars, and so on. There are so many things to do just when user presses a simple DEL button on a file. When I was not aware of the concept of automation-supported object model, I used to write code this way:

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

I needed to have a mental picture of the entire application while implementing any feature, however small it was. Of course I used a proper command pattern, very well designed classes to do the jobs, and a highly modularized application. Nevertheless, thoughts of the complete implementation always ran in my brain and that created a great pressure when the number of modules increased day by day.

When you have an automation-supported object model you do it this way:

  1. The Document Viewer waits for some notification that a file is removed and closes the tab that shows the document.
  2. The Solution Explorer tree waits for some notification that a file is removed and then removes the file from the tree.
  3. The File Menu gets notification and removes the file item.
  4. The Window Menu gets notification and removes the file name from its list.
  5. The file system manager gets notification and deletes the file.
  6. Some running plug-ins that have attached themselves with the Application.Files collection get Remove notification and act accordingly.
  7. The Status bar receives notification and shows a message.

Different modules of the application subscribe to different objects and collections exposed by the Application object and listen to notifications relevant to them. When a notification is received, they only do what is relevant to them and ignore other notifications. They don't care about the others' existence at all.

Comparison with the Model-View-Controller Pattern

Those who know the MVC design pattern must be saying to themselves, this is nothing but MVC! This type of object model does follow the principles of Model-View-Controller, which are:

  • You have a model that contains your data. For example, Car is a model.
  • You have controller(s) to make changes on the model. For example, Driver is a controller that drives a car.
  • You have view(s) that show output on the UI. The Controller uses View to show output from the model. For example, FuelIndicator may be a view that shows the Fuel property of the car object.
  • Controllers and views observe the model for changes and whenever a change is found, view reflects the change on the UI and controller updates the view and/or updates the model. For example, as soon as the "Car.Started" property changes to "True", view makes the sound "Vroom!" and controller updates the Car model by setting "Car.Accelerator=1000".
  • View receives user action and reflects the change on the model. For example, when driver switches off the car, it sets "Car.Started = false".

Figure 4. MVC sample object model

MVC is a very widely used design pattern for designing desktop applications. You will see significant difference in the complexity of a program's code before implementing MVC and after implementing MVC. The best description of MVC can be found at http://java.sun.com/blueprints/patterns/MVC.html. Here I quote two paragraphs from that page:

Several problems can arise when applications contain a mixture of data access code, business logic code, and presentation code. Such applications are difficult to maintain, because interdependencies between all of the components cause strong ripple effects whenever a change is made anywhere. High coupling makes classes difficult or impossible to reuse because they depend on so many other classes. Adding new data views often requires reimplementing or cutting and pasting business logic code, which then requires maintenance in multiple places. Data access code suffers from the same problem, being cut and pasted among business logic methods.
The Model-View-Controller design pattern solves these problems by decoupling data access, business logic, and data presentation and user interaction.

In our object model, the model is the Application.Files collection and the controller is the "Solution Explorer" module and view is the tree view that shows the files. However, the fundamental difference between regular MVC and this model is that the controller is not aware of the view. As I mentioned at the beginning—"Don't think about the UI always"—the controller does not know that there's a view. Whatever it needs to do, it does on the model. The model sits between the view and the controller to decouple them completely and provide an architecture where developers can think about and develop a system without ever worrying about the UIs.

Figure 5. MVC modified for our model

Moreover, we usually do not make an object model hierarchy that starts from a singleton class "Application" and provides a complete map of the entire UI. Usually our object model contains entity objects relevant to our business domains. For example, Person, Account, Transaction, and so on are the usual objects we expose through our object model. Normally the object model does not have Buttons, Toolbars, Menu, StatusBar, and so on. If we create an object model that not only reflects the business objects but also the UI structure and make our UI modules respond to actions performed on the object model, we create an object model that can provide automation. This is the principle that Microsoft products use in their applications. As a result, we can extend their applications with great control over the data containing objects and over the UI. We will use this same idea in our application in order to create an object model that provides automation support by using .NET Framework events and delegates and see the end result by making a sample application.

The Making of Smart Editor

The sample project that accompanies this article is a text editor called "Smart Editor." The this editor is "smart" because it has a very extensible and pluggable object model that supports automation. The application looks like the Visual Studio IDE. The object model is also heavily taken from Visual Studio's own object model, which is also similar to the Microsoft Word object model. Surprisingly, almost all Microsoft products have very similar concepts in their object models. After reading this article, you will also find out why all the products share the same design ideas behind their object model and how convenient such design really is.

Figure 6. Smart Editor UI

The Automation-Supported Object Model and Object Model of the Application

The application has a tiny object model that supports automation just like Microsoft Word. The root object is Application. Do not confuse this Application class with the Application class of the .NET Framework. System.Windows.Forms.Application is a class for Windows Forms. Our Application class is Editor.ObjectModel.Application. If you want to use our "Application" class as the default in your code instead of the Windows Forms one, use this declaration at the beginning of your file:

using Application = Editor.ObjectModel.Application;

From now on in this article, "Application" will refer to the Editor.ObjectMode.Application, not the one in the Windows Forms namespace.

Figure 7. Smart Editor Object model

Each of these objects maps to particular UI elements. The following picture explains how the UI is organized.

Figure 8. Smart Editor modules

Figure 8 shows how the entire UI is broken into small user controls. Each area actually represents one user control. The most important part of this UI that I will be referring to throughout the article is the Document Explorer at the top right. Document tabs where the content of a document is shown in a text box, is also frequently used.

Application

This is the singleton class and root of the entire object model. Its constructor initializes the object model:

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

The primary role of its constructor is to initialize the first level of objects in the object model hierarchy.

Each of these collections is mapped with some UI elements. The Toolbars and Menus collection is populated immediately when the UI loads.

Figure 9. Mapping UI elements with the Object Model

Document Collection

The Application class exposes a public collection named "Documents", which is an instance of DocumentCollection. It is derived from a custom-made collection class named SelectableCollectionBase. It exposes a Selected property from where you can always get the currently selected document. It's similar to the Listbox or Treeview control's Items collection, where you have a Selected property that always returns the currently selected item. How this happens is explained later on.

DocumentCollection has a very small amount of code because most of the work is done on the SelectableCollectionBase. It just provides some functions in order to make it strongly typed to the Document class only:

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 ) { }
}

The only requirement you have is to call the base( IsMultiSelect, ParentObject ) with true/false in order to indicate whether this is a multi-select collection and to identify who is the parent of this collection. In order to maintain the hierarchy, a weak reference to parent is always carried on with child objects.

The Document class is also very simple, as it extends from ItemBase (explained later on), which exposes all the features required for such an object model. All you need to do is call base(parent) and pass the parent object's reference:

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 )
   {
      ...
   }

}

Menu Collection

Menu collection is a similar collection of the Menu class. It inherits CollectionBase, not SelectableCollectionBase, as we are not interested in maintaining which menu is now selected. However, this can easily be done just by inheriting from SelectableCollectionBase instead of CollectionBase.

Whenever the UI loads, it creates the Menu object for each menu on the menu bar. For example, File, Edit, View, Tools all are menus and you will find one object for each menu in the Application.Menus collection.

Menu Item Collection

Each menu contains a MenuItem collection. The menu items are stored in this collection. For example,

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

This will return a reference to a representative object for the "File->New" menu item. You can then call its Click method to simulate a menu item click as if the user has clicked the menu item.

Tab Collection

A document is shown as a tab. For each document one tab is created that contains the document editor. Only the open tabs are available in the Tabs collection of the Application class.

Figure 10. Object model of tabs

Whenever a file is opened for editing, a tab is created and added in the collection; when the document is closed, the Tab object is removed from the collection.

You can at any time call the Show method of a Tab object to bring that tab on screen, or call the Close method to close it.

Toolbar collection

For each toolbar on the UI, one instance of Toolbar class is available in the Applications.Toolbars collection. You can get a reference to a toolbar from Application.Toolbars[ index ]. You can also add a new toolbar at run time using the Add method of the Toolbar collection.

Toolbar contains a collection of toolbar items, which can be buttons, drop-down menus, separators, and so on. For each toolbar item, an instance of ToolbarItem is available in the Items collection of Toolbar class. You can get a reference to the New button by using the following code:

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

You can then call the Click method to simulate the button click.

Sample Usage of the Object Model

You can call Application.Quit() from anywhere to terminate the application. You can also call Application.Save() in order to save the current open document, if there is any. The Application.ActiveDocument property always returns the currently selected document that is being edited or selected from Document Explorer. You can also call to the current tab that has the focus from Application.ActiveTab.

How the Presentation Layer Uses the Object Model

The sample application shows one way in which you can design your application when you have an automation-supported object model. The implementation is pretty straight-forward and I have skipped many best practices for the sake of simplicity. Do not consider this as the only way to build applications on top of such an object model.

Main Form

When the main form loads, it first subscribes to all the UI-related generic events that the Application class exposes. For example, Application.OnFileSaveDialog, Application.ShowStatus, etc. These are common and generic UI services that anyone can provide. In our case, it's the main form. It acts as a centralized UI service provider to the Application class. Of course, complicated applications will distribute a greater responsibility to smaller modules. But in this sample application, we will be providing all necessary generic UI support from the main form. Whenever there is a need for showing messages in the UI, changing the title bar of the application window, or showing a common file dialog box, the main form responds to these events raised by the Application class and acts accordingly.

Top Area

This control hosts the menu and toolbars. Although both the menu and toolbar are prepared at design time, at run time it populates the object model with the Menu, Toolbar, and ToolbarItems in order to make the UI elements available for automation.

First, it creates the Toolbar object for the first toolbar. The code is very simple:

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

Then, for each button on the toolbar, it creates ToolbarItems:

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

Similarly, it creates the Menu and MenuItem objects for each menu. For example, the File menu is created this way:

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

The "Top Area" control has several important responsibilities, which are:

  1. It listens to the object model for any changes in Menu, MenuItem, Toolbar, and ToolbarItems. Whenever there is a change, for example if a button is disabled or a caption is changed, it reflects the change in the UI. For example, the Toolbar object provides an OnChange event. Whenever any toolbar item is modified, this event is fired. The Toolbar hosting module receives this event and reflects the change in the UI.
    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. It listens to the object model for adding/removing Menu, MenuItem, Toolbar, and ToolbarItems. Another module can at any time add a new toolbar in the object model. In that case, the Toolbar also needs to be created on the UI with proper design elements. Similarly, if a MenuItem is removed from any Menu object's Items collection, that menu item from the UI also needs to be removed.

Every module that needs to provide automation needs to provide these two services—reflect actions on the object model to the UI and reflect actions on the UI to the object model.

Center Area

This control hosts the tabs for open documents. It listens to the changes made in Application.Documents and most important of all, Applications.Tabs. Whenever a new Tab object is added in the Application.Tabs collection, a Tab Control is created containing the document and shown on the UI.

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

It also listens to the Show method call of any Document object because if someone calls doc.Show(), a new tab needs to be created and added to the Tabs collection, which in turn shows the Document in edit mode.

Document Explorer Tree

Whenever the user creates a new file or opens a document, it is added to the Document Explorer Tree. This tree represents the Application.Documents collection. It performs two basic tasks:

  1. It listens to changes made in Application.Documents collection. Whenever an item is added/edited/deleted, it responds to the change and does something on the tree. For example, whenever a new Document is added in the DocumentCollection, the following code executes:
    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. Whenever the user performs some action on the tree, such as deleting a file, it the changes are reflected on the Application.Documents collection. For example, when the user presses the DEL button, the treeview keypress event is raised and is handled this way:
    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();
          }
       }
       
    }
    
    

All it does is just delete the Document object from the object model. As the control is already listening to the changes made in the collection and refreshing the tree, the node for the document gets removed as soon as it is removed from the collection.

Sample Execution of an Activity

Here's a sample illustration of what happens when the user clicks the Open button from the toolbar.

Figure 11. Execution flow when user clicks Open button

First, the toolbar receives the click event and calls the Click method of the Button object that is representing the Open button on the toolbar. A default click handler is attached with the button object that receives the notification. It then calls Application.Open(). The method does nothing but raise the OnOpen event. The main form captures this event and shows the .NET Framework File Open dialog box. When the user selects a file, the file is loaded and a new Document object is created for the file. As soon as the new document object is added in the Application.Documents collection, the Document Explorer control gets the notification and shows the file in the tree. Next the Show() method of the new document object is called. This raises the OnShow event of the document. The event is captured by the Document Tabs module, which is responsible for showing tabs for documents. As it receives the notification, it creates a new Tab object for the document that raised the event. As soon as the Tab object is added in the Application.Tabs collection, another event is fired that is also captured by the Document Tabs module. This event tells the module that there's a new tab object in the tabs collection that needs to be shown. So, the module creates a new tab control, hosts the text box, and shows the document content inside it.

Framework Classes

The automation-supported object model is built on top of three framework classes: ItemBase, CollectionBase, and SelectableCollectionBase. These three classes provide the most important feature of the object model: it is an Observable object model. If you inherit your class from any one of these, you can observe changes made to them. For example, the DocumentCollection class inherits from CollectionBase. CollectionBase contains all the code to raise events whenever an item is added, removed, cleared, and so on. Moreover, the collection class also listens to changes made in all the items it contains. As a result, it can capture events raised from child objects and forward them to its own observers.

Figure 12. Event bubbling of collection items

The picture above shows how events bubble from child objects to parent objects. As the events bubble, you can attach events to a collection and receive change notifications from any of the items it contains, no matter how deep it is in the hierarchy.

Figure 13. Subscribe to the collection in order to get notification from child items

The above picture shows how you can subscribe to individual objects for notification and also subscribe to a collection in order to receive notification from any object inside that collection.

The events are bubbled from the lowest level to the top-most level. Consider the menu item Exit in the File menu. Each MenuItem is added in MenuItemCollection. It is contained in a Menu object that in turn is contained in a MenuCollection. So, if you subscribe to any menu object for any event, you can get notification from any of the menu item it contains. Best of all, if you subscribe to Application.Menus you will get notification from any menu item of any menu object.

ItemBase

This class is for single objects like Document, Toolbar, ToolbarItem, and so on. Extend from this object only when you are interested in getting notification of changes from an object.

The class contains a very useful method called ListenCollection, which takes a collection and subscribes to all its events. You will need this method for those composite classes like Menu that contain their own collections like MenuItemCollection. In order to support event bubbling, you need to capture all the events raised from the child collection class so that you can bubble them upward in the object model hierarchy. So, just call the ListenCollection method and pass the collection that you want to listen to and you will be receiving all the events from it. All you need to do is raise events in such a way that the original object that raised the event at first is carried on.

CollectionBase

This handy class is for the collection classes that are collections of ItemBase inheritors. Examples include DocumentCollection, ToolbarCollection, and MenuCollection. It provides all the codes to support event bubbling from child items. Whenever an object is added to it, it subscribes to the events exposed by ItemBase. As a result, whenever an event is raised from the child object, first it receives the event and then raises the event via its own events. For example, if it receives an OnChange event from an item, it forwards the event by raising OnItemChange(item). This provides a very useful feature. You can subscribe to a collection, instead of individual objects, and get events from all the items inside it.

SelectableCollectionBase

This class extends the CollectionBase and exposes the Selected property and a SelectedItems collection. ItemBase contains the property Selected. When it is set to true, if the item is inside a Selectable Collection, the Selected property of the collection is set to that item. The item is also added in the SelectedItems collection of the Collection class.

There are two modes: single selection and multiple selection. In single selection mode, only one item is always selected. As soon as it receives a selection change event, it sets the previous item to deselected mode and sets the new item as currently selected. The multiselection mode is more complicated. You can study the code to understand the logic of it. However, as long as you just inherit the class, you don't need to understand the logic at all.

How to Make Your Own Application

Let's walk through how you can get started with your own application. Here's a step-by-step guide:

  1. Copy ItemBase, CollectionBase, and SelectableCollectionBase to your project.
  2. Make your own Application class. The class needs to be a singleton class. You can copy the sample class and then remove the unwanted code.
  3. Create classes for the models like Person, Contact, File, Relationship, and so on. Inherit from ItemBase for all single objects.
  4. Create classes for UI elements, such as Window, Toolbar, Menu, Menuitem, DockingPane, and so on. All these also inherit from ItemBase. Expose as many properties as you can in order to provide greater control over the UI from the object model.
  5. Create collection classes for the models. For example, PersonCollection, ContactCollection, WindowCollection. All these either inherit from CollectionBase or from SelectableCollectionBase.
  6. Expose the root objects or collections from the Application class. For example, expose public collections like Persons, Contacts, Windows, and also single objects like ActivePerson, CurrentContact, and ActiveWindow.
  7. Make UI elements perform two basic tasks:

    a. Listen to the object model for changes and reflect them on the UI. For example, when an OnClick event is fired from a Button object from the object model, reflect the click on the UI—make the button look pressed, for example.

    b. Reflect events from the UI to the object model. For example, when the user clicks a button, get reference to the representative button object from the object model and call Click().

Introducing Plug-ins

One of the greatest features of the automation-supported object model is that it provides extensibility at such a level that you can add plug-ins and scripting ability as designed. For example, in the sample program, file handling is done by external modules. There are two external modules that are loaded at run time just like an extension or plug-in. The first is NewFileHandler, which provides the features for creating new files and the second is TextFileLoader, which provides the text file loading and saving feature. You will be surprised to know that there is no code for handling the New and Open feature in the core application. The core application's Menu module just creates the menu items for New and Open and attaches a default click handler to them in order to invoke Application.New() and Application.Open() respectively. Run-time plug-ins attach themselves to Application.OnNew and Application.OnOpen/OnSave events when they load. They provide the actual functionality of creating a new file and opening and saving file content to a file. The following code shows how the NewFileHandler works:

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();

Similarly, the TextFileLoader plug-in subscribes to Application.OnOpen and Application.OnSave events and provides the text file open and save feature. You can follow the same idea and create BitmapFileLoader, MusicFileLoader, WordFileLoader (and so on) just by subscribing to the OnOpen and OnSave events of the Application class.

The object model makes it easy for developers to reduce the amount of code written in core assemblies and move most of the code to extension assemblies that are loaded at run time. The core application remains lightweight and contains very little business logic. As a result, it is easier to release patches, fixes, and updates to applications since it requires sending updated version of those extensions only.

Scripting Feature

Scripting is the most powerful extensibility feature of any Windows application. Scripts allow you to write code and execute on-the-fly, which makes the application do whatever you want it to do. The Visual Studio IDE has a Command Window where you can write one line of code at a time and instantly execute it.

Figure 14. Visual Studio Command Window

You can write code in VBA to drive the IDE and make it do whatever you want. Such powerful scripting ability can add true extensibility to your own applications. You have already seen that you can make the UI dance as you instruct it just by calling some methods from Application and its child objects. So, if you can execute some C# code at run time, then you can provide the scripting feature. Users will write code using the object model and you need to execute the code at run time.

The sample application shows you how this can be done. Using the System.CodeDom.Compiler namespace, we have the power to generate C# code, compile, and then run all at run time using our own code. Here's how it is done:

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 );
   }
   ...
}

We can provide users with a text box and collect the code to execute, and then we can wrap the code inside a method of a class and then compile an assembly out of it. After that, we can get the class and then call the method dynamically using reflection.

Smart Editor has a command window that looks like this:

Figure 15. Smart Editor Command Window

You can write code inside it, and get it executed at once. Try running the code it already contains and you will see how it creates a toolbar with 10 buttons and opens 10 new document tabs. Whatever you can do from your own C# code files, you can now do the same from this command window.

Furthermore, you can provide a script save and playback feature just like you can using Microsoft Word VBA. Such scripting ability can give your application "ultimate extensibility" and is a very handy feature for power users to perform routine jobs very quickly.

Conclusion

The possibilities of the automation-supported object model are endless. It makes your application highly reusable, using truly decoupled code and UI logic, and best of all it provides you the ultimate extensibility features—plug-ins and scripting.

 

About the author

Omar AL Zabir is a student of Computer Science in American International University—Bangladesh, (www.aiub.edu) currently working toward his B.Sc. He developed the Web-based automation and collaboration system for his university using the .NET Framework almost three years ago when it was in the Beta 1 stage. He has also worked for seven years (beginning when he was in high school) at Orion Technologies as Lead Developer, developing solutions for large banks in the United States. His love for Microsoft technologies can be seen at his Web site www.oazabir.com.

Show:
© 2014 Microsoft