Roy Osherove

View Original

Winforms Data Binding Woes

I recently needed to get into  Data Binding in win forms. I’ve read several articles(1,2,3) and came up with several conclusions/questions: 

 

1.      Win forms Data Binding is very cool and powerful

Using The BindingContext and CurrencyManager lets you do some powerful in a very simple way.

2.      You can easily create a master-details page using two datagrids , and  dataset containing a parent and child tables and a DataRelation object

3.      You cannot easily create a master-details form if you want a control other than a DataGrid to represent your Parent Table.

 

About item No.3

So why can’t you, for example, just slap on a form a ListBox and a DataGrid and make the ListBox change what the DataGrid shows? The answer is pretty simple, although I never would have figured it out myself;

When you bind a DataGrid to a relation in a dataset, the DataGrid tries to find all the controls on the form that use that relation’s Parent Table as their DataMember. It then retrieves the CurrencyManager Associated with those controls and creates Ad-Hoc Event Handlers to these to its “CurrentChanged” Event, thus allowing itself to react to the user moving between separate parent records.

 

Here’s the catch – A ListBox or ComboBox or TreeView control dones not have a DataMember property so any DataGrid that is on the form will not react when the user moves to a different item on that control. Sad but true.

So what do you do? How do you still make the DataGrid respond?

 

The short answer is that you have to manually set the BindingManager’s Position property (Which tells the bound controls on the form what records they are currently watching). You need to retrieve the Current Row in your ListBox and loop through the BindingManager rows until you find it.

Yeah, totally ugly. I couldn’t figure out a different way. I’m almost sure I’m missing something here.

 

What I did manage to come up with is a helper class that has but one static method that does all the hard work for you.

With this class in place, you can call it like this on your control’s IndexChange event:

      private void listBox1_SelectedIndexChanged(object sender, System.EventArgs e)

      {

            BindingManagerHelper.Synchronize(listBox1.SelectedItem,BindingContext,m_ds,"MyParentTable","MyPKColumn");

      }

 

Here’s the code for the class:

public class BindingManagerHelper

{

      public static void Synchronize(object CurrentRowView,

                                          BindingContext bindContext,

                                          object DataSource,

                                          string DataMember,

                                          string PrimaryKeyName)

      {

            //retrieve the Binding Manager that relates to this Data source and this Data member

            BindingManagerBase bm = bindContext[DataSource,DataMember];

            DataRow row =null;

           

            //A bound listbox can hold a DataRowView object

            //but sometimes you might just have a DataRow object inside

            //your control's Tag property.

            //this allows you to send either one.

 

            if(CurrentRowView is DataRow)

            {row = (DataRow)CurrentRowView;     }

 

            if(CurrentRowView is DataRowView )

            {row = ((DataRowView)CurrentRowView).Row;}

 

            if(row==null)

                  throw new NotSupportedException(

                              "This object is not suppoerted for this method");

           

            //Recover the value we will look for when looping

            //through the BindingContext, so we know when to stop

            string WantedValue = row[PrimaryKeyName].ToString();

 

            //Start looping from the first record

            bm.Position=0;

            while(true)

            {

                  try

                  {

                        //Check whether the current row is the one we are looking for

                        DataRow data=((DataRowView)bm.Current).Row;

                        if (data[PrimaryKeyName].ToString() == WantedValue)

                        {

                              break;

                        }

                  }

                  catch(Exception ex){}

                  //Stop looping if you get to the last record

                  if(bm.Position==bm.Count-1)

                        break;

                  //Advance the BindingContext to the next record

                  bm.Position++;

            }

      }          

}

 

If anyone has a better idea of how to overcome this problem, I’d sure like to know, because I have a distinct feeling this is not an optimal solution (especially if you consider that every time we advance the binding context by one, all the controls on the form refresh to display the new data (Consider having thousands of DataRows!).

What does make sense here is to derive from that ListBox or that TreeView and make a Data Bindable Control. This technique is very well documented in Dino Esposito’s MSDN column from Feb’ 2002, but unfortunately, that solution does not fit the need here. What he shows there is reacting to a CurrentChanged event in a custom control, and what we are trying to do here is to generate such an event. Even if I did create such a control, what way would I have to know the position of the DataRow that was just clicked on my control without resorting to looping?

Again – another mystery. Hopefully, I’ll get some feedback on this and have some news on the subject…