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#:
// 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:
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:
// 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:
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:
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:
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 valuesFunc<T, TResult>
- For callbacks that return valuesPredicate<T>
- For callbacks that test a condition and return a boolean
Here's an example using these standard delegate types:
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:
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:
// 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:
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:
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:
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:
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:
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
- Create a simple calculator that uses callbacks to perform operations (add, subtract, multiply, divide) and return results.
- Implement a method that processes a collection of items and uses a callback to report progress.
- Design a file search utility that uses callbacks to report each file found that matches certain criteria.
- Create a timer class that uses callbacks to notify when specific time intervals have passed.
- Implement a simple HTTP client class that uses callbacks to handle responses from web requests.
Additional Resources
- Microsoft Docs: Delegates
- Microsoft Docs: Events
- Microsoft Docs: Asynchronous Programming
- C# in Depth: Events and Delegates
Happy coding!
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)