Monday, February 27, 2006

What to do when BindingNavigator Raises Exception on AddNew

I got a great question from a reader recently. It's essence reads like this:

If I set up drag and drop data binding to a table that has non-nullable columns, and then press the Add New button twice in the BindingNavigator, I get an unhandled exception on the thread. Since all of the code involved in that call chain is in .NET code and assemblies, how can I handle the exception to keep it from blowing up my app?

If you are not already familiar, to get to this point, you have to create a data bound UI using the Data Sources window, or by hooking up the controls manually. What you end up with after dragging a collection from the Data Sources window onto a form is:

  • A DataGridView or Details form of individual controls
  • A BindingSource component that is set as the data source of the grid or the individual controls
  • A BindingNavigator control that is hooked up to the BindingSource component.

If your data source is a typed data set in the same project, you also get a table adapter instance and data set instance as members on the form, and a Form.Load event handler that fills the appropriate table of the data set so that the app functions without any hand written code. If your data source is coming from a different assembly (an Object data source), then it will be up to you to go retrieve an instance of the collection type and set it as the DataSource property on the BindingSource at runtime to complete the data binding chain.

The way the BindingNavigator gets hooked up, it just points to the BindingSource component and uses the API exposed by a BindingSource to navigate forward and back and to add and delete items from the underlying collection. When you press the Add New button on the BindingNavigator, it calls the AddNew method on BindingSource. The BindingSource passes the call to the underlying collection if it implements the IBindingList interface. Calling AddNew usually also implicitly calls EndEdit on the current item if that item type implements the IEditableObject interface, depending on the collection type's implementation of the AddNew method.

So when dealing with a data table as your collection, you are actually bound to its default DataView. The DataView class implements the IBindingList interface, and the DataRowView class (the items in the collection) implement IEditableObject. When a column in the table is set up so that it does not accept null values, the DataRowView implementation of EndEdit will throw and exception when EndEdit is called if the non-nullable columns have not been provided a value.

The call chain that sets all this up for a standard data set based application is that the BindingNavigator calls into the BindingSource and calls AddNew. This calls into the DataView and adds a new row to the table and starts an editing transaction by calling BeginEdit on the row. When you press the AddNew button a second time, EndEdit is called on the first row you added, which, if you haven't filled in the non-nullable columns, will throw an exception. Since the call chain goes from BindingNavigator to BindingSource to DataView to DataRowView, there is no user code in the call chain where you can logically insert an exception handler.

You could handle the situation in a crude form by having an Application.ThreadException handler, which will catch all unhandled exceptions on the thread. However, this doesn't get called until the stack has unraveled all the way back out to the base of the call stack, so it is a little late to be dealing with the exception in a recoverable way.

A better solution is to inherit from the BindingSource component and provide your own implementation to AddNew. The following implementation (thanks to Steve Lasker and Daniel Herling on the product team in Redmond for coming up with this) shows how:

private class MyBindingSource : BindingSource

{

   public MyBindingSource()

      : base()

   {

   }

 

   public override object AddNew()

   {

      object o = null;

      try

      {

          o = base.AddNew();

      }

      catch (System.Exception ex)

      {

          this.OnDataError(

             new BindingManagerDataErrorEventArgs(ex));

      }

      return o;

   }

}

With this in place, you can just handle the DataError event on the BindingSource component to do whatever is appropriate based on the exception.





Friday, March 17, 2006 6:47:37 AM (GMT Standard Time, UTC+00:00)
Somewhat related to this problem, I have a master-details form (Invoice-InvoiceLine) with a DataGridView and a BindingNavigator set up to add InvoiceLines to the current Invoice. However the problem I'm getting is for the second InvoiceLine you add to the Invoice, EndEdit() is being called via BindingSource.AddNew() as you described. However if the user has not yet added all the mandatory Invoice fields, EndEdit() throws an error. So when I use MyBindingSource, base.AddNew() always fails and I can't add any more InvoiceLines. Do you have any clever ideas to get around this? Or do you think I'm doing something fundamentally wrong? :-)

Thanks a lot.
Nick
Monday, April 10, 2006 10:02:58 PM (GMT Standard Time, UTC+00:00)
This answer doesnt seem to address the whole problem.
The first time you hit the AddNew button on the Navigator it will successfully create a new record and move focus to it.
If after that you then click the MovePrevious button for example, the same issue will occur as if you had hit the AddNew button a second time, but now the error is located in the MovePrevious() method of the BindingSource instead of the AddNew() method, and MovePrevious() is not overridable.
Cory Nabours
Wednesday, July 12, 2006 1:52:42 AM (GMT Standard Time, UTC+00:00)
I solve this problem by supplying default values to the new row at the time it is created. Thus, there are no null fields to throw and error.

More specifically, I overrode the AddNew of the DataView Class so it calls a SetDefaultValues() which does the actual work.

Greg Gum
Wednesday, July 19, 2006 9:30:57 PM (GMT Standard Time, UTC+00:00)
Here is more about the above as I have gotten several questions about how to do this:

I don't depend on the DataTable.DefaultView to supply the DataView.
My approach is to code everything by hand, ie I subclass the DataTable
class for a specific table, then I subclass the DataSet class and
create a specific DataSet for one or more tables as well as creating
the DataView by hand in the subclassed DataSet. In the end, I bind my
grid to the subclassed DataView. So I never use the DefaultView. In
code it looks something like this:

public class DataTable_Customers : GDataTable
{

protected override void Initialize()
{
this.TableName = CustomerTable.TableName;
this.KeyFieldName = CustomerTable.UniqueKey.FieldName;
this.SelectKeyFieldName = CustomerTable.UniqueKey.FieldName;
}


protected override void SetDefaultValues(System.Data.DataRowView drv)
{

if (drv.Row.IsNull(this.KeyFieldName))
{
drv[this.KeyFieldName] =
KeyProvider.GetNewKey(this.TableName, this.KeyFieldName);
drv[CustomersTable.Customer = "" ;
}
}

...

The DataSet Class looks like this:

public class DataSet_Customers : GDataSet
{
public DataTable_Customers Customers ;
public GDataView Customers_DV;


public override int Populate()
{
int tally = 0;
Customers = new DataTable_Customers();
this.Tables.Add(Customers);

tally = Customers.GetTable();
Customers_DV = new GDataView(Customers, "",
CustomerTable.Customer.FieldName, DataViewRowState.CurrentRows);

return tally;
}

}

In the subclassed DataView, I have this code which is the override:

public override DataRowView AddNew()
{
DataRowView dr = base.AddNew ();
this.gDataTable.OnAdd(dr);
return dr;
}

The OnAdd() passes it off to the DataTableClass noted above which then
supplies the default values.

Works great for me, I can put as many DataTables and DataViews in the
DataSet as I need and it solves the problem of the default values for
new rows.


Greg Gum

Greg Gum
Comments are closed.



















Sign In
Copyright © 2006-2007 Brian Noyes. All rights reserved.
designed by NUKEATION STUDIOS