Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Access UI thread while app is being suspended?

I have a UWP app that lets users create and modify text documents. I'm having a hard time getting the save mechanism to work with the app suspend/resume lifecycle.

Here is what I have:

  1. All disk access is done on a separate background thread to keep the UI responsive
  2. That background thread also serializes all disk access to ensure that at all times, only one read or write operation takes place
  3. Before and after a document is saved, the UI is updated to indicate that a save is in progress. This is scheduled on the UI thread via Dispatcher.RunAsync()

When the app is being suspended:

  • On suspending, I must save the document one final time to ensure that all changes are on disk (the app may get terminated while suspended)
  • So I request an ExtendedExecutionSession
  • I schedule one final save operation on my queue
  • I then wait for the queue to process all pending disk access operations
  • Finally, I mark the extended execution session as completed

My problem:

  • An already scheduled save operation completes its disk access and then tries to update the UI on the main thread via Dispatcher.RunAsync(). The background queue waits for this task to finish, but it never does, because by that time, the UI thread has been stopped already.

→ So my final save operation is never executed, because the background queue is stuck waiting to update the UI.

Here's a flow diagram:

App suspension flow chart

A few questions that arise:

  1. When exactly is the UI thread stopped while an app is being suspended? Only when it's idle or "right in between" anything? I.e. if I scheduled a block via Dispatcher.RunAsync(), will this at least finish or "freeze"?
  2. Is there a way for me to check if the UI thread of the current window is already stopped so that I know that I must not access it anymore in my background file access thread?
  3. Which threads are stoppend and when, while an app is being suspended?
  4. Is my background file access thread also at risk at being stopped while the app is being suspended?

To summarize the problem:

While the app is being suspended, I must ensure that I wait for a potentially pending disk access on a background thread to finish before I finally save the document one more time in case my app gets terminated later on.

like image 978
Mark Avatar asked Dec 12 '17 10:12

Mark


1 Answers

This sounds all too familiar!

What I understood from your question is the save operation ends with updating the UI, but since it's running in a background thread, it cannot touch the UI unless you schedule it with the Dispatcher. Just to visualize, does your code look something like this:

public async Task SaveAsync()
{
   // .. save to disk ..

   await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
   {
      // update UI here
   }

   // Save Complete!
}

If so, the problem is that a UI update is part of a queued background operation and if anything is blocked by either the UI update or the Dispatcher itself, no other operation will begin until you reach the line //Save Complete!, which never happens if the Dispatcher never executes your action.

Instead of concerning the save operation with UI updates, try exposing events that will trigger UI updates. For example:

public event EventHandler SaveStarted;
public event EventHandler SaveCompleted;

public async Task SaveAsync()
{
   SaveStarted?.Invoke(this, EventArgs.Empty);

   // .. save to disk ..

   SaveCompleted?.Invoke(this, EventArgs.Empty);
}

Your view can attach handlers to these events and show/hide the appropriate visuals, while your suspend handler can can safely call the SaveAsync() method without worrying about the UI.

When the view is Unloaded, make sure you detach these event handlers. Your save operation will not block the queue when raising these events.


Update 1

Yes, your event handlers must run on the UI thread using the Dispatcher:

public void OnSaveCompleted(object sender, EventArgs e)
{
   Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
   {
      // update UI here...
   }
}

Because the handler is forced to return void, it will not block the background thread.


Update 2

Sounds like you have some tight coupling. You'll need to redesign your application such that your text-document logic sits in a separate project. I always code in a .Net Standard project which forces all UI out. The project will not allow any UI code whatsoever and thus keeping your logic clean and decoupled.

If all your code sits inside UWP projects, then it's very easy to start including view logic, XAML controls, and in your case, the Dispatcher in parts of your app that have nothing to do with presentation.

In your case, you'll need figure out how to make your text-document logic become the owner of the state of the document, not the UI. hasChanges should only be modified by the same logic that owns SaveAsync(). If something breaks/blocks/fails/etc during a UI operation and it never makes it successfully to the text-document, then it's an incomplete operation and should not be part of the document, nor should it be the responsibility of the document to worry about it when it's time to suspend & save to disk.

If UWP kills your app for any reason during one of these operations, then it's gone. Your next task is to figure out how to resume this operation when the application wakes-up again.

like image 193
Laith Avatar answered Nov 13 '22 09:11

Laith