Intersoft ClientUI 8 > ClientUI Fundamentals > Extensibility Pattern Overview > Inversion-of-Control (IoC) Container Overview |
This topic describes the concept of dependency injection and IoC Container, and explains how to use the built-in IoC Container in ClientUI to build extensible applications with highly modular and loosely coupled architecture.
Inversion-of-Control (IoC) is a design pattern that inverts the flow of the application logic, which is achieved through the use of Dependency Injection for its binding process. The goal of this design pattern is to achieve maximum loose coupling where dependencies can be minimized or completely eliminated. With this approach, the application code can be easily extended as the concrete implementation can be dynamically specified or changed at runtime.
IoC is commonly paired with abstraction pattern such as dependency injection which enables the dependency to be resolved at latter time and injected to the control. The most common dependency injection approach is constructor injection.
The following code shows an example of a simple class that complies to the dependency injection pattern.
C# |
Copy Code
|
---|---|
public class MyStore { public MyStore(IPaymentProcessor paymentProcessor) { if (paymentProcessor == null) throw new ArgumentNullException("paymentProcessor"); this.PaymentProcessor = paymentProcessor; } public bool ProcessTransaction(ITransaction transaction) { return this.PaymentProcessor.ProcessPayment(transaction); } protected IPaymentProcessor PaymentProcessor { get; set; } } |
The example above uses constructor injection, which means that the code itself does not depend on specific implementations. Instead, the type that contains the concrete implementation is injected externally via the parameter in the constructor. The following code demonstrates the usage of the above class.
C# |
Copy Code
|
---|---|
public class MainPageViewModel : ViewModelBase { private void ExecuteProcess() { MyStore myStore = new MyStore(new PayPalPaymentProcessor()); myStore.ProcessTransaction(transaction); } } |
As seen in the code above, the class can be easily instantiated from any modules in the application such as ViewModel in MVVM-based applications. With the common class construction such as illustrated in the above example, however, it introduces a dependency on MyStore class which makes it difficult to extend in the future.
One of the well-known techniques to eliminate type dependencies is by inverting the flow of the logic to the controller host or also known as IoC container which manages the type registration, resolution and the life-cycle of the dependencies.
In constrast to the example in previous section, the following example shows the code that has been rewritten to use IoC container.
C# |
Copy Code
|
---|---|
public class MainPageViewModel : ViewModelBase { private void ExecuteProcess() { IStore store = Container.Resolve<IStore>(); store.ProcessTransaction(transaction); } } |
Notice that the class no longer depends on any concrete types. Instead, the IoC container will resolve an instance of object that implements IStore at runtime.
At the heart of IoC container are the Register and Resolve methods, which perform the following:
Most of the dependency registrations are usually done in a centralized place, commonly invoked when the program initializes. This centralized place is called Composition Root, which allows you to manage the composition of all components and services in a single place.
The following code shows how to use the Register method in the application initializer to register an interface type to a concrete type.
C# |
Copy Code
|
---|---|
public class AppInitializer : IApplicationInitializer { public void Initialize(ApplicationPackage package) { IDependencyContainer container = UXShell.Current.Container; // or new IocContainer() for standalone container; // Register the IStore interface to compose a new PayPalPaymentProcessor through a direct construction container.Register&tl;IStore>(o => new MyStore(new PayPalPaymentProcessor())); } } |
The code above is self explanatory – it registers the IStore interface type to a function that returns a new MyStore class complete with the required dependencies which is the PayPalPaymentProvider in this example.
Built with lightweight object factory, ClientUI provides comprehensive library to work with IoC container which supports a host of advanced container features such as open-generic support, lazy resolution, named containers and built-in lifetime managers.
ClientUI IoC container includes a number of overloads to perform type registration and resolution. The most basic APIs for type registration and resolution are as follow:
C# |
Copy Code
|
---|---|
IDependencyContainer container = UXShell.Current.Container; // or new IocContainer() for standalone container; // simple registration container.Register<IStore, MyStore>(); // registration with parameterized constructor container.Register<IStore>(o => new MyStore(new PayPalPaymentProcessor())); // registration with nested resolve where the IPaymentProcessor is first resolved by the container to be passed into MyStore container.Register<IPaymentProcessor, PayPalPaymentProcessor>(); container.Register<IStore>(o => new MyStore(o.Resolve<IPaymentProcessor>())); // resolve a new instance of IStore IStore store = container.Resolve<IStore>(); |
In addition to the common resolution that returns a new instance of the specified type, ClientUI IoC container also supports instance-based registration and resolution. This allows you to register an existing object instance into the container. During resolve, the container will return the registered object instead of creating a new instance.
You use the following methods to register an existing object instance:
C# |
Copy Code
|
---|---|
IDependencyContainer container = UXShell.Current.Container; // or new IocContainer() for standalone container; GooglePaymentProcessor googleProcessor = new GooglePaymentProcessor(); googleProcessor.Url = "https://checkout.google.com"; // initialize default properties of the processor container.RegisterInstance<IPaymentProcessor>(googleProcessor); |
By default, you can define only one registration with the same type at one time. However, there are numerous scenarios that might require you to define multiple registrations with the same interface type but associated to different implementation types. The IoC container library in ClientUI supports named containers that allow you to achieve this scenario by using different naming scope.
You use the following method overloads to register and resolve type using named containers:
C# |
Copy Code
|
---|---|
IDependencyContainer container = UXShell.Current.Container; // or new IocContainer() for standalone container; // registration with named containers container.Register<IStore>("Main", o => new MyStore(new PayPalPaymentProcessor())); container.Register<IStore>("Alternate", o => new MyStore(new GooglePaymentProcessor())); // resolve type from named containers IStore store = Container.Resolve<IStore>("Alternate"); store.ProcessTransaction(null); |
In certain scenarios, you may want to defer the creation of an object, particularly when the object is complex and may allocate relatively large system resources. The IoC container supports lazy resolution which is ideal to achieve this kind of scenarios.
You use the following methods to perform lazy resolution:
When the lazy resolution is used, the container returns a function that wraps the type instead of the real instance of the type. Whenever required, you can then execute the returned delegate function to get an object instance of the type.
C# |
Copy Code
|
---|---|
IDependencyContainer container = UXShell.Current.Container; // or new IocContainer() for standalone container; Func<IStore> fnStore = Container.Current.LazyResolve<IStore>(); // An instance of the IStore type is not created yet during Resolve. // To access the real instance, execute the delegate function. IStore store = fnStore(); store.ProcessTransaction(null); |
In addition to type registration and resolution, IoC container also serves as the controller that manages the lifetime of each dependency it generates. For instances, you may want to have the container to always create a new instance of the dependency, or alternatively, cache the instance for sharing use across multiple modules.
The built-in lifetime managers included in the ClientUI Framework are:
To specify a lifetime manager in a specific registration, use the WithLifetimeManager method filled with the lifetime manager as the parameter.
C# |
Copy Code
|
---|---|
IDependencyContainer container = UXShell.Current.Container; // or new IocContainer() for standalone container; container.Register<IStore>("GlobalCache", o => new MyStore(o.Resolve<IPaymentProcessor>())) .WithLifetimeManager(new ContainerLifetime()); // Using Container lifetime, also known as shared registration, // the container will instantiate a new object once for the first time // and return the same instance in the subsequent resolve calls. IStore store = Container.Resolve<IStore>("GlobalCache"); IStore store2 = Container.Resolve<IStore>("GlobalCache"); if (store == store2) MessageBox.Show("Store and Store2 instance are equal"); |
The IoC container implemented in the ClientUI Framework supports both standalone usage and integration with UXShell. For most common scenarios, it’s recommended that you use the default container instance which is accessible from the shell’s Container property. This technique enables you to resolve the registered dependencies anywhere in your code, including the dynamic XAP applications that are loaded on demand at runtime. For more information about ClientUI Application Framework, see Application Framework Overview.
In addition, the application framework now introduces a built-in bootstrapper to facilitate the dependencies composition and registration. Simply add a class that implements IApplicationInitializer interface in your projects, the Initialize method of the class will be automatically invoked when the application starts. In dynamic XAP scenarios, the Initialize method will be automatically invoked when the XAP package is downloaded and loaded to the application domain.
The following code example shows a class that implements IApplicationInitializer interface which contains the composition for the dependencies available in the module.
C# |
Copy Code
|
---|---|
namespace ClientUI_Ioc { // The class that implements IApplicationInitializer will be automatically instantiated // by ClientUI Application Framework when the application starts. // In dynamic XAP scenario, it will be called when the module is // loaded to the app domain through ClientUI application framework. public class AppInitializer : IApplicationInitializer { #region IApplicationInitializer Members public void Initialize(ApplicationPackage package) { // The technique of writing registration code in a centralized place is also known as "Composition Root" // which allows you to overview all components registration and easily maintain it for future extensibility. IDependencyContainer container = UXShell.Current.Container; // or new IocContainer() for standalone container; // Register the IStore interface to return a new PayPalPaymentProcessor through a direct construction container.Register<IStore>(o => new MyStore(new PayPalPaymentProcessor())); // Instance-registration sample GooglePaymentProcessor googleProcessor = new GooglePaymentProcessor(); // initialize default properties of the processor container.RegisterInstance<IPaymentProcessor>(googleProcessor); // Use Container lifetime manager to return shared instance of myStore container.Register<IStore>("Lifetime", o => new MyStore(o.Resolve())).WithLifetimeManager(new ContainerLifetime()); // Named container sample container.Register<IStore>("Alternate", o => new MyStore(new GooglePaymentProcessor())); } #endregion } } |