Marie
Marie

Reputation: 2217

Using a DataSource with an object in Windows Forms

I have been trying to create a small form application and I wanted to try out binding a DataGridView directly to a collection of objects.

I created the following classes

public class MyClassRepository
{
    public List<MyClass> MyClassList { get; set; } = new List<MyClass> { new MyClass { Name = "Test" } };
}

public class MyClass
{
    public string Name { get; set; }
}

and I added the following code to a form to test. I based this off of the code in the designer after setting the BindingSource through the UI (while following this walk through https://msdn.microsoft.com/en-us/library/ms171892.aspx)

var tmp = new BindingSource();
tmp.DataMember = "MyClassList";
tmp.DataSource = typeof(MyClassRepository);

When this didn't work I started running through the code behind BindingSource to see what was happening. The setter calls ResetList which tries to create a dataSourceInstance by calling ListBindingHelper.GetListFromType. This call ultimately calls SecurityUtils.SecureCreateInstance(Type) where type is a BindingList<MyClassRepository>. This passes null to args which is passed Activator.CreateInstance which returns an empty collection.

After this ListBindingHelper.GetList(dataSourceInstance, this.dataMember) is called. This method calls ListBindingHelper.GetListItemProperties which results in a PropertyDescriptor for my MyClassList property and assigns it to dmProp.

At this point GetList calls GetFirstItemByEnumerable(dataSource as IEnumerable) where dataSource is the previously created (and empty) instance of BindingList<MyClassRepository> and returns (currentItem == null) ? null : dmProp.GetValue(currentItem);.

The value of dmProp/MyClassList is never accessed and the BindingSource is never populated with the instance I created. Am I doing something wrong? If not is there a bug in the source code? It seems to me like either SecureCreateInstance(Type type, object[] args) should be called and MyClassList should be passed via args instead of the existing call to SecureCreateInstance(Type type) or the value of dmProp should be used regardless?

If that is not correct how do I make the Designers automatically generated code set the DataSource to an instance of the object? Or do I have to inherit from BindingSource? If the latter why does it give you the option to choose a class that does not inherit from BindingSource?

Upvotes: 2

Views: 15505

Answers (3)

gcode
gcode

Reputation: 2989

Building on the other answers here, the Visual Studio Designer will create a BindingSource object and set its DataSource property to the Type of the object that you intend to bind. The reason it does this is to inform the Designer of what properties are available to be bound. The Designer has no concept of object instances because your program isn't running at the time.

If you follow Microsoft's How to: Create a simple-bound control on a Windows Form, you might find yourself confused because the Form still doesn't appear to function as if there's any bound properties. Unfortunately, that guide in particular is lacking and does not follow up with the code to complete the goal. As the programmer, it's still your responsibility to create the instance of the object that you intend to bind, and then execute any code that will cause the object to become populated with the data that will eventually appear in your bound Form.

How we do that is best covered by Reza's answer, I think; create a private member in your Form's class that will hold an instance of the object you want to bind. You will then use the Form's Load event to instantiate your object (or acquire the instance by other means if it's created elsewhere in your code), assign it to the private member in your Form, and then set the BindingSource's .DataSource property to your new private object instance. This is the critical step I and I think many others missed, and which could be better documented by Microsoft. For example, take a look at their documentation for the BindingSource class.

binding1.DataSource = fonts
listBox1.DataSource = binding1
listBox1.DisplayMember = "Name"

The only part you need to worry about is that binding1.DataSource line; that is where you assign your backing object to act as the data source.

Upvotes: 2

JohnG
JohnG

Reputation: 9479

As Reza Aghaei points out, in the designer, setting the BindingSource.DataSource to the “MyClassRepository” may work, however you still need to initialize (create a new) MyClassRepository object. I do not see this line of code anywhere in the posted code: MyClassRepository myRepositiory = new MyClassRepository(); The data source is empty because you have not created an instance of “MyClassRepository” and as Reza points out, this is usually done in the forms Load event.

To keep it simple, remove the DataSource for the BindingSource in the designer and simply set up the BindingSource’s data source in the form load event like below. First, create a new “instance” of the MyClassRepository then use its MyClassList property as a data source to the BindingSource. I hope this helps.

MyClassRepository repOfMyClass;

public Form1() {
  InitializeComponent();
}

private void Form1_Load(object sender, EventArgs e) {
  repOfMyClass = new MyClassRepository();
  bindingSource1.DataSource = repOfMyClass.MyClassList;
  dataGridView1.DataSource = bindingSource1;
}

Edit-----

After further review… I agree that you should be able to do as you describe. I was able to get it working as expected with the code below.

BindingSource bindingSource1;
private void Form1_Load(object sender, EventArgs e) {
  bindingSource1 = new BindingSource();
  bindingSource1.DataSource = typeof(MyClassRepository);
  bindingSource1.DataMember = "MyClassList";
  dataGridView1.DataSource = bindingSource1;
}

I followed the same steps in the “designer” and it worked as expected. Is there something else I am missing? As you stated… using MyClassRepository mcr = new MyClassRepository() appears to be unnecessary. In addition, if you cannot get it to work using one of the two ways above… then something else is going on. If it does not work as above, what happens?

Edit 2

Without creating a “new” MyClassRepository, object was unexpected and I did not realize that new items added to the list/grid were going into the bindingSource1. The main point, is that without instantiating a “new” MyClassRepository object, the constructor will never run. This means that the property List<MyClass> MyClassList will never get instantiated. Nor will the default values get set.

Therefore, the MyClassList variable will be inaccessible in this context. Example in this particular case, if rows are added to the grid, then bindingSource1.Count property would return the correct number of rows. Not only will the row count be zero (0) in MyClassList but also more importantly… is “how” would you even access the MyClassList property without first instantiating a “new” MyClassRepository object? Because of this inaccessibility, MyClassList will never be used.

Edit 3 ---

What you are trying to achieve can be done in a myriad number of ways. Using your code and my posted code will not work if you want MyClassList to contain the real time changes made in the grid by the user. Example, in your code and mine… if the user adds a row to the grid, it will add that item to the “bindingSource” but it will NOT add it to MyClassList. I can only guess this is not what you want. Otherwise, what is the purpose of MyClassList. The code below “will” use MyClassList as expected. If you drop the “designer” perspective… you can do the same thing in three (3) lines of code... IF you fix the broken MyClassRepository class and create a new one on the form load event. IMHO this is much easier than fiddling with the designer.

Changes to MyClassRepository… added a constructor, added a size property and a method as an example.

class MyClassRepository {

  public List<MyClass> MyClassList { get; set; }
  public int MaxSize { get; set; }

  public MyClassRepository() {
    MyClassList = new List<MyClass>();
    MaxSize = 1000;
  }

  public void MyClassListSize() {
    MessageBox.Show("MyClassList.Count: " + MyClassList.Count);
  }

  // other list manager methods....

}

Changes to MyClass… added a property as an example.

class MyClass {
  public string Name { get; set; }
  public string Age { get; set; }
}

Finaly, the form load event to create a new MyClassRepository, set up the binding source to point to MyClassList and lastly set the binding source as a data source to the grid. NOTE: making myClassRepository and gridBindingSource global variables is unnecessary and is set this way to check that MyClassList is updated in real time with what the user does in the grid. This is done in the button click event below.

MyClassRepository myClassRepository;
BindingSource gridBindingSource;

public Form1() {
  InitializeComponent();
}

private void Form1_Load(object sender, EventArgs e) {
  try {
    myClassRepository = new MyClassRepository();
    gridBindingSource = new BindingSource(myClassRepository.MyClassList, "");
    dataGridView1.DataSource = gridBindingSource;
  }
  catch (Exception ex) {
    MessageBox.Show("Error: " + ex.Message); 
  }
}

private void button1_Click_1(object sender, EventArgs e) {
  MessageBox.Show("Binding source count:" + gridBindingSource.Count + Environment.NewLine +
                  "MyClassList count: " + myClassRepository.MyClassList.Count);
}

I hope this makes sense. ;-)

Upvotes: 4

Reza Aghaei
Reza Aghaei

Reputation: 125292

Designer sets DataSource = typeof(Something) for design-time support, for example to let you choose DataMember from a dropdown or to let you choose the data source property from dropdown while setting up data-bindings.

How do I make the Designers automatically generated code set the DataSource to an instance of the object?

Forcing the designer to do that doesn't make much sense, because the designer doesn't have any idea about what the real data source you are going to use to load data. It can be a web service, a WCF service, a business logic layer class.

So at run-time you need to assign an instance of your list to DataSource. For example in Load event of the form.

Upvotes: 2

Related Questions