My first son was born to us this week, entering into the world happy and alert. What a change! Now my days are filled with baby bottles and diaper changing. Here's a look at him during the weigh in.

Sooner or later, I'm convinced that most advanced implementations of CAB will have to write their own version of the IWorkItemActivationService. The WorkItemActivationService is closely tied into the lifecycle of the CAB WorkItem so please make sure you understand about WorkItem.Status otherwise this post will make no sense!
The concept of IWorkItemActivationSerice is simple. Everytime a WorkItem changes status it registers the status change with an implementation of IWorkItemActivationService by calling the ChangeStatus method as in the following code from the WorkItem class:
if (ActivationService != null)
{
ActivationService.ChangeStatus(this);
}The default implementation of IWorkItemActivationService is found in the SimpleWorkItemActivationClass:
public void ChangeStatus(WorkItem item)
{
lock (syncroot)
{
if (item != activeWorkItem && item.Status == WorkItemStatus.Active)
{
if (activeWorkItem != null && activeWorkItem.Status != WorkItemStatus.Terminated)
activeWorkItem.Deactivate();
activeWorkItem = item;
}
}
}
The purpose of this code is to ensure that only one WorkItem at a time is active (if a new WorkItem is being activated it simply deactivates the previously active one). Besides the bug (which I'll point out in a minute), this code may not suit your needs. In my case, when a child WorkItem activates I don't want to deactivate the parent WorkItem because the WorkItem is programmed to disable (hide) certain toolbars when the WorkItem deactivates. Hiding and showing toolbars caused too much flicker. The bug is subtle and would only cause a problem in rare instances. The bug is that during the execution of activeWorkItem there are two WorkItems with a status of Active! Obviously item.Status is Active but so is activeWorkItem.Status (using the object references from the code sample above). If you were to query the activation service for active work item inside the Deactivate event it should point to the workitem that just became active, not the workitem that is being activated.
So, to implement my custom activation service, I had to follow a couple of steps. First, subtype the IWorkItemActivationService to expose the currently active WorkItem. Sometimes its nice for client code to know which workitem is active:
public interface IMyWorkItemActivationService : IWorkItemActivationService
{
///
/// The currently active work item
///
WorkItem ActiveWorkItem { get; set; }
}
Then implement the new interface with the bug fix in place.
///
/// An implementation of IWorkItemActivationService that tracks the
/// last activated work item
///
[Service]
public class WorkItemActivationSerivce : IMyWorkItemActivationService
{
private object syncroot = new object();
private WorkItem activeWorkItem;
///
/// The currently active work item
///
public WorkItem ActiveWorkItem
{
get
{
return activeWorkItem;
}
set
{
activeWorkItem = value;
}
}
///
/// Initializes a new instance of the class.
///
public WorkItemActivationSerivce()
{
}
#region IWorkItemActivationService Members
public void ChangeStatus(WorkItem item)
{
lock (syncroot)
{
if (item != activeWorkItem && item.Status == WorkItemStatus.Active)
{
WorkItem previouslyActiveWorkItem = activeWorkItem;
//update the currently active work item
//this needs to be set BEFORE the deactivate is called
//so that during the execution of deactivate this service knows which work item is active
activeWorkItem = item;
//deactivate the currently active work item
//ONLY if the newly active work item is not a child work item
//TODO: may have to recursively descend through workitem containership hierarchy
//to look for descendants
if (previouslyActiveWorkItem != null
&& previouslyActiveWorkItem.Status != WorkItemStatus.Terminated
&& previouslyActiveWorkItem.WorkItems.ContainsObject(item) == false)
{
previouslyActiveWorkItem.Deactivate();
}
}
}
}
#endregion
}
Next, update your app.config to add the service.
<add serviceType="Microsoft.Practices.CompositeUI.IWorkItemActivationService, Microsoft.Practices.CompositeUI" instanceType="YourNamespace.WorkItemActivationSerivce, YourDll" />
serviceType="Microsoft.Practices.CompositeUI.IWorkItemActivationService, Microsoft.Practices.CompositeUI" instanceType="YourNamespace.WorkItemActivationSerivce, YourDll" /> I like to be able to query the system for the ActiveWorkItem from my WorkItem base class.
///
/// The currently active work item
///
public WorkItem ActiveWorkItem
{
get
{
IMyWorkItemActivationService wias = this.RootWorkItem.Services.Get() as IMyWorkItemActivationService;
if (wias == null) return null;
return wias.ActiveWorkItem;
}
set
{
IMyWorkItemActivationService wias = this.RootWorkItem.Services.Get() as IMyWorkItemActivationService;
if (wias != null)
{
wias.ActiveWorkItem = value;
}
}
}
Finally, I had to create a new implementation of the WorkItem.Activate method to deactivate (and then reactivate) the workitem in case a WorkItem outside of the currently active WorkItem hierarchy becomes active. This is a bit of a hack but it worked for my immediate purposes.
public new void Activate()
{
//because of the new workitemactivationservice, more than two work items can be active at a time
//Activate may now have to fire the activating work item events
// even if it is already activated.
//only suppress the deactivate if the previously activated work item was a child work item
if (this.Status == WorkItemStatus.Active
&& this.WorkItems.ContainsObject(this.ActiveWorkItem) == false)
{
Deactivate();
}
this.ActiveWorkItem = this;
base.Activate();
}
Unit testing of CAB modules can be difficult because of all the dependency injection going on. Typically you will unit test at least all your presenters. The problem is that you will probably need the presenter to be built up by Objectbuilder which injects the references to WorkItem and State objects on the presenter. What you need is some mock object that takes the place of the RootWorkItem providing a context under which to run your WorkItems and presenters. Take a cue from the PAG team and borrow their TestableRootWorkItem from the nUnit project that comes with CAB.
Although you may be able to use it out of the box, the reality is that you will probably need to modify it to add services that your WorkItems require. Just add these in the TestableAddServices method.
using Microsoft.Practices.CompositeUI.BuilderStrategies;
using Microsoft.Practices.CompositeUI.Commands;
using Microsoft.Practices.CompositeUI.EventBroker;
using Microsoft.Practices.ObjectBuilder;
using Microsoft.Practices.CompositeUI.Services;
namespace Microsoft.Practices.CompositeUI.Tests
{
public class TestableRootWorkItem : WorkItem //WorkItemBase
{
public TestableRootWorkItem()
{
InitializeRootWorkItem(CreateBuilder());
Services.AddNew<CommandAdapterMapService, ICommandAdapterMapService>();
Services.AddNew<TraceSourceCatalogService, ITraceSourceCatalogService>();
TestableAddServices();
BuildUp();
}
protected virtual void TestableAddServices()
{
//Do these module loaders really need to be added?
Services.AddNew<ModuleLoaderService, IModuleLoaderService>();
}
public Builder Builder
{
get { return InnerBuilder; }
}
public IReadWriteLocator Locator
{
get { return InnerLocator; }
}
private Builder CreateBuilder()
{
Builder builder = new Builder();
builder.Strategies.AddNew<EventBrokerStrategy>(BuilderStage.Initialization);
builder.Strategies.AddNew<CommandStrategy>(BuilderStage.Initialization);
builder.Strategies.AddNew<ObjectBuiltNotificationStrategy>(BuilderStage.PostInitialization);
builder.Policies.SetDefault<ObjectBuiltNotificationPolicy>(new ObjectBuiltNotificationPolicy());
builder.Policies.SetDefault<ISingletonPolicy>(new SingletonPolicy(true));
return builder;
}
}
}
Using the TestableRootWorkItem is easy. The key is to realize that ObjectBuilder builds up an object when you add the object to a ManagedObjectCollection such as WorkItem.Items. The simple act of adding the object to the collection causes all dependent objects to be injected. Here's a sample test.
[Test]
public void RunBatchClose()
{
TestableRootWorkItem root = new TestableRootWorkItem();
BatchCloseWorkItem wi = root.WorkItems.AddNew<BatchCloseWorkItem>();
BatchCloseController controller = wi.Items.AddNew<BatchCloseController>();
DataImportDTO batch = GetFirstOpenBatch();
Assert.IsNotNull(batch);
}