Friday, February 7, 2020

True background tasks in Xamarin Forms

This blog post is heavily based on https://robgibbens.com/backgrounding-with-xamarin-forms/ but I have removed extraneous material and added more detail. I've also removed the iOS stuff because Apple charges too much for a developer license.

The concept of background tasks in Xamarin Forms is quite complex because the different platforms implement the required technology very differently. Basically a background task is started/stopped and executed in the Portable project but the Service that hosts it is in the platform specific projects. We use Messaging to interact between the control process, the task process, and the service.

To help you understand what goes where here's the expanded solution for the finished project.



Start a C# Xamarin Forms project in Visual Studio. Call it testBackground.

Select a Blank template and deselect iOS.


Lets start by creating a simple MainPage that has [Start] and [Stop] buttons and a label to display a message. The MainPage.xaml looks like this.

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:d="http://xamarin.com/schemas/2014/forms/design"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:local="clr-namespace:testBackground"
             mc:Ignorable="d"
             BindingContext="{StaticResource MainViewModel}"
             x:Class="testBackground.MainPage">
    <ContentPage.Resources>
        <ResourceDictionary>
            <local:MainViewModel x:Key="MainViewModel"/>
        </ResourceDictionary>
    </ContentPage.Resources>
    <StackLayout>
        <Button Text="Start" Command="{Binding StartCommand}"/>
        <Label Text="{Binding StatusMessage}"/>
        <Button Text="Stop" Command="{Binding StopCommand}"/>
    </StackLayout>
</ContentPage>

As you can see we are using a view model so the code behind is simply the default from Visual Studio.

using System.ComponentModel;
using Xamarin.Forms;

namespace testBackground
{
    [DesignTimeVisible(false)]
    public partial class MainPage : ContentPage

    {
        public MainPage()
        {
            InitializeComponent();
        }
    }
}

I always have a base view model to handle INotifyPropertyChanged so create a new folder in the portable project called ViewModels.

Add a class to the ViewModels folder called BaseViewModel. It's good practice to have a base ViewModel class to handle INotifyPropertyChanged etc.

using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace testBackground
{
    public class ViewModelBase : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        protected void OnPropertyChanged([CallerMemberName] String PropertyName = null)
        {
            PropertyChangedEventHandler handler = PropertyChanged;
            if (handler != null)
                handler(this, new PropertyChangedEventArgs(PropertyName));
        }

        public bool SetProperty<T>(ref T Storage, T value, [CallerMemberName] string propertyName = null)
        {
            if (Object.Equals(Storage, value)) return false;
            Storage = value;
            OnPropertyChanged(propertyName);
            return true;
        }
    }
}

The MainPage ViewModel is in the same folder and is called MainViewModel. It defines the Start and Stop commands. I didn't implement CanExecute for them. After defining those commands I subscribe to incoming messages. This class will start and stop the background task and will receive periodic updates from it.

using System.Windows.Input;
using Xamarin.Forms;

namespace testBackground
{
    class MainViewModel:ViewModelBase
    {
        public ICommand StartCommand { get; set; }
        public ICommand StopCommand { get; set; }

        private string _StatusMessage = "";
        public string StatusMessage
        {
            get { return _StatusMessage; }
            set { SetProperty(ref _StatusMessage, value); }
        }

        public MainViewModel()
        {
            StartCommand = new Command(StartTicking);
            StopCommand = new Command(StopTicking);
            MonitorMessages();
        }

        public void StartTicking()
        {
            StartMessage msg = new StartMessage();
            MessagingCenter.Send(msg, "StartMessage");
        }

        public void StopTicking()
        {
            StopMessage msg = new StopMessage();
            MessagingCenter.Send(msg, "StopMessage");
        }

        public void MonitorMessages()
        {
            MessagingCenter.Subscribe<StatusMessage>(this, "StatusMessage", msg =>
            {
                StatusMessage = msg.msg;
            });
        }
    }
}

We use messages to communicate with the background task so we need to define those messages. They're classes so you can transfer any information you want back and forth. Create a new folder in the portable project and call it Messages. I have created a class file for each message, but you don't have to.

Here are those message definitions. As you can see, in this example only the StatusMessage actually has any properties.

using System;
using System.Collections.Generic;
using System.Text;

namespace testBackground
{
    public class CancelledMessage
    {
    }
}


using System;
using System.Collections.Generic;
using System.Text;

namespace testBackground
{
    public class StartMessage
    {
    }
}


using System;
using System.Collections.Generic;
using System.Text;

namespace testBackground
{
    public class StatusMessage
    {
        public string msg { get; set; }
    }
}


using System;
using System.Collections.Generic;
using System.Text;

namespace testBackground
{
    public class StopMessage
    {
    }
}

Now it is time to write the background task. Put this in a class called Ticker.cs in the portable project.That way we only have to write it once. In this example we simply send a message back to the subscriber once a second. We also keep an eye on the cancellation token to see if we've been stopped.

using System.Threading;
using System.Threading.Tasks;
using Xamarin.Forms;

namespace testBackground
{
    public class Ticker
    {
        public async Task TickTock(CancellationToken token)
        {
            await Task.Run(async () =>
            {
                int counter = 0;
                while (true)
                {
                    token.ThrowIfCancellationRequested();
                    await Task.Delay(1000);
                    counter += 1;
                    StatusMessage status = new StatusMessage() { msg = string.Format("The app has been running for {0} seconds!", counter) };
                    Device.BeginInvokeOnMainThread(() => { MessagingCenter.Send<StatusMessage>(status, "StatusMessage"); });
                }
            });
        }
    }
}

That's everything the portable class needs. Now we write the Android bit. We start by enhancing MainActivity to subscribe to the Start and Stop messages. Make your MainActivity.cs look like this.

using Android.App;
using Android.Content.PM;
using Android.Runtime;
using Android.OS;
using Xamarin.Forms;
using Android.Content;

namespace testBackground.Droid
{
    [Activity(Label = "testBackground", Icon = "@mipmap/icon", Theme = "@style/MainTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation)]
    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);

            Xamarin.Essentials.Platform.Init(this, savedInstanceState);
            global::Xamarin.Forms.Forms.Init(this, savedInstanceState);
            LoadApplication(new App());

            Subscribe();
        }

        public void Subscribe()
        {
            MessagingCenter.Subscribe<StartMessage>(this, "StartMessage", message => { Intent intent = new Intent(this, typeof(TickService)); StartService(intent); });
            MessagingCenter.Subscribe<StopMessage>(this, "StopMessage", message => { Intent intent = new Intent(this, typeof(TickService)); StopService(intent); });
        }

        public override void OnRequestPermissionsResult(int requestCode, string[] permissions, [GeneratedEnum] Android.Content.PM.Permission[] grantResults)
        {
            Xamarin.Essentials.Platform.OnRequestPermissionsResult(requestCode, permissions, grantResults);

            base.OnRequestPermissionsResult(requestCode, permissions, grantResults);
        }
    }
}

Add a new class called TickService.cs to the Android project. This is boilerplate with only the background task class name and message names changing. It is the Android service responsible for hosting the Ticker class. iOS does this bit differently.


using System.Threading;
using System.Threading.Tasks;
using Android.App;
using Android.Content;
using Android.OS;
using Xamarin.Forms;

namespace testBackground.Droid
{
    [Service]
    class TickService : Service
    {
        CancellationTokenSource cts;

        public override IBinder OnBind(Intent intent)
        {
            return null;
        }

        public override StartCommandResult OnStartCommand(Intent intent, StartCommandFlags flags, int id)
        {
            cts = new CancellationTokenSource();
            Task.Run(() =>
           {
               try
               {
                   testBackground.Ticker t = new testBackground.Ticker();
                   t.TickTock(cts.Token).Wait();
               }
               catch (Android.OS.OperationCanceledException)
               {
                   if (cts.IsCancellationRequested)
                   {
                       testBackground.CancelledMessage msg = new CancelledMessage();
                       Device.BeginInvokeOnMainThread(() => MessagingCenter.Send(msg, "CancelledMessage"));
                   }
               }
           }, cts.Token);

            return StartCommandResult.Sticky;
        }

        public override void OnDestroy()
        {
            if (cts != null)
            {
                cts.Token.ThrowIfCancellationRequested();
                cts.Cancel();
            }
            base.OnDestroy();
        }
    }
}

Run this on your Android phone. Start the app, go do something else, return to the app and see the counter has been incrementing even though the app was not in the foreground. You can even turn your phone off and the app will keep running.





1 comment:

  1. Concise, Clear, Clever.
    A perfect description of implementing a background process in Xamarin Forms for Android.
    Exactly what I was looking for.

    Thank you.

    ReplyDelete