Update: I wrote this before the official post here added more detail about this single process model for background tasks so read that first and then feel free to also read my early experiments below
For quite a while I’ve been meaning to write some posts about the Windows 10 Anniversary Update (currently in preview) and some of the changes that it brings to UWP app development but time has been getting the better of me.
One of the areas that I did pick up on at //Build was that the way in which background tasks are implemented was being changed and I really wanted to dig in on this and try it out.
This was discussed in the excellent session that Chris and Stefan ran;
where they introduce the idea of a “Single Process Execution Model” (around the 10min mark) for background tasks.
Background on Background Tasks
Since Windows 8.0, modern WinRT based apps have had a clear model for background execution based on the ideas of triggers, conditions and tasks and I walked through some of that myself back in 2015 at TechDays in Holland.
The essence is that a developer registers their background tasks to run in association with some trigger (e.g. a timer or the arrival of a push notification from the cloud) which can be further filtered by a condition (e.g. internet availability).
What’s always been a little bit painful though was the way in which you needed to implement background tasks in that;
- you had to write a custom WinRT component for your task
- the background task didn’t run in process with your foreground code at the times that your foreground code was running.
Having to write a custom WinRT component isn’t the end of the world but for a .NET developer it does mean that you have to create an interface to that code which is modelled in the type system of WinRT rather than the type system of .NET and so you don’t get to play with Task<T> and so on although there are extensions in the framework that take away much of that pain.
Having your foreground and background code executing in different processes (albeit in the same app container) is a little bit more limiting in that it makes exchanging even the most basic of information between the two a bit trickier than it would otherwise be. For example, the provided mechanisms help you to;
- invoke your background task from your foreground code via the ApplicationTrigger class which has its RequestAsync method and which allows a ValueSet (dictionary) of data to be passed to the background task and retrieved from the background task after it has done its work.
- hook your foreground code up to the Completed and Progress events of the IBackgroundTaskRegistration interface of your task registration and then to take action when you notice that your background code has Completed or when it uses the Progress method on that interface to pass an integer from the background to the foreground.
Those are fine but there are times when a uint isn’t really the way that you’d like to pass rich data between two processes and so the Anniversary Edition is simplifying this by adding a new way of building background tasks such that;
- you don’t have to build a separate WinRT component
- the background task and the foreground code run in the same process
Trying This Out
This felt like something that had to be tried rather than talked about and so I spun up Visual Studio on my installation of the 14352 preview build and set about experimenting.
I needed some scenario to play with and so I figured I might try and write a background task which “indexed” all the music files in the music library on the PC in the sense of visiting them and building up at least some level of information about them.
In a real music app, I’d imagine that it might be attractive to;
- Do this when the user demanded it
- Do this every so often as a background task to pro-actively refresh the library for the user
- Only do work if there was a need to do work – i.e. not to process the whole library if only a few files were modified
but I wanted to keep my code very simple and so I didn’t get bogged down in 3 and just went for 1 and 2 and wrote the code below;
namespace App2 { using System; using System.Collections.Generic; using System.IO; using System.Threading; using System.Threading.Tasks; using Windows.Storage; using Windows.Storage.Search; enum MusicIndexingEventType { Started, Addition, Completed } enum InvocationMode { None, Foreground, Background } class MusicIndexingEventArgs : EventArgs { public MusicIndexingEventType EventType { get; set; } public string Title { get; set; } } static class MusicIndexingService { public static event EventHandler<MusicIndexingEventArgs> MusicStatusChanged; public static bool IsRunning { get { return (Mode != InvocationMode.None); } } public static InvocationMode Mode { get; private set; } public static async Task RunAsync(InvocationMode mode) { if (Interlocked.CompareExchange(ref runningCount, 1, 0) == 0) { cancellationTokenSource = new CancellationTokenSource(); Mode = mode; completionSource = new TaskCompletionSource<bool>(); FireIndexingEvent(MusicIndexingEventType.Started); var musicLibrary = KnownFolders.MusicLibrary; try { await ClearStorageAsync(); await RecurseFolderAsync(musicLibrary); } catch (OperationCanceledException) { } finally { Interlocked.Exchange(ref runningCount, 0); cancellationTokenSource.Dispose(); cancellationTokenSource = null; } Mode = InvocationMode.None; FireIndexingEvent(MusicIndexingEventType.Completed); completionSource.SetResult(true); } else { throw new InvalidOperationException("Already running"); } } static async Task ClearStorageAsync() { try { var file = await ApplicationData.Current.LocalFolder.GetFileAsync(STORAGEFILE); await file.DeleteAsync(); } catch (FileNotFoundException) { } } public static async Task CancelAsync() { if (cancellationTokenSource != null) { cancellationTokenSource.Cancel(); await completionSource.Task; } } public static async Task<IReadOnlyList<string>> ReadTitlesAsync() { var list = new List<string>(); try { // TODO: refactor this file access into its own function. var file = await ApplicationData.Current.LocalFolder.GetFileAsync( STORAGEFILE); var lines = await FileIO.ReadLinesAsync(file); list.AddRange(lines); } catch (FileNotFoundException) { } return (list.AsReadOnly()); } static async Task AddToStorageAsync(string title) { var file = await ApplicationData.Current.LocalFolder.CreateFileAsync( STORAGEFILE, CreationCollisionOption.OpenIfExists); await FileIO.AppendLinesAsync(file, new string[] { title }); } static async Task RecurseFolderAsync(StorageFolder folder) { // TODO: stop doing this allocation every time. var queryOptions = new QueryOptions( CommonFileQuery.OrderByName, new string[] { ".mp3" } ); // NB: we could just use deep and let the system do the recursion for us! queryOptions.FolderDepth = FolderDepth.Shallow; var query = folder.CreateFileQueryWithOptions(queryOptions); var files = await query.GetFilesAsync(); foreach (var file in files) { var musicProperties = await file.Properties.GetMusicPropertiesAsync(); // Write to the file one line at a time 🙂 var title = $"{musicProperties.Artist}, {musicProperties.Album}, " + $"{musicProperties.TrackNumber} : {musicProperties.Title}"; await AddToStorageAsync(title); FireIndexingEvent(MusicIndexingEventType.Addition, title); // Simulate this taking a little more time than it might otherwise do. await Task.Delay(TimeSpan.FromMilliseconds(1000), cancellationTokenSource.Token); } var folders = await folder.GetFoldersAsync(); // We don't attempt to parallelise this in any way. foreach (var subFolder in folders) { cancellationTokenSource.Token.ThrowIfCancellationRequested(); await RecurseFolderAsync(subFolder); } } static void FireIndexingEvent(MusicIndexingEventType type, string title = null) { // TODO: stop doing this allocation every time MusicStatusChanged?.Invoke(null, new MusicIndexingEventArgs() { EventType = type, Title = title } ); } static TaskCompletionSource<bool> completionSource; static CancellationTokenSource cancellationTokenSource; static readonly string STORAGEFILE = "titles.txt"; static volatile int runningCount; } }
The essential idea here is of a static class with RunAsync and CancelAsync methods which then tries to recurse (in a naive way) through the music library, gathering files one by one and writing some of their music properties into a file calls titles.txt. The code deliberately introduces a 1s delay each time it finds a new file and it also fires an event to signify Start/NewFile/Completion as it’s going along.
It’s not great code but it’s something to play with and so I put a simple UI on top of that;
<Page x:Class="App2.MainPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:App2" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"> <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> <VisualStateManager.VisualStateGroups> <VisualStateGroup x:Name="VisualStateGroup"> <VisualState x:Name="_default" /> <VisualState x:Name="running"> <VisualState.Setters> <Setter Target="appBarButton.(UIElement.Visibility)" Value="Collapsed" /> <Setter Target="grid.(UIElement.Visibility)" Value="Visible" /> <Setter Target="appBarButton1.(UIElement.Visibility)" Value="Visible" /> </VisualState.Setters> </VisualState> </VisualStateGroup> </VisualStateManager.VisualStateGroups> <Grid.RowDefinitions> <RowDefinition /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions> <ListView Header="Songs" Margin="4" ItemsSource="{x:Bind Songs}"> </ListView> <Grid x:Name="grid" Grid.Row="0" Background="White" Opacity="0.8" Visibility="Collapsed"> <ProgressRing Width="100" Height="100" IsActive="True"/> </Grid> <StackPanel Grid.Row="1" Margin="4" Orientation="Horizontal" HorizontalAlignment="Center"> <AppBarButton x:Name="appBarButton" Label="Reindex" Click="OnReindex"> <AppBarButton.Content> <SymbolIcon Symbol="Play" /> </AppBarButton.Content> </AppBarButton> <AppBarButton x:Name="appBarButton1" Label="Cancel" Click="OnCancel" Visibility="Collapsed"> <AppBarButton.Content> <SymbolIcon Symbol="Stop" /> </AppBarButton.Content> </AppBarButton> </StackPanel> </Grid> </Page>
which provides a simple data-bound ListView and 2 buttons to Start/Cancel the indexing process and two visual states for [running/not running].
I scribbled a little code behind here;
namespace App2 { using System; using System.Collections.ObjectModel; using System.Threading.Tasks; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Navigation; public sealed partial class MainPage : Page { enum VisualStates { _default, running } public MainPage() { this.InitializeComponent(); this.Songs = new ObservableCollection<string>(); } public ObservableCollection<string> Songs { get; private set; } /// <summary> /// NB : no way to navigate away from this page other than to close the app. /// </summary> /// <param name="e"></param> protected async override void OnNavigatedTo(NavigationEventArgs e) { base.OnNavigatedTo(e); // It seems to me that we could land on this page when our music // indexing service is already running as part of some background // invocation so we should find out. There's a race or two here. MusicIndexingService.MusicStatusChanged += OnMusicIndexingUpdate; if (MusicIndexingService.IsRunning) { this.ChangeState(VisualStates.running); } var titles = await MusicIndexingService.ReadTitlesAsync(); foreach (var title in titles) { this.Songs.Add(title); } } void ChangeState(VisualStates state) { VisualStateManager.GoToState(this, state.ToString(), false); } async void OnMusicIndexingUpdate(object sender, MusicIndexingEventArgs e) { await this.DispatchAsync(() => { switch (e.EventType) { case MusicIndexingEventType.Started: this.Songs.Clear(); this.ChangeState(VisualStates.running); break; case MusicIndexingEventType.Addition: this.Songs.Add(e.Title); break; case MusicIndexingEventType.Completed: this.ChangeState(VisualStates._default); break; default: break; } }); } async Task DispatchAsync(Action action) { await this.Dispatcher.RunAsync( Windows.UI.Core.CoreDispatcherPriority.Normal, () => action()); } async void OnReindex(object sender, RoutedEventArgs e) { if (!MusicIndexingService.IsRunning) { await MusicIndexingService.RunAsync(InvocationMode.Foreground); } } async void OnCancel(object sender, RoutedEventArgs e) { await MusicIndexingService.CancelAsync(); } } }
and the code here which handles the observable collection, the button presses, the visual state changes and the events from the MusicIndexingService is fairly self-explanatory.
Something that I needed to think about a little bit more was the idea that if my MusicIndexingService was to be made available as a background task then it’s perfectly possible that at the time when my app is Launched and navigated to this MainPage and hits the OnNavigatedTo override it may be the case that the process it is running in was already up and running and running the MusicIndexingService in the background.
That’s why the code in OnNavigatedTo does not assume that the visual state for the newly navigated page should be _default but, instead, tries to ask the MusicIndexingService whether it is already running and determine the current visual state based on that.
Registering the Background Task
I changed the App.xaml.cs code that ships with the blank template in Visual Studio 2015 Update 2 so as to add some code to register my background task;
async Task RegisterBackgroundTaskAsync() { if (BackgroundTaskRegistration.AllTasks.Count == 0) { var status = await BackgroundExecutionManager.RequestAccessAsync(); if ((status != BackgroundAccessStatus.Denied) && (status != BackgroundAccessStatus.DeniedBySystemPolicy) && (status != BackgroundAccessStatus.DeniedByUser) && (status != BackgroundAccessStatus.Unspecified)) { var builder = new BackgroundTaskBuilder() { Name = "Music Indexing" }; builder.SetTrigger( new SystemTrigger(SystemTriggerType.TimeZoneChange, false)); // NB: NOT setting any entry point here - the app is its own background // task implementation. builder.Register(); } } }
Notice that the code here is using SystemTrigger with a TimeZoneChange but my real code would use a TimerTrigger with some maybe 6-12 hour interval.
Notice also that this code does not set up an EntryPoint property for the task, it leaves it blank and that means that the App class itself is going to provide the entry point and I’ve set up my manifest that way;
I can’t say for certain whether that’s the correct way to do it but I can say that it’s working for me at the time of writing. I then went ahead and implemented OnLaunched on my App.xaml.cs such that it called my RegisterBackgroundTaskAsync method every time the app started up;
protected override async void OnLaunched(LaunchActivatedEventArgs e) { // NB: We can _definitely_ (at least in these preview bits) land here when the // app is already running in the sense that the background task has been // triggered and we have our indexing service running away. await this.RegisterBackgroundTaskAsync(); Frame rootFrame = Window.Current.Content as Frame; if (rootFrame == null) { rootFrame = new Frame(); if (e.PreviousExecutionState == ApplicationExecutionState.Terminated) { // NB: not attempting to maintain the state of the indexing service across // suspend->terminate->launch. } Window.Current.Content = rootFrame; } if (e.PrelaunchActivated == false) { if (rootFrame.Content == null) { rootFrame.Navigate(typeof(MainPage), e.Arguments); } Window.Current.Activate(); } }
Other than that, it’s pretty generic and then I added in the new, crucial override for App.OnBackgroundActivated which is where my task can be implemented rather than having to have a separate project with a WinRT component;
protected override async void OnBackgroundActivated(BackgroundActivatedEventArgs args) { base.OnBackgroundActivated(args); var deferral = args.TaskInstance.GetDeferral(); args.TaskInstance.Canceled += OnBackgroundTaskCancelled; if (!MusicIndexingService.IsRunning) { await MusicIndexingService.RunAsync(InvocationMode.Background); } args.TaskInstance.Canceled -= OnBackgroundTaskCancelled; deferral.Complete(); } async void OnBackgroundTaskCancelled(IBackgroundTaskInstance sender, BackgroundTaskCancellationReason reason) { var deferral = sender.GetDeferral(); await MusicIndexingService.CancelAsync(); deferral.Complete(); }
and this is, hopefully, fairly simple and looks a lot like how you would have written an implementation of IBackgroundTask in the past but without the ceremony of the WinRT component and so on.
I also tried to factor in a little around suspend/resume although my music indexing service is fairly basic and so the best it can do is [start/stop] in response to [resume/suspend];
async void OnSuspending(object sender, SuspendingEventArgs e) { // TODO: Big question for me here is whether I should pause/stop the work of // the indexing service if; // a) it was started by the user in the foreground - assume yes. // b) it was started by a trigger in the background - not sure what the answer is // here but I'm going to assume yes for now based on what I see. var deferral = e.SuspendingOperation.GetDeferral(); if (MusicIndexingService.IsRunning) { this.suspendedWhileIndexing = MusicIndexingService.Mode == InvocationMode.Foreground; // NB: there's no pause/resume on the service so we cancel it. await MusicIndexingService.CancelAsync(); } deferral.Complete(); } async void OnResuming(object sender, object e) { if (this.suspendedWhileIndexing) { // NB: there's no pause/resume on the service so we restart it. await MusicIndexingService.RunAsync(InvocationMode.Foreground); this.suspendedWhileIndexing = false; } } bool suspendedWhileIndexing;
Writing even these small bits of code with this model did have me asking some questions though…
Questions
- What is the threading model here – does the OnBackgroundActivated method get called on the app’s main UI thread?
- What happens if the app process is spun up to purely to do background work and then the user launches the app while that background work is still ongoing?
- What happens if the app is doing background work when the user suspends or closes the app?
I should say that when I use the term ‘background work’ here what I mean is work that has been triggered by the system invoking the OnBackgroundActivated method rather than the user setting some indexing in progress by clicking a button.
Experimenting
Firstly, the code seems to pretty much hang together and work in the sense that I copied a couple of albums into my music library and then ran the code and pressing the ‘reindex button’ started the process;
and the ‘cancel’ button stopped it so that was good.
If I then changed the timezone on the machine, my background task did get invoked and I saw the UI refresh itself again as the music got reindexed;
A quick look with the debugger seemed to suggest that the answer to my first question (1) above is that the OnBackgroundActivated method does seem to get invoked on the UI thread.
I could also easily see that if I changed my timezone while the app was running then the file titles.txt in the app’s LocalState folder would get deleted and then rebuilt with the song titles and so that seemed to work ok – i.e. the background task is firing up in the background.
As an aside, the ‘lifecycle events’ dropdown on the Visual Studio ‘Debug Location’ toolbar still seems to work fine for invoking these new type of background task implementations
To answer my second question above, I made sure the app wasn’t running and then I changed the timezone and saw the process spin up in task manager;
and you can see that the app springs up under the ‘Background processes’ section. If I then launched the app while the background task was still running then I’d see the process migrate;
and move into the ‘Apps’ section of task manager and the code that I’d written in the OnNavigatedTo override of my MainPage is clearly getting run to handle the scenario when the app launches and finds that the music indexing service is already running inside that process. That is – it all works much as I’d expect it to.
The remaining question that I had is around what happens in a scenario where the foreground app suspends/closes while the background task is running in the same process.
Does the background task continue? Does it get cancelled? Does it just get killed?
I’m not 100% sure on the answer here yet but I’m fairly confident that the task doesn’t continue to run in the current preview bits.
I found that if I ran the app, caused the background task to be triggered and then closed the app then the process would disappear from task manager and my titles.txt file would stop being populated by the background task.
In so far as I could tell in the debugger the background task did not get cancelled either, the app simply got its Suspended event and then closed down.
In my code, I’d added a property onto my MusicIndexingService such that I could know whether it had been run from foreground code or background code.
The idea of that property was to have different behaviour in my suspending event handler – if the indexing was from the foreground, I’d shut it down whereas if it was from the background I’d try to leave it running.
Based on what I see at the moment, that code would be redundant but maybe that’s just part of the preview and things may change over time.
Summary
I really like what I see here – it’s much easier to write a background task using this infrastructure than the previous infrastructure involving WinRT components and different processes but it feels like there’s a need for a good write-up and sample somewhere which answers the questions around how this interacts with threading and how it interacts with the suspend/resume/terminate lifecycle. I’m sure that will come at some point in the future as the Anniversary Update and the accompanying SDK update get closer to release.
Pingback: Windows 10 Anniversary Update Preview–Background Tasks | Tech News
Pingback: The Morning Brew #2107 | Tech News
Pingback: Szumma #043 – 2016 23. hét | d/fuel