Winforms datagridview - variable number of columns - INotifyPropertyChanged

96 Views Asked by At

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.

2

There are 2 best solutions below

0
lork6 On BEST ANSWER
 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.

2
IV. On

Based on the go-ahead in your comment, here's a snippet that partially answers your question. A good place to start might be to finish out the implementation of your ListItemDescriptor class so that it maps column names like "Hour[00]" to the array object Day.Hour[0] in both read and write directions.

poc screenshot

It could look something like this:


class ListItemDescriptor : PropertyDescriptor
{
    public ListItemDescriptor(string name) : base(name, new Attribute[0]) { }
    public override Type ComponentType => typeof(Day);
    public override bool IsReadOnly => false;
    public override Type PropertyType => typeof(int);
    public override bool CanResetValue(object component) => true;
    public override void ResetValue(object component)
    {
        if(component is Day day)
        {
            switch (Name)
            {
                case nameof(Day.Number): 
                    day.Number = default; 
                    break;
                default:
                    var index =
                        Convert.ToInt32(_getIndex.Match(Name).Groups[1].Value);
                    day.Hours[index].Value = default; 
                    break;
            }
        }
    }
    Regex _getIndex = new Regex(@"\[(\d+)\]");
    public override object? GetValue(object? component)
    {
        if(component is Day day)
        {
            switch (Name)
            {
                case nameof(Day.Number): return day.Number;
                default:
                    var index = 
                        Convert.ToInt32(_getIndex.Match(Name).Groups[1].Value);
                    return day.Hours[index].Value;
            }
        }
        return default;
    }
    public override void SetValue(object? component, object? value)
    {
        if(component is Day day)
        {
            switch (Name)
            {
                case nameof(Day.Number): day.Number = Convert.ToInt32(value); break;
                default:
                    var index =
                        Convert.ToInt32(_getIndex.Match(Name).Groups[1].Value);
                    day.Hours[index].Value = Convert.ToInt32(value);
                    break;
            }
        }
    }
    public override bool ShouldSerializeValue(object component) => true;
}

I posted a clone link to my POC where I tried the two test cases but it will take more work if columns are going to come and go. One thing I am wondering is, since there are always 24 hours in a day, why not let DGV have a complete set of columns and modify the visibility of the columns instead? Do you have reasons other than making columns hidden or shown where you want to dynamically add and remove them?