async
Task.Run
MVVM
asynchronous programming
C#

async Task.Run with MVVM

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

In MVVM (Model-View-ViewModel) applications, Task.Run offloads CPU-bound work to a background thread so the UI stays responsive. The key challenge is updating UI-bound properties from the background thread — WPF, UWP, and MAUI require property changes to be raised on the UI thread. Use async/await with Task.Run in your ViewModel commands, and the continuation after await automatically returns to the UI thread (when using the default SynchronizationContext).

Basic Pattern

csharp
1public class MainViewModel : INotifyPropertyChanged
2{
3    private string _status;
4    public string Status
5    {
6        get => _status;
7        set { _status = value; OnPropertyChanged(); }
8    }
9
10    private bool _isBusy;
11    public bool IsBusy
12    {
13        get => _isBusy;
14        set { _isBusy = value; OnPropertyChanged(); }
15    }
16
17    public ICommand LoadDataCommand { get; }
18
19    public MainViewModel()
20    {
21        LoadDataCommand = new RelayCommand(async () => await LoadDataAsync());
22    }
23
24    private async Task LoadDataAsync()
25    {
26        IsBusy = true;  // UI thread — safe to update
27        Status = "Loading...";
28
29        var result = await Task.Run(() =>
30        {
31            // Background thread — CPU-bound work
32            Thread.Sleep(2000);  // Simulate heavy computation
33            return "Data loaded successfully";
34        });
35
36        // Back on UI thread after await
37        Status = result;  // Safe to update bound property
38        IsBusy = false;
39    }
40
41    public event PropertyChangedEventHandler PropertyChanged;
42    protected void OnPropertyChanged([CallerMemberName] string name = null)
43        => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
44}

After await Task.Run(...), execution resumes on the UI thread because WPF/MAUI captures the SynchronizationContext. This means you can safely update bound properties without explicit dispatching.

When to Use Task.Run

csharp
1// USE Task.Run for CPU-bound work
2await Task.Run(() => ComputeHeavyAlgorithm(data));
3await Task.Run(() => ParseLargeFile(filePath));
4await Task.Run(() => EncryptData(payload));
5
6// DO NOT use Task.Run for I/O-bound work — use async APIs directly
7var data = await httpClient.GetStringAsync(url);      // Already async
8var content = await File.ReadAllTextAsync(filePath);   // Already async
9var result = await dbContext.Users.ToListAsync();       // Already async

Task.Run adds thread pool overhead. For I/O-bound operations, the built-in async methods are more efficient because they do not consume a thread while waiting.

Async RelayCommand Implementation

csharp
1public class AsyncRelayCommand : ICommand
2{
3    private readonly Func<Task> _execute;
4    private readonly Func<bool> _canExecute;
5    private bool _isExecuting;
6
7    public AsyncRelayCommand(Func<Task> execute, Func<bool> canExecute = null)
8    {
9        _execute = execute;
10        _canExecute = canExecute;
11    }
12
13    public bool CanExecute(object parameter)
14        => !_isExecuting && (_canExecute?.Invoke() ?? true);
15
16    public async void Execute(object parameter)
17    {
18        if (!CanExecute(parameter)) return;
19
20        _isExecuting = true;
21        RaiseCanExecuteChanged();
22
23        try
24        {
25            await _execute();
26        }
27        finally
28        {
29            _isExecuting = false;
30            RaiseCanExecuteChanged();
31        }
32    }
33
34    public event EventHandler CanExecuteChanged;
35
36    public void RaiseCanExecuteChanged()
37        => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
38}

This command disables itself while executing, preventing double-clicks from launching parallel operations.

Reporting Progress from Task.Run

csharp
1private async Task ProcessItemsAsync()
2{
3    IsBusy = true;
4    var progress = new Progress<int>(percent =>
5    {
6        // This callback runs on the UI thread
7        ProgressValue = percent;
8    });
9
10    await Task.Run(() =>
11    {
12        var items = GetLargeDataSet();
13        for (int i = 0; i < items.Count; i++)
14        {
15            ProcessItem(items[i]);
16            ((IProgress<int>)progress).Report((i + 1) * 100 / items.Count);
17        }
18    });
19
20    IsBusy = false;
21    Status = "Processing complete";
22}

Progress<T> captures the SynchronizationContext at construction time, so its callback always runs on the UI thread. Create it before entering Task.Run.

Cancellation Support

csharp
1private CancellationTokenSource _cts;
2
3public ICommand CancelCommand { get; }
4
5private async Task LoadDataAsync()
6{
7    _cts = new CancellationTokenSource();
8    IsBusy = true;
9
10    try
11    {
12        var result = await Task.Run(() =>
13        {
14            for (int i = 0; i < 100; i++)
15            {
16                _cts.Token.ThrowIfCancellationRequested();
17                Thread.Sleep(50);  // Simulate work
18            }
19            return "Complete";
20        }, _cts.Token);
21
22        Status = result;
23    }
24    catch (OperationCanceledException)
25    {
26        Status = "Cancelled";
27    }
28    finally
29    {
30        IsBusy = false;
31        _cts.Dispose();
32    }
33}
34
35private void Cancel() => _cts?.Cancel();

Pass the CancellationToken to Task.Run and check it periodically inside the delegate. This lets users cancel long-running operations via a Cancel button bound to CancelCommand.

Error Handling

csharp
1private async Task LoadDataAsync()
2{
3    IsBusy = true;
4    ErrorMessage = null;
5
6    try
7    {
8        var result = await Task.Run(() =>
9        {
10            // This might throw
11            return RiskyComputation();
12        });
13        Status = result;
14    }
15    catch (InvalidOperationException ex)
16    {
17        ErrorMessage = $"Operation failed: {ex.Message}";
18    }
19    catch (Exception ex)
20    {
21        ErrorMessage = $"Unexpected error: {ex.Message}";
22    }
23    finally
24    {
25        IsBusy = false;
26    }
27}

Exceptions thrown inside Task.Run are captured by the Task and re-thrown at the await point. The catch block runs on the UI thread, so you can safely update error-display properties.

Thread Safety with ObservableCollection

csharp
1// ObservableCollection raises CollectionChanged on the calling thread
2// WPF requires CollectionChanged on the UI thread
3
4// Option 1: Collect results, then update on UI thread
5private async Task LoadItemsAsync()
6{
7    var items = await Task.Run(() => FetchAndProcessItems());
8
9    Items.Clear();
10    foreach (var item in items)
11    {
12        Items.Add(item);  // UI thread — safe
13    }
14}
15
16// Option 2: Use dispatcher for incremental updates
17private async Task LoadItemsIncrementalAsync()
18{
19    await Task.Run(() =>
20    {
21        foreach (var item in FetchItems())
22        {
23            App.Current.Dispatcher.Invoke(() => Items.Add(item));
24        }
25    });
26}

Never modify an ObservableCollection from a background thread in WPF. Either batch the results and add them after await, or use Dispatcher.Invoke for incremental updates.

MVVM Toolkit (CommunityToolkit.Mvvm)

csharp
1using CommunityToolkit.Mvvm.ComponentModel;
2using CommunityToolkit.Mvvm.Input;
3
4public partial class MainViewModel : ObservableObject
5{
6    [ObservableProperty]
7    private string _status;
8
9    [ObservableProperty]
10    private bool _isBusy;
11
12    [RelayCommand]
13    private async Task LoadDataAsync()
14    {
15        IsBusy = true;
16        Status = await Task.Run(() => HeavyComputation());
17        IsBusy = false;
18    }
19}

The [RelayCommand] source generator creates LoadDataCommand automatically, including async support with built-in CanExecute management.

Common Pitfalls

  • Using Task.Run for I/O: await Task.Run(() => httpClient.GetAsync(url)) wastes a thread pool thread. Use await httpClient.GetAsync(url) directly — I/O-bound async operations do not need Task.Run.
  • Updating UI properties inside Task.Run: Setting bound properties inside Task.Run(() => { Status = "Done"; }) raises PropertyChanged on a background thread, which can crash or silently fail in WPF. Always update UI properties after the await.
  • Fire-and-forget commands: async void Execute() in commands swallows exceptions silently if not wrapped in try/catch. Always add error handling inside async void methods.
  • Missing ConfigureAwait: In library code (not ViewModels), use await Task.Run(...).ConfigureAwait(false) to avoid capturing the synchronization context unnecessarily. In ViewModels, the default behavior (capturing context) is what you want.
  • Blocking with .Result or .Wait(): Calling Task.Run(...).Result or .Wait() blocks the UI thread and can cause deadlocks. Always use await to consume async results in ViewModels.

Summary

  • Use Task.Run in MVVM ViewModels to offload CPU-bound work to a background thread
  • After await Task.Run(...), execution returns to the UI thread — safe to update bound properties
  • Do not use Task.Run for I/O-bound work — use native async APIs instead
  • Use Progress<T> for progress reporting from background threads
  • Use CancellationTokenSource for cancellable operations
  • Never modify ObservableCollection from a background thread in WPF
  • Consider CommunityToolkit.Mvvm for source-generated async commands

Related reading
Course
Intermediate
27 lessons
14 hours
OOD Fundamentals

Master object-oriented design from first principles, SOLID, design patterns, and classic interview problems with hands-on coding.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.