Understanding C# Delegate
Delegates in C# are a powerful feature, yet many developers are not familiar with their effective use. Tim Corey’s video on "Delegates in C# - A practical demonstration, including Action and Func," provides a thorough explanation of what delegates are, how to use them, and why they are useful.
This article will provide you with Tim's expert insights on delegates in C#, offering a clear explanation of their usage and practical applications. You'll learn how delegates can enhance the flexibility and efficiency of your code, with examples like their use in a shopping cart system.
Introduction
Tim introduces the concept of delegates, highlighting their power and versatility in C#. He assures viewers that despite some intimidating terminology, the foundation of delegates is simple. Tim aims to demystify delegates and cover special types like func and action.
Demo Application Walk-through
Tim sets up a demo application to illustrate the use of delegates. The solution contains three projects: a Console UI, a Demo Library, and a WinForm UI. The focus initially is on the Console UI and Demo Library.
using System;
using System.Collections.Generic;
using System.Linq;
namespace ConsoleUI
{
class Program
{
static ShoppingCartModel cart = new ShoppingCartModel();
static void Main(string[] args)
{
PopulateCartWithDemoData();
Console.WriteLine($"The total for the cart is {cart.GenerateTotal():C2}");
Console.ReadLine();
}
private static void PopulateCartWithDemoData()
{
cart.Items.Add(new ProductModel { ItemName = "Cereal", Price = 3.63M });
cart.Items.Add(new ProductModel { ItemName = "Milk", Price = 2.95M });
cart.Items.Add(new ProductModel { ItemName = "Strawberries", Price = 7.51M });
cart.Items.Add(new ProductModel { ItemName = "Blueberries", Price = 6.75M });
}
}
}
public class ShoppingCartModel
{
public List<ProductModel> Items { get; set; } = new List<ProductModel>();
public decimal GenerateTotal()
{
decimal subtotal = Items.Sum(x => x.Price);
if (subtotal > 100)
{
return subtotal * 0.80M;
}
else if (subtotal > 50)
{
return subtotal * 0.85M;
}
else if (subtotal > 10)
{
return subtotal * 0.90M;
}
else
{
return subtotal;
}
}
}
public class ProductModel
{
public string ItemName { get; set; }
public decimal Price { get; set; }
}
using System;
using System.Collections.Generic;
using System.Linq;
namespace ConsoleUI
{
class Program
{
static ShoppingCartModel cart = new ShoppingCartModel();
static void Main(string[] args)
{
PopulateCartWithDemoData();
Console.WriteLine($"The total for the cart is {cart.GenerateTotal():C2}");
Console.ReadLine();
}
private static void PopulateCartWithDemoData()
{
cart.Items.Add(new ProductModel { ItemName = "Cereal", Price = 3.63M });
cart.Items.Add(new ProductModel { ItemName = "Milk", Price = 2.95M });
cart.Items.Add(new ProductModel { ItemName = "Strawberries", Price = 7.51M });
cart.Items.Add(new ProductModel { ItemName = "Blueberries", Price = 6.75M });
}
}
}
public class ShoppingCartModel
{
public List<ProductModel> Items { get; set; } = new List<ProductModel>();
public decimal GenerateTotal()
{
decimal subtotal = Items.Sum(x => x.Price);
if (subtotal > 100)
{
return subtotal * 0.80M;
}
else if (subtotal > 50)
{
return subtotal * 0.85M;
}
else if (subtotal > 10)
{
return subtotal * 0.90M;
}
else
{
return subtotal;
}
}
}
public class ProductModel
{
public string ItemName { get; set; }
public decimal Price { get; set; }
}
Tim explains the structure and functionality of the demo application:
Shopping Cart Model: Represents the shopping cart with a list of items (ProductModel) and calculates the total cost with discounts based on the subtotal.
Product Model: Represents individual items with name and price properties.
- Console Application: Populates the cart with demo data, calculates the total, and displays it.
Understanding Discounts
Tim walks through the discount logic in the GenerateTotal method, explaining how the subtotal determines the discount applied:
20% off for subtotals over $100.
15% off for subtotals over $50.
10% off for subtotals over $10.
- No discount for subtotals $10 or less.
Tim uses a breakpoint to demonstrate the calculation and discount logic, ensuring viewers understand the foundation before introducing delegates.
Explaining and Creating a Delegate
In this section, Tim Corey dives into the concept of delegates in C#, explaining how they work and demonstrating their use with practical code examples.
What is a Delegate?
Tim explains that a delegate is essentially a way to pass methods as parameters. Instead of passing a variable or a property, you pass a method, which allows for more flexible and reusable code.
Creating and Using a Delegate
Here's how Tim breaks down the process of creating and using a delegate:
Define the Delegate:
- The delegate is defined at the top of the class, specifying the return type and parameter types.
public delegate void MentionDiscount(decimal subtotal);
public delegate void MentionDiscount(decimal subtotal);
- This delegate specifies a method that returns void and takes a decimal as a parameter.
Using the Delegate in a Method:
- The delegate is used as a parameter in the GenerateTotal method of the ShoppingCartModel class.
public decimal GenerateTotal(MentionDiscount mentionDiscount) { decimal subtotal = Items.Sum(x => x.Price); mentionDiscount(subtotal); if (subtotal > 100) { return subtotal * 0.80M; } else if (subtotal > 50) { return subtotal * 0.85M; } else if (subtotal > 10) { return subtotal * 0.90M; } else { return subtotal; } }
public decimal GenerateTotal(MentionDiscount mentionDiscount) { decimal subtotal = Items.Sum(x => x.Price); mentionDiscount(subtotal); if (subtotal > 100) { return subtotal * 0.80M; } else if (subtotal > 50) { return subtotal * 0.85M; } else if (subtotal > 10) { return subtotal * 0.90M; } else { return subtotal; } }
Creating a Method to Pass to the Delegate:
- A method matching the delegate's signature is created in the Program class.
private static void SubtotalAlert(decimal subtotal) { Console.WriteLine($"The subtotal is {subtotal:C2}"); }
private static void SubtotalAlert(decimal subtotal) { Console.WriteLine($"The subtotal is {subtotal:C2}"); }
Calling the GenerateTotal Method:
- The method is passed to the GenerateTotal method via the delegate.
class Program { static ShoppingCartModel cart = new ShoppingCartModel(); static void Main(string[] args) { PopulateCartWithDemoData(); Console.WriteLine($"The total for the cart is {cart.GenerateTotal(SubtotalAlert):C2}"); Console.ReadLine(); } private static void PopulateCartWithDemoData() { cart.Items.Add(new ProductModel { ItemName = "Cereal", Price = 3.63M }); cart.Items.Add(new ProductModel { ItemName = "Milk", Price = 2.95M }); cart.Items.Add(new ProductModel { ItemName = "Strawberries", Price = 7.51M }); cart.Items.Add(new ProductModel { ItemName = "Blueberries", Price = 6.75M }); } }
class Program { static ShoppingCartModel cart = new ShoppingCartModel(); static void Main(string[] args) { PopulateCartWithDemoData(); Console.WriteLine($"The total for the cart is {cart.GenerateTotal(SubtotalAlert):C2}"); Console.ReadLine(); } private static void PopulateCartWithDemoData() { cart.Items.Add(new ProductModel { ItemName = "Cereal", Price = 3.63M }); cart.Items.Add(new ProductModel { ItemName = "Milk", Price = 2.95M }); cart.Items.Add(new ProductModel { ItemName = "Strawberries", Price = 7.51M }); cart.Items.Add(new ProductModel { ItemName = "Blueberries", Price = 6.75M }); } }
Running the Application
Tim runs the application to demonstrate how the delegate works. The console output shows the subtotal and the total for the cart, indicating that the SubtotalAlert method was successfully passed and executed via the delegate.
Func and Action: Problems You Can Solve with Delegates
Tim Corey then explores the use of func and action delegates in C#. These are special types of delegates provided by Microsoft to simplify delegate usage with generics.
Identifying the Problem
Tim highlights a common problem: hard-coded discount logic in the GenerateTotal method. This approach is inflexible and requires code changes, recompilation, and redeployment whenever discount rules change.
public decimal GenerateTotal()
{
decimal subtotal = Items.Sum(x => x.Price);
if (subtotal > 100)
{
return subtotal * 0.80M;
}
else if (subtotal > 50)
{
return subtotal * 0.85M;
}
else if (subtotal > 10)
{
return subtotal * 0.90M;
}
else
{
return subtotal;
}
}
public decimal GenerateTotal()
{
decimal subtotal = Items.Sum(x => x.Price);
if (subtotal > 100)
{
return subtotal * 0.80M;
}
else if (subtotal > 50)
{
return subtotal * 0.85M;
}
else if (subtotal > 10)
{
return subtotal * 0.90M;
}
else
{
return subtotal;
}
}
Introducing Func Delegate
Tim introduces the func delegate to address the hard-coded discount problem. The func delegate is a generic delegate that represents a method with a return type and up to 16 input parameters.
Defining the Func Delegate:
- The func delegate is used in the GenerateTotal method to handle discount calculations dynamically.
public decimal GenerateTotal(Func<List<ProductModel>, decimal, decimal> calculateDiscountedTotal) { decimal subtotal = Items.Sum(x => x.Price); MentionDiscount(subtotal); return calculateDiscountedTotal(Items, subtotal); }
public decimal GenerateTotal(Func<List<ProductModel>, decimal, decimal> calculateDiscountedTotal) { decimal subtotal = Items.Sum(x => x.Price); MentionDiscount(subtotal); return calculateDiscountedTotal(Items, subtotal); }
Creating the Discount Calculation Method:
- A method matching the func delegate's signature is created in the Program class.
private static decimal CalculateLevelDiscount(List<ProductModel> items, decimal subtotal) { if (subtotal > 100) { return subtotal * 0.80M; } else if (subtotal > 50) { return subtotal * 0.85M; } else if (subtotal > 10) { return subtotal * 0.90M; } else { return subtotal; } }
private static decimal CalculateLevelDiscount(List<ProductModel> items, decimal subtotal) { if (subtotal > 100) { return subtotal * 0.80M; } else if (subtotal > 50) { return subtotal * 0.85M; } else if (subtotal > 10) { return subtotal * 0.90M; } else { return subtotal; } }
Passing the Method to the Func Delegate:
- The CalculateLevelDiscount method is passed to the GenerateTotal method via the func delegate.
class Program { static ShoppingCartModel cart = new ShoppingCartModel(); static void Main(string[] args) { PopulateCartWithDemoData(); Console.WriteLine($"The total for the cart is {cart.GenerateTotal(CalculateLevelDiscount):C2}"); Console.ReadLine(); } private static void PopulateCartWithDemoData() { cart.Items.Add(new ProductModel { ItemName = "Cereal", Price = 3.63M }); cart.Items.Add(new ProductModel { ItemName = "Milk", Price = 2.95M }); cart.Items.Add(new ProductModel { ItemName = "Strawberries", Price = 7.51M }); cart.Items.Add(new ProductModel { ItemName = "Blueberries", Price = 6.75M }); } private static decimal CalculateLevelDiscount(List<ProductModel> items, decimal subtotal) { if (subtotal > 100) { return subtotal * 0.80M; } else if (subtotal > 50) { return subtotal * 0.85M; } else if (subtotal > 10) { return subtotal * 0.90M; } else { return subtotal; } } }
class Program { static ShoppingCartModel cart = new ShoppingCartModel(); static void Main(string[] args) { PopulateCartWithDemoData(); Console.WriteLine($"The total for the cart is {cart.GenerateTotal(CalculateLevelDiscount):C2}"); Console.ReadLine(); } private static void PopulateCartWithDemoData() { cart.Items.Add(new ProductModel { ItemName = "Cereal", Price = 3.63M }); cart.Items.Add(new ProductModel { ItemName = "Milk", Price = 2.95M }); cart.Items.Add(new ProductModel { ItemName = "Strawberries", Price = 7.51M }); cart.Items.Add(new ProductModel { ItemName = "Blueberries", Price = 6.75M }); } private static decimal CalculateLevelDiscount(List<ProductModel> items, decimal subtotal) { if (subtotal > 100) { return subtotal * 0.80M; } else if (subtotal > 50) { return subtotal * 0.85M; } else if (subtotal > 10) { return subtotal * 0.90M; } else { return subtotal; } } }
Running the Application
Tim demonstrates the modified application, showing that it functions correctly and dynamically calculates the discount based on the provided logic.
Differences Between Delegate and Func
Tim compares the custom delegate and func delegate:
Delegate: Requires explicit definition of the signature, providing clear documentation and structure.
- Func: More concise but requires specifying input and output types each time, which can be less clear.
Both approaches offer flexibility, but the choice depends on the specific use case and complexity of the application.
Why Use Delegates if All the Work is Done Elsewhere?
Tim Corey addresses a common question about the use of delegates: Why have a delegate if all the work seems to be done elsewhere?
Tim explains that the purpose of delegates is to provide flexibility and extensibility in the code. The GenerateTotal method in the ShoppingCartModel class might do more than just calculate discounts. It might also handle tasks like checking stock availability, validating cart contents, or other business logic. Delegates allow you to pass in specific methods for unique tasks or custom behavior without changing the core method. This makes the code more modular and easier to maintain.
Delegates are especially useful in scenarios where you want to:
Apply different business rules or logic dynamically.
Keep the core method generic and reusable.
- Implement custom behavior for specific cases without modifying the core method.
Action Delegate: Creating and Explaining
Tim introduces the Action delegate, another special type of delegate in C#. The Action delegate is similar to Func, but it doesn't return a value (i.e., it returns void).
Creating the Action Delegate:
- Define the Action delegate in the GenerateTotal method to handle alerts or messages.
public decimal GenerateTotal(Func<List<ProductModel>, decimal, decimal> calculateDiscountedTotal, Action<string> tellUserWeAreDiscounting) { decimal subtotal = Items.Sum(x => x.Price); MentionSubtotal(subtotal); tellUserWeAreDiscounting("We are applying your discount."); return calculateDiscountedTotal(Items, subtotal); }
public decimal GenerateTotal(Func<List<ProductModel>, decimal, decimal> calculateDiscountedTotal, Action<string> tellUserWeAreDiscounting) { decimal subtotal = Items.Sum(x => x.Price); MentionSubtotal(subtotal); tellUserWeAreDiscounting("We are applying your discount."); return calculateDiscountedTotal(Items, subtotal); }
Creating the Alert Method:
- Define a method to match the Action delegate's signature.
private static void AlertUser(string message) { Console.WriteLine(message); }
private static void AlertUser(string message) { Console.WriteLine(message); }
Passing the Method to the Action Delegate:
- Pass the AlertUser method to the GenerateTotal method via the Action delegate.
class Program { static ShoppingCartModel cart = new ShoppingCartModel(); static void Main(string[] args) { PopulateCartWithDemoData(); Console.WriteLine($"The total for the cart is {cart.GenerateTotal(CalculateLevelDiscount, AlertUser):C2}"); Console.ReadLine(); } private static void PopulateCartWithDemoData() { cart.Items.Add(new ProductModel { ItemName = "Cereal", Price = 3.63M }); cart.Items.Add(new ProductModel { ItemName = "Milk", Price = 2.95M }); cart.Items.Add(new ProductModel { ItemName = "Strawberries", Price = 7.51M }); cart.Items.Add(new ProductModel { ItemName = "Blueberries", Price = 6.75M }); } private static decimal CalculateLevelDiscount(List<ProductModel> items, decimal subtotal) { if (subtotal > 100) { return subtotal * 0.80M; } else if (subtotal > 50) { return subtotal * 0.85M; } else if (subtotal > 10) { return subtotal * 0.90M; } else { return subtotal; } } private static void AlertUser(string message) { Console.WriteLine(message); } }
class Program { static ShoppingCartModel cart = new ShoppingCartModel(); static void Main(string[] args) { PopulateCartWithDemoData(); Console.WriteLine($"The total for the cart is {cart.GenerateTotal(CalculateLevelDiscount, AlertUser):C2}"); Console.ReadLine(); } private static void PopulateCartWithDemoData() { cart.Items.Add(new ProductModel { ItemName = "Cereal", Price = 3.63M }); cart.Items.Add(new ProductModel { ItemName = "Milk", Price = 2.95M }); cart.Items.Add(new ProductModel { ItemName = "Strawberries", Price = 7.51M }); cart.Items.Add(new ProductModel { ItemName = "Blueberries", Price = 6.75M }); } private static decimal CalculateLevelDiscount(List<ProductModel> items, decimal subtotal) { if (subtotal > 100) { return subtotal * 0.80M; } else if (subtotal > 50) { return subtotal * 0.85M; } else if (subtotal > 10) { return subtotal * 0.90M; } else { return subtotal; } } private static void AlertUser(string message) { Console.WriteLine(message); } }
Creating Anonymous Methods: Anonymous Delegate
Tim shows how to use anonymous methods, allowing you to define methods on the fly without naming them.
Defining Anonymous Methods:
- Instead of creating named methods, you can define the methods directly where they are needed.
class Program { static ShoppingCartModel cart = new ShoppingCartModel(); static void Main(string[] args) { PopulateCartWithDemoData(); Console.WriteLine($"The total for the cart is {cart.GenerateTotal((items, subtotal) => { if (subtotal > 100) return subtotal * 0.80M; else if (subtotal > 50) return subtotal * 0.85M; else if (subtotal > 10) return subtotal * 0.90M; else return subtotal; }, (message) => Console.WriteLine(message)):C2}"); Console.ReadLine(); } private static void PopulateCartWithDemoData() { cart.Items.Add(new ProductModel { ItemName = "Cereal", Price = 3.63M }); cart.Items.Add(new ProductModel { ItemName = "Milk", Price = 2.95M }); cart.Items.Add(new ProductModel { ItemName = "Strawberries", Price = 7.51M }); cart.Items.Add(new ProductModel { ItemName = "Blueberries", Price = 6.75M }); } }
class Program { static ShoppingCartModel cart = new ShoppingCartModel(); static void Main(string[] args) { PopulateCartWithDemoData(); Console.WriteLine($"The total for the cart is {cart.GenerateTotal((items, subtotal) => { if (subtotal > 100) return subtotal * 0.80M; else if (subtotal > 50) return subtotal * 0.85M; else if (subtotal > 10) return subtotal * 0.90M; else return subtotal; }, (message) => Console.WriteLine(message)):C2}"); Console.ReadLine(); } private static void PopulateCartWithDemoData() { cart.Items.Add(new ProductModel { ItemName = "Cereal", Price = 3.63M }); cart.Items.Add(new ProductModel { ItemName = "Milk", Price = 2.95M }); cart.Items.Add(new ProductModel { ItemName = "Strawberries", Price = 7.51M }); cart.Items.Add(new ProductModel { ItemName = "Blueberries", Price = 6.75M }); } }
Understanding the Syntax:
The anonymous method syntax uses the => operator (lambda expression) to define the method body directly inline.
- No need to specify the return type or method name.
By using delegates, including Func, Action, and anonymous methods, developers can create more dynamic and modular code, allowing for flexible and reusable components.
Using Delegates in Other Projects: WinForms
In this segment, Tim Corey demonstrates the power of delegates by extending their use to a WinForms application. This highlights how delegates can facilitate different behaviors in various user interface (UI) contexts.
Setting Up the WinForms Application
WinForm UI with Two Buttons:
The form has two buttons: one for demonstrating message boxes and one for demonstrating text boxes.
- The ShoppingCartModel and a method to populate it with demo data are also included.
public partial class Dashboard : Form
{
ShoppingCartModel cart = new ShoppingCartModel();
public Dashboard()
{
InitializeComponent();
PopulateCartWithDemoData();
}
private void PopulateCartWithDemoData()
{
cart.Items.Add(new ProductModel { ItemName = "Cereal", Price = 3.63M });
cart.Items.Add(new ProductModel { ItemName = "Milk", Price = 2.95M });
cart.Items.Add(new ProductModel { ItemName = "Strawberries", Price = 7.51M });
cart.Items.Add(new ProductModel { ItemName = "Blueberries", Price = 6.75M });
}
private void messageBoxDemoButton_Click(object sender, EventArgs e)
{
decimal total = cart.GenerateTotal(SubtotalAlert, CalculateLevelDiscount, PrintOutDiscountAlert);
MessageBox.Show($"The total is {total:C2}");
}
private void textBoxDemoButton_Click(object sender, EventArgs e)
{
// Code for TextBox demo will go here
}
}
public partial class Dashboard : Form
{
ShoppingCartModel cart = new ShoppingCartModel();
public Dashboard()
{
InitializeComponent();
PopulateCartWithDemoData();
}
private void PopulateCartWithDemoData()
{
cart.Items.Add(new ProductModel { ItemName = "Cereal", Price = 3.63M });
cart.Items.Add(new ProductModel { ItemName = "Milk", Price = 2.95M });
cart.Items.Add(new ProductModel { ItemName = "Strawberries", Price = 7.51M });
cart.Items.Add(new ProductModel { ItemName = "Blueberries", Price = 6.75M });
}
private void messageBoxDemoButton_Click(object sender, EventArgs e)
{
decimal total = cart.GenerateTotal(SubtotalAlert, CalculateLevelDiscount, PrintOutDiscountAlert);
MessageBox.Show($"The total is {total:C2}");
}
private void textBoxDemoButton_Click(object sender, EventArgs e)
{
// Code for TextBox demo will go here
}
}
Creating Methods for Delegates
PrintOutDiscountAlert:
- This method will be used to show an alert with the discount information.
private void PrintOutDiscountAlert(string message) { MessageBox.Show(message); }
private void PrintOutDiscountAlert(string message) { MessageBox.Show(message); }
SubtotalAlert:
- This method will display the subtotal in a message box.
private void SubtotalAlert(decimal subtotal) { MessageBox.Show($"The subtotal is {subtotal:C2}"); }
private void SubtotalAlert(decimal subtotal) { MessageBox.Show($"The subtotal is {subtotal:C2}"); }
CalculateLevelDiscount:
- This method will calculate the discount based on the number of items in the cart.
private decimal CalculateLevelDiscount(List<ProductModel> items, decimal subtotal) { if (items.Count > 3) { return subtotal - 3M; } return subtotal - items.Count; }
private decimal CalculateLevelDiscount(List<ProductModel> items, decimal subtotal) { if (items.Count > 3) { return subtotal - 3M; } return subtotal - items.Count; }
Integrating the Delegates with WinForms
Using Delegates in Button Click Event:
- The messageBoxDemoButton_Click method demonstrates how to pass the delegates to the GenerateTotal method and handle the results using message boxes.
private void messageBoxDemoButton_Click(object sender, EventArgs e) { decimal total = cart.GenerateTotal(SubtotalAlert, CalculateLevelDiscount, PrintOutDiscountAlert); MessageBox.Show($"The total is {total:C2}"); }
private void messageBoxDemoButton_Click(object sender, EventArgs e) { decimal total = cart.GenerateTotal(SubtotalAlert, CalculateLevelDiscount, PrintOutDiscountAlert); MessageBox.Show($"The total is {total:C2}"); }
Running the Application:
When the button is clicked, the WinForms application shows the subtotal and total using message boxes, demonstrating the flexibility of delegates.
Conclusion
Tim Corey explains delegates in C# clearly, covering their basics, advanced usage, and practical examples like using delegates in a shopping cart. He shows how delegates enable flexible and reusable code, including Func, Action, and anonymous methods. Watch the full video to learn how to apply delegates effectively in your projects!