ObservableCollection
asynchronous iteration
hierarchical data
C#
programming tutorial

How to asynchronously iterate an ObservableCollection containing hierarchical elements?

Interview Questions practice on Codemia

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

Browse interview questions

Introduction

To asynchronously iterate an ObservableCollection<T> with hierarchical (tree-structured) elements in C#, use recursive async methods combined with await for each asynchronous operation within the traversal. Since ObservableCollection fires CollectionChanged events on modification, all changes must happen on the UI thread (via Dispatcher in WPF or SynchronizationContext). Use Task.Run for CPU-bound work, await for I/O-bound work, and process the hierarchy depth-first or breadth-first depending on your use case.

Defining the Hierarchical Model

csharp
1using System.Collections.ObjectModel;
2
3public class TreeNode
4{
5    public string Name { get; set; }
6    public ObservableCollection<TreeNode> Children { get; set; }
7        = new ObservableCollection<TreeNode>();
8
9    public TreeNode(string name) => Name = name;
10}

Example tree:

 
1Root
2├── A
3│   ├── A1
4│   └── A2
5├── B
6│   └── B1
7└── C

Depth-First Async Iteration

Process each node, then recursively process its children:

csharp
1using System;
2using System.Collections.ObjectModel;
3using System.Threading.Tasks;
4
5public class TreeProcessor
6{
7    public async Task ProcessTreeAsync(ObservableCollection<TreeNode> nodes)
8    {
9        foreach (var node in nodes)
10        {
11            // Perform async work on the current node
12            await ProcessNodeAsync(node);
13
14            // Recursively process children
15            if (node.Children.Count > 0)
16            {
17                await ProcessTreeAsync(node.Children);
18            }
19        }
20    }
21
22    private async Task ProcessNodeAsync(TreeNode node)
23    {
24        // Simulate async I/O (API call, file read, database query)
25        await Task.Delay(100);
26        Console.WriteLine($"Processed: {node.Name}");
27    }
28}
29
30// Usage
31var root = new ObservableCollection<TreeNode>
32{
33    new TreeNode("A")
34    {
35        Children = { new TreeNode("A1"), new TreeNode("A2") }
36    },
37    new TreeNode("B")
38    {
39        Children = { new TreeNode("B1") }
40    },
41    new TreeNode("C")
42};
43
44var processor = new TreeProcessor();
45await processor.ProcessTreeAsync(root);
46// Output: A, A1, A2, B, B1, C (depth-first)

Breadth-First Async Iteration

Process all nodes at the current level before descending:

csharp
1using System.Collections.Generic;
2using System.Collections.ObjectModel;
3using System.Threading.Tasks;
4
5public async Task ProcessBreadthFirstAsync(ObservableCollection<TreeNode> roots)
6{
7    var queue = new Queue<TreeNode>();
8
9    foreach (var root in roots)
10        queue.Enqueue(root);
11
12    while (queue.Count > 0)
13    {
14        var node = queue.Dequeue();
15        await ProcessNodeAsync(node);
16
17        foreach (var child in node.Children)
18            queue.Enqueue(child);
19    }
20}
21// Output: A, B, C, A1, A2, B1 (breadth-first)

Parallel Async Processing of Siblings

Process sibling nodes concurrently while maintaining parent-child ordering:

csharp
1public async Task ProcessParallelAsync(ObservableCollection<TreeNode> nodes)
2{
3    // Process all siblings at this level in parallel
4    var tasks = new List<Task>();
5
6    foreach (var node in nodes)
7    {
8        tasks.Add(ProcessSubtreeAsync(node));
9    }
10
11    await Task.WhenAll(tasks);
12}
13
14private async Task ProcessSubtreeAsync(TreeNode node)
15{
16    await ProcessNodeAsync(node);
17
18    if (node.Children.Count > 0)
19    {
20        await ProcessParallelAsync(node.Children);
21    }
22}

Updating ObservableCollection from Async Code (WPF)

ObservableCollection raises events on the calling thread. In WPF, modifications must happen on the UI thread:

csharp
1using System.Windows;
2using System.Windows.Threading;
3
4public class TreeViewModel
5{
6    public ObservableCollection<TreeNode> Nodes { get; }
7        = new ObservableCollection<TreeNode>();
8
9    public async Task LoadTreeAsync()
10    {
11        var data = await FetchDataFromApiAsync();
12
13        // Must update on UI thread for WPF data binding
14        Application.Current.Dispatcher.Invoke(() =>
15        {
16            Nodes.Clear();
17            foreach (var item in data)
18            {
19                Nodes.Add(item);
20            }
21        });
22    }
23
24    private async Task<List<TreeNode>> FetchDataFromApiAsync()
25    {
26        await Task.Delay(500); // Simulate API call
27        return new List<TreeNode>
28        {
29            new TreeNode("Root") { Children = { new TreeNode("Child1") } }
30        };
31    }
32}

Using IProgress for UI Updates

csharp
1public async Task ProcessWithProgressAsync(
2    ObservableCollection<TreeNode> nodes,
3    IProgress<string> progress)
4{
5    foreach (var node in nodes)
6    {
7        await ProcessNodeAsync(node);
8        progress.Report($"Processed: {node.Name}");
9
10        if (node.Children.Count > 0)
11        {
12            await ProcessWithProgressAsync(node.Children, progress);
13        }
14    }
15}
16
17// In the ViewModel
18var progress = new Progress<string>(message =>
19{
20    StatusText = message;  // Updates UI via data binding
21});
22
23await ProcessWithProgressAsync(Nodes, progress);

Cancellation Support

Add CancellationToken for long-running tree operations:

csharp
1public async Task ProcessTreeAsync(
2    ObservableCollection<TreeNode> nodes,
3    CancellationToken cancellationToken)
4{
5    foreach (var node in nodes)
6    {
7        cancellationToken.ThrowIfCancellationRequested();
8
9        await ProcessNodeAsync(node);
10
11        if (node.Children.Count > 0)
12        {
13            await ProcessTreeAsync(node.Children, cancellationToken);
14        }
15    }
16}
17
18// Usage with cancellation
19var cts = new CancellationTokenSource();
20
21// Cancel after 5 seconds or on user request
22cts.CancelAfter(TimeSpan.FromSeconds(5));
23
24try
25{
26    await processor.ProcessTreeAsync(nodes, cts.Token);
27}
28catch (OperationCanceledException)
29{
30    Console.WriteLine("Processing was cancelled");
31}

IAsyncEnumerable Approach (.NET Core 3.0+)

Flatten the hierarchy into an async stream:

csharp
1public async IAsyncEnumerable<TreeNode> FlattenAsync(
2    ObservableCollection<TreeNode> nodes)
3{
4    foreach (var node in nodes)
5    {
6        yield return node;
7
8        await foreach (var child in FlattenAsync(node.Children))
9        {
10            yield return child;
11        }
12    }
13}
14
15// Usage
16await foreach (var node in FlattenAsync(rootNodes))
17{
18    await ProcessNodeAsync(node);
19}

Common Pitfalls

  • Modifying ObservableCollection from a background thread: Changing the collection from a non-UI thread throws NotSupportedException in WPF. Always use Dispatcher.Invoke or Dispatcher.BeginInvoke to modify the collection on the UI thread.
  • Iterating and modifying the collection simultaneously: Adding or removing items from an ObservableCollection while iterating it with foreach throws InvalidOperationException. If you need to modify during iteration, work on a snapshot (nodes.ToList()) and apply changes afterward.
  • Not handling cancellation in deep recursive traversals: A deep tree with hundreds of nodes can take a long time to process. Without CancellationToken, the operation cannot be stopped. Always pass and check CancellationToken in recursive async methods.
  • Using Task.Run for every node in a large tree: Wrapping every ProcessNodeAsync in Task.Run creates excessive thread pool pressure for trees with thousands of nodes. Use Task.Run only for CPU-bound work, and use await directly for I/O-bound operations.
  • Forgetting that ObservableCollection is not thread-safe: Concurrent reads and writes from multiple async tasks can corrupt the collection. Use SemaphoreSlim or process nodes sequentially when both reading and modifying the collection during traversal.

Summary

  • Use recursive async methods with await for depth-first traversal of hierarchical ObservableCollection
  • Use Queue<T> for breadth-first async iteration
  • Process siblings in parallel with Task.WhenAll when order does not matter
  • Update ObservableCollection on the UI thread using Dispatcher.Invoke in WPF
  • Add CancellationToken support for cancellable long-running tree operations
  • Use IAsyncEnumerable (.NET Core 3.0+) to flatten hierarchies into async streams

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.