In my application I have an async Save() method, of type Task<bool> that via the bool will signal if the save was successfull. All kinds of stuff can happen in the Save(), it calls through another layer where exceptions are handled, possible dialogs shown, etc, but that doesn't matter, all I care about is the bool result.
Now I have to call this method from within a non-async method (which is an override from a used framework, so I cannot just make it async)
The code looks a bit like :
public override void SynchronousMethodFromFramework()
{
bool result = false;
Task.Run(async () => result = await Save());
return result;
}
The problem is, that the result is returned BEFORE the Save is finished (thus always false). How can it solve this ? I've tried the Task.WaitAll(), the .Result, the .ConfigureAwaiter(false) but everything I do just seems to completely freeze my application.
A bit more info :
The used WPF framework is Caliburn.Micro. My MainviewModel is a Conductor<IScreen>.Collection.OneActivethat conducts multiple viewmodels in a Tabcontrol. Each ViewModel is some sort of edit screen.
When the end user closes the application (via the right top red X ) I want to iterate over all Tabs to see if they have pending changes.
Code of the mainviewmodel :
public override void CanClose(Action<bool> callback)
{
//for each tab, go to it and try to close it.
//If pending changes and close is not succeeded (eg, user cancels), abort aplication close
bool canclose = false;
Action<bool> result = b => canclose = b;
for (int i = Items.Count - 1; i >= 0; i--)
{
var screen = Items[i];
screen.CanClose(result);
if (!canclose)
{
callback(false);
return;
}
}
callback(true);
}
Code in my "Edit"-ViewModels:
private async Task<bool> SavePendingChanges()
{
if (!Entity.HasDirtyContents())
return true;
bool? dialogResult = DialogProvider.ShowMessageBox("Save changes",
"There are pending changes, do you want to save them ?", MsgBoxButton.YesNoCancel);
if (dialogResult == null)
return false;//user cancelled
if (dialogResult == false)
return true;//user doesn't want to save, but continue
//try to save; if save failed => return false
return await (Save());
}
public override void CanClose(Action<bool> callback)
{
var task = SavePendingChanges();
task.Wait();
bool result = task.Result;
callback(result);
}
The "CanClose" is a CM-provided non-async framework method ...
The proper solution is to make SynchronousMethodFromFramework asynchronous, either by having it return Task or using something like deferrals (as I describe on my blog). So most definitely make your needs known to the framework author(s).
In the meantime, you can hack around it using one of the hacks from my article on async brownfield development.
The easiest solution is to make your Save method a purely background method - after all, if it is running as a background task, it shouldn't be "reaching into" the UI thread with any updates. If your code calls Save in a couple of places - once for a "regular" save and then this other "last chance synchronous only" save - then you can use IProgress<T> to update the UI instead of directly accessing ViewModel properties and/or using Dispatcher. If your code only calls Save here, then just rip out all the UI updates completely.
If you can make Save a truly background operation, then you can just block:
return Task.Run(() => Save()).GetAwaiter().GetResult();
But if you can't do that (I assume you've already considered it and rejected it), then you can invoke some serious Dark Magic to bend the runtime to your will. That is, use a Nested Message Loop (a registered trademark of Lucifer Enterprises).
Developer General's warning: Nested message loops are evil. Evil, evil, evil! Side effects include insanity and death. It is entirely likely that a future maintainer of this code will become violent and hunt you down.
It's been quite a while since I've done a nested message loop in WPF, but I believe this should do the trick:
private async Task<bool> EvilSaveAsync(DispatcherFrame frame)
{
try
{
return await Task.Run(() => Save());
}
finally
{
frame.Continue = false;
}
}
public override void SynchronousMethodFromFramework()
{
var frame = new DispatcherFrame();
var task = EvilSaveAsync(frame);
Dispatcher.PushFrame(frame);
return task.GetAwaiter().GetResult();
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With