dynamic, ExpandoObject and INotifyPropertyChanged

Adding a property changed event handler to a ExpandoObject object

Home DailyDrop

Daily Knowledge Drop

ExpandoObject implements INotifyPropertyChanged - a callback delegate can be added to be invoked when a property value on the ExpandoObject instances changes.


ExpandoObject

A quick overview of ExpandoObject - it represents an object whose members can be dynamically added and removed at run time.

In the examples below, it is being used to represent a product:

dynamic product = new ExpandoObject();
product.Name = "Green Shirt";
product.Rating = 4.3;

The properties Name and Rating are added at runtime. The properties can then be accessed as if they were traditional class properties:

Console.WriteLine(product.Name);
Console.WriteLine(product.Rating);

Calculation

In the examples below, we have a collection of Products, each with a customer rating:

public dynamic[] GetProducts()
{
    dynamic prod1 = new ExpandoObject();
    prod1.Name = "Green Shirt";
    prod1.Rating = 4.3;

    dynamic prod2 = new ExpandoObject();
    prod2.Name = "Blue Shirt";
    prod2.Rating = 4.9;

    dynamic prod3 = new ExpandoObject();
    prod3.Name = "Shoes";
    prod3.Rating = 3.8;

    dynamic prod4 = new ExpandoObject();
    prod4.Name = "Jeans";
    prod4.Rating = 5.0;

    dynamic prod5 = new ExpandoObject();
    prod5.Name = "Peak cap";
    prod5.Rating = 1.7;

    return new dynamic[] { prod1, prod2, prod3, prod4, prod5 };
}

We want to find the average rating for all products:

public void ComputeAverageRating()
{
    avgRating = products.Average(p => (double)p.Rating);
}

Manual invocation

Getting the average for all products is straight forward - call the ComputeAverageRating method:

var products = GetProducts();

var avgRating = products.Average(p => (double)p.Rating);
Console.WriteLine($"Average product rating is: {avgRating}");

However, every time a rating on one of the Products changes, the method needs to manually be called again:

var products = GetProducts();

var avgRating = products.Average(p => (double)p.Rating);
Console.WriteLine($"Average product rating is: {avgRating}");

// the product rating changed from 5 to 4.5
products[3].Rating = 4.5;

avgRating = products.Average(p => (double)p.Rating);
Console.WriteLine($"Average product rating is: {avgRating}");

The output from the above is:

Average product rating is: 3,94
Average product rating is: 3,84

While this method works without issue, as mentioned, it requires manually recalculating the average every time one of the Rating property values changes.

A different method, requiring less manual work - is to leverage the INotifyPropertyChanged functionality of ExpandoObject.


INotifyPropertyChanged

With this method, once we have the list of Products we can register a method to be called whenever a property on the Product changes:

void RegisterPropertyChange(dynamic[] products)
{
    foreach(var product in products)
    {
        // cast each product to INotifyPropertyChanged
        // and register a callback method to be called
        // every time a property changes.
        // The cast is valid as ExpandoObject implements
        // INotifyPropertyChanged
        ((INotifyPropertyChanged)product).PropertyChanged += 
            new PropertyChangedEventHandler(
                (sender, e) =>
                {
                    // when a property changes, call this method
                    ComputeAverageRating();
                }
            );
    }

    // calculate the average for the first time once 
    // the callbacks have all been registered
    ComputeAverageRating();
}

public void ComputeAverageRating()
{
    // avgRating is defined on the parent class
    avgRating = products.Average(p => (double)p.Rating);
}

This can now be utilized as follows:

var products = GetProducts();
RegisterPropertyChange(products);

Console.WriteLine($"Average product rating is: {avgRating}");

products[3].Rating = 4.5;

Console.WriteLine($"Average product rating is: {avgRating}");

With this approach, the average rating calculation doesn't have to be called manually:

Average product rating is: 3,94
Average product rating is: 3,84

Notes

While the ExpandoObject is a very useful class, it does have it's drawbacks, performance being a big one. However, when it is required it can prove to be invaluable - with the bonus advantage of being able to leverage the INotifyPropertyChanged functionality if required.


References

How to Create a Class Dynamically in C#?

Daily Drop 169: 27-09-2022

At the start of 2022 I set myself the goal of learning one new coding related piece of knowledge a day.
It could be anything - some.NET / C# functionality I wasn't aware of, a design practice, a cool new coding technique, or just something I find interesting. It could be something I knew at one point but had forgotten, or something completely new, which I may or may never actually use.

The Daily Drop is a record of these pieces of knowledge - writing about and summarizing them helps re-enforce the information for myself, as well as potentially helps others learn something new as well.
c# .net expando property propertychanged