The Updated .NET Core Event Pattern
The previous article discussed the most common event patterns. .NET
Core has a more relaxed pattern. In this version, the
EventHandler<TEventArgs>
definition no longer has the constraint that
TEventArgs
must be a class derived from System.EventArgs
.
This increases flexibility for you, and is backwards compatible. Let's
start with the flexibility. The class System.EventArgs introduces one
method: MemberwiseClone()
, which creates a shallow copy of the object.
That method must use reflection in order to implement
its functionality for any class derived from EventArgs
. That
functionality is easier to create in a specific derived class. That
effectively means that deriving from System.EventArgs is a constraint
that limits your designs, but does not provide any additional benefit.
In fact, you can change the definitions of FileFoundArgs
and
SearchDirectoryArgs
so that they do not derive from EventArgs
.
The program will work exactly the same.
You could also change the SearchDirectoryArgs
to a struct, if you make one more change:
internal struct SearchDirectoryArgs
{
internal string CurrentSearchDirectory { get; }
internal int TotalDirs { get; }
internal int CompletedDirs { get; }
internal SearchDirectoryArgs(string dir, int totalDirs, int completedDirs) : this()
{
CurrentSearchDirectory = dir;
TotalDirs = totalDirs;
CompletedDirs = completedDirs;
}
}
The additional change is to call the parameterless constructor before entering the constructor that initializes all the fields. Without that addition, the rules of C# would report that the properties are being accessed before they have been assigned.
You should not change the FileFoundArgs
from a class (reference
type) to a struct (value type). That's because the protocol for
handling cancel requires that the event arguments are passed by reference. If you made the same change, the file search class could
never observe any changes made by any of the event subscribers. A new
copy of the structure would be used for each subscriber, and that
copy would be a different copy than the one seen by the file search
object.
Next, let's consider how this change can be backwards compatible.
The removal of the constraint does not affect any existing code. Any
existing event argument types do still derive from System.EventArgs
.
Backwards compatibility is one major reason why they will continue
to derive from System.EventArgs
. Any existing event subscribers will
be subscribers to an event that followed the classic pattern.
Following similar logic, any event argument type created now would
not have any subscribers in any existing codebases. New event types
that do not derive from System.EventArgs
will not break those
codebases.
Events with Async subscribers
You have one final pattern to learn: How to correctly write event
subscribers that call async code. The challenge is described in
the article on async and await. Async methods can
have a void return type, but that is strongly discouraged. When your
event subscriber code calls an async method, you have no choice but
to create an async void
method. The event handler signature requires
it.
You need to reconcile this opposing guidance. Somehow, you must
create a safe async void
method. The basics of the pattern you need
to implement are below:
worker.StartWorking += async (sender, eventArgs) =>
{
try
{
await DoWorkAsync();
}
catch (Exception e)
{
//Some form of logging.
Console.WriteLine($"Async task failure: {e.ToString()}");
// Consider gracefully, and quickly exiting.
}
};
First, notice that the handler is marked as an async handler. Because
it is being assigned to an event handler delegate type, it will have
a void return type. That means you must follow the pattern shown in the
handler, and not allow any exceptions to be thrown out of the context
of the async handler. Because it does not return a task, there is no
task that can report the error by entering the faulted state. Because
the method is async, the method can't simply throw the exception. (The
calling method has continued execution because it is async
.) The
actual runtime behavior will be defined differently for different
environments. It may terminate the thread or the process that owns the thread,
or leave the process in an indeterminate state. All of these potential outcomes are highly undesirable.
That's why you should wrap the await statement for the async Task in your own try block. If it does cause a faulted task, you can log the error. If it is an error from which your application cannot recover, you can exit the program quickly and gracefully
Those are the major updates to the .NET event pattern. You will see many examples of the earlier versions in the libraries you work with. However, you should understand what the latest patterns are as well.
The next article in this series helps you distinguish between using
delegates
and events
in your designs. They are similar concepts,
and that article will help you make the best decision for your
programs.