Solution 1 - What if you do not wait at all?
If you wait for some task in the constructor then your app won't launch till it gets the data. Thus the launch time of the app is increased and UX will be somewhat less satisfying.
But if you just set some dummy default data and launch the data retrieval fully async without wait or await, you will have no need to Wait
and improve the overall UX. To prevent any unnecessary operations by user you can either make dependent controls disabled or use Null object pattern:
public class Waiter : INotifyPropertyChanged
{
public async Task<String> Get1()
{
await Task.Delay(2000);
return "Got value 1";
}
public async Task<String> Get2()
{
await Task.Delay(3000);
return "Got value 2";
}
private void FailFast(Task task)
{
MessageBox.Show(task.Exception.Message);
Environment.FailFast("Unexpected failure");
}
public async Task InitialLoad()
{
this.Value = "Loading started";
var task1 = Get1();
var task2 = Get2();
// You can also add ContinueWith OnFaulted for task1 and task2 if you do not use the Result property or check for Exception
var tasks = new Task[]
{
task1.ContinueWith(
(prev) =>
this.Value1 = prev.Result),
task2.ContinueWith(
(prev) =>
this.Value2 = prev.Result)
};
await Task.WhenAll(tasks);
this.Value = "Loaded";
}
public Waiter()
{
InitialLoad().ContinueWith(FailFast, TaskContinuationOptions.OnlyOnFaulted);
}
private String _Value,
_Value1,
_Value2;
public String Value
{
get
{
return this._Value;
}
set
{
if (value == this._Value)
return;
this._Value = value;
this.OnPropertyChanged();
}
}
public String Value1
{
get { return this._Value1; }
set
{
if (value == this._Value1)
return;
this._Value1 = value;
this.OnPropertyChanged();
}
}
public String Value2
{
get { return this._Value2; }
set
{
if (value == this._Value2)
return;
this._Value2 = value;
this.OnPropertyChanged();
}
}
public void OnPropertyChanged([CallerMemberName]String propertyName = null)
{
var propChanged = this.PropertyChanged;
if (propChanged == null)
return;
propChanged(this, new PropertyChangedEventArgs(propertyName));
}
public event PropertyChangedEventHandler PropertyChanged;
}
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new Waiter();
}
}
XAML:
<StackPanel>
<TextBox Text="{Binding Value}"/>
<TextBox Text="{Binding Value1}"/>
<TextBox Text="{Binding Value2}"/>
</StackPanel>
Important warning: As it has been pointed by @YuvalItzchakov originally posted solution will silently miss any exceptions that can occur in the async method, so you will have to wrap your async method bodies with some try-catch logic calling Environment.FailFast
to fail fast and loud or use appropriate ContinueWith
with TaskContinuationOptions.OnlyOnFaulted
.
Bad solution 2(!May actually fail!) - ConfigureAwait(false)
Configure await false on all async calls will prevent(in most cases) any use of the original sync context allowing you to wait:
public async Task<String> Get1()
{
await Task.Delay(2000).ConfigureAwait(false);
return "Got value 1";
}
public async Task<String> Get2()
{
await Task.Delay(3000).ConfigureAwait(false);
return "Got value 2";
}
public async Task InitialLoad()
{
this.Value = "Loading started";
var tasks = new Task[]
{
Get1().ContinueWith(
(prev) =>
this.Value1 = prev.Result),
Get2().ContinueWith(
(prev) =>
this.Value2 = prev.Result)
};
await Task.WhenAll(tasks).ConfigureAwait(false);
this.Value = "Loaded";
}
public Waiter()
{
InitialLoad().Wait();
}
It will work in most cases, but it is not actually guaranteed that it won't use the same thread to await leading to the same deadlock problem.
Bad solution 3 - use Task.Run to assuredly avoid any deadlocking.
You can use one not very good async practice and wrap your entire operation into new thread pool task with Task.Run:
private void SyncInitialize()
{
Task.Run(() =>
InitialLoad().Wait())
.Wait();
}
It will squander one thread from the thread pool on wait, but it will work for sure, while Solution 2 may fail.