DEV Community

mohamed Tayel
mohamed Tayel

Posted on

C# Advanced: Unsubscribing from Events

Meta Description:

Learn why unsubscribing from events is crucial in C#. This article explains the importance of properly unsubscribing from events using a stock management system, with assignments to practice at different difficulty levels.


Introduction

Events in C# allow for powerful communication between objects in your application, but they come with a critical responsibility: unsubscribing from events when they are no longer needed. Failing to do so can lead to memory leaks, unexpected behavior, and performance issues. In this article, we’ll explore why unsubscribing from events is important and demonstrate a common problem using a stock management system. We’ll also provide assignments to help you apply the concept in practice.


1. The Problem of Not Unsubscribing from Events

When you subscribe to an event, the object that raises the event (the publisher) holds a reference to the object that handles the event (the subscriber). If you forget to unsubscribe, the publisher continues to reference the subscriber, preventing it from being garbage collected. This can lead to memory leaks and unintended behavior, as we’ll illustrate with an example.

Let’s revisit our stock management system where the InventoryManager notifies the WarehouseManager and SupplierService when stock levels are low. We’ll introduce an issue where multiple instances of the WarehouseManager lead to unexpected results due to a failure to unsubscribe from events.


2. Stock Management System Example with Event Subscription Problem

In this example, every time an order is processed, a new instance of WarehouseManager is created and subscribed to the StockLow event. However, if we don’t unsubscribe the event when the WarehouseManager is no longer needed, its event handler will remain in the invocation list. This means the event will be fired multiple times, even if the original WarehouseManager instance no longer exists.

Code Example

using System;

public class StockEventArgs : EventArgs
{
    public string ItemName { get; set; }
    public int CurrentStock { get; set; }
}

public class InventoryManager
{
    public event EventHandler<StockEventArgs> StockLow;

    private int _stockLevel;
    public string ItemName { get; set; }

    public InventoryManager(string itemName, int initialStock)
    {
        ItemName = itemName;
        _stockLevel = initialStock;
    }

    public void ProcessSale(int quantity)
    {
        _stockLevel -= quantity;
        Console.WriteLine($"{quantity} {ItemName}(s) sold. Current stock: {_stockLevel}");

        if (_stockLevel < 5)
        {
            OnStockLow();
        }
    }

    protected virtual void OnStockLow()
    {
        StockLow?.Invoke(this, new StockEventArgs { ItemName = this.ItemName, CurrentStock = this._stockLevel });
    }
}

public class WarehouseManager
{
    private readonly InventoryManager _inventory;

    public WarehouseManager(InventoryManager inventory)
    {
        _inventory = inventory;
        _inventory.StockLow += HandleStockLow;  // Subscribe to the event
    }

    public void HandleStockLow(object sender, StockEventArgs e)
    {
        Console.WriteLine($"Warehouse: Restock {e.ItemName}. Current stock: {e.CurrentStock}");
    }

    public void Unsubscribe()
    {
        _inventory.StockLow -= HandleStockLow;  // Unsubscribe from the event
    }
}

public class Program
{
    public static void Main(string[] args)
    {
        InventoryManager inventory = new InventoryManager("Laptop", 10);

        // Simulating multiple order processes and warehouse restock handling
        for (int i = 0; i < 3; i++)
        {
            WarehouseManager warehouse = new WarehouseManager(inventory);
            inventory.ProcessSale(4);  // Stock level reduces and triggers StockLow event
            warehouse.Unsubscribe();   // Unsubscribing after each process
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • The WarehouseManager subscribes to the StockLow event in the InventoryManager.
  • The event is triggered when stock falls below a threshold.
  • We unsubscribe from the event by calling the Unsubscribe method when the warehouse instance is no longer needed.

3. Why Unsubscribing Matters: The Consequences of Not Unsubscribing

In the above example, if we forget to call Unsubscribe(), each new WarehouseManager instance will continue to be subscribed to the StockLow event. This results in multiple event handlers being executed when stock levels fall, even though only one instance of WarehouseManager should be active at a time. This can lead to:

  • Memory leaks: Because the publisher still holds a reference to the subscriber, the garbage collector cannot clean it up.
  • Unexpected behavior: The same event can be triggered multiple times, leading to issues like duplicate actions or slow performance.

4. Fixing the Problem: Unsubscribing Correctly

To avoid this problem, we must ensure that we unsubscribe from the event when the WarehouseManager is no longer needed. In a UI application, this might be done when a window is closed. In our example, we explicitly call Unsubscribe() after processing each sale. This ensures that the WarehouseManager is properly cleaned up and no longer reacts to events.


Full Code: Stock Management System with Event Unsubscribing

using System;

public class StockEventArgs : EventArgs
{
    public string ItemName { get; set; }
    public int CurrentStock { get; set; }
}

public class InventoryManager
{
    public event EventHandler<StockEventArgs> StockLow;

    private int _stockLevel;
    public string ItemName { get; set; }

    public InventoryManager(string itemName, int initialStock)
    {
        ItemName = itemName;
        _stockLevel = initialStock;
    }

    public void ProcessSale(int quantity)
    {
        _stockLevel -= quantity;
        Console.WriteLine($"{quantity} {ItemName}(s) sold. Current stock: {_stockLevel}");

        if (_stockLevel < 5)
        {
            OnStockLow();
        }
    }

    protected virtual void OnStockLow()
    {
        StockLow?.Invoke(this, new StockEventArgs { ItemName = this.ItemName, CurrentStock = this._stockLevel });
    }
}

public class WarehouseManager
{
    private readonly InventoryManager _inventory;

    public WarehouseManager(InventoryManager inventory)
    {
        _inventory = inventory;
        _inventory.StockLow += HandleStockLow;  // Subscribe to the event
    }

    public void HandleStockLow(object sender, StockEventArgs e)
    {
        Console.WriteLine($"Warehouse: Restock {e.ItemName}. Current stock: {e.CurrentStock}");
    }

    public void Unsubscribe()
    {
        _inventory.StockLow -= HandleStockLow;  // Unsubscribe from the event
    }
}

public class Program
{
    public static void Main(string[] args)
    {
        InventoryManager inventory = new InventoryManager("Laptop", 10);

        // Simulating multiple order processes and warehouse restock handling
        for (int i = 0; i < 3; i++)
        {
            WarehouseManager warehouse = new WarehouseManager(inventory);
            inventory.ProcessSale(4);  // Stock level reduces and triggers StockLow event
            warehouse.Unsubscribe();   // Unsubscribing after each process
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

5. Assignments to Practice Unsubscribing from Events

To help you practice the concept of unsubscribing from events, here are three assignments:

Easy Level:

Add a method to the SupplierService that subscribes to the StockLow event just like WarehouseManager. Ensure that it also unsubscribes after processing an order.

Hint: Follow the same pattern as WarehouseManager, with a method to subscribe and unsubscribe from the event.

Medium Level:

Modify the program to introduce multiple products in the InventoryManager. Ensure that the WarehouseManager can handle events for different products, and each instance properly unsubscribes after handling an event.

Hint: Use a collection to manage products and ensure that each WarehouseManager unsubscribes for each product after the stock check.

Difficult Level:

Make the event handling asynchronous. Modify the WarehouseManager to handle the StockLow event asynchronously using Task.Run(). Ensure that the event handlers are unsubscribed correctly even when asynchronous operations are involved.

Hint: Use async and await in the event handler and ensure proper synchronization when unsubscribing from events.


Conclusion

Unsubscribing from events is critical in ensuring your C# applications run smoothly without memory leaks or unexpected behavior. The stock management example demonstrates how failing to unsubscribe can lead to issues such as multiple event handlers being called unexpectedly. By following best practices and ensuring proper event management, you can avoid these pitfalls and create more efficient, maintainable applications.

Top comments (0)