Skip to main content

C# Callback Patterns

Callbacks are a fundamental programming concept that allows code to be executed after some operation has completed. In C#, callbacks are primarily implemented using delegates, which provide a type-safe way to reference methods. This tutorial will introduce you to various callback patterns in C# and show you how to implement them effectively.

What are Callbacks?

A callback is a piece of code or function that is passed as an argument to another function and is expected to be executed after a specific operation completes or when an event occurs. Callbacks allow for:

  • Asynchronous programming
  • Event-driven behavior
  • Customization of standard algorithms
  • Notification when long-running tasks complete

In C#, callbacks are typically implemented using delegates, which are type-safe function pointers.

Basic Callback Pattern

Let's start with the simplest form of callback in C#:

csharp
// Define a delegate type
delegate void ProcessCompletedCallback(string result);

class Processor {
// Method that accepts a callback
public void ProcessData(string data, ProcessCompletedCallback callback) {
// Simulating some processing
string result = data.ToUpper();

// When processing completes, call the callback
callback(result);
}
}

Here's how you'd use this basic callback pattern:

csharp
class Program {
static void Main(string[] args) {
Processor processor = new Processor();

// Pass a method as a callback
processor.ProcessData("hello world", DisplayResult);

// Using an anonymous method
processor.ProcessData("callback example", delegate(string result) {
Console.WriteLine($"Anonymous method result: {result}");
});

// Using a lambda expression
processor.ProcessData("lambda callback", (result) =>
Console.WriteLine($"Lambda result: {result}"));
}

static void DisplayResult(string result) {
Console.WriteLine($"Result is: {result}");
}
}

Output:

Result is: HELLO WORLD
Anonymous method result: CALLBACK EXAMPLE
Lambda result: LAMBDA CALLBACK

Callback with Multiple Parameters

Sometimes you need to pass more information through your callback:

csharp
// Delegate with multiple parameters
delegate void ProcessCompletedCallback(string result, bool success, int processingTime);

class AdvancedProcessor {
public void ProcessData(string data, ProcessCompletedCallback callback) {
// Start timing
DateTime startTime = DateTime.Now;

try {
// Simulate processing
Thread.Sleep(1000); // Simulate 1 second of work
string result = data.ToUpper();

// Calculate processing time
int processingTime = (int)(DateTime.Now - startTime).TotalMilliseconds;

// Call the callback with success
callback(result, true, processingTime);
}
catch (Exception ex) {
// Calculate processing time even for failures
int processingTime = (int)(DateTime.Now - startTime).TotalMilliseconds;

// Call the callback with failure
callback($"Error: {ex.Message}", false, processingTime);
}
}
}

Using this more advanced callback:

csharp
static void Main(string[] args) {
AdvancedProcessor processor = new AdvancedProcessor();

processor.ProcessData("complex operation", ProcessingCompleted);
}

static void ProcessingCompleted(string result, bool success, int processingTime) {
if (success) {
Console.WriteLine($"Operation completed successfully in {processingTime}ms");
Console.WriteLine($"Result: {result}");
} else {
Console.WriteLine($"Operation failed after {processingTime}ms");
Console.WriteLine($"Error: {result}");
}
}

Output:

Operation completed successfully in 1003ms
Result: COMPLEX OPERATION

Asynchronous Callbacks

One of the most common uses for callbacks is in asynchronous operations. Here's how you can implement asynchronous callbacks:

csharp
class AsyncProcessor {
public void ProcessDataAsync(string data, Action<string> callback) {
// Start a new task to process the data asynchronously
Task.Run(() => {
// Simulate time-consuming work
Thread.Sleep(2000);
string result = data.ToUpper();

// Invoke the callback when done
callback(result);
});

// This method returns immediately without waiting for the processing to complete
Console.WriteLine("ProcessDataAsync started and returned immediately");
}
}

Usage:

csharp
static void Main(string[] args) {
AsyncProcessor processor = new AsyncProcessor();

Console.WriteLine("Before calling ProcessDataAsync");

processor.ProcessDataAsync("async operation", (result) => {
Console.WriteLine($"Async callback received: {result}");
});

Console.WriteLine("After calling ProcessDataAsync");

// Keep the console application running to see the callback
Console.WriteLine("Press any key to exit...");
Console.ReadKey();
}

Output:

Before calling ProcessDataAsync
ProcessDataAsync started and returned immediately
After calling ProcessDataAsync
Press any key to exit...
Async callback received: ASYNC OPERATION

Using Standard Delegate Types

C# provides built-in delegate types that are useful for callbacks:

  • Action<T> - For callbacks that don't return values
  • Func<T, TResult> - For callbacks that return values
  • Predicate<T> - For callbacks that test a condition and return a boolean

Here's an example using these standard delegate types:

csharp
class DelegateProcessor {
// Method with Action<T> callback
public void ProcessWithAction(string data, Action<string> onCompleted) {
onCompleted(data.ToUpper());
}

// Method with Func<T, TResult> callback
public void ProcessWithFunc(string data, Func<string, int> calculateValue) {
int value = calculateValue(data);
Console.WriteLine($"Calculated value: {value}");
}

// Method with Predicate<T> callback
public void ProcessWithPredicate(string[] items, Predicate<string> filter) {
foreach (var item in items) {
if (filter(item)) {
Console.WriteLine($"Item passed filter: {item}");
}
}
}
}

Using these methods:

csharp
static void Main(string[] args) {
DelegateProcessor processor = new DelegateProcessor();

// Using Action<T>
processor.ProcessWithAction("hello", result =>
Console.WriteLine($"Action result: {result}"));

// Using Func<T, TResult>
processor.ProcessWithFunc("Calculate length", text => text.Length);

// Using Predicate<T>
string[] fruits = { "apple", "banana", "cherry", "date", "elderberry" };
processor.ProcessWithPredicate(fruits, fruit => fruit.Length > 5);
}

Output:

Action result: HELLO
Calculated value: 16
Item passed filter: banana
Item passed filter: cherry
Item passed filter: elderberry

Event-Based Callbacks

Events in C# are a special type of multicast delegate that provide a callback mechanism between an event publisher and subscribers:

csharp
// Define event arguments
public class DataProcessedEventArgs : EventArgs {
public string Result { get; set; }
public bool Success { get; set; }

public DataProcessedEventArgs(string result, bool success) {
Result = result;
Success = success;
}
}

class EventProcessor {
// Define the event
public event EventHandler<DataProcessedEventArgs> DataProcessed;

// Method to raise the event
protected virtual void OnDataProcessed(DataProcessedEventArgs e) {
// Safely invoke the event
DataProcessed?.Invoke(this, e);
}

public void ProcessData(string data) {
try {
// Simulate processing
string result = data.ToUpper();

// Raise the event with success
OnDataProcessed(new DataProcessedEventArgs(result, true));
}
catch (Exception ex) {
// Raise the event with failure
OnDataProcessed(new DataProcessedEventArgs(ex.Message, false));
}
}
}

Using the event-based callback:

csharp
static void Main(string[] args) {
EventProcessor processor = new EventProcessor();

// Subscribe to the event
processor.DataProcessed += (sender, e) => {
if (e.Success) {
Console.WriteLine($"Event handler received success: {e.Result}");
} else {
Console.WriteLine($"Event handler received error: {e.Result}");
}
};

// Process data which will trigger the event
processor.ProcessData("event example");
}

Output:

Event handler received success: EVENT EXAMPLE

Callback Context Considerations

When using callbacks, be mindful of the execution context:

csharp
class ContextExample {
private string _instanceData = "Instance data";

public void DemonstrateContext() {
Action callback = () => {
// This callback captures the instance context
Console.WriteLine($"Accessing instance data: {_instanceData}");
};

// Pass the callback to another method
ExecuteCallback(callback);
}

private void ExecuteCallback(Action callback) {
callback();
}
}

Usage:

csharp
static void Main(string[] args) {
var example = new ContextExample();
example.DemonstrateContext();
}

Output:

Accessing instance data: Instance data

Real-World Example: File I/O with Callbacks

Here's a practical example using callbacks for asynchronous file operations:

csharp
class FileProcessor {
public void ReadFileAsync(string filePath, Action<string, bool> onCompleted) {
Task.Run(() => {
try {
// Read file content asynchronously
string content = File.ReadAllText(filePath);

// Call the callback with success
onCompleted(content, true);
}
catch (Exception ex) {
// Call the callback with failure
onCompleted(ex.Message, false);
}
});
}

public void SaveFileAsync(string filePath, string content, Action<bool, string> onCompleted) {
Task.Run(() => {
try {
// Write content to file asynchronously
File.WriteAllText(filePath, content);

// Call the callback with success
onCompleted(true, filePath);
}
catch (Exception ex) {
// Call the callback with failure
onCompleted(false, ex.Message);
}
});
}
}

Using the file processor:

csharp
static void Main(string[] args) {
FileProcessor processor = new FileProcessor();

Console.WriteLine("Starting file operations...");

// Save a file asynchronously
processor.SaveFileAsync("example.txt", "This is a test file.", (success, result) => {
if (success) {
Console.WriteLine($"File saved successfully at: {result}");

// Read the file back asynchronously
processor.ReadFileAsync(result, (content, readSuccess) => {
if (readSuccess) {
Console.WriteLine($"File content: {content}");
} else {
Console.WriteLine($"Failed to read file: {content}");
}
});
} else {
Console.WriteLine($"Failed to save file: {result}");
}
});

Console.WriteLine("File operations initiated. Waiting for callbacks...");
Console.ReadLine();
}

Output:

Starting file operations...
File operations initiated. Waiting for callbacks...
File saved successfully at: example.txt
File content: This is a test file.

Summary

In this tutorial, you've learned about various callback patterns in C#:

  • Basic callbacks using custom delegates
  • Callbacks with multiple parameters
  • Asynchronous callbacks using Tasks
  • Using standard delegate types (Action, Func, Predicate)
  • Event-based callbacks
  • Context considerations
  • Real-world application with file operations

Callbacks are a powerful mechanism in C# that enable flexible and loosely coupled code. They're essential for event-driven programming and asynchronous operations. As you continue your C# journey, you'll find callbacks being used extensively throughout the .NET ecosystem.

Exercises

  1. Create a simple calculator that uses callbacks to perform operations (add, subtract, multiply, divide) and return results.
  2. Implement a method that processes a collection of items and uses a callback to report progress.
  3. Design a file search utility that uses callbacks to report each file found that matches certain criteria.
  4. Create a timer class that uses callbacks to notify when specific time intervals have passed.
  5. Implement a simple HTTP client class that uses callbacks to handle responses from web requests.

Additional Resources

Happy coding!



If you spot any mistakes on this website, please let me know at feedback@compilenrun.com. I’d greatly appreciate your feedback! :)