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>
using System.ComponentModel;
using Xamarin.Forms;
namespace testBackground
{
[DesignTimeVisible(false)]
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
}
}
}
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;
}
}
}
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;
});
}
}
}
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
{
}
}
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"); });
}
});
}
}
}
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);
}
}
}
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.
Concise, Clear, Clever.
ReplyDeleteA perfect description of implementing a background process in Xamarin Forms for Android.
Exactly what I was looking for.
Thank you.