C# Events and Delegates - when should I take what?

Markus Freitag 3,786 Reputation points
2023-01-23T18:28:06.5433333+00:00

Hello,

I'm looking for a correct way to handle events and delegates. Do you have a good overview, a good example?

When is an event enough, when do I have to switch to delegates?

If I trigger an event and want to pass a complete object, then I need a delegate. Can I say it like that?

I can also trigger an event with Action. Action is also a delegate or void function.

Thanks for the support, sample.

C#
C#
An object-oriented and type-safe programming language that has its roots in the C family of languages and includes support for component-oriented programming.
10,205 questions
0 comments No comments
{count} votes

Accepted answer
  1. Reza Aghaei 4,936 Reputation points MVP
    2023-01-24T00:30:11.3133333+00:00

    When to define an event, and when a delegate

    Here is the basic idea:

    • If something has happened inside your class, like a property changed, and you want to inform the possible subscribers about it, then you need to define an event in your class, and raise the event when something happens (like property changed).
    • If you want to inject a piece of functionality in a method, or a property, then your method or your property need to accept a delegate as parameter, one example is the link extension methods.

    Events are declared as class members, and you add a delegate to them using += operator.

    Delegates are type and will be declared usually as namespace members (like classes).

    After you defined a delegate (which is like defining signature of a method as a type with a name), then you can define fields, properties variables, input or output arguments of that delegate type.

    In the following examples you will see the usage, and see some differences.

    Delegate Example

    After you defined a delegate (which is like defining signature of a method as a type with a name), then you can define fields, properties variables, input or output arguments of that delegate type, for example, let's say you have defined:

    public delegate bool Criteria(string x);
    

    Then you can define the following in your class:

    public IEnumerable<string> Where(IEnumerable<string> input, Criteria criteria)
    {
        foreach (var item in input)
            if (criteria(item))
                yield return item;
    }
    

    Which is a good example of injecting a functionality, in a method.

    Event Example

    Assuming you have defined the following delegate (which is going to be used to define an event):

    public delegate void NameChangedEventHandler(string name);
    

    It basically is defining a type which explains a method, accepting a string parameter, and a void return value; and since I'm going to use it for an event, by assigning a name NameChangedEventHandler, I'm saying a method which is going to handle the NameChanged event, should follow that signature.

    Then you can define the event and the property and raise the event like this:

    public event NameChangedEventHandler NameChanged;
    private string name;
    public string Name
    {
        get
        {
            return name;
        }
        set
        {
            name = value;
            NameChanged?.Invoke(value);
        }
    }
    

    What is Action, or Action<T>, or Funct<T>?

    These are just some predefined delegates. Everytime that you want to pass a delegate as a method parameter, then you need to define the delegate. These actions and funcs makes it easier for you. For example in above delegate example, instead of defining a new delegate type, I could easily use Fucn<string, bool> which basically means a method accepting a string parameter which return bool.

    Looking at the source code of .NET framework, you see:

    namespace System
    {
        public delegate void Action<in T>(T obj);
        public delegate void Action();
        public delegate void Action<in T1, in T2>(T1 arg1, T2 arg2);
        public delegate void Action<in T1, in T2, in T3>(T1 arg1, T2 arg2, T3 arg3);
        public delegate void Action<in T1, in T2, in T3, in T4>(T1 arg1, T2 arg2, T3 arg3, T4 arg4);
        public delegate TResult Func<out TResult>();
        public delegate TResult Func<in T, out TResult>(T arg);
        public delegate TResult Func<in T1, in T2, out TResult>(T1 arg1, T2 arg2);
        public delegate TResult Func<in T1, in T2, in T3, out TResult>(T1 arg1, T2 arg2, T3 arg3);
        public delegate TResult Func<in T1, in T2, in T3, in T4, out TResult>(T1 arg1, T2 arg2, T3 arg3, T4 arg4);
        public delegate void Action<in T1, in T2, in T3, in T4, in T5>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5);
        public delegate void Action<in T1, in T2, in T3, in T4, in T5, in T6>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6);
        public delegate void Action<in T1, in T2, in T3, in T4, in T5, in T6, in T7>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7);
        public delegate void Action<in T1, in T2, in T3, in T4, in T5, in T6, in T7, in T8>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8);
        public delegate TResult Func<in T1, in T2, in T3, in T4, in T5, out TResult>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5);
        public delegate TResult Func<in T1, in T2, in T3, in T4, in T5, in T6, out TResult>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6);
        public delegate TResult Func<in T1, in T2, in T3, in T4, in T5, in T6, in T7, out TResult>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7);
        public delegate TResult Func<in T1, in T2, in T3, in T4, in T5, in T6, in T7, in T8, out TResult>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8);
        public delegate int Comparison<in T>(T x, T y);
        public delegate TOutput Converter<in TInput, out TOutput>(TInput input);
        public delegate bool Predicate<in T>(T obj);
    }
    

    What's the difference between a delegate field and an event?

    Lets say you have defined both of following in your class:

    1. public event EventHandler MyEvent;
    2. public EventHandler MyDelegateField;

    They are very similar (as they both rely on EventHandler which is a predefined delegate), but they are different as well:

    • The first one is an event, the second one is a public field of delegate type.
    • No one can assign a delegate to your event, they only can add (+=) or remove delegates (-=), but in addition to add (+=), and remove (-=), they can assign (=) delegate to the public field.
    • No one can raise event like a delegate outside of your class, but they can invoke your public delegate field.
    • Like properties and methods, you can define events in interfaces but you cannot do the same for fields.

    Here are a few examples to demonstrate the differences which I mentioned above:

    • class1.MyEvent = Something; doesn't work.
    • class1.MyDelegateField = Something works and will assign a new multicast delegate to the field.
    • class1.MyEvent(); doesn't work outside of your class.
    • class1.MyDelegateField(); works and will run all the delegates which has been assigned to the field.
    • interface ISomething { event EventHandler MyEvent;} is a valid declaration.
    • interface ISomething { EventHandler MyDelegateField} is invalid.

    More information

    You can learn more about the events:

    1 person found this answer helpful.

3 additional answers

Sort by: Most helpful
  1. Castorix31 81,461 Reputation points
    2023-01-23T19:23:04.4666667+00:00

    Did you read MSDN doc ? (Distinguishing Delegates and Events)

    0 comments No comments

  2. Bruce (SqlWork.com) 55,041 Reputation points
    2023-01-23T23:09:57.1566667+00:00

    its really pretty simple.

    an Event is really a collection of callbacks (delegates). when you invoke the event it loops thru the collection, and calls all the registered delegates.

    a delegate variable, only allows calling one. a delegate variable invoke can also return a value.

    if your code is calling the delegate to inform of an activity, then Event makes sense because there may be multiple listeners.

    if your code needs to call a delegate (callback) to get a value or only 1 registered callback is supported, then a delegate variable makes sense.

    to make defining delegate type signatures easier, the generic Action<>, Func<> and Predicate<> types can be used instead of defining an explicit delegate signature.


  3. Karen Payne MVP 35,031 Reputation points
    2023-01-24T20:14:00.1966667+00:00

    Keeping this simple, here a delegate and event are defined to permit listeners to get results in real time. Note there is more to delegates and events than what I've presented, if there is a specific question than ask.

    public class FolderOperations
    {
        public delegate void OnEmptyFolderFound(string folder);
    
        /// <summary>
        /// Callback for listeners to know the folder was not found
        /// </summary>
        public static event OnEmptyFolderFound EmptyFolderFound;
        /// <summary>
        /// Demo to show how to find empty folders
        /// </summary>
        /// <param name="path">folder to traverse</param>
        public static void FindEmptyFolders(string path)
        {
            foreach (var directory in Directory.GetDirectories(path))
            {
                FindEmptyFolders(directory);
                if (Directory.GetFiles(directory).Length == 0 && Directory.GetDirectories(directory).Length == 0)
                {
                    EmptyFolderFound?.Invoke(directory);
                }
            }
        }
    }
    

    In a windows form

    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
            FolderOperations.EmptyFolderFound += FolderOperations_EmptyFolderFound;
        }
    
        private void FolderOperations_EmptyFolderFound(string folder)
        {
            Debug.WriteLine(folder);
        }
    
        private void ExecuteButton_Click(object sender, EventArgs e)
        {
            FolderOperations.FindEmptyFolders("C:\\OED\\DotnetLand\\VS2019\\GlobbingSolution");
        }
    }
    

    One might ask about asynchronous? Sure but sometimes can slow things down for the sake of responsiveness.

    public class FolderOperations
    {
        public delegate void OnEmptyFolderFound(string folder);
    
        /// <summary>
        /// Callback for listeners to know the folder was not found
        /// </summary>
        public static event OnEmptyFolderFound EmptyFolderFound;
        /// <summary>
        /// Demo to show how to find empty folders
        /// </summary>
        /// <param name="path">folder to traverse</param>
        public static async Task FindEmptyFolders(string path)
        {
            await Task.Run(async () =>
            {
                await Task.Delay(1);
    
                foreach (var directory in Directory.GetDirectories(path))
                {
                    _ = FindEmptyFolders(directory);
                    if (Directory.GetFiles(directory).Length == 0 && Directory.GetDirectories(directory).Length == 0)
                    {
                        EmptyFolderFound?.Invoke(directory);
                    }
                }
    
            });
        }
    }
    

    Then there is also using them in try/catch

    public class DirectoryOperations1
    {
        public delegate void OnDelete(string status);
        /// <summary>
        /// Callback for subscribers to see what is being worked on
        /// </summary>
        public static event OnDelete OnDeleteEvent;
    
        public delegate void OnException(Exception exception);
        /// <summary>
        /// Callback for subscribers to know about a problem
        /// </summary>
        public static event OnException OnExceptionEvent;
    
        public delegate void OnUnauthorizedAccessException(string message);
        /// <summary>
        /// Raised when attempting to access a folder the user does not have permissions too
        /// </summary>
        public static event OnUnauthorizedAccessException UnauthorizedAccessEvent;
    
        public delegate void OnTraverseExcludeFolder(string sender);
        /// <summary>
        /// Called each time a folder is being traversed
        /// </summary>
        public static event OnTraverseExcludeFolder OnTraverseIncludeFolderEvent;
    
        public static bool Cancelled = false;
    
        /// <summary>
        /// Recursively remove an entire folder structure and files with events for monitoring and basic
        /// exception handling. USE WITH CARE
        /// </summary>
        /// <param name="directoryInfo"></param>
        /// <param name="cancellationToken"></param>
        public static async Task RecursiveDelete(DirectoryInfo directoryInfo, CancellationToken cancellationToken)
        {
            if (!directoryInfo.Exists)
            {
                OnDeleteEvent?.Invoke("Nothing to process");
                return;
            }
    
            OnDeleteEvent?.Invoke(directoryInfo.Name);
    
            DirectoryInfo folder = null;
    
            try
            {
                await Task.Run(async () =>
                {
                    foreach (DirectoryInfo dirInfo in directoryInfo.EnumerateDirectories())
                    {
    
                        folder = dirInfo;
    
                        if (
                            (folder.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden || 
                            (folder.Attributes & FileAttributes.System) == FileAttributes.System || 
                            (folder.Attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint) {
    
                            OnTraverseIncludeFolderEvent?.Invoke($"* {folder.FullName}");
    
                            continue;
    
                        }
    
                        OnTraverseIncludeFolderEvent?.Invoke($"Delete: {folder.FullName}");
    
                        if (!Cancelled)
                        {
                            await Task.Delay(1, cancellationToken);
                            await RecursiveDelete(folder, cancellationToken);
                        }
                        else
                        {
                            return;
                        }
    
                        if (cancellationToken.IsCancellationRequested)
                        {
                            cancellationToken.ThrowIfCancellationRequested();
                        }
    
                    }
    
                    /*
                     * assert if folder should be deleted, yes then
                     * directoryInfo.Delete(true);
                     */
                        
                }, cancellationToken);
    
            }
            catch (Exception exception)
            {
                switch (exception)
                {
                    case OperationCanceledException _:
                        Cancelled = true;
                        break;
                    case UnauthorizedAccessException _:
                        UnauthorizedAccessEvent?.Invoke($"Access denied '{exception.Message}'");
                        break;
                    default:
                        OnExceptionEvent?.Invoke(exception);
                        break;
                }
            }
        }
    }