Comprehensive Guide to C# Delegates, Generics, Events, Asynchronous Programming and Custom Control Development
Anonymous Methods
Anonymous methods are unnamed code blocks associated with delegate types, consisting only of the delegate keyword, parameter list, and execution body. They enable passing inline logic as a delegate parameter instead of defining a separate named method.
Syntax rules for anonymous methods: Start with the delegate keyword followed by a parameter list, wrap execution logic in curly braces, and terminate with a semicolon. This syntax combines delegate variable declaration and method definition into a single step, eliminating the need for a separate named method implementation. If a method accepts a delegate-type parameter, you can pass an anonymous method directly as the argument.
// Declare delegate type for binary integer operations
public delegate int MathOperationDele(int left, int right);
static void Main(string[] args)
{
// Assign anonymous method to delegate instance
MathOperationDele subtract = delegate (int a, int b)
{
return a - b;
};
Console.WriteLine(subtract(30, 10));
Console.ReadLine();
}
Lambda Expressions
Introduced in C# 3.0, lambda expressions are a more concise alternative to anonymous methods, following the syntax (parameter list) => { execution body } where => is pronounced "goes to".
Parameter types in lambda expressions can be explicitly declared or inferred by the compiler based on context. For readability, explicit type declarations are recommended for most scenarios. If the lambda has exactly one parameter, the surrounding parentheses can be omitted; if the execution body is a single statement, the curly braces and return keyword (for value-returning logic) can be omitted.
Lambda expressions fall into two forms: expression-bodied (input parameters) => expression and statement-bodied (input parameters) => { statement 1; statement 2; ... }.
Examples:
- Single parameter (parentheses optional):
Func<int, bool> isPositive = (num) => { return num > 0; }; - Simplified single parameter:
Func<int, bool> isPositiveSimplified = num => num > 0; - Multiple parameters:
Func<int, int, bool> areEqual = (x, y) => x == y; - No parameters:
Action printTimestamp = () => Console.WriteLine(DateTime.Now);
public delegate int MathOperationDele(int left, int right);
static void Main(string[] args)
{
// Full statement-bodied lambda
MathOperationDele sumLambda = (int a, int b) => { return a + b; };
// Simplified expression-bodied lambda
MathOperationDele sumLambdaSimplified = (a, b) => a + b;
Console.WriteLine(sumLambda(20, 30));
Console.WriteLine(sumLambdaSimplified(20, 30));
Console.ReadLine();
}
Lambda vs Anonymous Methods
Lambda expressions are a syntactic evolution of anonymous methods, with key differences:
- Lambda parameters support type inference, while anonymous methods require explicit parameter type declarations
- Lambdas support both single-expression and multi-statement bodies, while anonymous methods do not support single-expression syntax
Delegate Fundamentals
A delegate is a reference type derived from System.MulticastDelegate, with built-in methods including Invoke(), BeginInvoke(), and EndInvoke().
Delegate declaration syntax resembles a method signature prefixed with the delegate keyword. To instantiate a delegate, you must associate it with a method that has an identical return type and parameter list as the delegate declaration. Delegates can be invoked either by calling Invoke() directly or using the shorthand delegateInstance(parameters).
Core use cases for delegatse:
- Logical decoupling to reduce redundant code
- Asynchronous execution and multithreading
- Multicast delegate implementations for observer pattern workflows
// Delegate declaration for void methods accepting a single integer parameter
public delegate void LogMessageDele(int value);
// Method matching delegate signature
private void PrintInteger(int input)
{
Console.WriteLine($"Input value received: {input}");
}
public void RunDelegateDemo()
{
// Instantiate delegate and associate with method
LogMessageDele logHandler = new LogMessageDele(PrintInteger);
// Invoke delegate using both supported syntax
logHandler.Invoke(42);
logHandler(17);
}
Delegate Decoupling Example
Suppose you need to filter a list of Student objects based on variable conditions (name length, age, residential location, etc.). Without delegates, you would need to write separate iteration logic for each filter condition, leading to redundant code. Delegates let you extract the variable filter logic as a parameter, while reusing the shared iteration code.
public class Student
{
public string FullName { get; set; }
public int Age { get; set; }
public string ResidentialArea { get; set; }
}
public class StudentFilterService
{
// Sample student dataset
public List<Student> StudentDatabase = new List<Student>
{
new Student { FullName = "Zhang San", Age = 36, ResidentialArea = "Kunshan High-Tech Zone" },
new Student { FullName = "Li Siping", Age = 32, ResidentialArea = "Kunshan Development Zone" },
new Student { FullName = "Wang Mazi", Age = 18, ResidentialArea = "Shanghai Qingpu District" },
new Student { FullName = "Zhao Liu", Age = 36, ResidentialArea = "Tianjin Hei Zhima Hutong" }
};
// Delegate type for filter conditions
public delegate bool StudentFilterDele(Student candidate);
// Filter condition implementations
public bool IsNameTwoCharacters(Student student) => student.FullName.Length == 2;
public bool IsOver20YearsOld(Student student) => student.Age >= 20;
public bool ResidesInKunshan(Student student) => student.ResidentialArea.Contains("Kunshan");
public bool MeetsAllCriteria(Student student)
=> IsNameTwoCharacters(student) && IsOver20YearsOld(student) && ResidesInKunshan(student);
// Shared iteration logic accepting filter delegate as parameter
public int CountMatchingStudents(List<Student> source, StudentFilterDele filter)
{
int matchCount = 0;
foreach (var student in source)
{
if (filter.Invoke(student)) matchCount++;
}
return matchCount;
}
}
// Usage
var filterService = new StudentFilterService();
int twoCharNameCount = filterService.CountMatchingStudents(filterService.StudentDatabase, filterService.IsNameTwoCharacters);
int adultCount = filterService.CountMatchingStudents(filterService.StudentDatabase, filterService.IsOver20YearsOld);
int kunshanResidentCount = filterService.CountMatchingStudents(filterService.StudentDatabase, filterService.ResidesInKunshan);
int fullMatchCount = filterService.CountMatchingStudents(filterService.StudentDatabase, filterService.MeetsAllCriteria);
Console.WriteLine($"Two-character names: {twoCharNameCount}");
Console.WriteLine($"Adults (>=20): {adultCount}");
Console.WriteLine($"Kunshan residents: {kunshanResidentCount}");
Console.WriteLine($"Full criteria matches: {fullMatchCount}");
The same filtering can be implemented more concisely with LINQ:
int linqTwoCharName = filterService.StudentDatabase.Where(s => s.FullName.Length == 2).Count();
int linqAdultCount = filterService.StudentDatabase.Where(s => s.Age > 20).Count();
int linqKunshanCount = filterService.StudentDatabase.Where(s => s.ResidentialArea.Contains("Kunshan")).Count();
int linqFullMatch = filterService.StudentDatabase.Where(s => s.FullName.Length == 2 && s.Age > 20 && s.ResidentialArea.Contains("Kunshan")).Count();
Console.WriteLine($"LINQ two-character names: {linqTwoCharName}");
Console.WriteLine($"LINQ adults: {linqAdultCount}");
Console.WriteLine($"LINQ Kunshan residents: {linqKunshanCount}");
Console.WriteLine($"LINQ full matches: {linqFullMatch}");
Key limitations of multicast delegates:
- If any method in the invocation list throws an exception, subsequent methods will not execute
- Only the return value of the last method in the invocation list is returned when calling
Invoke() - Removing a method from the invocation list has O(n) time complexity
- Multicast delegates are not thread-safe by default
Delegates differ from C/C++ function pointers in three core ways:
- Delegates can reference multiple methods simultaneously
- Delegates can reference the same method multiple times
- Delegates store both the method entry point and the associated object instance, while function pointers only store the method address
Multicast Delegates
Multicast delegates support the += operator to add methods to the invocation list, which execute in order when Invoke() is called. The -= operator removes the first matching method from the end of the invocation list, and does not throw an exception if no matching method is found. Multicast delegates do not support asynchronous invocation via BeginInvoke(), and return only the value of the last executed method when invoked synchronously.
// Delegate type for methods returning an integer with no parameters
public delegate int NumberGeneratorDele();
private int GenerateOne() => 1;
private int GenerateTwo() => 2;
private int GenerateThree() => 3;
public void RunMulticastDemo()
{
NumberGeneratorDele generator = new NumberGeneratorDele(GenerateOne);
generator += new NumberGeneratorDele(GenerateTwo);
generator += new NumberGeneratorDele(GenerateThree);
// Iterate over invocation list to get individual return values
foreach (NumberGeneratorDele del in generator.GetInvocationList())
{
Console.WriteLine($"Invocation result: {del.Invoke()}");
}
// Direct invoke returns only last method's value
Console.WriteLine($"Direct invoke result: {generator.Invoke()}");
// Remove method from invocation list
generator -= new NumberGeneratorDele(GenerateThree);
foreach (NumberGeneratorDele del in generator.GetInvocationList())
{
Console.WriteLine($"Post-removal invocation result: {del.Invoke()}");
}
Console.WriteLine($"Post-removal direct invoke result: {generator.Invoke()}");
}
Multicast Delegate Use Case - Cat Meow Event Chain
When a cat meows, it triggers a series of dependent actions (mouse runs, baby cries, etc.). Using multicast delegates decouples the cat's meow logic from the dependent actions, making it easy to add or remove actions without modifying the cat class.
public delegate void CatMeowDele();
public class Cat
{
// Tight coupling implementation (not recommended)
public void MeowTightCoupling()
{
Console.WriteLine($"{GetType().Name} meows");
new Mouse().Flee();
new Baby().Cry();
new Mother().Whisper();
}
// Decoupled implementation using multicast delegate
public CatMeowDele OnMeow = null;
public void MeowDecoupled()
{
Console.WriteLine($"{GetType().Name} meows");
OnMeow?.Invoke();
}
// Event implementation (further restricted access)
public event CatMeowDele OnMeowEvent = null;
public void MeowWithEvent()
{
Console.WriteLine($"{GetType().Name} meows");
OnMeowEvent?.Invoke();
}
}
// Dependent class examples
public class Mouse { public void Flee() => Console.WriteLine("Mouse runs away"); }
public class Baby { public void Cry() => Console.WriteLine("Baby starts crying"); }
public class Mother { public void Whisper() => Console.WriteLine("Mother hushes the baby"); }
// Usage
var cat = new Cat();
Console.WriteLine("~~~~~~~~~ Multicast Delegate Demo ~~~~~~~~~");
cat.OnMeow += new CatMeowDele(new Mouse().Flee);
cat.OnMeow += new CatMeowDele(new Baby().Cry);
cat.OnMeow += new CatMeowDele(new Mother().Whisper);
cat.MeowDecoupled();
Console.WriteLine("~~~~~~~~~ Event Demo ~~~~~~~~~");
cat.OnMeowEvent += new CatMeowDele(new Mouse().Flee);
cat.OnMeowEvent += new CatMeowDele(new Baby().Cry);
cat.OnMeowEvent += new CatMeowDele(new Mother().Whisper);
cat.MeowWithEvent();
Events
An event is a delegate instance prefixed with the event keyword, which restricts access to the delegate instance: only the declaring class can invoke the event or assign it a new value, while external code can only add or remove handlers via += and -=. This prevents accidental overwriting or invocation of the delegate from external contexts.
Events enable separating stable core logic from variable extension logic in application architecture: the core framework implements fixed functionality, and exposes events as extension points for external code to inject custom behavior.
Custom Generic Delegates
Generic delegates let you define a single delegate type that works with multiple parameter and return types, eliminating the need to declare separate delegates for different type signatures. Type parameters are specified when instantiating the delegate.
// Generic delegate for binary operations on matching type parameters
public delegate T BinaryOpDele<T>(T left, T right);
class Program
{
static int SumIntegers(int a, int b) => a + b;
static double SubtractDoubles(double a, double b) => a - b;
static void Main(string[] args)
{
BinaryOpDele<int> intCalculator = SumIntegers;
BinaryOpDele<double> doubleCalculator = SubtractDoubles;
Console.WriteLine(intCalculator(15, 25));
Console.WriteLine(doubleCalculator(7.9, 2.3));
// Use with anonymous method
BinaryOpDele<int> intMultiply = delegate (int x, int y) { return x * y; };
Console.WriteLine(intMultiply(4, 5));
// Use with lambda expression
BinaryOpDele<double> doubleMultiply = (x, y) => x * y;
Console.WriteLine(doubleMultiply(2.5, 4.0));
Console.ReadLine();
}
}
Built-in Generic Delegates
Func<TResult> and Func<T1, T2, ..., TResult>
Func delegates are pre-defined generic delegates for methods that return a value. The last generic type parameter specifies the return type, while preceding parameters define input types.
static double MultiplyIntReturnDouble(int a, int b) => a * b;
static void Main(string[] args)
{
Func<int, int, double> multiplyFunc = MultiplyIntReturnDouble;
double result = multiplyFunc(10, 20);
// Lambda implementation
Func<int, int, double> multiplyLambda = (a, b) => a * b;
Console.WriteLine(multiplyFunc(10, 20));
Console.WriteLine(multiplyLambda(10, 20));
Console.ReadLine();
}
Use case: Calculate sum or product of a range of array elements
static void Main(string[] args)
{
int[] values = { 10, 9, 8, 7, 6, 5, 4, 3, 2 };
Console.WriteLine("------ Without Delegate -------");
Console.WriteLine(GetRangeSum(values, 0, 3));
Console.WriteLine(GetRangeProduct(values, 0, 3));
Console.WriteLine("------ With Func Delegate -------");
Console.WriteLine(CalculateSequence(Sum, values, 0, 3));
Console.WriteLine(CalculateSequence(Product, values, 0, 3));
Console.WriteLine("------ With Func + Lambda -------");
Console.WriteLine(CalculateSequence((a, b) => a + b, values, 0, 3));
Console.WriteLine(CalculateSequence((a, b) => a * b, values, 0, 3));
Console.ReadLine();
}
// Generic calculation method accepting operation delegate
static int CalculateSequence(Func<int, int, int> op, int[] source, int startIndex, int endIndex)
{
int result = source[startIndex];
for (int i = startIndex + 1; i <= endIndex; i++)
{
result = op(result, source[i]);
}
return result;
}
static int Sum(int a, int b) => a + b;
static int Product(int a, int b) => a * b;
// Non-delegate implementation (duplicated logic)
static int GetRangeSum(int[] source, int startIndex, int endIndex)
{
int sum = 0;
for (int i = startIndex; i <= endIndex; i++) sum += source[i];
return sum;
}
static int GetRangeProduct(int[] source, int startIndex, int endIndex)
{
int product = 1;
for (int i = startIndex; i <= endIndex; i++) product *= source[i];
return product;
}
Action<T>
Action delegates are pre-defined generic delegates for methods with no return value (void return type). They are commonly used for cross-thread UI control updates in WinForms applications.
Predicate<T>
Predicate<T> is a pre-defined delegate for methods that return a boolean value, used to represent filter conditions. Its signature is public delegate bool Predicate<T>(T obj).
public class Student
{
public int StudentId { get; set; }
public string FullName { get; set; }
}
static void Main(string[] args)
{
List<Student> studentRoster = new List<Student>
{
new Student { StudentId = 10001, FullName = "Yang" },
new Student { StudentId = 10002, FullName = "Zhu" },
new Student { StudentId = 10003, FullName = "Wang" },
new Student { StudentId = 10004, FullName = "Li" },
new Student { StudentId = 10005, FullName = "Liu" },
new Student { StudentId = 10006, FullName = "Zhang" },
new Student { StudentId = 10007, FullName = "Mei" }
};
// Use Predicate via List.FindAll
List<Student> filteredById = studentRoster.FindAll(s => s.StudentId > 10003);
foreach (var student in filteredById)
{
Console.WriteLine($"{student.FullName}: {student.StudentId}");
}
// Comparison with LINQ query syntax
var linqQueryResult = from s in studentRoster where s.StudentId > 10003 select s;
Console.WriteLine("---------- LINQ Query Syntax ----------");
foreach (var student in linqQueryResult)
{
Console.WriteLine($"{student.FullName}: {student.StudentId}");
}
// Comparison with LINQ method syntax
var linqMethodResult = studentRoster.Where(s => s.StudentId > 10003);
Console.WriteLine("---------- LINQ Method Syntax ----------");
foreach (var student in linqMethodResult)
{
Console.WriteLine($"{student.FullName}: {student.StudentId}");
}
Console.ReadLine();
}
Generics Fundamentals
Generics provide type safety at compile time, eliminate boxing/unboxing operations for value types, and reduce redundant code. Common generic constructs include generic classes, generic methods, and generic delegates.
Generic Classes
Generic classes use type parameters to define reusable classes that work with multiple types. Syntax: public class ClassName<T> { } where T is a placeholder for the actual type specified at instantiation. Multiple type parameters are supported, separated by commas.
Generic Stack Implementation:
public class GenericStack<T>
{
private readonly T[] _stackStorage;
private readonly int _capacity;
private int _topIndex = -1;
public GenericStack(int capacity)
{
_capacity = capacity;
_stackStorage = new T[capacity];
}
public void Push(T item)
{
if (_topIndex >= _capacity - 1)
{
Console.WriteLine("Stack overflow error");
return;
}
_topIndex++;
_stackStorage[_topIndex] = item;
}
public T Pop()
{
if (_topIndex < 0)
{
Console.WriteLine("Stack underflow error");
return default;
}
T item = _stackStorage[_topIndex];
_topIndex--;
return item;
}
}
// Usage
static void Main(string[] args)
{
var intStack = new GenericStack<int>(5);
intStack.Push(1);
intStack.Push(2);
intStack.Push(3);
intStack.Push(4);
intStack.Push(5);
Console.WriteLine(intStack.Pop());
Console.WriteLine(intStack.Pop());
Console.WriteLine(intStack.Pop());
Console.WriteLine(intStack.Pop());
Console.WriteLine(intStack.Pop());
Console.ReadLine();
}
default Keyword in Generics
The default keyword returns the default value for a generic type parameter: null for reference types, and zero/empty for value types.
class GenericSample<T, U>
{
private T _firstField;
private U _secondField;
public GenericSample()
{
// Assign default values for generic types
_firstField = default(T);
_secondField = default(U);
}
}
Constrained Generic Classes
Generic type parameters can be constrained to limit the types allowed when instantiating the generic class. Common constraints include:
where T : struct: T must be a value typewhere T : class: T must be a reference typewhere T : new(): T must have a public parameterless constructor
// Constrained generic class for e-commerce store
public class ShopStore<TIndex, TProduct, TVendor>
where TIndex : struct
where TProduct : class
where TVendor : new()
{
public List<TProduct> ProductCatalog { get; set; }
public TVendor StoreVendor { get; set; }
public ShopStore()
{
ProductCatalog = new List<TProduct>();
StoreVendor = new TVendor();
}
public TProduct PurchaseItem(TIndex index)
{
dynamic idx = index;
return ProductCatalog[idx];
}
}
// Supporting classes
public class Course
{
public string CourseName { get; set; }
public int DurationMonths { get; set; }
}
public class Instructor
{
public string Name { get; set; }
public int CourseCount { get; set; }
}
// Usage
static void Main(string[] args)
{
var courseStore = new ShopStore<int, Course, Instructor>();
courseStore.StoreVendor = new Instructor { Name = "Chang Teacher", CourseCount = 20 };
courseStore.ProductCatalog = new List<Course>
{
new Course { CourseName = ".NET Full Stack VIP Course", DurationMonths = 6 },
new Course { CourseName = ".NET Desktop Development VIP Course", DurationMonths = 3 },
new Course { CourseName = ".NET Web Development VIP Course", DurationMonths = 3 }
};
var purchasedCourse = courseStore.PurchaseItem(0);
string purchaseInfo = $"Purchased course: {purchasedCourse.CourseName}, Duration: {purchasedCourse.DurationMonths} months, Instructor: {courseStore.StoreVendor.Name}";
Console.WriteLine(purchaseInfo);
Console.ReadLine();
}
Generic Methods
Generic methods define type parameters at the method level, enabling type-safe operations across multiple types without declaring a full generic class.
class Program
{
static void Main(string[] args)
{
Console.WriteLine(GenericAdd("Hello ", "World"));
Console.WriteLine($"10 + 20 = {GenericAdd(10, 20)}");
Console.WriteLine($"10.5 + 20.3 = {GenericAdd(10.5, 20.3)}");
// Constrained generic method (only value types allowed)
Console.WriteLine($"5 + 7 = {NumericAdd(5, 7)}");
Console.ReadLine();
}
static T GenericAdd<T>(T a, T b)
{
dynamic val1 = a;
dynamic val2 = b;
return val1 + val2;
}
static T NumericAdd<T>(T a, T b) where T : struct
{
dynamic val1 = a;
dynamic val2 = b;
return val1 + val2;
}
}
Cross-Form Messaging in WinForms
Single Child to Parent Messaging
// Delegate declaration (scoped to namespace)
public delegate void UpdateLabelDele(string message);
// Main Form
public partial class MainForm : Form
{
public MainForm()
{
InitializeComponent();
var childForm = new CounterChildForm();
childForm.UpdateHandler = UpdateCounterDisplay;
childForm.Show();
}
private void UpdateCounterDisplay(string counterValue)
{
lblCounter.Text = counterValue;
}
}
// Child Form
public partial class CounterChildForm : Form
{
public UpdateLabelDele UpdateHandler;
private int _clickCount = 0;
private void btnIncrement_Click(object sender, EventArgs e)
{
_clickCount++;
UpdateHandler?.Invoke(_clickCount.ToString());
}
}
Parent to Multiple Child Broadcast
public delegate void BroadcastCounterDele(string counterValue);
// Main Form
public partial class MainForm : Form
{
private BroadcastCounterDele _broadcastHandler;
private int _counter = 0;
public MainForm()
{
InitializeComponent();
var childA = new CounterFormA();
var childB = new CounterFormB();
var childC = new CounterFormC();
_broadcastHandler += childA.UpdateCounter;
_broadcastHandler += childB.UpdateCounter;
_broadcastHandler += childC.UpdateCounter;
childA.Show();
childB.Show();
childC.Show();
}
private void btnIncrement_Click(object sender, EventArgs e)
{
_counter++;
_broadcastHandler?.Invoke(_counter.ToString());
}
private void btnReset_Click(object sender, EventArgs e)
{
_counter = 0;
_broadcastHandler?.Invoke(_counter.ToString());
}
}
// Child Form implementation (identical for all child forms)
public partial class CounterFormA : Form
{
public void UpdateCounter(string value)
{
lblCounter.Text = value;
}
}
Event-Based Messaging
Events provide safer messaging by preventing external code from overwriting the delegate invocation list.
Parent to Child Broadcast
public delegate void MessageBroadcastDele(string message);
// Main Form (sender)
public partial class MainForm : Form
{
public event MessageBroadcastDele BroadcastMessage;
public MainForm()
{
InitializeComponent();
var clientA = new ChatClientFormA();
var clientB = new ChatClientFormB();
BroadcastMessage += clientA.ReceiveMessage;
BroadcastMessage += clientB.ReceiveMessage;
clientA.Show();
clientB.Show();
}
private void btnSend_Click(object sender, EventArgs e)
{
BroadcastMessage?.Invoke(txtMessageInput.Text.Trim());
}
}
// Child Form (receiver)
public partial class ChatClientFormA : Form
{
public void ReceiveMessage(string message)
{
txtMessageDisplay.Text = message;
}
}
Child to Parent Messaging
public delegate void ChildMessageDele(string message);
// Main Form (receiver)
public partial class MainForm : Form
{
public MainForm()
{
InitializeComponent();
var client1 = new ChatClient1();
var client2 = new ChatClient2();
client1.SendToParent += HandleIncomingMessage;
client2.SendToParent += HandleIncomingMessage;
client1.Show();
client2.Show();
}
private void HandleIncomingMessage(string message)
{
txtMessageLog.AppendText(message + Environment.NewLine);
}
private void btnClearLog_Click(object sender, EventArgs e)
{
txtMessageLog.Clear();
}
}
// Child Form 1 (sender)
public partial class ChatClient1 : Form
{
public event ChildMessageDele SendToParent;
private void btnSendMessage_Click(object sender, EventArgs e)
{
SendToParent?.Invoke($"Client 1: {txtInput.Text.Trim()}");
}
}
// Child Form 2 (sender)
public partial class ChatClient2 : Form
{
public event ChildMessageDele SendToParent;
private void btnSendMessage_Click(object sender, EventArgs e)
{
SendToParent?.Invoke($"Client 2: {txtInput.Text.Trim()}");
}
}
Delegate vs Event Comparison
- Events can only be invoked from within the declaring class, while delegates can be invoked from any context with access
- External code can only use
+=and-=to modify event handlers, while delegates allow direct assignment (e.g.delegateInstance = null) - Events are safer for public API exposure, as they prevent accidental overwriting of the invocation list
Asynchronous Programming
Asynchronous programming in C# is built on top of delegates, using BeginInvoke() and EndInvoke() to execute methods on background threads.
BeginInvoke()accepts method parameters, an optional callback delegate, and a state object, returning anIAsyncResultinstance to track execution statusEndInvoke()accepts theIAsyncResultinstance and returns the result of the asynchronous operation, blocking until execution completes if called before the operation finishes
public delegate int ComputeSquareDele(int input);
public partial class AsyncDemoForm : Form
{
public AsyncDemoForm()
{
InitializeComponent();
}
// Synchronous execution (blocks UI)
private void btnSyncRun_Click(object sender, EventArgs e)
{
lblResult1.Text = LongRunningSquare(30).ToString();
lblResult2.Text = QuickSquare(40).ToString();
}
// Asynchronous execution
private void btnAsyncRun_Click(object sender, EventArgs e)
{
ComputeSquareDele squareHandler = new ComputeSquareDele(LongRunningSquare);
IAsyncResult asyncResult = squareHandler.BeginInvoke(50, null, null);
lblResult1.Text = "Calculating...";
// Execute other work while waiting for async operation
lblResult2.Text = QuickSquare(60).ToString();
// Get async result
int squareResult = squareHandler.EndInvoke(asyncResult);
lblResult1.Text = squareResult.ToString();
}
private int LongRunningSquare(int input)
{
Thread.Sleep(5000);
return input * input;
}
private int QuickSquare(int input)
{
return input * input;
}
}
Multiple Concurrent Async Tasks with Callback
public delegate int ComputeSquareWithDelayDele(int input, int delayMs);
public partial class MultiAsyncDemoForm : Form
{
private ComputeSquareWithDelayDele _squareHandler;
public MultiAsyncDemoForm()
{
InitializeComponent();
_squareHandler = new ComputeSquareWithDelayDele(CalculateSquareWithDelay);
}
private int CalculateSquareWithDelay(int input, int delayMs)
{
Thread.Sleep(delayMs);
return input * input;
}
private void btnStartTasks_Click(object sender, EventArgs e)
{
for (int i = 1; i <= 10; i++)
{
_squareHandler.BeginInvoke(i * 10, i * 1000, TaskCompletionCallback, i);
}
}
private void TaskCompletionCallback(IAsyncResult result)
{
int output = _squareHandler.EndInvoke(result);
int taskId = (int)result.AsyncState;
Console.WriteLine($"Task {taskId} completed, result: {output}");
}
}
Async Wait Handling
Action<string> longRunningOp = LongRunningOperation;
IAsyncResult asyncResult = null;
AsyncCallback completionCallback = ia =>
{
Console.WriteLine(object.ReferenceEquals(asyncResult, ia)); // True
Console.WriteLine(ia.AsyncState); // "custom-state"
Console.WriteLine($"Operation completed on thread {Thread.CurrentThread.ManagedThreadId:00}");
};
asyncResult = longRunningOp.BeginInvoke("test-input", completionCallback, "custom-state");
// Wait for operation completion
asyncResult.AsyncWaitHandle.WaitOne(); // Wait indefinitely
asyncResult.AsyncWaitHandle.WaitOne(1000); // Wait max 1000ms
private void LongRunningOperation(string input)
{
Console.WriteLine($"Starting operation {input} on thread {Thread.CurrentThread.ManagedThreadId:00}");
long sum = 0;
for (int i = 0; i < 1000000000; i++) sum += i;
Console.WriteLine($"Operation {input} completed, sum: {sum}");
}
Async with Func Delegate
Func<int> getCurrentMonth = () =>
{
Thread.Sleep(2000);
return DateTime.Now.Month;
};
// Synchronous call
Console.WriteLine($"Sync result: {getCurrentMonth.Invoke()}");
// Asynchronous call with callback
IAsyncResult asyncResult = getCurrentMonth.BeginInvoke(ia =>
{
int result = getCurrentMonth.EndInvoke(ia);
Console.WriteLine($"Async state: {ia.AsyncState}");
Console.WriteLine($"Async result: {result}");
}, "custom-state");
Always call EndInvoke() to release resources associated with asynchronous operations; if not called explicitly, the CLR will release resources during garbage collection.
Asynchronous Programming Summary
Asynchronous operations execute on background thread pool threads, making them suitable for short-running, independent background tasks that do not access UI controls directly. For tasks requiring synchronization or shared resource access, explicit multithreading with synchronization primitives is recommended.
Multithreading with Thread Class
A process is an instance of a running application, allocated system resources by the OS. A thread is the smallest unit of execution scheduled by the OS, with multiple threads able to run in parallel on multi-core CPUs. The Thread class in .NET represents a managed thread, associated with a method via the ThreadStart or ParameterizedThreadStart delegate.
private void btnStartSumThread_Click(object sender, EventArgs e)
{
int total = 0;
Thread sumThread = new Thread(delegate ()
{
for (int i = 1; i <= 10; i++)
{
total += i;
Console.WriteLine($"Sum thread: {total}");
Thread.Sleep(500);
}
});
sumThread.IsBackground = true;
sumThread.Start();
}
private void btnStartPrintThread_Click(object sender, EventArgs e)
{
Thread printThread = new Thread(() =>
{
for (int i = 1; i < 100; i++)
{
Console.WriteLine($"Print thread: {i}");
Thread.Sleep(20);
}
});
printThread.IsBackground = true;
printThread.Start();
}
Cross-Thread UI Access
UI controls can only be modified from the thread that created them. Use Invoke() to marshal updates back to the UI thread from background threads.
private void btnStartThread1_Click(object sender, EventArgs e)
{
Thread calcThread = new Thread(() =>
{
for (int i = 1; i <= 10; i++)
{
if (lblOutput1.InvokeRequired)
{
lblOutput1.Invoke(new Action<string>(val => lblOutput1.Text = val), (i * i).ToString());
}
Thread.Sleep(100);
}
});
calcThread.IsBackground = true;
calcThread.Start();
}
private void btnStartThread2_Click(object sender, EventArgs e)
{
Thread calcThread = new Thread(() =>
{
for (int i = 11; i <= 20; i++)
{
if (lblOutput2.InvokeRequired)
{
lblOutput2.Invoke(new Action<string>(val => lblOutput2.Text = val), (i * i).ToString());
}
Thread.Sleep(200);
}
});
calcThread.IsBackground = true;
calcThread.Start();
}
Custom WinForms Control Development
To create a reusable validatable textbox control:
- Create a new Class Library project, add a Component class, and modify it to inherit from
TextBox - Add a
ErrorProvidercomponent to the control - Implement validation methods for required fields and regex matching
public class ValidatableTextBox : TextBox
{
private ErrorProvider _validationError = new ErrorProvider();
/// <summary>
/// Validate that the textbox is not empty
/// </summary>
/// <returns>1 if valid, 0 if empty</returns>
public int ValidateRequired()
{
if (string.IsNullOrWhiteSpace(this.Text))
{
_validationError.SetError(this, "This field is required");
return 0;
}
_validationError.SetError(this, string.Empty);
return 1;
}
/// <summary>
/// Validate input against a regular expression
/// </summary>
/// <param name="regexPattern">Regex pattern to match</param>
/// <param name="errorMessage">Error message to display on failure</param>
/// <returns>1 if valid, 0 if invalid</returns>
public int ValidateRegex(string regexPattern, string errorMessage)
{
if (ValidateRequired() == 0) return 0;
Regex regex = new Regex(regexPattern, RegexOptions.IgnoreCase);
if (regex.IsMatch(this.Text.Trim()))
{
_validationError.SetError(this, string.Empty);
return 1;
}
_validationError.SetError(this, errorMessage);
return 0;
}
}
Usage in a Form:
private void btnSave_Click(object sender, EventArgs e)
{
int nameValid = txtValidName.ValidateRequired();
int ageValid = txtValidAge.ValidateRegex(@"^[1-9]\d*$", "Age must be a positive integer");
if (nameValid * ageValid == 1)
{
MessageBox.Show("Save successful");
}
}