I have a TreeView in a form, that is dock-filled to a groupbox. The problem to solve is, that there is an operation that is run on a Task, which loads data from a server application. When this is running, there should be a progress indicator displayed in the location of the TreeView. That is, it should be shown instead of the TreeView, and take its place fully. The following is what the code for this looks like:
private async void preload_data(object sender, System.EventArgs args)
{
try
{
// the treeView should be disabled/invisible at this point
// make the CircularProgressBar enabled/visible
// get the information from the server
await Configuration.network.fetch_stuff();
}
catch (Exception ex)
{
// something bad happened
}
finally
{
// whatever happened, the treeView should be back
}
}
The CircularProgressBar (a third-party control) should appear as in the code above, and should replace the TreeView. It should fill the exact same space as the TreeView would, which is dock-filled. Below is a screenshot of this:
This form and all its controls have been designed in the designer, and I don't want to do it there, I want to do it programmatically. What is the best way to go about this? I have looked at examples of Controls.Remove() and Controls.Add(), but it's not clear if that fits this purpose.
It is quite common to change the visual output while actions are running, like you do. Think of disabling buttons, to discourage operators to press the button again, or show something visually, to inform operators about the progress.
For simplicity, without the try-catch
private async Task PreloadDataAsync()
{
this.ShowFetchingData(true);
// start fetching data, do not await:
var taskFetchData = Configuration.network.fetch_stuff();
// while taskFetchData not completed, await some time
TimeSpan updateTime = TimeSpan.FromSeconds(0.250);
int progressCounter = 0;
while (!taskFetchData.IsCompleted)
{
this.ShowProgress(progressCounter);
var taskWait = Task.Delay(updateTime);
await Task.WhenAny(new Task[] {taskFetchData, taskWait};
// either taskFetchData.IsCompleted, or Delay time waited
++progressCounter;
}
this.ShowFetchingData(false);
}
private void ShowFetchindData(bool show)
{
// disable/enable certain buttons, menu items, show progressbar?
this.ButtonFetchData.Enabled = !show;
this.MenuFetchData.Enabled = !show;
this.ProgressBarFetchData.Visible = show;
}
private bool IsFetchingData => this.ProgressBarFetchData.Visible;
private void ShowProgress(int progress)
{
this.ProgressBarFetchData.Position = progress;
}
For simplicity, I've left out checks for the position in the progress bar, but you get the gist.
Usage:
private async void OnButtonFetchData(object sender, EventArgs e)
{
await this.PreloadDataAsync();
}
Room for improvement
The problem with this is that there is no timeout at all: if FetchStuff does not complete, you are in an endless wait. The method that microsoft proposes is the use of a CancellationToken. Almost every async method has an overload with a CancellationToken. Consider creating one yourself:
// existing method:
private async Task<MyData> fetch_Stuff()
{
await this.fetch_stuff(CancellationToken.None);
}
// added method with CancellationToken
private async Task<MyData> fetch_Stuff(CancellationToken token)
{
// Call async function overloads with the token,
// Regularly check if cancellation requested
while (!token.IsCancellationRequested)
{
... // fetch some more data, without waiting too long
}
}
Instead of IsCancellationRequested, consider to throw an exception: ThrowIfCancellationRequested.
Usage:
private async Task PreloadDataAsync()
{
// preloading should be finished within 30 seconds
// let the cancellationTokenSource request cancel after 30 seconds
TimeSpan maxPreloadTime = TimeSpan.FromSeconds(30);
using (var cancellationTokenSource = new CancellationTokenSource(maxPreloadTime))
{
await PreloadDataAsync(cancellationTokenSource.Token);
}
}
The overload with CancellationToken:
private async Task PreloadDataAsync(CancellationToken token)
{
this.ShowFetchingData(true);
// execute code similar to above, use overloads that accept token:
try
{
var taskFetchData = Configuration.network.fetch_stuff(token);
TimeSpan updateTime = TimeSpan.FromSeconds(0.250);
int progressCounter = 0;
while (!taskFetchData.IsCompleted)
{
token.ThrowIfCancellationRequested();
this.ShowProgress(progressCounter);
var taskWait = Task.Delay(updateTime, token);
await Task.WhenAny(new Task[] {taskFetchData, taskWait};
// either taskFetchData.IsCompleted, or Delay time waited
++progressCounter;
}
}
catch (TaskCancelledException exc)
{
this.ReportPreloadTimeout();
}
finally
{
this.ShowFetchingData(false);
}
}
Or if you want a button that cancels the task:
private CancellationTokenSource cancellationTokenSource = null;
private book IsPreloading => this.CancellationTokenSource != null;
private async Task StartStopPreload()
{
if (!this.IsPreloading)
StartPreload();
else
CancelPreload();
}
private async Task StartPreload()
{
// preload not started yet; start it without timeout;
try
{
this.cancellationTokenSource = new CancellationTokenSource();
await PreloadDataAsync(this.cancellationTokenSource.Token);
}
catch (TaskCancelledException exc)
{
this.ReportPreloadCancelled();
}
finally
{
this.cancellationTokenSource.Dispose();
this.cancellationTokenSource = null;
}
}
}
The method where operators can stop preloading:
private async void StopPreload()
{
this.cancellationTokenSource.Cancel();
// the method that created this source will Dispose it and assign null
}
All you have to do is create buttons / menu items to start / stop preloading
Solved using Visible properties of controls.
Related
I have a code. The goal of this is to cancel a task with a CancellationToken, I know that it possible to do with return; in loop, but I want to do it with CancellationToken. I tried to do it, but it does not work and I have no idea why.
The task
break a task loop on dropNumber
static CancellationTokenSource cancellationTokenSource = null;
static async Task Main(string[] args)
{
cancellationTokenSource = new CancellationTokenSource();
try
{
Task.Run(() => CountLoopAsync(cancellationTokenSource.Token, 4),cancellationTokenSource.Token);
}
catch(OperationCanceledException ex)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.BackgroundColor = ConsoleColor.White;
Console.WriteLine("Task is cancelled!");
Console.ResetColor();
}
finally
{
cancellationTokenSource.Dispose();
}
}
private static void CountLoopAsync(CancellationToken token, int dropNumber)
{
for(int i = 0; i < 10; i++)
{
Console.WriteLine(i);
if (dropNumber == i)
{
cancellationTokenSource.Cancel();
}
}
}
}
Your Task.Run it's done with await so you don't go to Cancel sentence until the task has finished. Use Task.Run without await to allow continue running and execute the Cancel
UPDATE
I think that your example if not so good because you are trying to execute all code in a sequencial way when the use of task is usually to run code in background, in an asynchronous form. Also, I think that stop with a predefined value is a non sense: in that case, change the final step in your "for" instead of the use of a token.
You don't Cancel in the task code. If you know in that code when to cancel, you simply return. The purpose of the token is allow to cancel externally to task code. And doing that, you can't control when the task finish because it depends of something external. Maybe, for example, when an user click "Cancel" button.
Usually, your counter code try to calculate all. But, been a long time operation, you give to the user the oportunity to cancel in any moment.
Encapsulate your code in a class:
public class YourClass : IDisposable
{
private CancellationTokenSource _cancellationTokenSource = null;
private Task _task = null;
public void Wait(int milliSeconds)
{
this._task.Wait(milliSeconds, this._cancellationTokenSource.Token);
}
public void Dispose()
{
this._cancellationTokenSource?.Dispose();
this._task?.Dispose();
}
public async Task RunLongOperationInBackground()
{
this._cancellationTokenSource = new CancellationTokenSource();
this._task = Task.Run(
() => CountLoopAsync(this._cancellationTokenSource.Token),
this._cancellationTokenSource.Token);
await this._task;
}
public void Abort()
{
// Cancel the operation
this._cancellationTokenSource?.Cancel();
}
private static void CountLoopAsync(CancellationToken token)
{
for (int i = 0; i < 10; i++)
{
Console.WriteLine(i);
// Uncomment to simulate task takes some time to finish
//Thread.Sleep(3000);
// You don't know when the action will be cancelled. If you know that, you don't
// need the cancellation: you can do the for until your well known end
if (token.IsCancellationRequested)
{
break;
}
}
}
}
This class allow you run an operation (RunLongOperationInBackground) and also cancel in any moment (Abort). So, you run your task and, in any moment, you can cancel the task. If you look the CountLoopAsync code, it tries to execute all but, it checks sometimes (in each iteration in this case) the token, and if someone has request to cancel, you exit the for. But you can do whatever you want. For example, you may run always up to next hundred so, even if token has been cancelled, you may continue up to next hundred. Or if the cancellation has been requested near to the end of the operation, you may decide continue. The token only tell you that outside wants terminate.
Create a Form (instead a console) with 2 buttons, for a more realistic example:
public partial class Form1 : Form
{
private readonly YourClass _yourClass;
public Form1()
{
this.InitializeComponent();
this._yourClass = new YourClass();
}
private async void OnStartButtonClick(object sender, EventArgs e)
{
await this._yourClass.RunLongOperationInBackground();
}
private void OnCancelButtonClick(object sender, EventArgs e)
{
this._yourClass.Abort();
}
private void OnForm_FormClosed(object sender, FormClosedEventArgs e)
{
if (this._yourClass != null)
{
// Wait, for example 30 seconds before end the appication
this._yourClass.Wait(30000);
this._yourClass.Dispose();
}
}
}
You create your class in the constructor. The Start button run your long time operation (you may want use a Delay in each for iteration to be able to cancel before terminate). In any time you can click de Abort button to cancel the operation. And in that moment, in your "for" the token tell you that has been cancelled and you exit the for.
I think that the problem is here:
cancellationTokenSource.Dispose();
It seems that the cancellationTokenSource is disposed prematurely. You are not supposed to dispose it before all associated work has completed. In your case you must probably wait for the completion of the Task.Run before calling Dispose.
Task.Run(() => CountLoopAsync(cancellationTokenSource.Token, 4),
cancellationTokenSource.Token).Wait();
I have a C# Windows Service that runs a few tasks inside.
One of the tasks is a infinite async looping and the others are triggered from a Timer and then execute the task.
private readonly QueueProcessor _queueProcessor;
protected override void OnStart(string[] args)
{
// first task
_queueTask = _queueProcessor.Run(_cancellation.Token);
// second task
affiliate_timer = new System.Timers.Timer();
affiliate_timer.AutoReset = true;
affiliate_timer.Interval = _model.Interval_Affiliate * 60000;
affiliate_timer.Elapsed += new
System.Timers.ElapsedEventHandler(affiliate_timer_Elapsed);
// third task
invoice_timer = new System.Timers.Timer();
invoice_timer.AutoReset = true;
invoice_timer.Interval = _model.Interval_Invoice * 60000;
invoice_timer.Elapsed += new
System.Timers.ElapsedEventHandler(invoice_timer_Elapsed);
}
private void invoice_timer_Elapsed(object source, System.Timers.ElapsedEventArgs e)
{
if (!_isAffiliateBusy)
{
_isAffiliateBusy= true;
var task = Task.Run(() => StartAffiliateTask());
task.Wait();
_isAffiliateBusy= false;
}
}
private void invoice_timer_Elapsed(object source, System.Timers.ElapsedEventArgs e)
{
if (!_isInvoiceBusy)
{
_isInvoiceBusy = true;
var task = Task.Run(() => StartInvoiceTask());
task.Wait();
_isInvoiceBusy = false;
}
}
private void StartAffiliateTask()
{
_affiliateModule = new Modules.Affiliate();
_affiliateModule.RunSync();
}
private void StartInvoiceTask()
{
_invoiceModule = new Modules.Invoice();
_invoiceModule.RunSync();
}
This is my QueueProcessor class that implements await/async to execute a infinite looping job:
public class QueueProcessor
{
private readonly IQueueBroker _serviceBroker;
public QueueProcessor()
{
}
public async Task Run(CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
var receiveMessageResponse = await _serviceBroker.ReceiveMessageAsync("test", cancellationToken);
if (!receiveMessageResponse.Messages.Any())
{
continue;
}
foreach (var message in receiveMessageResponse.Messages)
{
// some other tasks here...
await _serviceBroker.DeleteMessageAsync(message, cancellationToken);
}
}
}
}
My Affiliate and Invoice module classes doesn't implement any await/async code inside looks like this:
public class Affiliate
{
/// <summary>
/// Start the sync process
/// </summary>
public void RunSync()
{
try
{
// some code here...
}
catch (Exception ex)
{
}
}
}
My question is:
When my queue procesor infinite loop is running, does my other tasks that are triggered by the timers still can run independently?
When I use:
var task = Task.Run(() => StartAffiliateTask());
task.Wait();
Does the Wait method stop the whole service thread until this task is finished? or that won't block my StartInvoiceTask to run independantly?
Any recommendation on the best way to have my 3 tasks running independant on each other?
Summing up multiple potential issues:
Race condition (access/write to _isBusy).
Potential deadlock (in low ThreadPool size).
Potential incosistent state of flag in case of errors or thread aborts (_isBusy can be left in 'true' state).
Further I will assume your 'task' should be running in single instance, so we will disgard timer callbacks if it is still running.
You should change your timer event handlers like so (best to just wrap it in some kind of class):
//the flag, do mention volatile modifier - it tells particular
//systems to watch for variable changes by reference,
//instead of just copying it into thread stack by value.
private volatile bool _isAffiliateBusy = false;
//sync object for flag to eliminate race condition
private object _affiliateSync = new object();
private void affiliate_timer_Elapsed(object source, System.Timers.ElapsedEventArgs e)
{
//very fast lookup at flag to filter threads which comes when task is still processing
if(_isAffiliateBusy)
return;
lock(_affiliateSync) //taking lock
{
//checking again for those threads which 'happen' to be faster than you think.
if(_isAffiliateBusy)
return;
//aquire lock for business 'task'
_isAffiliateBusy = true;
}
try
{
StartAffiliateTask();
}
finally
{
//resetting singleton business 'task' lock.
//do not forget to use finally block, to handle disposing
//even if something rise up in 'try' section - you will not be left with invalid state of flag.
_isAffiliateBusy = false;
}
}
I have a form with a button and a list box. I want to add to the list box the results from two functions. These two functions can take an unknown amount of time to complete and I want to execute them simultaneously. As soon as either of the functions completes the computation, I want to display the result in the list box (before the other function completed). At the moment, the results are displayed after both functions complete. I wouldn't mind if the functions update the list box themselves.
async Task<string> LongTaskAsync()
{
for(int i = 0; i < 50; i++) {
Thread.Sleep(100);
}
return "Completed long task async";
}
async Task<string> ShortTaskAsync()
{
for(int i = 0; i < 5; i++) {
Thread.Sleep(100);
}
return "Completed short task async";
}
async void BtnRunClick(object sender, EventArgs e)
{
listBox1.Items.Clear();
var longTask = Task.Run(() => LongTaskAsync());
var shortTask = Task.Run(() => ShortTaskAsync());
listBox1.Items.Add(await longTask);
listBox1.Items.Add(await shortTask);
}
The reason why it shows 2 of them at the same time related how you chain your awaits.
listBox1.Items.Add(await longTask);
listBox1.Items.Add(await shortTask);
You are awaiting longer task before the shorter one. The second line runs after long task done its work in this time shorter one was already completed that's why you see them at the same time. But in a world you don't know what task will take longer to execute you need to have a better solution.
Action<Task<string>> continuationFunction = t => { this.listBox1.Items.Add(t.Result); };
Task.Run(() => LongTaskAsync()).ContinueWith(continuationFunction, TaskScheduler.FromCurrentSynchronizationContext());
Task.Run(() => ShortTaskAsync()).ContinueWith(continuationFunction, TaskScheduler.FromCurrentSynchronizationContext());
TaskScheduler.FromCurrentSynchronizationContext() is for to avoid cross thread access exceptions.
You don't have to use ContinueWith for this. It's almost always possible to avoid mixing async/await and ContinueWith-style of continuations. In your case, it can be done like this:
async void BtnRunClick(object sender, EventArgs e)
{
listBox1.Items.Clear();
async Task longTaskHelperAsync() {
// probably, Task.Run is redundant here,
// could just do: var item = await LongTaskAsync();
var item = await Task.Run(() => LongTaskAsync());
listBox1.Items.Add(item);
}
async Task shortTaskHelperAsync() {
// probably, Task.Run is redundant here, too
var item = await Task.Run(() => ShortTaskAsync());
listBox1.Items.Add(item);
}
await Task.WhenAll(longTaskHelperAsync(), shortTaskHelperAsync());
}
I believe this way it's more readable and you don't have to worry about synchronization context, FromCurrentSynchronizationContext, etc.
Also, most likely you'd want to take care of possible re-entrancy, if BtnRunClickis clicked again while those async ctasks are still in-flight.
You could solve it a bit more generically by creating a method that awaits a task, and also adds the result of the task to the ListBox.
async Task ProcessAndAddToListAsync(Func<Task<string>> function)
{
var value = await Task.Run(function); // Start the task in a background thread
listBox1.Items.Add(value); // Update the control in the UI thread
}
Then use this method inside the event handler of the button-click event:
async void BtnRunClick(object sender, EventArgs e)
{
listBox1.Items.Clear();
var longTask = ProcessAndAddToListAsync(LongTaskAsync);
var shortTask = ProcessAndAddToListAsync(ShortTaskAsync);
await Task.WhenAll(longTask, shortTask); // optional
// Here do anything that depends on both tasks being completed
}
In my WPF app I need to run a quick routine on startup that checks for a new available version. If the version is available, we do the update and then would like to immediately restart the app. Since this is run before the main window appears to the user, it simply appears as though the app took a split second longer to start up.
We're using Squirrel.Windows for our updater. I've made the class below to handle checking for/applying updates.
public class UpdateVersion
{
private readonly UpdateManager _updateManager;
public Action<int> Progress;
public event Action Restart;
public UpdateVersion(string squirrelUrl)
{
_updateManager = new UpdateManager(squirrelUrl);
}
public async Task UpdateVersions()
{
using (_updateManager)
{
UpdateInfo updateInfo = await _updateManager.CheckForUpdate(progress:Progress);
if (updateInfo.CurrentlyInstalledVersion == null)
{
if (updateInfo.FutureReleaseEntry != null)
{
await _updateManager.UpdateApp(Progress);
// Job crashes here
Restart?.Invoke();
}
}
else if (updateInfo.CurrentlyInstalledVersion.Version < updateInfo.FutureReleaseEntry.Version)
{
await _updateManager.UpdateApp(Progress);
// Job crashes here
Restart?.Invoke();
}
}
}
}
Unfortunately Squirrel has made their update process async only, which means the CheckForUpdate and UpdateApp method must use await, making the entire update method asynchronous. I assign the asnyc call to a Task, then simply .Wait() for the update to finish.
The problem comes when I try to restart my app. Based on what I've read, I need to use Dispatcher.Invoke to call the restart due to the fact I am on a non-UI thread when performing the update. However, despite the code below, I still get the same error message:
The Calling thread cannot access this object because a different thread owns it
Any idea how to correctly implement Dispatcher.Invoke in order to restart the app?
// Instantiate new UpdateVersion object passing in the URL
UpdateVersion updateVersion = new UpdateVersion(System.Configuration.ConfigurationManager.AppSettings.Get("SquirrelDirectory"));
// Assign Dispatch.Invoke as Restart action delegate
updateVersion.Restart += () =>
{
Dispatcher.Invoke(() =>
{
Process.Start(ResourceAssembly.Location);
Current.Shutdown();
});
};
// This is here for debugging purposes so I know the update is occurring
updateVersion.Progress += (count) =>
{
Debug.WriteLine($"Progress.. {count}");
};
var task = Task.Run(async () => { await updateVersion.UpdateVersions(); });
task.Wait();
EDIT
Below is a screen shot of the Target attribute of the Restart action. The debugger was paused at the Restar?.Invoke line from above.
Instead of trying to convert asynchronous programming to the old event based pattern, just use it properly. You don't need events to detect when an asynchronous operation finished, nor do you need Invoke to move back to the UI thread. await takes care of both.
You could write code as simple as this:
static readonly SemanticVersion ZeroVersion = new SemanticVersion(0, 0, 0, 0);
private async void Application_Startup(object sender, StartupEventArgs e)
{
await CheckForUpdatesAsync();
}
private async Task CheckForUpdatesAsync()
{
string squirrelUrl = "...";
var updateProgress = new Progress<int>();
IProgress<int> progress = updateProgress;
//Create a splash screen that binds to progress and show it
var splash = new UpdateSplash(updateProgress);
splash.Show();
using (var updateManager = new UpdateManager(squirrelUrl))
{
//IProgress<int>.Report matches Action<i>
var info = await updateManager.CheckForUpdate(progress: progress.Report);
//Get the current and future versions.
//If missing, replace them with version Zero
var currentVersion = info.CurrentlyInstalledVersion?.Version ?? ZeroVersion;
var futureVersion = info.FutureReleaseEntry?.Version ?? ZeroVersion;
//Is there a newer version?
if (currentVersion < futureVersion)
{
await updateManager.UpdateApp(progress.Report);
Restart();
}
}
splash.Hide();
}
private void Restart()
{
Process.Start(ResourceAssembly.Location);
Current.Shutdown();
}
This is just enough code to extract to a separate class:
private async void Application_Startup(object sender, StartupEventArgs e)
{
var updater = new Updater();
await updater.CheckForUpdatesAsync(...);
}
// ...
class Updater
{
static readonly SemanticVersion ZeroVersion = new SemanticVersion(0, 0, 0, 0);
public async Task CheckForUpdatesAsync(string squirrelUrl)
{
var updateProgress = new Progress<int>();
IProgress<int> progress = updateProgress;
//Create a splash screen that binds to progress and show it
var splash = new UpdateSplash(updateProgress);
splash.Show();
using (var updateManager = new UpdateManager(squirrelUrl))
{
var updateInfo = await updateManager.CheckForUpdate(progress: progress.Report);
//Get the current and future versions. If missing, replace them with version Zero
var currentVersion = updateInfo.CurrentlyInstalledVersion?.Version ?? ZeroVersion;
var futureVersion = updateInfo.FutureReleaseEntry?.Version ?? ZeroVersion;
//Is there a newer version?
if (currentVersion < futureVersion)
{
await updateManager.UpdateApp(progress.Report);
Restart();
}
}
splash.Hide();
}
private void Restart()
{
Process.Start(Application.ResourceAssembly.Location);
Application.Current.Shutdown();
}
}
So the actual exception is somewhere in the Restart handler is trying to access the MainWindow get property from another thread based on the stack trace. This is a complete guess, but I would store the original Dispatcher in the OnStartup method and use the stored Dispatcher in the Restart event handler.
Why you are not using SplashScreen ? This SplashScreen would check for new versions, and either download updates, or start the old application.
A lovely tutorial to get you started : EASILY CREATE A WPF SPLASH SCREEN WITH STATUS UPDATES VIA MVVM
I'd like to load my viewmodel after a page has loaded but I can't seem to figure out how to do this.
Here's the viewModel methods where both methods inside LoadViewModel are long running async methods:
public async Task LoadViewModel()
{
await GetAllTiles();
await UpdateAllScenarioCardsAsync();
}
In the view I'm trying something like this:
private async void NavigationHelper_LoadState(object sender, LoadStateEventArgs e)
{
var viewModel = DataContext as StatsViewModel;
if (viewModel != null)
{
var statsViewModel = viewModel;
await statsViewModel.LoadViewModel();
}
}
For some reason the LoadViewModel() method blocks the entire UI even if I remove the awaits for GetAllTiles() and UpdateAllScenarioCardsAsync()... The NavigationHelper_LoadState method is run before the page is loaded so I've tried registering LoadViewModel() to the Loaded event of the page but I can't seem to get it to work.
EDIT
Here is my UpdateAllScenarioCardsAsync() class. UpdateTotalTilesAsync() and UpdataTodayTilesAsync() have await statements inside the code as well but it still blocks the UI. I used closedScenario because I thought the issue could be closing over a variable over the wrong scope like answered in this question, but still now luck. I'm tempted to think it has something to do with the foreach loop because I have successfully done this elsewhere in my solution and it doesn't block the UI thread, but that code had no foreach loop.
private async Task UpdateAllScenarioCardsAsync()
{
IsPending = true;
try
{
// Load all of the scenario cards
foreach (var scenario in _scenariosList)
{
var closedScenario = scenario;
var data = new ScenarioDataCard(closedScenario);
await UpdateTotalTiles(data);
await UpdateTodayTestedTiles(data);
ScenarioDataCards.Add(data);
}
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
}
finally
{
IsPending = false;
}
}
You need to add an await inside the LoadState event handler. Otherwise it will simply block whilst waiting for the LoadViewModel() to return. Like this...
private async void NavigationHelper_LoadState(object sender, LoadStateEventArgs e)
{
var viewModel = DataContext as StatsViewModel;
if (viewModel != null)
{
await viewModel.LoadViewModel();
}
}
I also assume that the GetAllTiles and UpdateAllScenarioCardsAsync methods are implemented so that they really do perform work on another thread so that they are not blocking the main user interface thread.