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.
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.
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 š
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:
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
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
.
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)?
Add the package to your netstandard project:
Install-Package Microsoft.Extensions.DependencyInjection
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.
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:
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
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; }
}
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?
Add the package to your netstandard project:
Install-Package Microsoft.Extensions.Localization
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>
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.