Mobile
28th September 2020
by Christopher Dresel

This blog post will show you a way of integrating configuration, dependency injection, logging and localization of the .NET extensions stack into your Xamarin.Forms application.

background.png

With the rise and evolution of ASP.NET Core a lot of practical libraries were created and bundled as .NET Extensions. In the offical .NET Extensions Github repository (hint: select a corresponding release branch if you want to check it out) it is described as follows:

.NET Extensions is an open-source, cross-platform set of APIs for commonly used programming patterns and utilities, such as dependency injection, logging, and app configuration. Most of the API in this project is meant to work on many .NET platforms, such as .NET Core, .NET Framework, Xamarin, and others.

This blog posts will focus on the integration of .NET extensions into Xamarin.Forms.

Update: It seems that Microsoft will integrate some of the .NET Extensions into .NET Multi-platform App UI by default šŸ˜Ž

Make it configurable

If you need to store configuration data like connection strings, log settings, cloud services variables and so on ConfigurationBuilder comes to the rescue. At Microsoft Docs you'll find some ASP.NET flavored Configuration documentation.

I won't go into too much detail here but the core essence is that you can specify one or many providers that can handle configuration from different sources.

Every provider added will override settings defined by a previous provider. This is great for the following use cases:

  • Use different values for debug / release builds
  • Set platform specific configuration for Android / iOS devices or emulators and simulators (e.g. Host IP)
  • Specify optional sources that could contain sensitive information added locally or during continuous deployments

Install NuGet packages

I prefer json configuration files, so we add Microsoft.Extensions.Configuration.Json to the netstandard project:

Install-Package Microsoft.Extensions.Configuration.Json

For Xamarin.Forms embedded resources are the way to go, so we also add Microsoft.Extensions.FileProviders.Embedded:

Install-Package Microsoft.Extensions.FileProviders.Embedded

Configuration and setup

In your netstandard and platform specific projects, add an appsettings.json and set the Build Action to Embedded Resource.

Add a Setup.cs to your platform specific project (note that you have to specify the base namespace for Android projects):

public class Setup
{
    public static Action<ConfigurationBuilder> Configuration => (builder) =>
    {
        builder.AddJsonFile(new EmbeddedFileProvider(typeof(Setup).Assembly, typeof(Setup).Namespace),
            "appsettings.json", false, false);
    };
}

Pass this action when instantiating your App:

LoadApplication(new App(Setup.Configuration));

Add a Setup.cs to your netstandard project:

public static class Setup
{
    public static ConfigurationBuilder Configuration => new ConfigurationBuilder();

    public static ConfigurationBuilder ConfigureNetStandardProject(this ConfigurationBuilder builder)
    {
        builder.AddJsonFile(new EmbeddedFileProvider(typeof(Setup).Assembly), "appsettings.json", false, false);

        return builder;
    }

    public static ConfigurationBuilder ConfigurePlatformProject(this ConfigurationBuilder builder,
        Action<ConfigurationBuilder> configure)
    {
        configure(builder);

        return builder;
    }
}

Modify your App constructor:

public App(Action<ConfigurationBuilder> configuration)
{
    InitializeComponent();

    IConfigurationRoot configurationRoot = Setup.Configuration
        .ConfigureNetStandardProject()
        .ConfigurePlatformProject(configuration)
        .Build();

    // TODO: Store configuration root, bind configuration to concrete classes, ...
    string value = configurationRoot["Key2"];

    MainPage = new AppShell();
}

This is the foundation of how you can use .NET Configuration in you Xamarin.Forms app. Later on we'll focus on a more practical sample but let's continue with Microsoft.Extensions.DependencyInjection.

Replace the Xamarin.Forms Service Locator Anti-Pattern

I'm sure you came across the Xamarin.Forms DependencyService class when creating platform specific services. This class is quite limited and also considered an Anti-Pattern. This is why frameworks like Prism add their own dependency injection mechanism.

In fact it's no rocket science to integrate the dependency injection framework of your choice. Since this blog post is focusing on .NET extensions we'll use Microsoft.Extensions.DependencyInjection as you can imagine. If you have never used the DI extension before, read the ASP.NET flavored Dependency Injection documentation.

Wouldn't it be nice to design your view models as composable objects and let DI inject all your dependencies? Or even better let it create your pages and automatically bind your view models (yes this idea is inspired by Prism)?

Install NuGet packages

Add the package to your netstandard project:

Install-Package Microsoft.Extensions.DependencyInjection

Configuration and setup

I'll go the same route as for configuration and define generic services in the netstandard project and platform specific one in the corresponding Android and iOS project.

Add this to your Setup.cs in your platform specific project (remove IConfigurationRoot if you do not need it here):

    public static Action<IServiceCollection, IConfigurationRoot> DependencyInjection =>
        (serviceCollection, configurationRoot) =>
        {
            // TODO: Add your platform services
        };

Add this to your Setup.cs in your netstandard project:

public static IServiceCollection ConfigureNetStandardProject(this IServiceCollection serviceCollection,
    IConfigurationRoot configurationRoot)
{
    // TODO: Add your services

    return serviceCollection;
}

public static IServiceCollection ConfigurePlatformProject(this IServiceCollection serviceCollection,
    IConfigurationRoot configurationRoot, Action<IServiceCollection, IConfigurationRoot> configure)
{
    configure(serviceCollection, configurationRoot);

    return serviceCollection;
}

Extend your App constructor again:

public App(Action<ConfigurationBuilder> configuration,
    Action<IServiceCollection, IConfigurationRoot> dependencyServiceConfiguration)
{
    InitializeComponent();

    IConfigurationRoot configurationRoot = Setup.Configuration
        .ConfigureNetStandardProject()
        .ConfigurePlatformProject(configuration)
        .Build();

    IServiceProvider serviceProvider = Setup.DependencyInjection
        .ConfigureNetStandardProject(configurationRoot)
        .ConfigurePlatformProject(configurationRoot, dependencyServiceConfiguration)
        .BuildServiceProvider();

    MainPage = new AppShell(serviceProvider);
}

As you see I'm passing the constructed IServiceProvider to the AppShell. There it is stored as a Property and can be retrieved via Shell.Current.ServiceProvider() with the following extension method:

public static class ShellExtension
{
    public static IServiceProvider ServiceProvider(this Shell shell)
    {
        return (shell as AppShell)?.ServiceProvider;
    }
}

Now everything is setup and we can start defining our services:

For every view model do serviceCollection.AddTransient<TViewModel> or even easier register all at once with the help of Scrutor:

public static IServiceCollection AddViewModels<T>(this IServiceCollection serviceCollection)
{
    return serviceCollection.Scan(selector => selector
        .FromAssemblies(typeof(T).Assembly)
        .AddClasses(filter => filter.InNamespaceOf(typeof(T)))
        .AsSelf()
        .WithTransientLifetime());
}

Do the same for your views (pages). If you want to perform the mentioned auto binding, do something like this:

private static IServiceCollection AddView<TView, TViewModel>(this IServiceCollection serviceCollection) where TView : Page
{
    return serviceCollection.AddTransient<TView>(serviceProvider =>
    {
        TView view = ActivatorUtilities.CreateInstance<TView>(serviceProvider);

        // Automatically bind the view model
        view.BindingContext = serviceProvider.GetRequiredService<TViewModel>();

        // You could also forward Appearing and Disappearing page events to your view model (again inspired by Prism)
        view.Appearing += (sender, args) => (((BindableObject)sender).BindingContext as IPageLifeCycleAware)?.OnAppearing();
        view.Disappearing += (sender, args) => (((BindableObject)sender).BindingContext as IPageLifeCycleAware)?.OnDisappearing();

        return view;
    });
}

I can imagine there is a lot more you could think of (especially when the Shell Navigation and Structural Management enhancement gets integrated), but let's keep it simple.

The remaining question is how to construct your views:

  • Manually

    You can always manually create your objects if it is desired. If you are using the classical INavigation mechanism for example, you have to pass the Page object. So you could either access the IServiceProvider via Shell.Current.ServiceProvider() or pass a factory to the creating object.

  • DataTemplateExtension

    The ShellContent specifies a ContentTemplate property, where you can pass a DataTemplate. If specified in XAML it is used together with the DataTemplateExtension. This extension takes a string creates the DataTemplate with the matched Type:

    public DataTemplate(Type type)
    

    Good thing though there is also an overloaded constructor which takes a factory:

    public DataTemplate(Func<object> loadTemplate)
    

    I guess you know what I'm up for? Just copy the DateTemplateExtension and replace one line:

    [ContentProperty(nameof(DataTemplateExtension.TypeName))]
    public sealed class MyDataTemplateExtension : IMarkupExtension<DataTemplate>
    {
        public string TypeName { get; set; }
    
        public DataTemplate ProvideValue(IServiceProvider serviceProvider)
        {
            if (serviceProvider == null)
            {
                throw new ArgumentNullException(nameof(serviceProvider));
            }
    
            if (!(serviceProvider.GetService(typeof(IXamlTypeResolver)) is IXamlTypeResolver typeResolver))
            {
                throw new ArgumentException("No IXamlTypeResolver in IServiceProvider");
            }
    
            if (string.IsNullOrEmpty(TypeName))
            {
                IXmlLineInfo li = serviceProvider.GetService(typeof(IXmlLineInfoProvider)) is IXmlLineInfoProvider lip
                    ? lip.XmlLineInfo
                    : new XmlLineInfo();
                throw new XamlParseException("TypeName isn't set.", li);
            }
    
            if (typeResolver.TryResolve(TypeName, out Type type))
            {
                // Change the DataTemplate creation and use the .NET extensions DependencyInjection
                return new DataTemplate(() => Shell.Current.ServiceProvider().GetRequiredService(type));
            }
    
            IXmlLineInfo lineInfo = serviceProvider.GetService(typeof(IXmlLineInfoProvider)) is IXmlLineInfoProvider lineInfoProvider
                ? lineInfoProvider.XmlLineInfo
                : new XmlLineInfo();
            throw new XamlParseException($"MyDataTemplateExtension: Could not locate type for {TypeName}.", lineInfo);
        }
    
        object IMarkupExtension.ProvideValue(IServiceProvider serviceProvider)
        {
            return (this as IMarkupExtension<DataTemplate>).ProvideValue(serviceProvider);
        }
    }
    
  • RegisterRoute

    Routing.RegisterRoute also allows to pass a RouteFactory. It's not a big deal to provide your own implementation:

    public class MyRouteFactory<T> : RouteFactory where T : Element
    {
        protected IServiceProvider ServiceProvider { get; }
    
        public MyRouteFactory(IServiceProvider serviceProvider)
        {
            ServiceProvider = serviceProvider;
        }
    
        public override Element GetOrCreate() => ServiceProvider.GetRequiredService<T>();
    }
    

    Now you can register your routes like this:

    Routing.RegisterRoute("NewItem", ServiceProvider.GetRequiredService<MyRouteFactory<NewItemPage>>());
    

    You could make this prettier with an extension method, but I think you get the point. When you now use the URI based shell navigation and call Shell.Current.GoToAsync, the pages will be created by the .NET extensions DependencyInjection.

Start logging with Serilog

Microsoft simplified logging a lot with the Microsoft.Extensions.Logging package. Program to the logging interface and choose from a variety of different logging solutions. This makes logging a real plug and play experience.

I'm a big fan of Serilog, which plays nicely in combination with .NET extensions DependencyInjection & Configuration. It also provides some useful sinks here:

  • Xamarin: The sink for classic NSLog and AndroidLog logging while debugging your application.
  • File: A file based sink. Useful if you want to collect logs locally and provide the possibility to share those logs on demand.
  • Seq: A sink for convenient remote logging.

Install NuGet packages

Add this packages to your netstandard project:

Install-Package Microsoft.Extensions.Logging
Install-Package Serilog
Install-Package Serilog.Extensions.Logging
Install-Package Serilog.Settings.Configuration

Add the Xamarin (or whatever sink you prefer) to your platform specific projects:

Install-Package Serilog.Sinks.Xamarin

Configuration and setup

Add a logging section to your appsettings.json for the netstandard project:

{
    "Logging": {
        "LogLevel": {
            "Default": "Debug"
        }
    }
}

This is the appsettings.json for the Android project:

{
    "Logging": {
        "Serilog": {
            "WriteTo": [
                {
                    "Name": "AndroidLog"
                }
            ]
        }
    }
}

This is the appsettings.json for the iOS project:

{
    "Logging": {
        "Serilog": {
            "WriteTo": [
                {
                    "Name": "NSLog"
                }
            ]
        }
    }
}

And finally the addition to your Setup.cs:

public static IServiceCollection ConfigureLogging(this IServiceCollection serviceCollection,
    IConfigurationRoot configurationRoot)
{
    return serviceCollection.AddLogging(builder =>
    {
        builder.AddSerilog(new LoggerConfiguration().ReadFrom.Configuration(configurationRoot.GetSection("Logging"))
            .CreateLogger());
    });
}

Don't forget to call ConfigureLogging in your App constructor.

And that's all we need to do. With dependency injection and configuration setup, this was a piece of cake. Now we can inject our logger where appropriate:

public class AboutViewModel : BaseViewModel
{
    public AboutViewModel(IDataStore<Item> dataStore, ILogger<AboutViewModel> logger) : base(dataStore)
    {
        Title = "About";
        OpenWebCommand = new Command(async () => await Browser.OpenAsync("https://xamarin.com"));

        // Here we go (log)
        logger.LogWarning($"Hello from {nameof(AboutViewModel)}");
    }

    public ICommand OpenWebCommand { get; }
}

ASP.NET flavored localization support

I really like the ASP.NET localization system. Especially the resource file naming schema used by the default ResourceManagerStringLocalizerFactory, which makes finding and managing localizations way more enjoyable. There are also blog posts about adding different localization sources, like databases which is also easy if you adhere to the IStringLocalizer interface.

So why not try to bring this into Xamarin.Forms?

Install NuGet packages

Add the package to your netstandard project:

Install-Package Microsoft.Extensions.Localization

Configuration and setup

Add a localization section to your appsettings.json for the netstandard project:

{
    "Localization": {
        "ResourcesPath": "Resources"
    }
}

Add this to your Setup.cs in your netstandard project:

public static IServiceCollection ConfigureLocalization(this IServiceCollection serviceCollection,
    IConfigurationRoot configurationRoot)
{
    return serviceCollection.AddLocalization(options =>
    {
        options.ResourcesPath = configurationRoot.GetSection("Localization")["ResourcesPath"];
    });
}

Don't forget to call ConfigureLocalization in your App constructor.

Now you can use the known StringLocalizer class where nedded:

public class AboutViewModel : BaseViewModel
{
    public AboutViewModel(IDataStore<Item> dataStore, IStringLocalizer<AboutViewModel> localizer) : base(dataStore)
    {
        Title = localizer["Title"];
        OpenWebCommand = new Command(async () => await Browser.OpenAsync("https://xamarin.com"));
    }

    public ICommand OpenWebCommand { get; }
}

For the above view model you would add an AboutViewModel.resx to the "Resources\ViewModels" folder with the Title translation key.

Localizing xaml files is a bit trickier as you will need an IMarkupExtension.

[ContentProperty("Text")]
public class TranslateExtension : IMarkupExtension
{
    public string Text { get; set; }

    public object ProvideValue(IServiceProvider serviceProvider)
    {
        if (serviceProvider == null)
        {
            throw new ArgumentNullException(nameof(serviceProvider));
        }

        return TranslateExtension.GetStringLocalizer(GetRootObjectType(serviceProvider))[Text];
    }

    protected static IStringLocalizer GetStringLocalizer(Type type)
    {
        Type stringLocalizerTypeOfT = typeof(IStringLocalizer<>).MakeGenericType(type);

        return (IStringLocalizer)Shell.Current.ServiceProvider().GetService(stringLocalizerTypeOfT);
    }

    // See https://stackoverflow.com/questions/55869794/access-contentpage-from-imarkupextension
    protected Type GetRootObjectType(IServiceProvider serviceProvider)
    {
        if (serviceProvider == null)
        {
            throw new ArgumentNullException(nameof(serviceProvider));
        }

        IProvideValueTarget valueProvider = serviceProvider.GetService<IProvideValueTarget>() ??
            throw new ArgumentException("serviceProvider does not provide an IProvideValueTarget");

        PropertyInfo cachedPropertyInfo = valueProvider.GetType()
            .GetProperty("Xamarin.Forms.Xaml.IProvideParentValues.ParentObjects", BindingFlags.NonPublic | BindingFlags.Instance);

        if (cachedPropertyInfo != null)
        {
            IEnumerable<object> parentObjects = cachedPropertyInfo.GetValue(valueProvider) as IEnumerable<object>;

            if (parentObjects == null)
            {
                throw new ArgumentException("Unable to access parent objects");
            }

            IEnumerable<object> enumerable = parentObjects as object[] ?? parentObjects.ToArray();

            foreach (object target in enumerable)
            {
                if (target is Page page)
                {
                    return target.GetType();
                }
            }

            return enumerable.Last().GetType();
        }

        throw new XamlParseException($"Unable to access parent page");
    }
}

The following shows how you would localize the app name in the AboutPage.xaml:

<FormattedString>
    <FormattedString.Spans>
        <Span Text="{i18n:Translate AppName}" FontAttributes="Bold" FontSize="22" />
        <Span Text=" " />
        <Span Text="1.0" ForegroundColor="{StaticResource LightTextColor}" />
    </FormattedString.Spans>
</FormattedString>

Conclusion

In this blog post I showed you a way to integrate the .NET extensions into your Xamarin.Forms application. With Microsoft.Extensions.Configuration and Microsoft.Extensions.DependencyInjection a lot of configurability and flexibility is added to your app. Microsoft.Extensions.Logging will make your debugging life a lot easier. In the last section I demonstrated a way to use Microsoft.Extensions.Localization within your application, which might be valuable for you especially if you are used to ASP.NET. You might also want to check for other .NET extensions functionality, like Microsoft.Extensions.Caching or similar.

Don't forget to take a look into the sample code repo. We hope you enjoyed these guides and please don't hesitate contacting us if you have any questions or feedback.