Dependency resolution in Xamarin.Forms
This article explains how to inject a dependency resolution method into Xamarin.Forms so that an application's dependency injection container has control over the creation and lifetime of custom renderers, effects, and DependencyService implementations.
In the context of a Xamarin.Forms application that uses the Model-View-ViewModel (MVVM) pattern, a dependency injection container can be used for registering and resolving view models, and for registering services and injecting them into view models. During view model creation, the container injects any dependencies that are required. If those dependencies have not been created, the container creates and resolves the dependencies first. For more information about dependency injection, including examples of injecting dependencies into view models, see Dependency Injection.
Control over the creation and lifetime of types in platform projects is traditionally performed by Xamarin.Forms, which uses the Activator.CreateInstance
method to create instances of custom renderers, effects, and DependencyService
implementations. Unfortunately, this limits developer control over the creation and lifetime of these types, and the ability to inject dependencies into them. This behavior can be changed by injecting a dependency resolution method into Xamarin.Forms that controls how types will be created – either by the application's dependency injection container, or by Xamarin.Forms. However, note that there is no requirement to inject a dependency resolution method into Xamarin.Forms. Xamarin.Forms will continue to create and manage the lifetime of types in platform projects if a dependency resolution method isn't injected.
Note
While this article focuses on injecting a dependency resolution method into Xamarin.Forms that resolves registered types using a dependency injection container, it's also possible to inject a dependency resolution method that uses factory methods to resolve registered types.
Injecting a dependency resolution method
The DependencyResolver
class provides the ability to inject a dependency resolution method into Xamarin.Forms, using the ResolveUsing
method. Then, when Xamarin.Forms needs an instance of a particular type, the dependency resolution method is given the opportunity to provide the instance. If the dependency resolution method returns null
for a requested type, Xamarin.Forms falls back to attempting to create the type instance itself using the Activator.CreateInstance
method.
The following example shows how to set the dependency resolution method with the ResolveUsing
method:
using Autofac;
using Xamarin.Forms.Internals;
...
public partial class App : Application
{
// IContainer and ContainerBuilder are provided by Autofac
static IContainer container;
static readonly ContainerBuilder builder = new ContainerBuilder();
public App()
{
...
DependencyResolver.ResolveUsing(type => container.IsRegistered(type) ? container.Resolve(type) : null);
...
}
...
}
In this example, the dependency resolution method is set to a lambda expression that uses the Autofac dependency injection container to resolve any types that have been registered with the container. Otherwise, null
will be returned, which will result in Xamarin.Forms attempting to resolve the type.
Note
The API used by a dependency injection container is specific to the container. The code examples in this article use Autofac as a dependency injection container, which provides the IContainer
and ContainerBuilder
types. Alternative dependency injection containers could equally be used, but would use different APIs than are presented here.
Note that there is no requirement to set the dependency resolution method during application startup. It can be set at any time. The only constraint is that Xamarin.Forms needs to know about the dependency resolution method by the time that the application attempts to consume types stored in the dependency injection container. Therefore, if there are services in the dependency injection container that the application will require during startup, the dependency resolution method will have to be set early in the application's lifecycle. Similarly, if the dependency injection container manages the creation and lifetime of a particular Effect
, Xamarin.Forms will need to know about the dependency resolution method before it attempts to create a view that uses that Effect
.
Warning
Registering and resolving types with a dependency injection container has a performance cost because of the container's use of reflection for creating each type, especially if dependencies are being reconstructed for each page navigation in the application. If there are many or deep dependencies, the cost of creation can increase significantly.
Registering types
Types must be registered with the dependency injection container before it can resolve them via the dependency resolution method. The following code example shows the registration methods that the sample application exposes in the App
class, for the Autofac container:
using Autofac;
using Autofac.Core;
...
public partial class App : Application
{
static IContainer container;
static readonly ContainerBuilder builder = new ContainerBuilder();
...
public static void RegisterType<T>() where T : class
{
builder.RegisterType<T>();
}
public static void RegisterType<TInterface, T>() where TInterface : class where T : class, TInterface
{
builder.RegisterType<T>().As<TInterface>();
}
public static void RegisterTypeWithParameters<T>(Type param1Type, object param1Value, Type param2Type, string param2Name) where T : class
{
builder.RegisterType<T>()
.WithParameters(new List<Parameter>()
{
new TypedParameter(param1Type, param1Value),
new ResolvedParameter(
(pi, ctx) => pi.ParameterType == param2Type && pi.Name == param2Name,
(pi, ctx) => ctx.Resolve(param2Type))
});
}
public static void RegisterTypeWithParameters<TInterface, T>(Type param1Type, object param1Value, Type param2Type, string param2Name) where TInterface : class where T : class, TInterface
{
builder.RegisterType<T>()
.WithParameters(new List<Parameter>()
{
new TypedParameter(param1Type, param1Value),
new ResolvedParameter(
(pi, ctx) => pi.ParameterType == param2Type && pi.Name == param2Name,
(pi, ctx) => ctx.Resolve(param2Type))
}).As<TInterface>();
}
public static void BuildContainer()
{
container = builder.Build();
}
...
}
When an application uses a dependency resolution method to resolve types from a container, type registrations are typically performed from platform projects. This enables platform projects to register types for custom renderers, effects, and DependencyService
implementations.
Following type registration from a platform project, the IContainer
object must be built, which is accomplished by calling the BuildContainer
method. This method invokes Autofac's Build
method on the ContainerBuilder
instance, which builds a new dependency injection container that contains the registrations that have been made.
In the sections that follow, a Logger
class that implements the ILogger
interface is injected into class constructors. The Logger
class implements simple logging functionality using the Debug.WriteLine
method, and is used to demonstrate how services can be injected into custom renderers, effects, and DependencyService
implementations.
Registering custom renderers
The sample application includes a page that plays web videos, whose XAML source is shown in the following example:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:video="clr-namespace:FormsVideoLibrary"
...>
<video:VideoPlayer Source="https://archive.org/download/BigBuckBunny_328/BigBuckBunny_512kb.mp4" />
</ContentPage>
The VideoPlayer
view is implemented on each platform by a VideoPlayerRenderer
class, that provides the functionality for playing the video. For more information about these custom renderer classes, see Implementing a video player.
On iOS and the Universal Windows Platform (UWP), the VideoPlayerRenderer
classes have the following constructor, which requires an ILogger
argument:
public VideoPlayerRenderer(ILogger logger)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
On all the platforms, type registration with the dependency injection container is performed by the RegisterTypes
method, which is invoked prior to the platform loading the application with the LoadApplication(new App())
method. The following example shows the RegisterTypes
method on the iOS platform:
void RegisterTypes()
{
App.RegisterType<ILogger, Logger>();
App.RegisterType<FormsVideoLibrary.iOS.VideoPlayerRenderer>();
App.BuildContainer();
}
In this example, the Logger
concrete type is registered via a mapping against its interface type, and the VideoPlayerRenderer
type is registered directly without an interface mapping. When the user navigates to the page containing the VideoPlayer
view, the dependency resolution method will be invoked to resolve the VideoPlayerRenderer
type from the dependency injection container, which will also resolve and inject the Logger
type into the VideoPlayerRenderer
constructor.
The VideoPlayerRenderer
constructor on the Android platform is slightly more complicated as it requires a Context
argument in addition to the ILogger
argument:
public VideoPlayerRenderer(Context context, ILogger logger) : base(context)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
The following example shows the RegisterTypes
method on the Android platform:
void RegisterTypes()
{
App.RegisterType<ILogger, Logger>();
App.RegisterTypeWithParameters<FormsVideoLibrary.Droid.VideoPlayerRenderer>(typeof(Android.Content.Context), this, typeof(ILogger), "logger");
App.BuildContainer();
}
In this example, the App.RegisterTypeWithParameters
method registers the VideoPlayerRenderer
with the dependency injection container. The registration method ensures that the MainActivity
instance will be injected as the Context
argument, and that the Logger
type will be injected as the ILogger
argument.
Registering effects
The sample application includes a page that uses a touch tracking effect to drag BoxView
instances around the page. The Effect
is added to the BoxView
using the following code:
var boxView = new BoxView { ... };
var touchEffect = new TouchEffect();
boxView.Effects.Add(touchEffect);
The TouchEffect
class is a RoutingEffect
that's implemented on each platform by a TouchEffect
class that's a PlatformEffect
. The platform TouchEffect
class provides the functionality for dragging the BoxView
around the page. For more information about these effect classes, see Invoking events from effects.
On all the platforms, the TouchEffect
class has the following constructor, which requires an ILogger
argument:
public TouchEffect(ILogger logger)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
On all the platforms, type registration with the dependency injection container is performed by the RegisterTypes
method, which is invoked prior to the platform loading the application with the LoadApplication(new App())
method. The following example shows the RegisterTypes
method on the Android platform:
void RegisterTypes()
{
App.RegisterType<ILogger, Logger>();
App.RegisterType<TouchTracking.Droid.TouchEffect>();
App.BuildContainer();
}
In this example, the Logger
concrete type is registered via a mapping against its interface type, and the TouchEffect
type is registered directly without an interface mapping. When the user navigates to the page containing a BoxView
instance that has the TouchEffect
attached to it, the dependency resolution method will be invoked to resolve the platform TouchEffect
type from the dependency injection container, which will also resolve and inject the Logger
type into the TouchEffect
constructor.
Registering DependencyService implementations
The sample application includes a page that uses DependencyService
implementations on each platform to allow the user to pick a photo from the device's picture library. The IPhotoPicker
interface defines the functionality that is implemented by the DependencyService
implementations, and is shown in the following example:
public interface IPhotoPicker
{
Task<Stream> GetImageStreamAsync();
}
In each platform project, the PhotoPicker
class implements the IPhotoPicker
interface using platform APIs. For more information about these dependency services, see Picking a photo from the picture library.
On iOS and UWP, the PhotoPicker
classes have the following constructor, which requires an ILogger
argument:
public PhotoPicker(ILogger logger)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
On all the platforms, type registration with the dependency injection container is performed by the RegisterTypes
method, which is invoked prior to the platform loading the application with the LoadApplication(new App())
method. The following example shows the RegisterTypes
method on UWP:
void RegisterTypes()
{
DIContainerDemo.App.RegisterType<ILogger, Logger>();
DIContainerDemo.App.RegisterType<IPhotoPicker, Services.UWP.PhotoPicker>();
DIContainerDemo.App.BuildContainer();
}
In this example, the Logger
concrete type is registered via a mapping against its interface type, and the PhotoPicker
type is also registered via a interface mapping.
The PhotoPicker
constructor on the Android platform is slightly more complicated as it requires a Context
argument in addition to the ILogger
argument:
public PhotoPicker(Context context, ILogger logger)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
The following example shows the RegisterTypes
method on the Android platform:
void RegisterTypes()
{
App.RegisterType<ILogger, Logger>();
App.RegisterTypeWithParameters<IPhotoPicker, Services.Droid.PhotoPicker>(typeof(Android.Content.Context), this, typeof(ILogger), "logger");
App.BuildContainer();
}
In this example, the App.RegisterTypeWithParameters
method registers the PhotoPicker
with the dependency injection container. The registration method ensures that the MainActivity
instance will be injected as the Context
argument, and that the Logger
type will be injected as the ILogger
argument.
When the user navigates to the photo picking page and chooses to select a photo, the OnSelectPhotoButtonClicked
handler is executed:
async void OnSelectPhotoButtonClicked(object sender, EventArgs e)
{
...
var photoPickerService = DependencyService.Resolve<IPhotoPicker>();
var stream = await photoPickerService.GetImageStreamAsync();
if (stream != null)
{
image.Source = ImageSource.FromStream(() => stream);
}
...
}
When the DependencyService.Resolve<T>
method is invoked, the dependency resolution method will be invoked to resolve the PhotoPicker
type from the dependency injection container, which will also resolve and inject the Logger
type into the PhotoPicker
constructor.
Note
The Resolve<T>
method must be used when resolving a type from the application's dependency injection container via the DependencyService
.