VbaDI is an Windows Excel VBA add-in (.xlam) that provides an Inversion of Control (IoC) container for VBA projects.
- The means to statically declare class dependencies.
- A fluent registration interface for configuring application objects.
- The ability to Build/Resolve an application's object graph despite the absence of parameterized constructors in VBA.
- A generalized and re-usable alternative to Pure DI.
- Download VbaDI.xlam from this repository and copy it to your Excel add-in folder.
- To find where add-ins are stored on your system, open any Excel workbook: Click File -> Options -> Add-ins. Location of current add-ins will be visible.
- Open an existing Excel project (.xlsm) or open a new workbook and press Alt+F11 to open the Visual Basic Editor (VBE)
- In the VBE, click on Tools->References and find and check the box for 'VbaDIAddIn'. It may be necessary to click Browse... to find the add-in.
- If you opened a new workbook in step 2 - save the workbook as a macro-enabled .xlsm file.
- Get Rubberduck - Make your VBA development efforts more efficient, productive, and correct.
- See #1
- "Dependency Injection in .NET" by Mark Seeman.
- CastleWinsor (CW) Inversion of Control (IoC) container for C# - A production OSS IoC container that was used as a model for VbaDI.
- Register classes, interfaces, and configuration values with the Container.
- Resolve the application's object graph. Compose registered classes with their dependencies.
- Release the Container and its object references.
This process flow is referred to as the Register -> Resolve -> Release pattern:
Configuration expressions begin by calling 1 of 2 functions exposed by the VbaDI module: VbaDI.Instance
or VbaDI.ForInterface
.
These functions are the entry points to a fluent builder API - IVbaDIFluentRegistration
1.
Some examples of registering elements with the Container:
Registration of a Class instance
container.Register VbaDI.Instance(New MyServiceImpl) 'defaults to Singleton lifestyle
'Or, declare Lifestyle explicitly:
'Singleton
container.Register VbaDI.Instance(New MyServiceImpl).AsSingleton()
'Transient
container.Register VbaDI.Instance(New MyServiceImpl).AsTransient()
Interface(s) registration with an implementing class instance
container.Register VbaDI.ForInterface(TypeName(New IMyService)).Use(New MyServiceImpl)
container.Register VbaDI _
.ForInterface(TypeName(New ICopier), TypeName(New IFax), TypeName(New IShredder)) _
.Use(New BizMachine)
Registration of a component that has a Value dependency
Dim cxnString As String
cxnString = "Provider= Microsoft.ACE.OLEDB.12.0; Data Source='" & filepath & "';"
container.Register VbaDI.ForInterface(TypeName(IRepo)).Use(New AppRepo) _
.DependsOnValue("AdoConnectionString", cxnString)
- Classes, Interface assignments, and Value dependencies are identified by RegistrationIDs.
- The first registered RegistrationID/Instance pair is the only RegistrationID/Instance pair cached by the Container.
- The first registration of an interface or an object value dependency wins. Subsequent registrations are ignored.
- If a class has no dependencies, but is a dependency of another registered class, then it needs to be registered with the container.
The registration process can be accomplished by RegistrationLoaders which are Class Modules
that implement the single method IVbaDIRegistrationLoader
interface:
Public Sub LoadToContainer(ByVal pContainer As IVbaDIContainer)
End Sub
An example of typical RegistrationLoader object content:
Option Explicit
Implements IVbaDIRegistrationLoader
Private Sub IVbaDIRegistrationLoader_LoadToContainer(ByVal pContainer As IVbaDIContainer)
pContainer.Register VbaDI.ForInterface(TypeName(New IMyService)) _
.Use(New MyServiceImpl)
pContainer.Register VbaDI.ForInterface(TypeName(New ILogger)) _
.Use(New FileLoggerImpl)
'... and so on
End Sub
- Using a RegistrationLoader to configure the IoC container is recommended.
- One or more RegistrationLoaders can be used to configure a container.
- RegistrationLoaders are custom class modules of an application, not the add-in.
- RegistrationLoaders help organize/modularize IoC container configuration.
Once all classes involved in DI are registered with the Container, the Resolve method is invoked on the Container and the application's object graph is assembled. The Resolve process requires far less user code when compared to the Register process. For each use of an IoC Container, there should be a single call to IVbaDIContainer.Resolve(<RegistrationID>)
.
The Resolve process relies upon the IVbaDIQueryCompose
interface (declared by the add-in). The IVbaDIQueryCompose
interface provides the ability for class modules to statically declare their dependencies. IVbaDIQueryCompose
is also the mechanism by which the IoC Container injects dependencies.
'@Description "Returns the dependency RegistrationIDs that an object requires
Public Property Get RegistrationIDs() As Collection
End Property
'@Description "Retrieve dependencies by RegistrationID using IVbaDIDependencyProvider"
Public Sub ComposeObject(ByVal pProvider As IVbaDIDependencyProvider)
End Sub
If a registered object implements the IVbaDIQueryCompose
interface, then the Container will:
- Call the
RegistrationIDs
property- The implementing object is responsible to return a
Collection
of required RegistrationIDs
- The implementing object is responsible to return a
- Invoke
ComposeObject
delivering the required set of fully composed dependencies accessible by RegistrationID
For comparison: a parameterized C# instance constructor and its equivalent for a VBA object using IVbaDIQueryCompose
:
public class ManipulateStuff
{
private readonly DoStuff _doStuff;
private readonly IDoOtherStuff _doOtherStuff;
private readonly string _myStuffFilepath;
//parameterized instance constructor
public ManipulateStuff(DoStuff doStuff, IDoOtherStuff doOtherStuff, string stuffPath)
{
_doStuff= doStuff;
_doOtherStuff= doOtherStuff;
_myStuffFilepath = stuffPath;
}
}
'VBA equivalent using VbaDI
'Class Module ManipulateStuff.cls
Option Explicit
Implements IVbaDIQueryCompose
Private Type TManipulateStuff
DoStuff As DoStuff
DoOtherStuff As IDoOtherStuff
MyStuffFilepath As String
End Type
Private this As TManipulateStuff
Private Property Get IVbaDIQueryCompose_RegistrationIDs() As Collection
Set IVbaDIQueryCompose_RegistrationIDs = New Collection
With IVbaDIQueryCompose_RegistrationIDs
.Add TypeName(New DoStuff)
.Add TypeName(New IDoOtherStuff)
.Add "ConfigFilepath"
End With
End Property
Private Sub IVbaDIQueryCompose_ComposeObject(ByVal pProvider As IVbaDIDependencyProvider)
With pProvider
Set this.DoStuff = .ObjectFor(TypeName(New DoStuff))
Set this.DoOtherStuff = .ObjectFor(TypeName(New IDoOtherStuff))
this.MyStuffFilepath = .ValueFor("ConfigFilepath")
End With
End Sub
- Class, Interface and Value dependencies are identified by RegistrationIDs.
IVbaDIQueryCompose.RegistrationIDs
is analogous to the C# example's constructor parameter list.IVbaDIQueryCompose.ComposeObject
is analogous to the C# example's constructor function body.- If a class has no object, interface, or value dependencies, the developer can:
- Choose to not implement
IVbaDIQueryCompose
, or - Implement
IVbaDIQueryCompose
and return an empty RegistrationIDCollection
whenIVbaDIQueryCompose.RegistrationIDs
is called.
- Choose to not implement
At completion of ComposeObject
, an object is fully composed and initialized.
Lifestyle settings determines how the lifetime of an injected dependency is managed. The VbaDI container supports two object lifestyles: SINGLETON and TRANSIENT.
-
SINGLETON:
- Registration example:
container.Register VbaDI.Instance(New MyServiceImpl).AsSingleton()
- If a Lifestyle is not specified, SINGLETON is the default.
- The container provides the same, single, fully resolved, instance for each dependency request.
- The SINGLETON instance cached by the Container is released when the Container is destroyed.
- Registration example:
-
TRANSIENT:
- Registration example:
container.Register VbaDI.Instance(New MyServiceImpl).AsTransient()
- Objects with TRANSIENT lifestyle are created each time they are requested as a dependency.
- The object's lifetime is controlled by the requesting object.
- Although the Container retains a reference to the first registered instance of a class, the cached instance is never provided in response to a transient dependency request.
- To support TRANSIENT lifestyle, a class must implement the
IVbaDIDefaultFactory
interface (declared by the add-in). The Container will invokeIVbaDIDefaultFactory.Create()
to create a new instance of the class. The Container then provides the created (and resolved) instance when requested as a dependency.
- Registration example:
- RegistrationIDs are unique
String
keys to identify classes, interfaces, and values. - RegistrationIDs are used/relevant only during the Registration and Resolve phases.
- Object instances are registered using functions with an optional RegistrationID parameter.
- If the optional RegistrationID parameter is not specified, the RegistrationID is set equal to
TypeName(<object instance>)
. - The optional parameter is provided to support user-defined RegistrationIDs.
- If the optional RegistrationID parameter is not specified, the RegistrationID is set equal to
- Interfaces and Value dependencies are registered using a RegistrationID (string) only.
- Recommendation: Generate RegistrationIDs for classes and interfaces using
TypeName(<class object instance>)
/TypeName(New <class module>)
or function(s) that leverage theTypeName
function.- Motivation: Enlists compiler support to keep RegistrationIDs consistent with class/interface module name changes over time.
- Recommendation: Do not implement Lifecycle Handlers 2 for classes registered with the VbaDI container.
- When using VbaDI, place code that would have been in
Class_Initialize()
within theIVbaDIQueryCompose.ComposeObject
implementation. - Motivation: Creating RegistrationIDs using
TypeName
can result in multiple, often temporary, object instantiations.- Lifecycle Handler implementations that 'do too much' can be a source of errors and/or slow performance (see #7 below)
- When using VbaDI, place code that would have been in
- If LifeCycle Handlers are implemented, a disciplined/best-practice approach to their implementations is necessary:
- Do not include code that has side-effects, for example:
- calls to an external resource (e.g., file and database operations)
- interactions with other modules and forms
- Limit
Class_Initialize()
code to simple field initializations.
- Do not include code that has side-effects, for example:
CompositionRoot is where all object/dependencies are registered and the object graph is resolved. CompositionRoot functionality is executed at application startup.
An example of what implementing CompositionRoot in a Standard Module
might look like:
'@Folder "CompositionRoot"
Option Explicit
Private Type TAppEntryPoint
App As App
End Type
Private this As TAppEntryPoint
'@EntryPoint
Public Sub Main()
On Error GoTo ErrorExit
'Create the IoC Container
Dim xContainer As IVbaDIContainer
Set xContainer = VbaDI.CreateContainer()
'Register...Register using a RegistrationLoader
xContainer.RegisterUsingLoader New AppIoCLoader
'Resolve...One call does it all
Set this.App = xContainer.Resolve(TypeName(New App))
'Invoke the App...All object map instances are assembled and initialized
this.App.Main
'the Container is released when it goes out of scope
Exit Sub
ErrorExit:
Debug.Print "Startup Error: " & Err.Description
MsgBox "Error Encountered during Application Startup"
End Sub
As can be seen in the CompositionRoot example above, VbaDI does not expose a Release method on the IVbaDIContainer
interface. VbaDI relies on garbage collection to implement the Release phase.
Footnotes
-
VbaDI's fluent registration API represent a small fraction of the options provided by full-featured IoC containers like CastleWinsor. ↩
-
VBA Lifecycle Handlers are
Class_Initialize()
andClass_Terminate()
↩