views:

376

answers:

2

I'm having trouble with a situation that I know must be pretty common, so I'm hoping the solution is simple. I have an object that contains a List<> of objects. It also has some properties that reflect aggregate data on the objects in the List<> (actually a BindingList<> so I can bind to it). On my form, I have a DataGridView for the List, and some other fields for the aggregate data. I can't figure out how to trigger a refresh of the aggregate data when values in the DataGridView get changed.

I have tried raising a PropertyChanged event when the properties of the objects in the List are changed, but that doesn't seem to refresh the display of the aggregate data. If I access an aggregate property (eg, display it in a messagebox), the textbox on the main form is refreshed.

Here's some simplified code to illustrate what I'm trying to do:

namespace WindowsFormsApplication1 {
public class Person {

    public int Age {
        get;
        set;
    }

    public String Name {
        get;
        set;
    }
}

public class Roster : INotifyPropertyChanged {

    public BindingList<Person> People {
        get;
        set;
    }

    public Roster () {
        People = new BindingList<Person>();
    }

    private int totalage;
    public int TotalAge {
        get {
            calcAges();
            return totalage;
        }
        set {
            totalage = value;
            NotifyPropertyChanged("TotalAge");
        }
    }

    private void calcAges () {
        int total = 0;
        foreach ( Person p in People ) {
            total += p.Age;
        }
        TotalAge = total;
    }

    #region INotifyPropertyChanged Members
    public event PropertyChangedEventHandler PropertyChanged;
    private void NotifyPropertyChanged ( String info ) {
        if ( PropertyChanged != null ) {
            PropertyChanged(this, new PropertyChangedEventArgs(info));
        }
    }
    #endregion
}
}
A: 

I believe you may be looking for something like this

ITypedList

Also a Google Search of ITypedList leads you to a few nice blogs on how to implement.

When I use an ORM I typically have to do a few of these for nice datagrid binding and presentation.

joshlrogers
@joshlrogers: Sorry for the downvote, but this isn't an issue of the type of data that is being bound to, but rather, the binding and notification not happening correctly. ITypedList doesn't add anything to the notification experience that isn't already exposed by INotifyPropertyChanged.
casperOne
Hmmm.....I've reread the question and your answer and I guess I am just missing something, maybe too early in the morning. I was assuming that he was modifying a property of one of the objects in one of his aggregates. So in other words he was modifying a property of Person in the BindingList<Person> so he really wasn't modifying the collection per se just an object of the collection. So if he modifies how he binds the grid by using ITypedList he could exclusively catch changes to those underlying properties and notify. Maybe I am just not thinking it through or overcomplicating the issue.
joshlrogers
+3  A: 

The calcAges method and the TotalAge property look very suspicious.

First, TotalAge should be read-only. If you allow it to be public and writable, what is the logic for changing the components that make up the age?

Second, every time you get the value, you are firing the PropertyChanged event, which is not good.

Your Roster class should look like this:

public class Roster : INotifyPropertyChanged {

    public Roster ()
    {
        // Set the binding list, this triggers the appropriate
        // event binding which would be gotten if the BindingList
        // was set on assignment.
        People = new BindingList<Person>();
    }

    // The list of people.
    BindingList<Person> people = null;

    public BindingList<Person> People 
    {
        get 
        { 
            return people; 
        }
        set 
        { 
            // If there is a list, then remove the delegate.
            if (people != null)
            {
                // Remove the delegate.
                people.ListChanged -= OnListChanged;
            }

            /* Perform error check here */ 
            people = value;

            // Bind to the ListChangedEvent.
            // Use lambda syntax if LINQ is available.
            people.ListChanged += OnListChanged;

            // Technically, the People property changed, so that
            // property changed event should be fired.
            NotifyPropertyChanged("People");

            // Calculate the total age now, since the 
            // whole list was reassigned.
            CalculateTotalAge();
        }
    }

    private void OnListChanged(object sender, ListChangedEventArgs e)
    {
        // Just calculate the total age.
        CalculateTotalAge();
    }

    private void CalculateTotalAge()
    {
        // Store the old total age.
        int oldTotalAge = totalage;

        // If you can use LINQ, change this to:
        // totalage = people.Sum(p => p.Age);

        // Set the total age to 0.
        totalage = 0;

        // Sum.
        foreach (Person p in People) {
            totalage += p.Age;
        }

        // If the total age has changed, then fire the event.
        if (totalage != oldTotalAge)
        {
            // Fire the property notify changed event.
            NotifyPropertyChanged("TotalAge");
        }
    }

    private int totalage = 0;

    public int TotalAge 
    {
        get 
        {
            return totalage;
        }
    }
    public event PropertyChangedEventHandler PropertyChanged;

    private void NotifyPropertyChanged ( String info ) {
        if ( PropertyChanged != null ) {
            PropertyChanged(this, new PropertyChangedEventArgs(info));
        }
    }
}

Now, when the properties in the list items are changed, the parent object will fire the property changed event, and anything bound to it should change as well.

casperOne
Erf, gg, I was a few seconds too late ;-)
Julien Poulin
Don't forget to implement INotifyPropertyChanged in the Person class though. And you should consider to unbind the event on the ListChanged to avoid memory leaks...
Julien Poulin
@Julien Poulin: Good point, changed the code to reflect. Also, technically, it's not a leak, as it will all be cleaned up when the Roster instance is collected (a true leak would never be collected). However, it is something that should be done.
casperOne
Hi casperOne - this does the trick. I didn't understand that it was the list itself that needs to raise the event, rather than the objects in the list. Many thanks!
Hoser