This article is part of a series called Playground:
- Playground – Part 1: A Xamarin.Forms Microsoft.Extended journey (this one)
- Playground – Part 2: Settings with Options pattern
- Playground – Part 3: Catch it, trace it, log it and upload it
Into this Playground blog post series, I’ll try to experiment some concepts, frameworks and plugins, to find out how it could be useful to build a base project. A project that could be a good starting point for further customer specific mobile developments. You know we are talking here about this famous templating project we probably all try to create, reshape, and trash and again and again like almost every year.
So this one should be the one? Well it’s not the point actually. I just like to play with all exiting stuff coming from the open source community like Xamarin.Forms, Prism or Shiny, and my base target is to open up the doors of Microsoft.Extensions goodness into our Xamarin.Forms app developments.
As it’s an experimental project, it’s not really a beginner series presenting how to build a Xamarin.Forms app in general from scratch to stores. There’re so much articles about it over the web, and James Montemagno seems to be starting a new general walkthrough, promising to be really interesting.
Here is a personal experiment journey mainly through the .Net mobile development world, plus some web things to. I mean, it’s called Playground after all, so feel free to share your thought and may be contribute to it as it’s all hosted on GitHub.
Parental advisory: If you come from a static addict planet, I guess you’ll feel sick down there, because it’s all about DI explicit contents 🙂
So let’s play!
First thing first, let’s create the minimal project.
My minimal empty Playground project from where I’ll be starting will be created with few things.
- Visual Studio with Xamarin mobile development tools (free for Community)
- Resharper (starting at 12.90$/month for indies) and/or MFractor (limited free or full for 100$/year) but both are optionals, focused on productivity and code quality
- XamlStyler (free) but optional, focused on xaml readability
I’m a .Net developer, so I use Xamarin.Forms to create mobile apps.
I could also create my Playground with Uno Platform which seems to be an interesting alternative, probably someday after. For now, my focus is to create something generic and most largely used.
When talking about MVVM frameworks, we have choices. I could mention MvvmCross, MvvmLight, FreshMvvm, ReactiveUI and Prism, but there are others. I used all of them at least once for customer projects, specially Prism, ReactiveUI and MvvmCross for many of it.
They are all pretty good but I have to pick one so I selected the most downloaded from NuGet and it’s Prism currently. As MvvmCross download counter is really closed, maybe I’ll create another playground/branch for it, someday after. In all cases, ReactiveUI could work seamlessly like twin shadow to get best of both frameworks.
From its creator “Shiny was built on the premise of making dependency injection and cross platform backgrounding a consistent & testable experience”.
I’ll use Shiny mainly to get the same DI experience as we get from .Net Core world.
Same experience, same skills. Less risk, less time to maintain.
Well, Shiny offers so much more than just DI things, like backgrounding, platform services and plugins, etc. It also opens the mobile development world to the .Net Core NuGet extensions ecosystem. And it’s plethoric.
From its official web site “Xamarin.Essentials provides a single cross-platform API that works with any iOS, Android, or UWP application that can be accessed from shared code no matter how the user interface is created.”
Well, actually I’ll be using the Xamarin.Essentials.Interfaces version to make it play like a charm with our DI world.
Because lazier is smarter, we do have the opportunity to get our empty starting project created and all wired with Forms and Prism by couple of clicks.
Others
I’m a mobile focused developer, but what’s a mobile app without data? I’ll play with some front and backend web things to make my playground world… just play!
I’ll try to discover some concepts like DDD, CQRS, Event-sourcing with libs like MediatR, Optional and whatever could improve code quality and architecture.
Maybe if I’m not as lazy as I’m saying, I’ll play with some unit test frameworks (I know I should do it, I know…)
First create a Prism Blank App project for Xamarin.Forms with or without template.
Personally I created 4 projects with the template called Playground.Forms, Playground.Forms.Android, Playground.Forms.iOS and Playground.Forms.UWP.
So later I could add projects like Playground.Uno.iOS or Playground.Wpf or Playground.Web.Api or whatever…
Then two options: the “classic” or the “magic”
Make sure to install on every project:
- Prism.DryIoc.Extensions >= v8.0.48* (or whatever container suits to you)
- ReactiveUI.Fody (optional but I told you I wan’t the best of both worlds)
- Shiny.Prism >= v8.0.48*
*You’d better use any version >= v8.0.48 to support multiple implementation registering and resolving like container.Resolve<IEnumerable<ISomething>>();
This is needed by many extention packages.
Open your App.xaml.cs, empty the constructor and clean other methods to get something like:
public partial class App { public App() { } protected override void OnInitialized() { InitializeComponent(); NavigationService.NavigateAsync("NavigationPage/MainPage"); } protected override void RegisterTypes(IContainerRegistry containerRegistry) { containerRegistry.RegisterForNavigation<NavigationPage>();
containerRegistry.RegisterForNavigation<MainPage, MainPageViewModel>(); } protected override void OnNavigationError(INavigationError navigationError) { #if DEBUG // Ensure we always break here while debugging! Debugger.Break(); #endif base.OnNavigationError(navigationError); } }
Create a Startup class inheriting from PrismStartup into your standard project :
public class Startup : PrismStartup { protected override void ConfigureServices(IServiceCollection services) { // Add Xamarin Essentials services.AddTransient<IAppInfo, AppInfoImplementation>(); } }
You can see we register AppInfo Essentials api from the Startup ConfigureServices method, that’s why I removed it from the App RegisterTypes one. The Startup class is where you can play with your DI registrations (see Shiny and .Net Core documentations about it).
Open up the ViewModelBase and make it look like this:
public abstract class ViewModelBase : ReactiveObject, IApplicationLifecycleAware, IConfirmNavigation, IConfirmNavigationAsync, IDestructible, IInitialize, IInitializeAsync, INavigationAware, IPageLifecycleAware { protected readonly INavigationService NavigationService; protected readonly IConnectivity ConnectivityService; protected ViewModelBase(INavigationService navigationService) { NavigationService = navigationService; ConnectivityService = ShinyHost.Resolve<IConnectivity>(); Disposables = new CompositeDisposable(); // Get internet status ConnectivityService.WhenInternetStatusChanged() .ToPropertyEx(this, x => x.IsOnline) .DisposeWith(Disposables); // Set IsBusy to true while BusyCounter > 0 (handling parallelism) this.WhenAnyValue(x => x.BusyCounter) .Select(busyCounter => busyCounter > 0) .ObserveOn(RxApp.MainThreadScheduler) .ToPropertyEx(this, x => x.IsBusy) .DisposeWith(Disposables); } [Reactive] public string Title { get; set; } [ObservableAsProperty] public bool IsOnline { get; } [ObservableAsProperty] public bool IsBusy { get; } [Reactive] protected int BusyCounter { get; set; } protected CompositeDisposable Disposables { get; private set; } protected virtual void HandleIsBusy(bool isBusy) { if (isBusy) BusyCounter++; else if (BusyCounter > 0) BusyCounter--; } public virtual void OnResume() { } public virtual void OnSleep() { } public virtual bool CanNavigate(INavigationParameters parameters) { return true; } public virtual Task<bool> CanNavigateAsync(INavigationParameters parameters) { return Task.FromResult(true); } public virtual void Destroy() { Disposables.Dispose(); Disposables = null; } public virtual void Initialize(INavigationParameters parameters) { } public virtual Task InitializeAsync(INavigationParameters parameters) { return Task.CompletedTask; } public virtual void OnNavigatedFrom(INavigationParameters parameters) { } public virtual void OnNavigatedTo(INavigationParameters parameters) { } public virtual void OnAppearing() { } public virtual void OnDisappearing() { } }
You can see ViewModelBase derives from ReactiveObject but also implements Prism interfaces so that we could get best of both worlds.
We subscribe for connectivity changes (provided by Shiny) and BusyCounter changes (for parallelized task execution).
Properties decorated with Reactive and ObservableAsProperty attributes will be IL weaved with notify property changes handling code at build time (thanks to ReactiveUI.Fody).
Hopefully, the standard project should build with success at this point (may need a Clean & Rebuild).
What if we could let something somewhere define almost everything we basically need. Lazy am I!
This is where the well named Prism.Magician steps in as it makes use of Code Gen and IL weaving to write for us what’s boring to. And it’s boring to write something commonly used like view registration, service registration, viewmodelbase, initialization steps, property changing handling, bindable property definition and so on…
Prism.Magician is not an open source library available via NuGet. In order to use Prism.Magician you must be one of the following:
- A Microsoft Employee
- A Microsoft MVP
- An Open Source Author / Maintainer
- One of AvantiPoint’s Enterprise Support customers
- One of Dan’s GitHub Sponsors currently starting at 10$/month to get a SponsorConnect packages feed access
If you’re none of it and don’t want to support Dan’s work for at least 10$/month, you can definitely use the previous classic free way and jump to the FOR BOTH WAYS section.
Otherwise, if you get your ticket to Prism.Magician, next is all about it.
First of all, if you’re coming from the classic way, please remove from every project:
- Prism.DryIoc.Extensions (or whatever container you installed)
- ReactiveUI.Fody
- Shiny.Prism
Make sure to install on every project:
- Prism.Magician (from SponsorConnect feed or AvantiPoint’s Customer Portal)
- Prism.DryIoc.Forms (or whatever container suits to you)
- ReactiveUI (optional but I wan’t the best of both worlds)
- Shiny.Core
From here, open your App.xaml.cs, empty the constructor and clean almost everything to get something like:
[AutoRegisterViews] public partial class App { public App() { } protected override void OnInitialized() { NavigationService.NavigateAsync($"{NavigationKeys.NavigationPage}/{NavigationKeys.MainPage}") .OnNavigationError(OnNavigationError); } private void OnNavigationError(Exception ex) { System.Diagnostics.Debugger.Break(); } }
As Prism.Magician provide an attribute to auto register views, I just decorated my App class with the AutoRegisterViews attribute so Prism will do it for me. Also, the Magician maintains some navigation keys to help us preserving MVVM separation when asking for navigation.
Create a Startup partial class inheriting from ShinyStartup into your standard project :
public partial class Startup : ShinyStartup { public override void ConfigureServices(IServiceCollection services) { // Add Xamarin Essentials services.AddTransient<IAppInfo, AppInfoImplementation>(); } }
It’s important to note almost everything should be partial as we need the Magician to implement every other boring boilerplate things for us.
You can see we register AppInfo Essentials api from the Startup ConfigureServices method, that’s why I removed it from the App RegisterTypes one. The Startup class is where you can play with your DI registrations (see Shiny and .Net Core documentations about it).
We must now create a folder called “Services”. This is where we’ll define our services obviously and this is needed by the Magician for its BaseServices which will be injected into our ViewModels.
Open up the ViewModelBase and make it look like:
[ViewModelBase] public abstract partial class ViewModelBase : ReactiveObject { }
We decorated our ViewModelBase class with the ViewModelBase attribute to tell the Magician to implement everything we’re expected to find into… a ViewModelBase obviously!
It inherits from ReactiveObject so that the Magician knows I’am asking for best of both worlds, meaning Prism lifecycle management alongside Reactive property changing handling (to be short).
We do need to adjust the MainPageViewModel code like:
public partial class MainPageViewModel : ViewModelBase { public MainPageViewModel(BaseServices baseServices) : base(baseServices) { } }
And that’s all! Nothing more for now!
Hopefully, the standard project should build with success at this point (may need a Clean & Rebuild).
Here is currently what the Magician should have provided to your extended partial ViewModelBase class after build:
public partial class ViewModelBase : ReactiveObject, IApplicationLifecycleAware, IConfirmNavigation, IConfirmNavigationAsync, IDestructible, IInitialize, IInitializeAsync, INavigationAware, IPageLifecycleAware { [GeneratedCodeAttribute("ViewModelBaseGenerator", "8.0.125-beta+1a5e505db3")] protected readonly Prism.Navigation.INavigationService NavigationService; [GeneratedCodeAttribute("ViewModelBaseGenerator", "8.0.125-beta+1a5e505db3")] protected readonly Prism.Services.IPageDialogService PageDialogService; [GeneratedCodeAttribute("ViewModelBaseGenerator", "8.0.125-beta+1a5e505db3")] protected readonly Prism.Services.Dialogs.IDialogService DialogService; [GeneratedCodeAttribute("ViewModelBaseGenerator", "8.0.125-beta+1a5e505db3")] protected readonly Prism.Events.IEventAggregator EventAggregator; [GeneratedCodeAttribute("ViewModelBaseGenerator", "8.0.125-beta+1a5e505db3")] protected readonly Shiny.Net.IConnectivity Connectivity; [GeneratedCodeAttribute("ViewModelBaseGenerator", "8.0.125-beta+1a5e505db3")] private readonly ObservableAsPropertyHelper<bool> _isBusyHelper; [GeneratedCodeAttribute("ViewModelBaseGenerator", "8.0.125-beta+1a5e505db3")] private readonly ObservableAsPropertyHelper<bool> _isNotBusyHelper; [GeneratedCodeAttribute("ViewModelBaseGenerator", "8.0.125-beta+1a5e505db3")] private readonly ObservableAsPropertyHelper<bool> _isOnline; [GeneratedCodeAttribute("ViewModelBaseGenerator", "8.0.125-beta+1a5e505db3")] private readonly ObservableAsPropertyHelper<bool> _isOffline; private string _title; private string _subTitle; [GeneratedCodeAttribute("ViewModelBaseGenerator", "8.0.125-beta+1a5e505db3")] protected ViewModelBase(Playground.Services.BaseServices baseServices) { Disposables = new CompositeDisposable(); NavigationService = baseServices.NavigationService; PageDialogService = baseServices.PageDialogService; DialogService = baseServices.DialogService; EventAggregator = baseServices.EventAggregator; Connectivity = baseServices.Connectivity; GeneratedInitialization(); GetIsBusy().ToProperty(this, nameof(IsBusy), out _isBusyHelper).DisposeWith(Disposables); this.WhenAnyValue(x => x.IsBusy).Select(x => !x).ToProperty(this, nameof(IsNotBusy), out _isNotBusyHelper, true).DisposeWith(Disposables); _isOnline = baseServices.Connectivity.WhenInternetStatusChanged().ToProperty(this, x => x.IsOnline, baseServices.Connectivity.IsInternetAvailable()).DisposeWith(Disposables); _isOffline = this.WhenAnyValue(x => x.IsOnline).Select(x => !x).ToProperty(this, x => x.IsOffline, !IsOnline).DisposeWith(Disposables); } [GeneratedCodeAttribute("ViewModelBaseGenerator", "8.0.125-beta+1a5e505db3")] protected CompositeDisposable Disposables { get; private set; } [GeneratedCodeAttribute("ViewModelBaseGenerator", "8.0.125-beta+1a5e505db3")] public string Title { get => _title; set => this.RaiseAndSetIfChanged(ref _title, value); } [GeneratedCodeAttribute("ViewModelBaseGenerator", "8.0.125-beta+1a5e505db3")] public string SubTitle { get => _subTitle; set => this.RaiseAndSetIfChanged(ref _subTitle, value); } [GeneratedCodeAttribute("ViewModelBaseGenerator", "8.0.125-beta+1a5e505db3")] public ReactiveUI.ReactiveCommand<string, Unit> NavigateCommand { get; } [GeneratedCodeAttribute("ViewModelBaseGenerator", "8.0.125-beta+1a5e505db3")] public ReactiveUI.ReactiveCommand<Unit, Unit> GoBackCommand { get; } [GeneratedCodeAttribute("ViewModelBaseGenerator", "8.0.125-beta+1a5e505db3")] public bool IsBusy => _isBusyHelper?.Value ?? false; [GeneratedCodeAttribute("ViewModelBaseGenerator", "8.0.125-beta+1a5e505db3")] public bool IsNotBusy => _isNotBusyHelper?.Value ?? true; [GeneratedCodeAttribute("ViewModelBaseGenerator", "8.0.125-beta+1a5e505db3")] public bool IsOnline => _isOnline?.Value ?? false; [GeneratedCodeAttribute("ViewModelBaseGenerator", "8.0.125-beta+1a5e505db3")] public bool IsOffline => _isOffline?.Value ?? true; [GeneratedCodeAttribute("ViewModelBaseGenerator", "8.0.125-beta+1a5e505db3")] protected virtual void GeneratedInitialization() { Title = Regex.Replace(GetType().Name, "ViewModel$", string.Empty); } [GeneratedCodeAttribute("ViewModelBaseGenerator", "8.0.125-beta+1a5e505db3")] protected virtual IObservable<bool> GetIsBusy() { return this.WhenAnyObservable(x => x.NavigateCommand.IsExecuting, x => x.GoBackCommand.IsExecuting); } [GeneratedCodeAttribute("ViewModelBaseGenerator", "8.0.125-beta+1a5e505db3")] protected Task<NavigationState> HandleNavigationRequest(string uri, params (string key, object value)[] parameters) { var navigationParameters = new NavigationParameters(); foreach ((var key, var value) in parameters) { navigationParameters.Add(key, value); } return HandleNavigationRequest(uri, navigationParameters); } [GeneratedCodeAttribute("ViewModelBaseGenerator", "8.0.125-beta+1a5e505db3")] protected async Task<NavigationState> HandleNavigationRequest(string uri, INavigationParameters parameters) { var result = await NavigationService.NavigateAsync(uri, parameters); if (result.Exception != null) { HandleNavigationError(uri, parameters, result.Exception); return NavigationState.Failed; } return result.Success ? NavigationState.Succeeded : NavigationState.Cancelled; } [GeneratedCodeAttribute("ViewModelBaseGenerator", "8.0.125-beta+1a5e505db3")] protected Task<NavigationState> HandleGoBackRequest(params (string key, object value)[] parameters) { var navigationParameters = new NavigationParameters(); foreach ((var key, var value) in parameters) { navigationParameters.Add(key, value); } return HandleGoBackRequest(navigationParameters); } [GeneratedCodeAttribute("ViewModelBaseGenerator", "8.0.125-beta+1a5e505db3")] protected async Task<NavigationState> HandleGoBackRequest(INavigationParameters parameters) { var result = await NavigationService.GoBackAsync(parameters); if (result.Exception != null) { HandleNavigationError(null, parameters, result.Exception); return NavigationState.Failed; } return result.Success ? NavigationState.Succeeded : NavigationState.Cancelled; } [GeneratedCodeAttribute("ViewModelBaseGenerator", "8.0.125-beta+1a5e505db3")] protected virtual void HandleNavigationError(string uri, INavigationParameters parameters, Exception ex) { var message = string.IsNullOrEmpty(uri) ? "Error occurred while navigating back" : $"Error navigating to '{{uri}}'"; Console.WriteLine(); Console.WriteLine("---------- Navigation Error ----------"); Console.WriteLine(message); Console.WriteLine(ex); Console.WriteLine(); } [GeneratedCodeAttribute("ViewModelBaseGenerator", "8.0.125-beta+1a5e505db3")] bool IConfirmNavigation.CanNavigate(INavigationParameters parameters) { return CanNavigate(parameters); } [GeneratedCodeAttribute("ViewModelBaseGenerator", "8.0.125-beta+1a5e505db3")] protected virtual bool CanNavigate(INavigationParameters parameters) { return true; } [GeneratedCodeAttribute("ViewModelBaseGenerator", "8.0.125-beta+1a5e505db3")] Task<bool> IConfirmNavigationAsync.CanNavigateAsync(INavigationParameters parameters) { return CanNavigateAsync(parameters); } [GeneratedCodeAttribute("ViewModelBaseGenerator", "8.0.125-beta+1a5e505db3")] protected virtual Task<bool> CanNavigateAsync(INavigationParameters parameters) { return Task.FromResult(true); } [GeneratedCodeAttribute("ViewModelBaseGenerator", "8.0.125-beta+1a5e505db3")] void IInitialize.Initialize(INavigationParameters parameters) { GeneratedInitialization(parameters); Initialize(parameters); } [GeneratedCodeAttribute("ViewModelBaseGenerator", "8.0.125-beta+1a5e505db3")] protected virtual void Initialize(INavigationParameters parameters) { // Intentionally left blank } [GeneratedCodeAttribute("ViewModelBaseGenerator", "8.0.125-beta+1a5e505db3")] protected virtual void GeneratedInitialization(INavigationParameters parameters) { // Intentionally left blank } [GeneratedCodeAttribute("ViewModelBaseGenerator", "8.0.125-beta+1a5e505db3")] Task IInitializeAsync.InitializeAsync(INavigationParameters parameters) { return InitializeAsync(parameters); } [GeneratedCodeAttribute("ViewModelBaseGenerator", "8.0.125-beta+1a5e505db3")] protected virtual Task InitializeAsync(INavigationParameters parameters) { return Task.CompletedTask; } [GeneratedCodeAttribute("ViewModelBaseGenerator", "8.0.125-beta+1a5e505db3")] void INavigatedAware.OnNavigatedFrom(INavigationParameters parameters) { OnNavigatedFrom(parameters); } [GeneratedCodeAttribute("ViewModelBaseGenerator", "8.0.125-beta+1a5e505db3")] protected virtual void OnNavigatedFrom(INavigationParameters parameters) { // Intentionally left blank } [GeneratedCodeAttribute("ViewModelBaseGenerator", "8.0.125-beta+1a5e505db3")] void INavigatedAware.OnNavigatedTo(INavigationParameters parameters) { OnNavigatedTo(parameters); } [GeneratedCodeAttribute("ViewModelBaseGenerator", "8.0.125-beta+1a5e505db3")] protected virtual void OnNavigatedTo(INavigationParameters parameters) { // Intentionally left blank } [GeneratedCodeAttribute("ViewModelBaseGenerator", "8.0.125-beta+1a5e505db3")] void IApplicationLifecycleAware.OnResume() { OnResume(); } [GeneratedCodeAttribute("ViewModelBaseGenerator", "8.0.125-beta+1a5e505db3")] protected virtual void OnResume() { // Intentionally left blank } [GeneratedCodeAttribute("ViewModelBaseGenerator", "8.0.125-beta+1a5e505db3")] void IApplicationLifecycleAware.OnSleep() { OnSleep(); } [GeneratedCodeAttribute("ViewModelBaseGenerator", "8.0.125-beta+1a5e505db3")] protected virtual void OnSleep() { // Intentionally left blank } [GeneratedCodeAttribute("ViewModelBaseGenerator", "8.0.125-beta+1a5e505db3")] void IPageLifecycleAware.OnDisappearing() { OnDisappearing(); } [GeneratedCodeAttribute("ViewModelBaseGenerator", "8.0.125-beta+1a5e505db3")] protected virtual void OnDisappearing() { // Intentionally left blank } [GeneratedCodeAttribute("ViewModelBaseGenerator", "8.0.125-beta+1a5e505db3")] void IPageLifecycleAware.OnAppearing() { OnAppearing(); } [GeneratedCodeAttribute("ViewModelBaseGenerator", "8.0.125-beta+1a5e505db3")] protected virtual void OnAppearing() { // Intentionally left blank } [GeneratedCodeAttribute("ViewModelBaseGenerator", "8.0.125-beta+1a5e505db3")] void IDestructible.Destroy() { Destroy(); Disposables.Dispose(); Disposables = null; } [GeneratedCodeAttribute("ViewModelBaseGenerator", "8.0.125-beta+1a5e505db3")] protected virtual void Destroy() { // Intentionally left blank } }
You can see what has been generated on your side by exploring the path \obj\Debug\netstandard2.0\g\.
You can really say “less is more” as everything is in place with a simple attribute decoration and each property accessible from any ViewModel.
Now, platforms!
The first thing to do is to change the MainApplication class to:
public class MainApplication : ShinyAndroidApplication<Startup> { public MainApplication(IntPtr javaReference, JniHandleOwnership transfer) : base(javaReference, transfer) { } }
As you can see we make our MainApplication class inherit from ShinyAndroidApplication with our newly created Startup class.
That means Shiny will be the first thing to be initialized, before Xamarin.Forms and others.
Now the MainActivity:
public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity { protected override void OnCreate(Bundle savedInstanceState) { TabLayoutResource = Resource.Layout.Tabbar; ToolbarResource = Resource.Layout.Toolbar; base.OnCreate(savedInstanceState); global::Xamarin.Forms.Forms.Init(this, savedInstanceState); Xamarin.Essentials.Platform.Init(this, savedInstanceState); LoadApplication(new App()); } protected override void OnNewIntent(Intent intent) { base.OnNewIntent(intent); this.ShinyOnNewIntent(intent); } public override void OnRequestPermissionsResult(int requestCode, string[] permissions, Android.Content.PM.Permission[] grantResults) { this.ShinyRequestPermissionsResult(requestCode, permissions, grantResults); Xamarin.Essentials.Platform.OnRequestPermissionsResult(requestCode, permissions, grantResults); base.OnRequestPermissionsResult(requestCode, permissions, grantResults); } }
We just removed Prism platform initialization and wired some OnSomethingOccured overrides
On iOS side, just make the AppDelegate look like:
public partial class AppDelegate : global::Xamarin.Forms.Platform.iOS.FormsApplicationDelegate { public override bool FinishedLaunching(UIApplication app, NSDictionary options) { this.ShinyFinishedLaunching(new Startup()); global::Xamarin.Forms.Forms.Init(); LoadApplication(new App()); return base.FinishedLaunching(app, options); } public override void PerformFetch(UIApplication application, Action<UIBackgroundFetchResult> completionHandler) => this.ShinyPerformFetch(completionHandler); public override void HandleEventsForBackgroundUrl(UIApplication application, string sessionIdentifier, Action completionHandler) => this.ShinyHandleEventsForBackgroundUrl(sessionIdentifier, completionHandler); }
As for Android, we started with Shiny initialization with our Startup class and wired some OnSomethingOccured overrides
Open your UWP’s App class an make it look like:
/// <summary> /// Provides application-specific behavior to supplement the default Application class. /// </summary> sealed partial class App : Application { /// <summary> /// Initializes the singleton application object. This is the first line of authored code /// executed, and as such is the logical equivalent of main() or WinMain(). /// </summary> public App() { this.InitializeComponent(); this.ShinyInit(new Startup()); this.Suspending += OnSuspending; } /// <summary> /// Invoked when the application is launched normally by the end user. Other entry points /// will be used such as when the application is launched to open a specific file. /// </summary> /// <param name="e">Details about the launch request and process.</param> protected override void OnLaunched(LaunchActivatedEventArgs e) { var rootFrame = Window.Current.Content as Frame; if (rootFrame == null) { rootFrame = new Frame(); rootFrame.NavigationFailed += this.OnNavigationFailed; Xamarin.Forms.Forms.Init(e); Window.Current.Content = rootFrame; } if (rootFrame.Content == null) rootFrame.Navigate(typeof(MainPage), e.Arguments); Window.Current.Activate(); } /// <summary> /// Invoked when Navigation to a certain page fails /// </summary> /// <param name="sender">The Frame which failed navigation</param> /// <param name="e">Details about the navigation failure</param> void OnNavigationFailed(object sender, NavigationFailedEventArgs e) { throw new Exception("Failed to load Page " + e.SourcePageType.FullName); } /// <summary> /// Invoked when application execution is being suspended. Application state is saved /// without knowing whether the application will be terminated or resumed with the contents /// of memory still intact. /// </summary> /// <param name="sender">The source of the suspend request.</param> /// <param name="e">Details about the suspend request.</param> private void OnSuspending(object sender, SuspendingEventArgs e) { var deferral = e.SuspendingOperation.GetDeferral(); //TODO: Save application state and stop any background activity deferral.Complete(); } }
The only thing we added here is this.ShinyInit(new Startup()); into the constructor to initialize Shiny.
Your MainPage class should look like:
/// <summary> /// An empty page that can be used on its own or navigated to within a Frame. /// </summary> public sealed partial class MainPage { public MainPage() { this.InitializeComponent(); this.LoadApplication(new Playground.Forms.App()); } }
Actually nothing to change here.
Finaly, edit your appmanifest with an XML editor to add some Shiny extensions:
<?xml version="1.0" encoding="utf-8"?> <Package xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10" xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest" xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10" IgnorableNamespaces="uap mp"> <Identity Name="8f3e85c1-fd0e-46cd-bb8a-69ec2d9ce408" Publisher="CN=respawnsive" Version="1.0.0.0" /> <mp:PhoneIdentity PhoneProductId="8f3e85c1-fd0e-46cd-bb8a-69ec2d9ce408" PhonePublisherId="00000000-0000-0000-0000-000000000000"/> <Properties> <DisplayName>Playground.Forms.UWP</DisplayName> <PublisherDisplayName>Respawnsive</PublisherDisplayName> <Logo>Assets\StoreLogo.png</Logo> </Properties> <Dependencies> <TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.0.0" MaxVersionTested="10.0.0.0" /> </Dependencies> <Resources> <Resource Language="x-generate"/> </Resources> <Applications> <Application Id="App" Executable="$targetnametoken$.exe" EntryPoint="Playground.Forms.UWP.App"> <uap:VisualElements DisplayName="Playground" Square150x150Logo="Assets\Square150x150Logo.png" Square44x44Logo="Assets\Square44x44Logo.png" Description="Playground Forms UWP" BackgroundColor="transparent"> <uap:DefaultTile Wide310x150Logo="Assets\Wide310x150Logo.png"/> <uap:SplashScreen Image="Assets\SplashScreen.png" /> </uap:VisualElements> <Extensions> <Extension Category="windows.backgroundTasks" EntryPoint="Shiny.Support.Uwp.ShinyBackgroundTask"> <BackgroundTasks> <Task Type="general"/> <Task Type="systemEvent"/> <Task Type="timer"/> <Task Type="deviceUse" /> <Task Type="deviceConnectionChange" /> </BackgroundTasks> </Extension> </Extensions> </Application> </Applications> <Capabilities> <Capability Name="internetClient" /> </Capabilities> <Extensions> <Extension Category="windows.activatableClass.inProcessServer"> <InProcessServer> <Path>CLRHost.dll</Path> <ActivatableClass ActivatableClassId="Shiny.Support.Uwp.ShinyBackgroundTask" ThreadingModel="both" /> </InProcessServer> </Extension> </Extensions> </Package>
That’s all for platforms.
It’s now showtime!
Just run your app on each platform and all should be playing as expected.
Remember our Startup.cs file? It should look like:
public partial class Startup : ShinyStartup { public override void ConfigureServices(IServiceCollection services) { // Add Xamarin Essentials services.AddTransient<IAppInfo, AppInfoImplementation>(); } }
*partial is optional, depending if you followed the classic or magic way.
Well, it looks good so far with only one container registration, but as Xamarin.Essentials offers currently more than 40 apis, and as Xamarin.Essentials won’t be the only registrations we need, I want to keep my Startup readable.
That’s where ShinyModule comes in.
Create a Modules folder and an EssentialsModule.cs file in it, and make it look like:
public class EssentialsModule : ShinyModule { public override void Register(IServiceCollection services) { // Add Xamarin Essentials services.AddTransient<IAppInfo, AppInfoImplementation>(); //services.AddTransient<IEmail, EmailImplementation>(); //services.AddTransient<ISms, SmsImplementation>(); // and so on... } }
Shiny modules let us group container registrations by concerns, which I think is a good practice.
We just have to update our Startup.cs to:
public partial class Startup : ShinyStartup { public override void ConfigureServices(IServiceCollection services) { // Add Xamarin Essentials services.RegisterModule<EssentialsModule>(); } }
*partial is optional, depending if you followed the classic or magic way.
Clean, clear and simple!
Here is our minimal empty project!
Next article will talk about Settings with Options pattern.
Playground source code is available on GitHub.
As the playground project is constantly moving and growing, be sure to select the corresponding tag, then the commit number and finally the Browse files button to explore the blog post matching version.