Skip to main content
    Courses/C#/Events: Advanced Patterns

    Lesson 17 • Advanced Track

    Events: Advanced Patterns & Custom EventArgs

    By the end of this lesson you'll be able to design professional, type-safe events: carry rich data with a custom EventArgs, raise events the standard .NET way with a protected virtual OnXxx method, do it safely on multiple threads, and avoid the memory leaks that catch out even experienced developers.

    What You'll Learn

    • Create a custom EventArgs subclass that carries data with an event
    • Declare type-safe events with the EventHandler and EventHandler<T> pattern
    • Raise events the standard way with a protected virtual OnXxx method
    • Raise events safely (null-conditional ?.Invoke and a local copy for threads)
    • Subscribe and unsubscribe handlers — and read the event data in a handler
    • Prevent memory leaks and understand weak-event basics

    💡 Real-World Analogy

    An event is a newsletter. The publisher sends out an issue without knowing who reads it; subscribers sign up (+=) and cancel (-=) at will. A plain EventArgs.Empty is a blank postcard that just says "something happened". A custom EventArgs is the actual article stapled to the postcard — it carries the details: which order, how much, what the new temperature was. And the OnXxx method is the printing press in the basement: the only machine allowed to put an issue in the post, kept private so nobody outside can fake a mailing.

    Running C# Locally: Install the .NET SDK or use dotnetfiddle.net to run every console example below.

    📊 The Event Toolkit at a Glance

    PieceWhat it isExampleWhen to use
    EventHandlerStandard delegate, no extra dataevent EventHandler Clicked;Event carries nothing
    EventHandler<T>Standard delegate carrying data Tevent EventHandler<OrderEventArgs> OrderPlaced;Event carries details
    : EventArgsYour data class for an eventclass OrderEventArgs : EventArgsDefine what to send
    OnXxx(...)protected virtual raise methodprotected virtual void OnOrderPlaced(...)The one place that raises
    ?.InvokeNull-safe raiseOrderPlaced?.Invoke(this, e);Always, when raising
    += / -=Subscribe / unsubscribesvc.OrderPlaced += OnPlaced;From outside the class

    The signature every handler matches is always (object sender, TEventArgs e)sender is who raised it, e is the data. Memorising that one shape unlocks every event in .NET.

    1. Custom EventArgs — Carrying Data With an Event

    A bare event can shout "something happened!", but real apps need to know what happened. You attach that detail by creating a class that inherits from EventArgs and declaring the event as EventHandler<YourEventArgs>. By convention the data class ends in EventArgs and its properties are read-only (set once in the constructor) so a subscriber can't tamper with the data other subscribers will receive. Read this worked example and run it, then you'll write your own.

    Worked example: a custom OrderEventArgs

    One PlaceOrder call hands every subscriber an OrderEventArgs full of detail.

    Try it Yourself »
    C#
    using System;
    
    // A custom EventArgs subclass CARRIES DATA to every subscriber.
    // Convention: the class name ends in "EventArgs" and the
    // properties are read-only (set once in the constructor).
    class OrderEventArgs : EventArgs
    {
        public string OrderId { get; }
        public decimal Amount { get; }
        public DateTime Placed { get; }
    
        public OrderEventArgs(string orderId, decimal amount)
        {
            OrderId = orderId;
            Amount = amount;
            Placed = DateTime.Now;
        }
    }
    
    // Th
    ...

    Your turn. Define an EventArgs subclass that carries a message, then raise an event with it. Fill in the three blanks marked ___ using the // 👉 hints, then run it.

    🎯 Your turn: define EventArgs & raise an event

    Inherit from EventArgs and raise the event safely with ?.Invoke.

    Try it Yourself »
    C#
    using System;
    
    // 🎯 YOUR TURN #1 — define a custom EventArgs and raise an event.
    
    // 1) Finish this EventArgs subclass so it CARRIES a message string.
    class AlertEventArgs : ___          // 👉 inherit from EventArgs
    {
        public string Message { get; }
        public AlertEventArgs(string message) => Message = message;
    }
    
    class Alarm
    {
        // 2) Declare an event using the generic standard delegate.
        public event EventHandler<AlertEventArgs>? Triggered;   // (already done)
    
        public void Fire(s
    ...

    2. Subscribing — Reading the Event Data

    Defining the data is only half the job; a subscriber has to read it. Every handler receives two arguments: sender (the object that raised the event) and e (your EventArgs). You reach the details through ee.OrderId, e.Amount, e.Points, whatever your class exposed. Fill in the two blanks below: choose the operator that adds a subscriber, and pull the value off e.

    🎯 Your turn: subscribe and read e.Points

    Subscribe with += and print the points carried on the EventArgs.

    Try it Yourself »
    C#
    using System;
    
    class ScoreEventArgs : EventArgs
    {
        public int Points { get; }
        public ScoreEventArgs(int points) => Points = points;
    }
    
    class Game
    {
        public event EventHandler<ScoreEventArgs>? ScoreChanged;
        public void AddPoints(int points)
            => ScoreChanged?.Invoke(this, new ScoreEventArgs(points));
    }
    
    class Program
    {
        static void Main()
        {
            var game = new Game();
    
            // 🎯 YOUR TURN #2 — subscribe a handler that READS the event data.
    
            // 1) Use the 
    ...

    3. The protected virtual OnXxx Raise Method

    In professional code you almost never raise an event inline. Instead you funnel every raise through a single protected virtual method named On + the event nameOnPriceChanged, OnOrderPlaced. There are two reasons. protected lets a subclass raise the event too (handy for base classes). virtual lets a subclass override the method to add behaviour — log it, suppress it, run code before or after — without touching the property that triggered it. It also gives you exactly one place to make the raise thread-safe.

    Worked example: the OnPriceChanged pattern

    A property setter calls a protected virtual OnXxx method to raise the event.

    Try it Yourself »
    C#
    using System;
    
    class PriceChangedEventArgs : EventArgs
    {
        public decimal OldPrice { get; }
        public decimal NewPrice { get; }
        public PriceChangedEventArgs(decimal oldPrice, decimal newPrice)
        {
            OldPrice = oldPrice;
            NewPrice = newPrice;
        }
    }
    
    class Stock
    {
        public event EventHandler<PriceChangedEventArgs>? PriceChanged;
    
        private decimal _price;
        public decimal Price
        {
            get => _price;
            set
            {
                if (_price == value) return;
    ...

    4. Thread-Safe Raising & Unsubscribing

    There's a subtle race condition when events are used across threads. Between checking that an event has subscribers and actually calling it, another thread could unsubscribe the last handler — leaving you calling null. The fix is to copy the event into a local variable first, then call the copy. Good news: the ?.Invoke operator you've been using already does this copy for you, which is the main reason it's the recommended way to raise. The example also shows the other half of subscription hygiene: a named handler so you can -= it later (you can't unsubscribe an anonymous lambda).

    Worked example: local-copy raise & unsubscribe

    Snapshot the handler list for thread safety, then unsubscribe a named handler.

    Try it Yourself »
    C#
    using System;
    
    class TickEventArgs : EventArgs
    {
        public int Tick { get; }
        public TickEventArgs(int tick) => Tick = tick;
    }
    
    class Clock
    {
        public event EventHandler<TickEventArgs>? Ticked;
    
        protected virtual void OnTicked(TickEventArgs e)
        {
            // THREAD-SAFE RAISE: copy the event to a local variable first.
            // Another thread could unsubscribe the last handler between the
            // null-check and the call; the local copy can't be changed
            // out from under y
    ...

    🔎 Deep Dive: memory leaks & weak events

    When you write publisher.SomeEvent += subscriber.Handler, the publisher now holds a reference to the subscriber. If the publisher lives a long time (a singleton, a static service, a long-running window) and you never call -=, the garbage collector cannot reclaim the subscriber — it's kept alive purely by that subscription. Repeat this in a loop and memory climbs forever. This is the single most common "managed memory leak" in C#.

    The everyday fix is discipline: unsubscribe with -= when the subscriber is done, often inside Dispose(). For cases where you can't guarantee that — UI frameworks especially — there are weak event patterns where the publisher holds only a weak reference, so the subscriber can still be collected. WPF ships WeakEventManager; many teams use a small WeakEventManager<T> helper for the same effect.

    // LEAK: long-lived publisher keeps 'view' alive forever
    service.DataUpdated += view.Refresh;
    
    // FIX: unsubscribe when done (e.g. in Dispose)
    service.DataUpdated -= view.Refresh;
    
    // Pass the SAME reference you subscribed with — a fresh lambda
    // like  (s,e) => view.Refresh(s,e)  is a NEW object and won't detach.

    Pro Tips

    • 💡 Always raise with ?.Invoke(...): it both skips zero-subscriber events and takes the thread-safe local copy for you — two safety nets in one operator.
    • 💡 Use the protected virtual OnXxx pattern so subclasses can extend the behaviour and there's exactly one place that raises the event.
    • 💡 Keep EventArgs properties read-only: set them in the constructor so one subscriber can't mutate the data the next subscriber sees.
    • 💡 Use a record for terse EventArgs (C# 9+): record OrderEventArgs(string Id, decimal Amount) : EventArgs; generates the constructor and properties for you.
    • 💡 Name handlers so you can detach them: a named method or stored delegate can be passed to -=; an inline lambda can't be unsubscribed.

    Common Errors (and the fix)

    • "System.NullReferenceException" when raising: an event with no subscribers is null, so OrderPlaced(this, e) crashes. Always raise it as OrderPlaced?.Invoke(this, e); — the ?. simply does nothing when nobody is listening.
    • Forgetting to unsubscribe → memory leak: a subscriber a long-lived publisher still references can never be garbage-collected. Call -= when you're done (often in Dispose()), passing the same handler reference you subscribed with.
    • Wrong handler signature: for EventHandler<T> the handler must be (object sender, T e). A method like void Handle(T e) won't compile — it's missing the sender. Match the standard (object sender, TEventArgs e) shape exactly.
    • "CS0070: The event 'X' can only appear on the left hand side of += or -=": you tried to raise (or assign) an event from outside the declaring class. Only the class that declares the event may raise it — wrap the raise in a public/protected method and call that instead.
    • EventArgs not inheriting from EventArgs: EventHandler<T> requires T to derive from EventArgs. If you forget : EventArgs, the event declaration won't compile.

    📋 Quick Reference

    TaskCodeNotes
    Define event dataclass OrderEventArgs : EventArgsRead-only props
    Declare a data eventevent EventHandler<OrderEventArgs> OrderPlaced;On the publisher
    Raise methodprotected virtual void OnOrderPlaced(...)One place to raise
    Raise safelyOrderPlaced?.Invoke(this, e);Null-safe + thread-safe
    Thread-safe local copyvar h = OrderPlaced; h?.Invoke(this, e);What ?.Invoke does
    Handler signaturevoid H(object sender, OrderEventArgs e)Always (sender, e)
    Subscribe / unsubscribesvc.OrderPlaced += H; svc.OrderPlaced -= H;Same reference to -=
    Read the data$"{e.OrderId}: {e.Amount:C}"Inside the handler

    Frequently Asked Questions

    Q: Do I have to inherit from EventArgs for my data class?

    To use the built-in EventHandler<T> delegate, yes — its constraint requires T : EventArgs. It's a tiny base class with no members, so inheriting costs nothing and signals "this is event data" to every C# developer and tool.

    Q: Why use a protected virtual OnXxx method instead of raising inline?

    It gives you one single place that raises the event, which keeps thread-safety logic in one spot. protected lets subclasses raise it, and virtual lets them override to add behaviour (logging, suppression) without rewriting the trigger. It's the convention every .NET library follows.

    Q: Is ?.Invoke really enough for thread safety?

    For the classic "subscriber unsubscribed between the null-check and the call" race, yes — ?.Invoke evaluates the event once into a hidden local, so it can't become null mid-call. It does not make the handlers themselves thread-safe; if your handlers touch shared state, they still need their own locking.

    Q: Why can't I unsubscribe a lambda I subscribed with?

    Each lambda you write is a distinct delegate object. x -= (s,e) => ... creates a brand-new lambda that doesn't match the one you added, so nothing is removed. Store the handler in a variable or use a named method, then pass that same reference to both += and -=.

    Mini-Challenge: a TemperatureSensor

    No blanks this time — just a brief and an outline. Build a TemperatureSensor that raises a ThresholdExceeded event (with a custom EventArgs carrying the value) when a reading goes above its threshold, and a subscriber that reacts with an alert. Run it and check your output against the expected lines in the comments.

    🎯 Mini-Challenge: build a TemperatureSensor event

    Define ThresholdEventArgs, raise ThresholdExceeded with ?.Invoke, and react in a subscriber.

    Try it Yourself »
    C#
    using System;
    
    // 🎯 MINI-CHALLENGE: a TemperatureSensor that raises ThresholdExceeded
    //
    // 1. Create a custom EventArgs class "ThresholdEventArgs" that carries
    //    a  double Value  (set it in the constructor, read-only property).
    // 2. Give TemperatureSensor an event:
    //        public event EventHandler<ThresholdEventArgs>? ThresholdExceeded;
    //    ...and a  double Threshold  property (e.g. 30.0).
    // 3. Add a method  Report(double value)  that prints the reading, and if
    //    value > Thresho
    ...

    🎉 Lesson Complete

    • ✅ A custom EventArgs subclass carries data; keep its properties read-only
    • EventHandler (no data) and EventHandler<T> (with data) are the standard event delegates
    • ✅ Every handler matches the (object sender, TEventArgs e) signature
    • ✅ Raise through a protected virtual OnXxx method — one place, easy to override
    • ✅ Always raise with ?.Invoke: it's null-safe and takes the thread-safe local copy
    • ✅ Unsubscribe with -= (same reference) to prevent memory leaks; weak events help when you can't
    • Next lesson: Expression Trees — building and inspecting code as data at runtime

    Sign up for free to track which lessons you've completed and get learning reminders.

    Previous

    Cookie & Privacy Settings

    We use cookies to improve your experience, analyze traffic, and show personalized ads. You can manage your preferences below.

    By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising. You can customize your preferences or reject non-essential cookies.

    Privacy PolicyTerms of Service