Search code examples
c#winformsdata-bindingdatagridviewinotifypropertychanged

Winforms datagridview - variable number of columns - INotifyPropertyChanged


Let's say I have two classes:

public class Hour : INotifyPropertyChanged
{
    public int Value
    {
        // ...
    }

    // INotifyPropertyChanged implementation
}

public class Day
{
    public int Number
    {
        // ...
    }
    public BindingList<Hour> Hours { get; set; } = new BindingList<Hour>();

    // INotifyPropertyChanged implementation
}

I want to show those data in the datagridview - first column is day number, and then there are columns for each hour. I've used this solution:

WinForms DataGridView - databind to an object with a list property (variable number of columns)

Everything is working fine until I change the value programatically - I can change the number of day and the value is refreshed automatically inside the datagridview but when I change hour, datagridview is not refreshed and the new value is refreshed when I click on the specific row. I change it like this:

days[0].Number = 55;

days[0].Hours[0].Value = 55;

Could someone tell me if it's even possible what I am trying to achieve and if so, how can my goal be achieved.

I've tried to look inside PropertyDescriptor and datagridview to see, how is it working internally but I am still stuck on this problem.

EDIT:

This is my current solution:

public class Hour : INotifyPropertyChanged
{
    private int _value;

    public int Value
    {
        get => _value;
        set
        {
            SetField(ref _value, value, "Value");
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    protected bool SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
    {
        if (EqualityComparer<T>.Default.Equals(field, value)) return false;
        field = value;
        OnPropertyChanged(propertyName);
        return true;
    }
}

    public class Day : INotifyPropertyChanged
    {
        private int _number;

        public int Number
        {
            get => _number;
            set
            {
                SetField(ref _number, value, "Number");
            }
        }

        public BindingList<Hour> Hours { get; set; } = new BindingList<Hour>();

        public event PropertyChangedEventHandler PropertyChanged;

        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }

        protected bool SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
        {
            if (EqualityComparer<T>.Default.Equals(field, value)) return false;
            field = value;
            OnPropertyChanged(propertyName);
            return true;
        }
}

    class DayList : BindingList<Day>, ITypedList
    {
        public PropertyDescriptorCollection GetItemProperties(PropertyDescriptor[] listAccessors)
        {
            var origProps = TypeDescriptor.GetProperties(typeof(Day));
            List<PropertyDescriptor> newProps = new List<PropertyDescriptor>(origProps.Count);
            PropertyDescriptor doThisLast = null;
            PropertyDescriptor doThisFirst = null;
            foreach (PropertyDescriptor prop in origProps)
            {
                if (prop.Name == "Hours") doThisLast = prop;
                else newProps.Add(prop);
            }
            if (doThisLast != null)
            {
                var min = this.Min(f => f.Hours.Min(h => h.Value));
                var max = this.Max(f => f.Hours.Max(h => h.Value));

                if (max > 0)
                {
                    Type propType = typeof(Hour);

                    for (var i = min; i <= max; i++)
                    {
                        var item = new ListItemDescriptor(doThisLast, (int)(i - min), i, propType);
                        newProps.Add(item);
                    }
                }
            }

            return new PropertyDescriptorCollection(newProps.ToArray());
        }

        public string GetListName(PropertyDescriptor[] listAccessors)
        {
            return "";
        }
    }

    class ListItemDescriptor : PropertyDescriptor
    {
        private static readonly Attribute[] nix = new Attribute[0];
        private readonly PropertyDescriptor tail;
        private readonly Type type;
        private readonly int index;
        public ListItemDescriptor(PropertyDescriptor tail, int index, int value, Type type) : base(tail.Name + "[" + value + "]", nix)
        {
            this.tail = tail;
            this.type = type;
            this.index = index;
        }
        public override object GetValue(object component)
        {
            IList list = tail.GetValue(component) as IList;
            return (list == null || list.Count <= index) ? 0 : ((Hour)list[index]).Value;
        }
        public override Type PropertyType
        {
            get { return type; }
        }
        public override bool IsReadOnly
        {
            get { return true; }
        }
        public override void SetValue(object component, object value)
        {
            throw new NotSupportedException();
        }
        public override void ResetValue(object component)
        {
            throw new NotSupportedException();
        }
        public override bool CanResetValue(object component)
        {
            return false;
        }
        public override Type ComponentType
        {
            get { return tail.ComponentType; }
        }
        public override bool ShouldSerializeValue(object component)
        {
            return false;
        }
    }

Then I create DayList and set is as datasource for datagridview.

This is the result:

datagridview

As I said when I change the number, everything is ok (datagridview cell is refreshed) "days[0].Number = 55;" but when I set the value of an hour "days[0].Hours[0].Value = 55;", the cell is refreshed when I click on the specific row.


Solution

  •  public Day() {
         Hours.ListChanged += Hours_ListChanged;
     }  
     private void Hours_ListChanged(object sender, ListChangedEventArgs e) {     
         OnPropertyChanged("Value"); 
     } 
    

    OnPropertChanged has to have the Value string in it that way DataGridView will update the column.