C#
WebClient
DownloadFileCompleted
event handling
programming tutorial

Pass parameters to WebClient.DownloadFileCompleted event

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

In older .NET code that still uses WebClient, DownloadFileCompleted is an event callback, so the handler often needs some context about which download just finished. The standard way to pass that context is the userToken argument on DownloadFileAsync, which later appears as e.UserState in the completion event.

That is much safer than storing per-download state in global variables or shared fields. It also scales correctly when several downloads are running at the same time.

Use the userToken Overload

DownloadFileAsync has an overload that accepts a third argument for caller-provided state:

csharp
1using System;
2using System.ComponentModel;
3using System.Net;
4
5public class DownloadContext
6{
7    public string FileId { get; set; } = string.Empty;
8    public string SourceUrl { get; set; } = string.Empty;
9}
10
11public static class Program
12{
13    public static void Main()
14    {
15        var client = new WebClient();
16        client.DownloadFileCompleted += OnDownloadFileCompleted;
17
18        var context = new DownloadContext
19        {
20            FileId = "invoice-2026-001",
21            SourceUrl = "https://example.com/file.pdf"
22        };
23
24        client.DownloadFileAsync(
25            new Uri(context.SourceUrl),
26            "file.pdf",
27            context
28        );
29
30        Console.ReadLine();
31    }
32
33    private static void OnDownloadFileCompleted(object? sender, AsyncCompletedEventArgs e)
34    {
35        var context = e.UserState as DownloadContext;
36
37        if (e.Cancelled)
38        {
39            Console.WriteLine($"Cancelled: {context?.FileId}");
40            return;
41        }
42
43        if (e.Error != null)
44        {
45            Console.WriteLine($"Failed: {context?.FileId} - {e.Error.Message}");
46            return;
47        }
48
49        Console.WriteLine($"Completed: {context?.FileId}");
50    }
51}

This is the intended pattern for attaching request-specific data to the later completion event.

Why UserState Matters for Multiple Downloads

If several downloads happen concurrently, the completion handler needs a reliable way to distinguish them. Using a token object per request solves that:

csharp
1for (int i = 1; i <= 3; i++)
2{
3    var ctx = new DownloadContext
4    {
5        FileId = $"file-{i}",
6        SourceUrl = "https://example.com/sample.bin"
7    };
8
9    client.DownloadFileAsync(new Uri(ctx.SourceUrl), $"sample-{i}.bin", ctx);
10}

Each completion callback receives the matching context through e.UserState. That avoids race conditions that appear when all downloads share the same mutable field.

Check Success, Failure, and Cancellation Separately

Do not treat DownloadFileCompleted as automatic success. The event fires for completion, failure, and cancellation, so the handler should check:

  • 'e.Cancelled'
  • 'e.Error'
  • success only if both are absent

That is especially important in legacy event-driven code, where it is easy to log "done" without checking the actual outcome.

A Small Helper Pattern

If the codebase still uses several WebClient downloads, a small wrapper can make the event pattern less repetitive:

csharp
1void StartDownload(WebClient client, string url, string path, string fileId)
2{
3    var context = new DownloadContext
4    {
5        FileId = fileId,
6        SourceUrl = url
7    };
8
9    client.DownloadFileAsync(new Uri(url), path, context);
10}

That keeps the context creation and event-state pattern consistent across call sites.

WebClient Is Legacy

For new code, HttpClient with task-based async APIs is usually a better choice. It is easier to compose, test, and cancel cleanly:

csharp
using var http = new HttpClient();
byte[] bytes = await http.GetByteArrayAsync("https://example.com/file.pdf");
await File.WriteAllBytesAsync("file.pdf", bytes);

Still, when you are maintaining an existing event-based codebase, userToken plus e.UserState is the right pattern for passing parameters into DownloadFileCompleted.

Common Pitfalls

The biggest mistake is storing per-download context in shared fields instead of passing it through userToken. That falls apart as soon as multiple downloads overlap.

Another common issue is ignoring e.Error and e.Cancelled and assuming that the completion event means success.

Developers also sometimes cast e.UserState without checking for null or the wrong type. If different callers reuse the same handler, be defensive.

Finally, mixing old event-driven WebClient code with newer async patterns without a plan can make the codebase harder to follow. If migration is underway, isolate legacy downloads behind a small adapter.

Summary

  • Pass request-specific parameters through the userToken overload of DownloadFileAsync.
  • Read the token back from e.UserState inside DownloadFileCompleted.
  • Use a separate context object for each concurrent download.
  • Check cancellation and error states explicitly before treating a download as successful.
  • Prefer HttpClient for new development, but use UserState correctly in legacy WebClient code.

Course illustration
Course illustration

All Rights Reserved.