Monday, December 31, 2012

Passing Data Between Forms Redux

Almost exactly two years ago, I published the Passing Data Between Forms post. I have had numerous questions and comments about that post and now I’ve decided that it deserves an additional example.

In that post, I had an example (the 3rd one) that utilized Interfaces. In the example, we had a MainForm that passed a DataSet to Form1. Form1 then allowed the user to make some changes to the data in that DataSet and then we wanted to have MainForm automatically reflect those changes ... even while the user was still working in Form1. An interface worked well for that scenario. At the time, I wanted to keep the post simple so as not to confuse beginners. But, I really need to expand on that example and show how to accomplish this task using delegates/events.

So, without further ado (pun intended), here it is:

First, let's take a look at Form1. Minimally, you can implement this whole thing with a simple event, which uses standard EventArgs, as follows:

public class Form1 : Form
{
    public event EventHandler DataChanged;
    private CustomerDataSet oData;

    public Form1(CustomerDataSet dsCust)
    {
        this.oData = dsCust;
    }
    public void DoStuff()
    {
        // code to do stuff with this.oData
        // ...
        // and then fire the event if anyone has subscribed
        this.OnDataChanged(new EventArgs());
    }
    private void OnDataChanged(EventArgs e)
    {
        if (this.DataChanged != null)
            this.DataChanged(this, e);
    }
}

Then, the MainForm looks like this:

public class MainForm : Form
{
    private CustomerDataSet dsCustomer;

    // ...

    // then code elsewhere to instantiate and fill your data
    this.dsCustomer = new CustomerDataSet();
    // plus maybe other code to fill the dataset


    // ...

    // code to instantiate Form1, pass it the DataSet, handle the event
    Form1 oForm = new Form1(this.dsCustomer);
    oForm.DataChanged += new EventHandler(DataChanged);
    oForm.Show();

    public void DataChanged(object sender, EventArgs e)
    {
        // code here to do stuff with this.dsCustomer
    }
}

In this particular case, we don't really need EventArgs. Because we passed the DataSet into Form1 to begin with, we already know everything we need to know, namely the data that has changed will be the same DataSet that we passed into Form1. So, since we don’t need any EventArgs, we could create a custom delegate to use as our event handler. We will change our two forms like this:

Form1:

public class Form1 : Form
{
    public delegate void DataChangedEventHandler();
    public DataChangedEventHandler DataChanged;
    private CustomerDataSet oData;

    public Form1(CustomerDataSet dsCust)
    {
        this.oData = dsCust;
    }
    public void DoStuff()
    {
        // code to do stuff with this.oData
        // ...
        // and then fire the event if anyone has subscribed
        this.OnDataChanged();
    }
    private void OnDataChanged()
    {
        if (this.DataChanged != null)
            this.DataChanged();
    }
}

The only difference in MainForm is that the event handler doesn't need the usual parameters (object sender, EventArgs e), so change the DataChanged event handler to look like this:

// code to instantiate Form1, pass it the DataSet, handle the event
Form1 oForm = new Form1(this.dsCustomer);
oForm.DataChanged += new Form1.DataChangedEventHandler(DataChanged);
oForm.Show();

public void DataChanged()
{
    // code here to do stuff with this.dsCustomer
}

Another way of handling the event in the MainForm is to not even bother with the above DataChanged() method and use an anonymous delegate instead, so you could change the MainForm code to look like this:

// code to instantiate Form1, pass it the DataSet, handle the event
Form1 oForm = new Form1(this.dsCustomer);
oForm.DataChanged += delegate
{ 
    // code here to do stuff with this.dsCustomer
};
oForm.Show();

There are other things you can do when creating your own delegates and/or events and, in fact, I have a written a blog post about a DataAccess class that does some interesting things with anonymous delegates: DataAccess - Part III.

However, expanding on ideas for delegates and/or events is beyond the scope of this simple post. Perhaps in the future I’ll write something more extensive, but in the meantime, you can always start off with an MSDN article such as this one: Handling And Raising Events

Sunday, December 16, 2012

ComboBox Gotchas

This is pretty well-known for experienced Windows Forms developers, but I still see the problem frequently asked about on the forums. Typically, the questions will be something like:

  1. “Every time I make a different selection from my ComboBox, the row in my grid changes.”
  2. “I have two ComboBoxes on my Form, but they’re in sync. Every time a selection is made in  one, the selection changes in the other.”

So, what is it that they’re doing wrong? Well, the DataSource of a ComboBox cannot be in use by any other databound control on the Form … unless, of course, your intention is to use the ComboBox for navigation … but then, you wouldn’t be asking these questions!

It doesn’t matter if you are using an actual DataTable for your Combo’s DataSource, or if you’re using a BindingSource, the bottom line is that you cannot use the same object to databind other controls. How about a few illustrative examples, first with DataTable and then with BindingSource? In my scenario, I’ll use 3 controls, a DataGridview, a TextBox and a ComboBox. I want the DataGridView and the TextBox to be in sync. When I move through the grid, the value in the TextBox changes accordingly. The ComboBox, on the other hand, should behave independently.

Using DataTables as DataSource

First, let’s set up the databinding for the grid and textbox:

this.dataGridView1.DataSource = this.dtMyTable;
this.txtLastName.DataBindings.Add("Text", this.dtMyTable, "Code");

And now, in order to see how NOT TO bind the ComboBox, try the following code and notice what happens! 

// As we now know, by setting the ComboBox.DataSource to a DataTable that's already bound elsewhere
// moving through the Combo also affects the bound textbox and the grid. This is BAD CODE!!
this.cboDescription.DisplayMember = "Description";
this.cboDescription.DataSource = this.dtMyTable;

Well, ok, it’s bad. What can we do instead? We have two choices:

// Setting the DataSource to a DataView of the same DataTable works just fine
DataView dv = new DataView(this.dtMyTable);
this.cboDescription.DisplayMember = "Description";
this.cboDescription.DataSource = dv;

-OR-

// We could use a Copy of the original DataTable
this.cboDescription.DisplayMember = "Description";
this.cboDescription.DataSource = this.dtMyTable.Copy();

Either way gives you the correct results: when the user changes the selection in the Combo,  nothing at all happens to the grid or textbox.

Using BindingSources as DataSource

Getting the same results using BindingSources is similar. First, set up the binding of the grid and textbox:

BindingSource bs = new BindingSource();
bs.DataSource = this.dtMyTable;
this.txtLastName.DataBindings.Add("Text", bs, "Code");
this.dataGridView1.DataSource = bs;

And again, in order to see how NOT TO bind the ComboBox, try the following code and notice what happens!

// Now the Combo DataSource: As expected, using the same BindingSource doesn't work. BAD CODE
this.cboDescription.DisplayMember = "Description";
this.cboDescription.DataSource = bs;

And what can we do instead?

// A new BindingSource works fine
this.cboDescription.DataSource = new BindingSource(this.dtMyTables, null);

And here’s an interesting one to try:

// A new BindingSource based on the old BindingSource
// Yep! This one works too!
this.cboDescription.DataSource = new BindingSource(bs, null);

Binding the ComboBox

Now, there’s one more thing I’d like to explore. What do you code if you need to have the SelectedValue of the ComboBox be tied to the what’s being displayed in the grid (or perhaps elsewhere, say maybe a label). Previously, we have only set the Combo’s DataSource, but this doesn’t do anything with whatever the user has selected from the Combo. So now we want to databind it, as we’ve databound the grid and the textbox. Let’s say we have another control, a label, and we want it to reflect the chosen Description in the ComboBox. We’d need to bind the label and Combo to the same thing.

// Using the DataTable:
this.lblDescription.DataBindings.Add("Text", this.dtMyTable, "Description");
this.cboDescription.DataBindings.Add("SelectedValue", this.dtMyTable, "Description");

// Or, using the BindingSource instead:
this.lblDescription.DataBindings.Add("Text", bs, "Description");
this.cboDescription.DataBindings.Add("SelectedValue", bs, "Description");

Try either methodology. Select an item from the Combo and notice that the label changes to reflect that selection. Then, move between rows in the grid. You should notice the label changes then too. When on a different row in the grid, change the selection of the Combo again. When moving back and forth between rows in the grid, notice that the ones you changed with the Combo selection remain at that new value.