In my WPF application I use the async await pattern a lot to keep the UI responsive. However I noticed sometimes it freezes anyway.
I looked into it and now I understand that an async method itself still runs on the UI thread, so blocking is natural.
What I don't understand then is why the build in Async methods also freeze my UI. If I call for example await HttpClient().GetAsync(); my UI will freeze.
To keep my UI actually responsive, I need to wrap that call into Task.Run(), which will make it execute on a background thread.
I did some testing with doing actual work. No Task.Delay().
I have a simple WPF window with a loading animation, so I can clearly see when the UI freezes. Edit: Added more details to show I don't do anything crazy.
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new MyViewModel();
}
}
public class MyViewModel : ObservableObject
{
public AsyncRelayCommand ButtonCommand { get; }
public MyViewModel()
{
ButtonCommand = new AsyncRelayCommand(Load);
}
public async Task Load()
{
await GetALotOfBlogs(); // blocks the first time i execute it, 2nd time seems fine
await Task.Run(GetALotOfBlogs); // does not block ever
await HttpRequestAsync(); // does not seem to actually block
await Task.Run(HttpRequestAsync); // does not block
await ReadFile(); // "laggy blocking"
await Task.Run(ReadFile); // does not block
}
public async Task ReadFile()
{
for (int i = 0; i < 100; i++)
await File.ReadAllBytesAsync(@"C:\Yee\SomeFile.rar");
}
public async Task HttpRequestAsync()
{
HttpResponseMessage response = await new HttpClient().GetAsync("http://slowwly.robertomurray.co.uk/delay/5000/url/http://www.google.co.uk");
response.EnsureSuccessStatusCode();
string responseBody = await response.Content.ReadAsStringAsync();
}
public Task<List<Blog>> GetALotOfBlogs()
{
var ctx = new BloggingContext();
return ctx.Blogs.ToListAsync();
}
}
XAML is simply
<StackPanel>
<Button Command="{Binding ButtonCommand}" Content="Go"/>
<local:LoadingSpinner />
</StackPanel>
This seems conclusive enough and not unreasonable at all. The "naked" await statements execute on the UI thread, blocking it. The Task.Run statements execute on background threads, not blocking the UI thread.
The conclusion I have to take from this is: In ViewModels wrap every call to an async method in Task.Run
I read a lot about WPF development. I have seen a good amount of sample and commercial projects. Yet I have never come across this before. At least not so clearly. That makes me seriously doubt my conclusion.
Am I missing something?
Edit:
I think I understand whats happening. Especially the ef core scenario was important to me. I noticed the program is freezing even before the Task is hit, although there is no code before.
When I click the button for the first time a lot of DLLs get loaded. I get a bunch of these messages in myoutput window 'WpfApp3.exe' (CoreCLR: clrhost): Loaded 'C:\Program Files\dotnet\shared\Microsoft.NETCore.App\3.1.8\System.Linq.Queryable.dll'. Skipped loading symbols. Module is optimized and the debugger option 'Just My Code' is enabled.
Net Core seems to load these DLLs dynamically as needed. It also looks like net core looks at the what's in the method before executing and then loads the DLLs necessary. Because the dll loading takes place before any other code before the actual task method.
public async Task Button()
{
Text = "Starting";
await GetALotOfBlogs();
Text = "Done";
}
Text is bound to a TextBlock. The first time I click it actually goes
- Load DLLs (freezing my UI, if done on the UI thread)
Text = "Starting"await GetALotOfBlogs();Text = "Done"
Which is interesting, because to execute Text = "Starting" it obviously does not need all those DLLs.
This DLL loading process is the one that was blocking my UI. The actual ToListAsync Task does not block. The 2nd button click executes just fine, no DLLs to load anymore after all.