WPF
Async
Combobox
Two-way Binding
Programming

How to invoke async-operation on two-way bound Combobox WPF

Interview Questions practice on Codemia

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

Browse interview questions

Introduction

To invoke an async operation when a two-way bound ComboBox selection changes in WPF, use an async property setter in the ViewModel that calls an async void or async Task method when the bound property changes. The key challenge is that property setters cannot be async, so the setter must fire an async method without awaiting it directly, or use ICommand with an async execute handler. The MVVM pattern keeps async logic in the ViewModel while the ComboBox binding remains standard.

Basic Two-Way Bound ComboBox

xml
1<!-- MainWindow.xaml -->
2<ComboBox ItemsSource="{Binding Categories}"
3          SelectedItem="{Binding SelectedCategory, Mode=TwoWay}"
4          DisplayMemberPath="Name" />
5
6<TextBlock Text="{Binding StatusText}" />
7<ItemsControl ItemsSource="{Binding Products}" />
csharp
1// MainViewModel.cs
2public class MainViewModel : INotifyPropertyChanged
3{
4    private string _selectedCategory;
5    private string _statusText;
6
7    public ObservableCollection<string> Categories { get; }
8        = new() { "Electronics", "Books", "Clothing" };
9
10    public ObservableCollection<Product> Products { get; } = new();
11
12    public string SelectedCategory
13    {
14        get => _selectedCategory;
15        set
16        {
17            if (_selectedCategory != value)
18            {
19                _selectedCategory = value;
20                OnPropertyChanged();
21                // Fire async operation
22                _ = LoadProductsAsync(value);
23            }
24        }
25    }
26
27    public string StatusText
28    {
29        get => _statusText;
30        set { _statusText = value; OnPropertyChanged(); }
31    }
32
33    private async Task LoadProductsAsync(string category)
34    {
35        StatusText = "Loading...";
36        Products.Clear();
37
38        var products = await _productService.GetByCategoryAsync(category);
39
40        foreach (var product in products)
41            Products.Add(product);
42
43        StatusText = $"Loaded {products.Count} products";
44    }
45
46    public event PropertyChangedEventHandler PropertyChanged;
47    protected void OnPropertyChanged([CallerMemberName] string name = null)
48        => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
49}

The Discard Pattern (_ = Task)

The setter uses _ = LoadProductsAsync(value) because property setters cannot be async:

csharp
1public string SelectedCategory
2{
3    get => _selectedCategory;
4    set
5    {
6        _selectedCategory = value;
7        OnPropertyChanged();
8
9        // Cannot use 'await' in a setter — fire and forget with discard
10        _ = LoadProductsAsync(value);
11    }
12}

The _ discard suppresses the compiler warning about unawaited tasks. However, this means exceptions are not observed unless handled inside LoadProductsAsync.

Safe Async with Error Handling

Always wrap the async operation in try/catch:

csharp
1private async Task LoadProductsAsync(string category)
2{
3    try
4    {
5        IsLoading = true;
6        StatusText = "Loading...";
7        Products.Clear();
8
9        var products = await _productService.GetByCategoryAsync(category);
10
11        foreach (var product in products)
12            Products.Add(product);
13
14        StatusText = $"{products.Count} products loaded";
15    }
16    catch (HttpRequestException ex)
17    {
18        StatusText = $"Network error: {ex.Message}";
19    }
20    catch (Exception ex)
21    {
22        StatusText = $"Error: {ex.Message}";
23    }
24    finally
25    {
26        IsLoading = false;
27    }
28}

Using ICommand with AsyncRelayCommand

For cleaner MVVM, use AsyncRelayCommand from CommunityToolkit.Mvvm:

csharp
1using CommunityToolkit.Mvvm.ComponentModel;
2using CommunityToolkit.Mvvm.Input;
3
4public partial class MainViewModel : ObservableObject
5{
6    [ObservableProperty]
7    private string _selectedCategory;
8
9    [ObservableProperty]
10    private ObservableCollection<Product> _products = new();
11
12    partial void OnSelectedCategoryChanged(string value)
13    {
14        // Auto-generated by [ObservableProperty] — called when SelectedCategory changes
15        LoadProductsCommand.Execute(value);
16    }
17
18    [RelayCommand]
19    private async Task LoadProducts(string category)
20    {
21        Products.Clear();
22        var products = await _productService.GetByCategoryAsync(category);
23        foreach (var p in products)
24            Products.Add(p);
25    }
26}

Cancellation Support

Cancel the previous operation when the user selects a new category quickly:

csharp
1public class MainViewModel : INotifyPropertyChanged
2{
3    private CancellationTokenSource _cts;
4    private string _selectedCategory;
5
6    public string SelectedCategory
7    {
8        get => _selectedCategory;
9        set
10        {
11            _selectedCategory = value;
12            OnPropertyChanged();
13            _ = LoadProductsAsync(value);
14        }
15    }
16
17    private async Task LoadProductsAsync(string category)
18    {
19        // Cancel previous operation
20        _cts?.Cancel();
21        _cts = new CancellationTokenSource();
22        var token = _cts.Token;
23
24        try
25        {
26            StatusText = "Loading...";
27            await Task.Delay(300, token);  // Debounce
28
29            token.ThrowIfCancellationRequested();
30            var products = await _productService.GetByCategoryAsync(category, token);
31
32            token.ThrowIfCancellationRequested();
33            Products.Clear();
34            foreach (var p in products)
35                Products.Add(p);
36
37            StatusText = $"Loaded {products.Count} items";
38        }
39        catch (OperationCanceledException)
40        {
41            // Expected — user selected a different category
42        }
43        catch (Exception ex)
44        {
45            StatusText = $"Error: {ex.Message}";
46        }
47    }
48}

Loading Indicator with IsBusy

csharp
1// ViewModel
2private bool _isLoading;
3public bool IsLoading
4{
5    get => _isLoading;
6    set { _isLoading = value; OnPropertyChanged(); }
7}
xml
1<!-- XAML -->
2<ComboBox ItemsSource="{Binding Categories}"
3          SelectedItem="{Binding SelectedCategory, Mode=TwoWay}"
4          IsEnabled="{Binding IsLoading, Converter={StaticResource InverseBoolConverter}}" />
5
6<ProgressBar IsIndeterminate="True"
7             Visibility="{Binding IsLoading, Converter={StaticResource BoolToVisibilityConverter}}" />

EventToCommand Alternative

Use System.Windows.Interactivity to bind the SelectionChanged event to a command:

xml
1<ComboBox ItemsSource="{Binding Categories}"
2          SelectedItem="{Binding SelectedCategory, Mode=TwoWay}">
3    <i:Interaction.Triggers>
4        <i:EventTrigger EventName="SelectionChanged">
5            <i:InvokeCommandAction Command="{Binding CategoryChangedCommand}" />
6        </i:EventTrigger>
7    </i:Interaction.Triggers>
8</ComboBox>
csharp
1public ICommand CategoryChangedCommand => new AsyncRelayCommand(async () =>
2{
3    await LoadProductsAsync(SelectedCategory);
4});

Common Pitfalls

  • Using async void in the property setter: async void methods swallow exceptions silently and cannot be awaited. Use _ = LoadAsync() (fire-and-forget with a Task method that has internal try/catch) instead of making the setter async void.
  • Not handling rapid selection changes: If the user changes the ComboBox selection quickly, multiple async operations run simultaneously and may overwrite each other's results. Use CancellationTokenSource to cancel the previous operation when a new selection is made.
  • Updating ObservableCollection from a background thread: ObservableCollection raises events on the calling thread. If await resumes on a background thread, adding items throws. Use Application.Current.Dispatcher.Invoke() or ensure the synchronization context is captured.
  • Not disabling the ComboBox during loading: Users can change the selection while data is loading, causing race conditions. Bind IsEnabled to an inverse of IsLoading to prevent interaction during async operations.
  • Forgetting that SelectedItem binding fires on initial load: When the ViewModel sets the initial SelectedCategory value, the property setter fires and triggers the async operation. Guard against this with a null check or an _isInitialized flag.

Summary

  • Use _ = LoadAsync(value) in the property setter to fire async operations from two-way bindings
  • Always wrap async operations in try/catch since property setters cannot propagate exceptions
  • Use CancellationTokenSource to cancel previous operations on rapid selection changes
  • Consider CommunityToolkit.Mvvm's [ObservableProperty] and [RelayCommand] for cleaner code
  • Disable the ComboBox during loading and show a progress indicator for better UX

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.