Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Calcium: A modular application toolset leveraging PRISM – Part 1

0.00/5 (No votes)
1 Jun 2009 13  
Calcium provides much of what one needs to rapidly build a multifaceted and sophisticated modular application. Includes a host of modules and services, and an infrastructure that is ready to use in your next application.

Caclium Logo

Contents

Introduction

Calcium is a WPF composite application toolset that leverages the Composite Application Library. It provides much of what one needs to rapidly build a multifaceted and sophisticated modular application.

Calcium consists of a client application and server based WCF services, which allow interaction and communication between clients. Out of the box, Calcium comes with a host of modules and services, and an infrastructure that is ready to use in your next application.

I have deployed Calcium to a demonstration server so you can see it in action. My server has some connectivity issues due to its firewall, so you'll need to download the project to see all of its features.

Calcium Overview

Diagram: Calcium overview.

The following is a list of some of Calcium’s main features:

  • Duplex messaging services for interacting with the user from the client or server using the same API. Interact with the user using Message Boxes from the server!
  • Module Manager for enabling or disabling of modules at runtime.
  • A User Affinity module that assists collaboration with other users of the application.
  • A Command Service to associate WPF ICommands with content interfaces that only become active when an active view or viewmodel implements the interface.
  • Region Adapters for ToolBars and Menus.
  • Client-server logging ready to work out-of-the-box.
  • Includes modules, such as a Web Browser, Text Editor, Output Window, and many more.
  • Tabbed interface with dirty file indication (reusable across modules).
  • And many more!

Calcium Screenshot

Diagram: Calcium Desktop Environment

We’ve got a lot of ground to cover. For that reason I’ve decided to break this article up into a series of three, maybe four articles.

  1. Introduction to Calcium, Module Manager. (this article)
  2. Message Service, WebBrowser module, Output Module
  3. File Service, View Service, Rebranding Calcium
  4. TBA

In this article you will learn how to:

  • use Prism to load controls into UI regions;
  • define regions;
  • create a custom Prism IModuleCatalog in order to customize how Prism locates assemblies;
  • customize module initialization to deal with exceptions;
  • create a custom Module Manager to allow the user to selectively disable or enable modules;

Some of the content in this series of articles are not at an advanced level and will suit even those that are new to Prism. While others, such as the messaging system, will suit more advanced readers. Hopefully there are things here to learn for everyone.

This article is, in some respects, an introduction to some areas of Prism. Although, if you are completely new to Prism, you may find yourself somewhat overloaded at times, and I would recommend taking a look at some beginner Prism articles before you tackle Calcium.

Background

Building large applications is a challenge. As the size of an application grows, and as the size of the team grows, the difficulty in managing that growth becomes exponential. Team members may find themselves increasingly stepping on one another’s toes. Prism provides the means to construct an application using discrete pieces, known as modules, which interact with one another via events, commands, and services. The advantages of modular development are not limited to team and developer independence; even a small team can benefit from this approach. Modules are discovered at runtime and can be selectively enabled and disabled. Modular development can improve maintainability and testability. Also, modularization allows for licensing based on features, where a particular group of modules may be associated with e.g., a standard version, while a greater set may form a professional version.

While Prism comes with some great example applications, it still takes some time to get up to speed and create a usable shell. And there is certainly a lot more infrastructure required besides Prism. This is where Calcium comes in. It is an example application, but it is also a starter-kit with substantial infrastructure consisting of a host of services, utilities, and UI components.

I’m sending Calcium out into the world, so that others may gain a head start in developing their own composite applications.

Components and Dependencies

We begin this article by briefly looking at the composition of the client and server applications. Calcium is comprised of a set of dedicated client-side, dedicated server-side, and shared components. The following, albeit busy, diagram provides an overview of some of the main types used in Calcium

Calcium Component Diagram

Diagram: Calcium Component Diagram

The Calcium DesktopClient project has a number of required modules contained within the project, while the remainder is located externally to the main DesktopClient project.

Application Startup

The entry point for the application is the Client.Launcher.App class. A launcher project is used to cater for differing deployment scenarios. This approach can be rather useful in cases such as when using e.g., ClickOnce, where multiple deployment settings are required for different environments. We can provide different settings by using a launcher project for each ClickOnce scenario (e.g., Development/Release).

Our Launcher entry point piggybacks the AppStarter class in the primary Client project. It is the AppStarter that displays the splash screen, loads resources, runs the Bootstrapper, and displays the shell or main window.

Startup sequence diagram

Diagram: Appliication initialization and startup

Bootstrapper

The Bootstrapper’s task is to initialize the environment for the application. This includes registering types to be resolved by the Dependency Injection container (Unity), providing Prism with its required IModuleCatalog instance so that it can load our modules. It is also tasked with creating the application’s main window or shell in the Prism parlance.

DesktopShell

The DesktopShell is the main window for our Desktop CLR version of Calcium. The shell in Calcium implements an interface called IShell, which is located in the Client project.

Calcium screenshot with browser

Diagram: DesktopShell regions

The DesktopShell has four regions that may be populated:

  • Tools (left)
  • Workspace (center)
  • Properties (right)
  • Footer (bottom)

The StandardMenu and StandardToolBarTray controls also play host to a number of regions, which can be used to add module specific Menus, MenuItems, and ToolBars.

Content Interface Approach

One of the challenges when creating a multi-view application in WPF is to allow RoutedCommands to be properly activated when the command target may not be the view that has focus. The approach I have taken is to allow ICommands to be arbitrarily associated with particular content interfaces, and registered with the shell. Thus, we can use custom logic to evaluate when a command should become active.

Certain tasks should be performed the same way, in all situations, by all parts of an application. These include such things as saving files or printing documents. Thus we require a mechanism to have tasks such as these delegated by a centralized facility. For this we make use of a Command Service. We will go into further detail and see how this is performed when we look at the WebBrowser module in the next article in the series.

Modularity

In Prism, we define scaffolding, consisting of the shell Bootstrapper etc., to which we add modules, which are the building blocks of an application.

In order to accomplish runtime discovery of modules, Prism makes use of an IModuleCatalog implementation, which must be defined in our Bootstrapper. Calcium’s CustomModuleCatalog is based on Prism’s DirectoryModuleCatalog. One difference, however, is that we allow modules to be loaded from assemblies that have already been loaded into the AppDomain, as is the case with our core modules: ModuleManager, Communication, Output, and WebBrowser modules. These are all located in the Client project.

Modules

This release of Calcium comes with several modules.

  • ModuleManager
    Provides the user with the capability to enable and disable modules.
  • WebBrowser
    A simple web browser module, which includes an address bar that becomes active when a browser window is displayed.
  • Output
    Displays output messages dispatched via a composite event.
  • TextEditor
  • A notepad like module that demonstrates the content command associability.
  • UserAffinity
    A module that provides the Calcium client with awareness of other users using the application.
  • Communication
    Provides duplex messaging support so that the server may directly interact with the user during WCF calls.

Dependency Injection

  • Prism provides support for dependency injection (DI) with its Unity extensions. It also, however, allows Unity to be replaced with another DI container if we so wish. As with most things related to configuring our applications runtime environment, configuring the Unity container is done in the Bootstrapper. The following excerpt demonstrates how we use both the app.config file for configuring the container as well as well known core services.
/// <summary>
/// We configure the unity container using the config file as well as imperatavely.
/// </summary>
protected override void ConfigureContainer()
{
    var section = (UnityConfigurationSection)ConfigurationManager.GetSection("unity");
    section.Containers.Default.Configure(Container);
    /* For resolving types with delegates. */
    Container.AddNewExtension<StaticFactoryExtension>(); 

    /* Composite logging is sent to Clog. */
    Container.RegisterType<ILoggerFacade, CompositeLogAdapter>();
    /* IChannelManager is used to create simplex and duplex WCF channels. */
    Container.RegisterInstance<IChannelManager>(ChannelManagerSingleton.Instance);
    /* By registering the UI thread dispatcher 
     * we are able to invoke controls from anywhere. */
    Container.RegisterInstance<Dispatcher>(Dispatcher.CurrentDispatcher);
    /* Register the module load error strategy 
     * so that if a module doesn't load properly it will be excluded. */
    Container.RegisterType<IModuleLoadErrorStrategy, ModuleLoadErrorStrategy>();
    /* We use a custom initializer so that we can handle load errors. */
    Container.RegisterType<IModuleInitializer, CustomModuleInitializer>();
    /* IViewService will hide and show visual elements 
     * depending on workspace content. */
    Container.RegisterInstance<IViewService>(new ViewService());
    /* Message Service */
    Container.RegisterInstance<IMessageService>(new MessageService());
    /* File Service */
    Container.RegisterInstance<IFileService>(new FileService());

    /* To avoid parameter injection everywhere we expose unity via a singleton. */
    UnitySingleton.Initialize(Container);

    base.ConfigureContainer();
} 

While I have provided out of the box support for config file container configuration, I myself generally try to avoid using config for container configuration, and only do so when there is a known requirement for the extra flexibility that it affords. I do this because config files filled with type mappings can become a maintenance headache, and I also prefer compile-time verification that my type names are correct. May I recommend that if the reader does decide to use mostly config for your application, be sure that there is a unit test defined for all required type mappings. When one refactors or renames something, it can be easy to forget about the container config.

Region Adaptation

Refresher on Prism Regions

To Prism, views are merely objects, which can be placed in containers known as regions, using out-of-the-box or custom Region Adapters. Region Adapters implement IRegionAdapter and decouple the method for populating regions. When building a Prism based application, it is generally the custom to decorate the shell or custom controls with an attached property that defines locations within the shell as regions. In the following excerpt we see how this RegionName attached property declares the tabControl_Right as being the Region with the unique name Properties.

<TabControl x:Name="tabControl_Right" MinWidth="0"
        HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
        IsSynchronizedWithCurrentItem="True" BorderThickness="0,0,0,0" 
        cal:RegionManager.RegionName="{x:Static Desktop:RegionNames.Properties}"

/> 

Once we have a Region defined, we can populate it with view objects from anywhere, but most usefully from our modules. The following excerpt is taken from the ModuleManagerModule and shows how the Region Manager is retrieved from the Unity container, the Properties region is located by name, and then a new instance of the ModuleManagerView is added to the region.

public void Initialize()
{
    var regionManager = UnitySingleton.Container.Resolve<IRegionManager>();
    regionManager.Regions[RegionNames.Properties].Add(new ModuleManagerView());
}

Regions are at the heart of Prism’s UI composition infrastructure. They enable the module author to associate content with known areas within the UI, without specific knowledge about the container within the shell, nor the actual location of the region within the shell. With that, we gain flexibility so that we can avoid reengineering modules if the layout implementation changes.

Calcium Region Adapters

Now that we have a solid understanding of how regions work, let’s examine the method by which Prism is able to take a UI element and place it correctly into a region. The difficulty here is that adding to a region where the host container is of a different type, must normally be done by a different means. For example, we can’t add items to a ToolBar in the same way we would for a TabControl.

In Calcium we have a number of custom IRegionAdapters. These include the ToolBarTrayRegionAdapter, and the CustomItemsControlRegionAdapter. The first ToolBarTrayRegionAdapter allows us to create a ToolBar in a module, and to then slot it into a ToolBarTray at runtime. The second, CustomItemsControlRegionAdapter, can be used for most ItemsControls, but is used explicitly for MenuItems and Menus. With it we are able to define a Menu or MenuItem as being a region. When we add to the region, child Menus and MenuItems are seen.

In order to provide Prism with our custom IRegionAdapter implementations, we overload the ConfigureRegionAdapterMappings in the Bootstrapper, as the following excerpt demonstrates:

protected override RegionAdapterMappings ConfigureRegionAdapterMappings()
{
    var mappings = base.ConfigureRegionAdapterMappings() ?? Container.Resolve<RegionAdapterMappings>();
    mappings.RegisterMapping(typeof(ToolBarTray), Container.Resolve<ToolBarTrayRegionAdapter>());
    mappings.RegisterMapping(typeof(Menu), Container.Resolve<CustomItemsControlRegionAdapter>());
    mappings.RegisterMapping(typeof(MenuItem), Container.Resolve<CustomItemsControlRegionAdapter>());
    return mappings;
}

Region Adapters provide as with not only control over how a UI element is placed in a region’s host container, but also affords us the opportunity to wire up event handlers etc., to suit whatever purpose.

Calcium M-V-VM

Prism is very flexible when it comes to the composition of views. In fact it is oblivious to the structure of our views. Calcium, on the other hand, is as agnostic to view composition as Prism, however Calcium’s IShell implementation has a number of expectations about its views. While they are not requirements, they assist the shell in performing its tasks. One such expectation is that a view placed in a Prism region, shall implement the IViewinterface, shown below.

/// <summary>
/// Represents the visual interface that a user interacts with.
/// </summary>
public interface IView 
{
    /// <summary>
    /// Gets the view model for the view. 
    /// The ViewModel is usually the DataContext of the view.
    /// </summary>
    /// <value>The view model.</value>
    IViewModel ViewModel { get; }
}

Each view associates itself with a particular IViewModel instance. A view’s IViewModel is generally used as its DataContext.

The following excerpt shows the IViewModel interface.

public interface IViewModel
{
    /// <summary>The header on the host tab.</summary>
    object TabHeader { get; }
}

The design requires that a view knows about its viewmodel, but not vice versa.

IView Class Diagram

Diagram: IView, IViewModel, and inheritors.

Generally speaking, it’s not necessary for the ViewModel to interact with the View directly, and some may argue that it shouldn’t at all. It is, in fact, somewhat of a contentious issue. Yet, having knowledge of the view from the viewmodel can be useful in some scenarios, and perhaps isn’t so bad if the view can be easily mocked for testing; and interaction is done exclusively via an interface. So, we see in Calcium that the base IView implementation (ViewControl) has an IViewModel, and likewise the base IViewModel (ViewModelBase) has an IView.

ViewControl Class Diagram

Diagram: ViewControl and ViewModelBase class hierarchies.

The following excerpt shows the ViewModelBase class in its entirety, and shows how an IView instance is required when it is itself instantiated.

public abstract class ViewModelBase : DependencyObject, 
    IViewModel, INotifyPropertyChanged
{
    readonly IView view;

    protected ViewModelBase(IView view)
    {
        ArgumentValidator.AssertNotNull(view, "view");
        notifier = new PropertyChangedNotifier(this);
        this.view = view;
    }

    public IView View
    {
        get
        {
            return view;
        }
    }

    #region TabHeader Dependency Property

    public static DependencyProperty TabHeaderProperty 
        = DependencyProperty.Register(
            "TabHeader", typeof(object), typeof(ViewModelBase));

    [Description("The text to display on a tab.")]
    [Browsable(true)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
    public object TabHeader
    {
        get
        {
            return (object)GetValue(TabHeaderProperty);
        }
        set
        {
            SetValue(TabHeaderProperty, value);
        }
    }

    #endregion

    #region Property Changed Notification
    readonly PropertyChangedNotifier notifier;

    public event PropertyChangedEventHandler PropertyChanged
    {
        add
        {
            notifier.PropertyChanged += value;
        }
        remove
        {
            notifier.PropertyChanged -= value;
        }
    }

    protected void OnPropertyChanged(string propertyName)
    {
        notifier.OnPropertyChanged(propertyName);
    }
    #endregion
}

The supplied TabItemDictionary ResourceDictionary uses the TabHeader property from the view’s ViewModel to display it in the UI. The following shows the DataTemplate for the TabHeader. The styles have been excluded for clarity.

<DataTemplate x:Key="TabHeaderDataTemplate">
    <StackPanel Orientation="Horizontal" VerticalAlignment="Stretch">
        <TextBlock x:Name="textBlock" 
            Text="{Binding TabHeader}" 
            TextTrimming="CharacterEllipsis" 
            TextWrapping="NoWrap" />
        <TextBlock Text="*" 
            Visibility="{Binding Path=Content.Dirty, FallbackValue=Collapsed, Converter={StaticResource BooleanToVisibilityConverter}}" />
        <Button x:Name="button"  
            Command="ApplicationCommands.Close" CommandParameter="{Binding Path=View}" 
            Template="{DynamicResource CloseTabButtonControlTemplate}" ToolTip="Close" />
    </StackPanel>
</DataTemplate>

The two key points to notice with this template are:

Firstly, the binding to the Content.Dirty property of the TextBlock that is used to display an asterisk if the file is dirty and requires saving.

Visibility="{Binding Path=Content.Dirty, FallbackValue=Collapsed, Converter={StaticResource BooleanToVisibilityConverter}}"

Notice the use of the FallbackValue specifier. It is used to hide the TextBlock if the dependency property Content.Dirty can’t be found. If we didn’t use this, the TextBlock visibility would default to Visible if not found.

Secondly, the close button on the tab uses the ApplicationCommands.Close command, and uses the View as the CommandParameter. This enables us to resolve the view to be closed when the button is clicked.

The TabItem template, which references the previous TabHeaderDataTemplate binds the Header property to the View’s ViewModel as shown in the following excerpt.

<Style TargetType="{x:Type TabItem}">
    <Setter Property="Header" Value="{Binding ViewModel}" />
    <Setter Property="HeaderTemplate" 
        Value="{DynamicResource TabHeaderDataTemplate}" />
    <Setter Property="Template"></Style>

The IShell implementation DesktopShell capitalizes on this usage pattern, enabling module creators to specify content interfaces in views or view-models.

What is a content interface? A content interface in Calcium is used to provide shell level functionality, such as saving files or printing, in a standard way; and one that allows the shell to enable or disable the functionality depending on the presence of such content. For example, if a view implements ISavableContent, then the shell will automatically enable/disable the ApplicationCommands.Save and SaveAs commands. We’ll cover this in more detail in the third article of this series.

Module Manager

The Module Manager is itself a Prism module that allows the user to selectively disable and enable modules from within the UI. This capability does not come with Prism out of the box.

In order to accomplish the disabling and enabling of modules, we require several enhancements to Prism. How do we customize module loading with Prism? The answer is that we provide a customized Microsoft.Practices.Composite.Modularity.IModuleCatalog. The purpose of the IModuleCatalog is to provide Prism with an application’s modules, and to determine the dependencies between modules.

Our implementation of IModuleCatalog is DanielVaughan.Calcium.Client.Modularity.CustomModuleCatalog. It, in fact, extends the ModuleCatalog base class from the Prism Composite.Desktop project, and is based on the Prism’s DirectoryModuleCatalog, but with some important differences. In particular, our CustomModuleCatalog class makes use of a number of lists exposed by the Calcium’s ModuleManagerSingleton: FailedModules and NonStartupModules. During startup, when Prism uses the custom IModuleCatalog to locate the modules to be loaded, the custom module catalog excludes those modules in these lists, and all of their dependent modules.

For obvious reasons, the ModuleManager module itself cannot be disabled from the UI.

Module Manager Screenshot

Diagram: Screenshot of the ModuleManagerView tab within Calcium.

Handling Module Load Exceptions

When a module is initialized it can fail. Prism won’t help us much in this regard, and in fact, if one doesn’t provide a custom implementation of Microsoft.Practices.Composite.Modularity.IModuleInitializer, and a module throws an exception during initialization, an application crash may ensue. In the previous version of Prism, there existed no extensibility point for handling module initialization errors. Now, fortunately, we are able to do so by providing a custom implementation of IModuleInitializer. Our CustomModuleInitializer class does just this, and delegates the handling of initialization exceptions to an IModuleLoadErrorStrategy as can be seen in the following excerpt.

public override void HandleModuleInitializationError(
    ModuleInfo moduleInfo, string assemblyName, Exception exception)
{
    /* Here we use the configured error strategy to deal with the error. */
    var errorStrategy = UnitySingleton.Container.Resolve<IModuleLoadErrorStrategy>();
    if (errorStrategy != null)
    {
        errorStrategy.HandleModuleLoadError(moduleInfo, assemblyName, exception);
        return;
    }
    base.HandleModuleInitializationError(moduleInfo, assemblyName, exception);
}

The IModuleLoadErrorStrategy informs the ModuleManagerSingleton that the module has failed, and dispatches a Prism CompositeEvent for good measure; as the following excerpt shows.

class ModuleLoadErrorStrategy : IModuleLoadErrorStrategy
{
    public void HandleModuleLoadError(
        ModuleInfo moduleInfo, string assemblyName, Exception exception)
    {
        ArgumentValidator.AssertNotNull(moduleInfo, "moduleInfo");

        ModuleManagerSingleton.Instance.AddFailedModuleName(moduleInfo.ModuleName);

        var moduleException = new ModularityException(
        moduleInfo.ModuleName, exception.Message, exception);

        Log.Error(moduleException.Message, moduleException);

        var messageService = UnitySingleton.Container.Resolve<IMessageService>();
        /* TODO: Make localizable resource. */
        string moduleLoadUserMessage = string.Format(
            "Module {0} failed to load and has been disabled.", moduleInfo.ModuleName);
        messageService.ShowError(moduleLoadUserMessage);

        var moduleLoadError = new ModuleLoadError { AssemblyName = assemblyName, 
            Exception = exception, ModuleInfo = moduleInfo };
        var eventAggregator = UnitySingleton.Container.Resolve<IEventAggregator>();
        var moduleLoadErrorEvent = eventAggregator.GetEvent<ModuleLoadErrorEvent>();
        moduleLoadErrorEvent.Publish(moduleLoadError);
    }
}

This places the module’s name in a list of failed modules, and will prevent the module and dependent modules from being loaded the next time the application starts.

ModuleManager Module Implementation

The Module Manager consists of the IModule implementation ModuleManagerModule class, the view (ModuleManagerView) and viewmodel (ModuleManagerViewModel).

<Gui:ViewControl x:Class="DanielVaughan.Calcium.Client.Modules.ModuleManager.ModuleManagerView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:Gui="clr-namespace:DanielVaughan.Calcium.Client.Gui">
    <DockPanel x:Name="ContentPanel">
        <DockPanel.Resources>
            <DataTemplate x:Key="checkBoxTemplate">
                <Border>
                    <CheckBox IsChecked="{Binding Path=Enabled, Mode=TwoWay}" 
                        IsEnabled="{Binding Path=UserCanEnable, Mode=OneWay}" />
                </Border>
            </DataTemplate>
        </DockPanel.Resources>

        <ListView HorizontalContentAlignment="Stretch" 
            ItemsSource="{Binding ApplicationModules}" FontSize="8">
            <ListView.View>
                <GridView>
                    <GridViewColumn 
                        CellTemplate="{DynamicResource checkBoxTemplate}" Header="Enabled"/>
                    <GridViewColumn Header="Name" 
                        DisplayMemberBinding="{Binding Path=ModuleName}" Width="75"/>
                    <GridViewColumn Header="State" 
                        DisplayMemberBinding="{Binding Path=State}" />
                </GridView>
            </ListView.View>
        </ListView>
    </DockPanel>
</Gui:ViewControl>

Notes on Modularity

One thing that we have not addressed in this article is the question of how to deal with those modules that achieve only partial initialization. For example, during initialization a module may successfully add a menu item with an associated command to the shell, yet it may throw an exception before it completes initialization, and may be in an inconsistent state. As there exists no automatic mechanism for removing the UI menu item, it will become orphaned and the module may not be capable of actionning the command when it is invoked. To address this issue we could enhance our IModuleInitializer implementation to monitor the state of common UI components for changes during the initialization process, so that if a module should fail, it could be automatically unloaded; views added to regions could be removed etc. I have also done some work on an Undoable Action infrastructure that I will integrate at a later stage. This may also assist in safely undoing a modules partial initialization. Until we have these in place it is the module author that is responsible for detecting and properly cleaning up when his or her module fails.

Future enhancement of the Module Manager

At the moment, disabling and enabling of modules requires an application restart. We can imagine an enhancement to Calcium which would enable dynamic unloading without a restart. I see this as perhaps being accomplished through an implementation of e.g. IUnloadableModule interface at the module level. Each module would know how to unload itself and its views etc. Of course dependencies would also need to be analyzed to prevent the sudden failure of dependent modules.

Conclusion

In this article we have seen how Calcium can be used as a head start for creating a multifaceted modular application. In particular we have seen an overview of Calcium’s Bootstrapper and shell implementations, examined the M-V-VM approach employed in Calcium, explored the content interface approach to command delegation, and looked at developing a Module Manager for enabling or disabling of modules at runtime.

In our next article we shall examine the duplex messaging system, which provides interaction capability with the user from the client or server, using the same API. We can interact with the user using Message Boxes instigated from the server! We will also look at some of Calcium’s other modules, such as the Web Browser and Output Module. We still have a lot to cover, and I hope you will join me for our next installment.

I hope you find this project useful. If so, then I'd appreciate it if you would rate it and/or leave feedback below. This will help me to make my next article better.

History

May 2009

  • Initial release.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here