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

The Service Tree Model

0.00/5 (No votes)
9 Jul 2010 1  
A new application architecture as an alternative to composite architectures such as Prism
Screenshot.png

Introduction

I recently blogged an article which criticized the global event model. By global event model, I mean the pattern found in architectures such as Prism where there is an event aggregator, and components communicate by means of broadcast events. I proposed an alternative model which I call the "service tree" model.

A number of people felt that they could not adequately judge the merits of the argument without a sample application. Fair enough. This article will introduce the alternative model with an application exemplifying it.

Background: An Overview of the Architecture

The goals of this architecture are to:

  • Localize component interdependencies
  • Make contracts between components explicit
  • Connect the components via a configuration file (allowing for quick reconfiguration and reuse), and
  • Allow components to be developed in isolation, without the need for an application infrastructure to drive them

The basic idea is that there is an extended chain of small services (the service tree). Each service performs one function only, and is tightly coupled to its neighbours above and below it in the chain. Communication of events passes up and down the tree, allowing events to be contextualized and translated in the process.

It's useful to note that the service tree is rather similar to the visual tree in a WPF application. One might consider it simply a logical extension of the MVVM pattern. In fact, the sample application even uses XAML to store the configuration of the service trees.

The Application

The sample application requires the 4.0 .NET Framework, as it makes use of certain features that are improved in WPF 4.

The application itself is simply a multi-tab text editor. Its container hierarchy has three levels:

  1. The "root" container - This is created by the shell and contains essential services.
  2. The "application" container - This is created by the application configuration, and determines at a high level what the app will look like.
  3. The "document" container - There is one of these per tab.

In a more complicated application, the hierarchy would be much deeper.

Starting Point: The Configuration

The application configuration file, which is attached either as a resource or as a content file, looks like this:

<Application>
    <Menu>
        <-- Application menu configuration goes here -->
    </Menu>
    <Blocks>
        <Container ContainerId="ApplicationContainer" >
            <Container.Components>
                <-- A resource dictionary that contains the components -->
                <s:DocumentsService x:Key="DocumentsService"/>
                <v:DocumentsPersona x:Key="DocumentsPersona" 
		DocumentContainerId="DocumentContainer" 
		DocumentLayoutId="DocumentView" />
                <v:TabbedDocumentView x:Key="RootLayout"/>
            </Container.Components>
            <Container.Wirings>
                <-- The wiring diagram; how the components are connected -->
                <c:PropertyWiring Target="DocumentsService" 
		Property="CommandsService" Source="CommandsService" />
                <c:PropertyWiring Target="DocumentsPersona" 
		Property="DocumentsService" Source="DocumentsService" />
                <c:PropertyWiring Target="RootLayout" 
		Property="DataContext" Source="DocumentsPersona" />
            </Container.Wirings>
        </Container>
        <-- ... other containers... -->
    </Blocks>
</ListView.View>

The interesting bit here is the collection of Container elements. Each of these is an XAML script for building a container. The container consists of a number of components, and a set of connections between them. Each connection is expressed as a property wiring, which sets the property value either by creating a new instance of the required service or acquiring it by name from the current container or one of its parent containers.

This is pretty standard container behavior, and there are plenty of containers out there which can do what is described here.

When we developed this architecture, we created a "wiring builder" which generates this XAML. The wiring builder allows us to produce a graph showing the component interdependencies. Because we include commands as a type of dependency, we can also generate possible workflows by checking which commands are wired to which services. This can be helpful when comparing the actual architecture to the functional specifications.

Initialization

The application shell creates an initial container. In the sample, this container already has a commands service added; this is used to hook up the application menu to command handlers. When a service is instantiated, it has an opportunity to register commands; when a given command is available, it will be connected to the appropriate menu item. The code is in MainWindow.xaml:

<Window>
    <Window.Resources>
      <XmlDataProvider x:Key="ConfigXml" Source="Config/AppConfig.xml"/>
        <c:Container x:Key="ApplicationContainer">
            <c:Container.Components>
                <local:ApplicationPersona x:Key="ApplicationPersona" />
                <s:ApplicationEventsService x:Key="ApplicationEventsService" />
                <s:CommandsService x:Key="CommandsService"/>
            </c:Container.Components>
        </c:Container>
    </Window.Resources>

<-- ...menu items are hooked up to the commands service... -->
        <Style TargetType="MenuItem">
            <Setter Property="Command">
                <Setter.Value>
                    <s:CommandExecutor CommandsService=
			"{Binding Source={StaticResource ApplicationContainer}, 
			Path=CommandsService}" />
                </Setter.Value>
            </Setter>
            <Setter Property="CommandParameter" Value="{Binding XPath=@Parameter}"/>
        </Style>

<-- ...the rest of the window contains the root layout... -->
    <ContentControl Content="{Binding Source=
	{StaticResource ApplicationContainer}, Path=ApplicationPersona.RootLayout}"/>
</Window>

The ApplicationPersona class fetches the root layout by instantiating a container from configuration using a predefined id:

internal class ApplicationPersona
{
    private void LoadRootLayout()
    {
        var myContainer = this.GetContainer();
        var rootLayoutContainer = Container.Create(RootContainerId, myContainer);
        this.rootLayout = rootLayoutContainer.Components[RootLayoutId];
    }
}

Registering Commands

In the sample, the "application" container has a DocumentsService component. The DocumentsService class has a service dependency on CommandsService; as soon as this dependency is provided, it registers two commands: NewFile and OpenFile. Registering these commands causes the relevant File menu items to be enabled.

When the NewFile command is executed, the DocumentsService raises a DocumentOpened event. The DocumentsPersona (which is the view-model for the tabbed documents view) depends on the DocumentsService:

<c:PropertyWiring Target="DocumentsPersona" Property="DocumentsService" 
	Source="DocumentsService" />

As soon as this service dependency is provided, the view-model hooks the DocumentOpened event (it also unhooks the event when it is disposed). Opening a document causes the DocumentsPersona to create a new container:

private void DocumentOpened(object sender, EventArgs document)
{
    DocumentContext context = new DocumentContext {  Document = document.Data };
    Container container = Container.Create(DocumentContainerId, this.GetContainer());
    context.DocumentsService = DocumentsService;
    context.View = container.Components[DocumentLayoutId];
    context.IsActive = true;
    container.Inject("DocumentContext", context);
    this.Documents.Add(context);
}

Notice that it injects a DocumentContext into the new container. Using the configurable property DocumentLayoutId, it extracts the content object associated with the document view (in this case a user control, although it could well be a view-model). This content is presented by the tabbed document view.

Summary

This model might take a little getting used to, but in our experience it leads to much faster development, and is far easier to maintain. One of the principle advantages of having a tree of small services is that virtually any part of the application can be tested in isolation, using mocked dependencies. Additionally, having a configuration file that essentially dictates how the app behaves allows the components to be quickly reconfigured into a totally different application. This obviously helps with reusability.

No architecture is suitable for every purpose. This pattern will not be appropriate in all cases. But I think you'll find it covers a lot of bases; and at any rate, examination of the code may spark some new ideas.

History

  • 8th July, 2010: Initial version

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