Master-Detail Binding Support for Custom Object Datasources? (for DataGridView’s)
Hi,
Can VS2005 Beta 2 support having 2 DataGridView’s in a Master-Detail arrangement, but when the underlying data which the DataGridView’s are bound to (albeit via a bindingsource) are custom Lists (e.g. BindingList<T>), where the master list of A objects, each A object contains another list of item B objects. An example of potential A, B classes are shown below (where class Person has a list of Detail objects in it).
If this is possible is there a simple example which can be provided?
BTW - The actual object is to break-up management of a detailed list of data into a Master view (which is summarised) and a drill down Detailed view. I’ve made the assumption above that the best way to implement this is via having a List inside a List. If there is a better approach I’d love to hear about it.
Example classes:
--
BindingList<Person> myList = new BindingList<Person>();
<<populate myList>>
--
using System;
using System.Collections.Generic;
using System.Text;
using System.ComponentModel;
namespace WindowsApplication1
{
class Person
{
private string name;
public string Name
{
get { return name; }
set { name = value; }
}
private int age;
public int Age
{
get { return age; }
set { age = value; }
}
private string occupation;
public string Occupation
{
get { return occupation; }
set { occupation = value; }
}
BindingList<details> detailsList;
public BindingList<details> detailsList
{
get { return detailsList; }
set { detailsList = value; }
}
}
--
using System;
using System.Collections.Generic;
using System.Text;
using System.ComponentModel;
namespace WindowsApplication1
{
class Details
{
private string detailsitem;
public string Detailsitem
{
get { return detailsitem; }
set { detailsitem = value; }
}
}
--
[3736 byte] By [
GregC] at [2007-12-16]
Yes - you need to use object binding to setup binding to your Person type. You can then user the Data Sources window (Data -> Show Data Sources) to drag the Person table onto the Form (it will auto-create the master DataGridView and a PersonBindingSource). Then use the Data Sources window to drag the "Details" table onto the form (it is a child of Person in the Data Sources window) - this will auto-create another DataGridView and create a new child BindingSource (detailsBindingSource).
You can do this manually, by setting up the first BindingSource to bind to the Person type and then add another BindingSource and set it's DataSource to the PersonBindingSource and set it's DataMember to "Details".
At runtime, you only need to set the PersonBindingSource.DataSource to BindingList<Person>.
Joe Stegman
The Windows Forms Team
Microsoft Corp.
This posting is provided "AS IS" with no warranties, and confers no rights.
Thanks Joe - very cool.
Generally speaking if you want to make sure a separate control, which is bound to the same data you are working on, is updated straight away it would seem one needs to trigger this update, for example by:
this.dataGridView1.Invalidate();
this.dataGridView1.Update();
Is this acceptable/best practice in those cases where it may be difficult to determine exactly what cells in teh separate control are affected? (i.e. just do a global invalidate/update instead)
Thanks
Greg
As long as you are using a list that supports change notification (e.g. ADO.NET, BindingList<T>) you won't need to do this. The advantage of BindingList<T> over List<T> is that BindingList<T> has events that notify consumers (controls) when the data has changed. The consumers (controls) will automatically update in response to the change notification.
In general, the best practice for data binding is to make sure your data source supports change notification (is an IBindingList).
Joe Stegman
The Windows Forms Team
Microsoft Corp.
This posting is provided "AS IS" with no warranties, and confers no rights.
Hi Joe,
Just a clarification on the above. I noted that when I have a Master-Detail relationship using BindingList<T> for the collection mechanism, and two datagridview's, that when I update an item in the DETAIL datagridview the implied change does not appear in the MASTER straight away (but appears after you put the cursor into the MASTER). Using an "invalidate"/"update" on the MASTER does fix this.
In particular note that my MASTER custom class has properties which are derived from the DETAIL collection (e.g. number of detail objects). I guess there are two categories here where the master custom class has:
i) property which is based directly on one only property from the DETAIL and
ii) composite property - that is where the value is based on more than one property from the detail custom class, and my have logic to derive the value
Can I ask:
a) Is this behaviour expected (i.e. am I missing something)
b) What is the best practice way to update the Master in this case? Is there a way of flagging the master custom class properties that are based on the detail class to listen for updates (i.e. to avoid a full master datagridview "invalidate")
Thanks again
Greg
I have a reasonable solution but a longer explanation. The short answer is you should implement INotifyPropertyChanged on your business objects.
The longer answer is BindingList (IBindingList) provides list changed notification (e.g. an item was added to the list) as well as list item property changed information (e.g. one of the properties on the item at position n changed). In your case, the BindingList is not firing property changed information because your business objects don't support it. If your business objects supported property changed notifications (via INotifyPropertyChanged), then the grids would update as expected. To support this interface, you need to do the following:
| | class Person : INotifyPropertyChanged { private string name; public string Name { get { return name; } set { if (name != value) { name = value; OnPropertyChanged("Name"); } } } #region INotifyPropertyChanged Members private void OnPropertyChanged(string propertyName) { OnPropertyChanged(new PropertyChangedEventArgs(propertyName)); } protected virtual void OnPropertyChanged(PropertyChangedEventArgs e) { if (null != PropertyChanged) { PropertyChanged(this, e); } } public event PropertyChangedEventHandler PropertyChanged; #endregion } |
Joe Stegman
The Windows Forms Team
Microsoft Corp.
This posting is provided "AS IS" with no warranties, and confers no rights.
Hi Joe,
I tried this approach on my current code but had problems, which I think is due to my approach.
In my master class I have a property which is derived from the values of the details objects, however the manner in which I have this working is via calulation of the result within the master "get" method. e.g.
private float oldestAge;
public float OldestAge
{
get
{
float f = 0;
foreach (mp3DTO m in mp3List)
{
if (f < m.FileAgeDays)
f = m.FileAgeDays;
}
//OnPropertyChanged("OldestAge"); ==> CAUSES PROBLEM
return f;
}
set { oldestAge = value; }
}
When I add in the "OnPropertyChanged" here it causes a startup problem (looks like an endless loop) which probably makes sense.
I'm guessing that what I would need to do the following (but would appreciate you feedback):
(a) put the calculation smarts in the MASTER "set" method - in this case the "value" never really gets used I suppose(?). i.e. "masterObject.oldestAge=0" where the 0 value will never get used as the method will use necessary values from the DETAIL objects to set "oldestAge"
(b) Keep a handle to the MASTER object ("person" in my code above) in each DETAIL object (the "detail" class in my code above)
(c) when any DETAIL object parameters change that impact a MASTER parameter value then, within the SET method of the DETAIL object, call back to the MASTER object parameter to reset it (e.g. masterObject.oldestAge=0").
(d) then ensure the "OnPropertyChanged" is added at the end of the "set" method within the MASTER class property (e.g. within the "oldestAge" set method).
How does this sound? Is this OK or am I missing something?
Thanks
Greg