Creating a custom collection that can be bound to

2019-05-29 01:57发布

问题:

I work for an Architecture firm and I am creating a plug-in for a 3D modeling program to assist design. I have a Building class, and a Floor class. The building contains a reference to a FloorList collection of floors. I'm trying to figure out what to base the FloorList collection off of so that I can minimize the amount of work I need to do to create an interface to edit the collection.

The Floor collection represents a series of building floors that are stacked on top of one another. Each Floor has a Floor.Height property that is read write, and an Floor.Elevation property that is read only and set by totaling the floor heights below the current Floor. So, whenever a Floor is added, removed, moved, or changed in the collection the Floor.Elevation properties need to be updated.

In addition, I want to create a UI to edit this collection. I was thinking of using a DataGrid control where each Floor is listed with its Height and other properties as a row of the control. The user should be able to add, remove, and re order floors using the control. I'd like setting this up to be as easy and flexible as possible. Meaning I'd like to simply be able to bind the collection of Floors to the DataGrid and have the columns of the DataGrid populate based on the properties of the Floor class. If possible I'd like to be able to utilize the built in Add/Remove UI interface of the DataGrid control with out having to mess with setting up a bunch of event relationships between my collection and the DataGrid.

To complicate things further in the future I'll need to be able to allow the user to dynamically add custom properties to the Floors that I'll want them to be able to see and edit in the DataGrid as well. I think I'd end up doing that by having Floor class implement IExtenderProvider. So ultimately the DataGrid would look something like this:

Initial Properties      Future Custom User Properties

Height    Elevation     ProgramType    UnitType  UnitCount  
15'       70'           Residential    Luxury    5
15'       55'           Residential    Luxury    5
15'       40'           Residential    Budget    10
20'       20'           Retail         N/A       2
20'       0'            Retail         N/A       3

My question now is what should I base my FloorList collection off of to allow for this functionality? The options I'm considering are as follows.

1) Inherit from List(Floor)

  • Methods such as Add/Remove are not vitrual and thus I can't not override them to update the elevations

2) Implement IList(Floor)

  • OnChange event is not built in so if the lists changes the DataGrid will not update (I think?)

  • I think this is probably the best option but what would I need to do the ensure that changes to the FloorList collection or DataGrid are synced with one another?

3) Inherit from BindingList(Floor)

  • Method like Add/Remove are not virtual so I can't modify them to update the floor elevations.

4) Implement IBindingList

  • IBindinglist is not generic and I only want my collection to contain Floor objects

回答1:

You should use BindingList for your collection or implement IBindingList as this will notify back the DataGridView about any changes in the list.

Then implement INotifyPropertyChanged interface for Floor class, this will allow for a TwoWay binding mode between your individual Floor items and the DataGridView.

Eric, you could also do something like this

   public class MyFloorCollection : BindingList<Floor>
            {
                public MyFloorCollection()
                    : base()
                {
                    this.ListChanged += new ListChangedEventHandler(MyFloorCollection_ListChanged);

                }

                void MyFloorCollection_ListChanged(object sender, ListChangedEventArgs e)
                {

                 if (e.ListChangedType == ListChangedType.ItemAdded)
                 {

                    Floor newFloor = this[e.NewIndex] as Floor;

                    if (newFloor != null)
                    {
                        newFloor.HeightChanged += new Floor.HeightChangedEventHandler(newFloor_HeightChanged);
                    }
                  }

                }

                void newFloor_HeightChanged(int newValue, int oldValue)
                {
                    //recaluclate
                }


            }

Of course you can create your own HeightChangedEvent and subscribe to that, that way you dont have to go by property names in an if statement.

So your Floor class will look like this

 public class Floor : INotifyPropertyChanged
        {
            private int _height;

            public int Height
            {
                get { return _height; }
                set 
                {
                    if (HeightChanged != null)
                        HeightChanged(value, _height);

                    _height = value;
                    OnPropertyChanged("Height");

                }
            }




            public int Elevation { get; set; }

            private void OnPropertyChanged(string property)
            {
                if (this.PropertyChanged != null)
                    this.PropertyChanged(this, new PropertyChangedEventArgs(property));
            }

            #region INotifyPropertyChanged Members

            public event PropertyChangedEventHandler PropertyChanged;

            #endregion

            public delegate void HeightChangedEventHandler(int newValue, int oldValue);
            public event HeightChangedEventHandler HeightChanged;
        }

this way you only have to subscribe to your HeightChanged variable, instead of to PropertyChanged. PropertyChanged will be consumed by the DataGridView to keep a TwoWay binding. I do believe this way is cleaner.

You can also change the delegate and pass the item as the sender.

public delegate void HeightChangedEventHandler(Floor sender, int newValue, int oldValue);

EDIT: To unsubscribe from HeightChanged event you need to override RemoveItem

  protected override void RemoveItem(int index)
        {
            if (index > -1)
                this[index].HeightChanged -= newFloor_HeightChanged;

            base.RemoveItem(index);
        }


回答2:

Implement IList(Floor), and have your new collection also implement INotifyPropertyChanged interface.



回答3:

You could try exposing an ObservableCollection<Floor> property on some data containing class. Also, the Floor object would need to implement INotifyPropertyChanged to support two-way binding from the UI.