Friday, May 02, 2008

SCSF Dependency Injection Container, CAB WorkItem

Getting started with CAB/SCSF can be a bit daunting, and this article is a summary of the most important aspects regarding the mysterious CAB workitem. I've found this to be a good intro to CAB, start with part 18:

The CAB workitem is simply a dependency injection container, even if "wrongly" promoted as an implementation of a use case. A use case contains no UI-details or service implementation references, but of course these artifacts are needed to realize a use case in an application. The workitem is a container of resources needed to implement a use case.

A DI-container is a collection of resources than can be located using a resource locator that is used to resolve resource injection requests at run-time. The resources can be loaded into the container either dynamically or through XML config. The workitem DI-container has several collections of resources, which can be quite confusing. A workitem has these collections:

  • Items: all objects added to any of the child collections including this, except Workspace.SmartParts; independent of type of resource.
  • WorkItems: child WorkItem objects, must inherit CAB.WorkItem.
  • Services: any object added as a service. NOTE: the container can only contain one instance of each service type.
  • Workspaces: encapsulate a particular visual layout of controls and SmartParts, such as within tabbed pages.
  • SmartParts: any object marked with [SmartPart] in the class definition, do not have to be a visual component.
  • Workspace.SmartParts: visual components shown in the workspace, must inherit System.Windows.Forms.Control. NOTE: these items are not added to the .Items or .SmartParts collections on the workitem.
In addition, the workitem DI-containers can be nested, i.e. a workitem can contain other workitems.

When the DI-container tries to resolve a [ServiceDependency] injection or just Get<IService> for a service, it looks in the nested set of containers from the current workitem and up through the parent workitems, including the root workitem to find the service. The service locator does not look through the set of child workitems. Thus, shared services should be placed towards the root or in the root workitem for the service locator to find them.

One aspect of the workitem Services collection that most CAB developers seems to have missed, is that even if you can add only one instance of a service to a workitem, you still can add separate instances of the same service to child workitems. The service locator will find the nearest service in the tree towards the root. E.g. the ActionCatalog service can be added to several ModuleController first-level workitems, in addition to the root workitem. We have used this feature to add several scope-based services such as a SharedModelCacheSevice and a RibbonControllerService at different levels of the workitem structure. This way, the same service can have different scopes in a CAB solution, dependent on where it is injected. The scope controls how workitem resources are located by the resource locator, e.g. where the locator looks for Services, Commands and UIExtensionSites.

Note that the resource locator strategy only applies to these workitem collections: Services, Workspaces, Commands, EventTopics, UIExtensionSites.

When the SmartClient application starts, it will create the root workitem and add a set of CAB services to it:

public ... class SmartClientApplication<TWorkItem, TShell> ...
protected override void AddServices()
RootWorkItem.Services.AddNew <ProfileCatalogModuleInfoStore, IModuleInfoStore>();
RootWorkItem.Services.AddNew <WorkspaceLocatorService, IWorkspaceLocatorService>();
RootWorkItem.Services.AddNew <XmlStreamDependentModuleEnumerator, IModuleEnumerator>();
RootWorkItem.Services.AddNew <DependentModuleLoaderService, IModuleLoaderService>();
RootWorkItem.Services.AddOnDemand <ActionCatalogService, IActionCatalogService>();
RootWorkItem.Services.AddOnDemand <EntityTranslatorService, IEntityTranslatorService>();

These standard CAB services are available from all over CAB as they are added to the root workitem that becomes the ultimate parent of all other workitems loaded into the SmartClient application.

In addition to the standard set of CAB services, the custom CAB modules that get loaded will typically add extra CAB services into the root workitem. SCSF will load CAB modules as defined in ProfileCatalog.XML and this will trigger the CAB module initialization mechanism filling the workitem collections and followed by the DI-container resolving.

What adds to the somewhat complicated workitem collections mechanism is that several of these CAB services themselves are collections of other shared objects. E.g. the EntityTranslatorService is a collection of translators that can be added to and located within that service. Note that the DI-container only performs the resource resolving and dependency injection on objects that are added directly to a workitem collection. Thus, objects not added directly to a workitem collection will not get any DI magic performed on them. E.g. if you create a service agent from within a CAB service to call a web-service, that agent will not be subject to any DI resolving. This can be very confusing when [ServiceDependency] objects are not injected as planned, thus be very careful when designing your solution.

NOTE: do not try to access resources that must be resolved by the DI-container, from within class constructors, as the resource may not have been injected yet. Use the [InjectionConstructor] attribute on your constructor if you need to force the resource locator to resolve and inject services that you need.

Using the DI-Container from CAB Modules

CAB Services are usually implemented using CAB foundation modules, but can also be implemented using CAB business modules. The former module type is applicable for shared CAB services and components, such as modules that provide access to web-services; while the latter module type is suitable for use-case specific CAB components. Note that both types of resources are "services" in CAB as they are added to the workitem Services collection. This overloading of the term "service" should not make you confuse CAB service modules with WCF service gateways, even if the CAB service used the WCF service.

Adding CAB services from CAB modules

A foundation module is a bare-bones module (the original CAB module) as it is just for implementing a container for shared components.

A business module is a module (new in SCSF, extending the original CAB module) enhanced with the application controller pattern as it for implementing a container for use-case specific components. The SCSF module adds the
ControlledWorkItem to separate the use-case related code from the workitem container related code (in CAB you had to put the use-case controller code in the workitem class, mixing the DI-container logic with the use-case logic).

So even if you could implement an AddServices method in the ModuleController and call it from its Run method, I would recommended that you still add module services from Module.AddServices method. This keeps the container related code separated from the use-case related code:

public override void AddServices()
//NOTE: must add UserAccessClaimSet first, as NavigationService depends on this service
WorkItem.RootWorkItem.Services.AddNew <UserAccessClaimSet, IUserAccessClaimSet>();
WorkItem.RootWorkItem.Services.AddNew <NavigationService, INavigationService>();

Prefer adding services like this over the other mechanisms provided by SCSF (
app.config / [Service] attribute).

Resolving and using CAB services

To be able to use the CAB services they must be gotten from the workitem DI-container. There are two ways of doing this, using the service collection Get<IService> method or using dependency injection with the [ServiceDependency] attribute. The DI way is preferred as the class using the service can then get the service reference once and use it multiple places. The Get<IService> way has the advantage that it can be called with a bool EnsureExists parameter that will throw an exception if the service is not (yet) registered in the workitem DI-container.

Use the [ServiceDependency] attribute either to inject a service into a class property or in the class c'tor:

public IUserAccessClaimSet Service
{set { _service = value; }}

public NavigationService([ServiceDependency] IUserAccessClaimSet userAccessClaimSet)
_userAccessClaimSet = userAccessClaimSet;

Getting the service directly is more explicit and allows for using the EnsureExists parameter:

IUserAccessClaimSet costOfGasService = WorkItem.Services.Get<IUserAccessClaimSet>(true);

Without the EnsureExists parameter, the returned service can be null and your code will then not throw an exception when getting the service, but rather on the first service invokation later on, which can make it harder to diagnose the actual cause of the failure. Using the [ServiceDependency] attribute has the same weakness, and this is a common cause of CAB perplexity.
Still, I would recommend using the [ServiceDependency] attribute.

Using the DI-container from CAB views

All CAB views exist within a CAB business module and must thus use one of the recommended methods for resolving service dependencies, preferring the [ServiceDependency] attribute. CAB views are designed using the 'supervising controller' MVP pattern, and the presenter object is added to the workitem Items collection like this:

_presenter = _workitem.Items.AddNew<TaskViewPresenter>();

Adding it to the Items collection of the workitem DI-container triggers the dependency injection resolver.

No comments: