PTWin\RolesEdis.cs: deal with "Index X does not have a value" exceptions

PTWin\RolesEdis.cs: deal with "Index X does not have a value" exceptions

Old forum URL: forums.lhotka.net/forums/t/4125.aspx


so24wo posted on Saturday, January 05, 2008

This issue is addressed at least to:
PTWin\RolesEdis.cs, up to rev.1900 (current)
PTWin\WinPart.cs, up to rev.1697 (current)
.Net 2.0


There is yet another trouble with Project Tracker.

If I try to change roles list with the RolesEdit control so the .Save() method
throws an exception then not only a Csla.DataPortalException is thrown
but also there can be few or more "Index X does not have a value"
exceptions thrown from the RolesDataGridView control.

Even more if I try to do anything causing the RolesDataGridView control to be repainted
(to do this I simply move any messagebox over this control) then the control continues
to throw exceptions and at the end there will be so many messageboxes so the only way
to close Project Tracker will be to kill its process.

To reproduce these things you need to make the .Save() method to throw an exception.
You can, for example, change the Id field of the Project Manager entry from 1 to 2
and then press the Save Button. Then a validation exception will be thrown (the Id is checked
to be unique).
Or you can, for another example, change the Id field of the Project Manager entry from 1 to 111
and then press the Save Button. Then the "Row has been edited by another user" exception
will be thrown (it comes from the design of the [updateRole] stored procedure).


So what is going on here?

When the RolesEdit control saves the roles list it uses these two routines:

from RolesEdit.cs (rev.1900):

    private void SaveButton_Click(object sender, EventArgs e)
    {
      // stop the flow of events
      this.rolesBindingSource.RaiseListChangedEvents = false;
     
      // commit edits in memory and unbind
      UnbindBindingSource(this.rolesBindingSource, true, true);

      try
      {
        _roles = _roles.Save();
        this.Close();
      }
      catch (Csla.DataPortalException ex)
      {
        MessageBox.Show(ex.BusinessException.ToString(),
          "Error saving", MessageBoxButtons.OK,
          MessageBoxIcon.Exclamation);
      }
      catch (Exception ex)
      {
        MessageBox.Show(ex.ToString(), "Error saving",
          MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
      }
      finally
      {
        this.rolesBindingSource.DataSource = _roles;

        this.rolesBindingSource.RaiseListChangedEvents = true;
        this.rolesBindingSource.ResetBindings(false);
      }
    }

and from WinPart.cs (rev.1697):

    protected void UnbindBindingSource(BindingSource source, bool apply, bool isRoot)
    {
      System.ComponentModel.IEditableObject current =
        source.Current as System.ComponentModel.IEditableObject;
      if (isRoot)
        source.DataSource = null;
      if (current != null)
        if (apply)
          current.EndEdit();
        else
          current.CancelEdit();
    }

As you can see the rolesBindingSource.DataSource is set to null before the .Save() method call.
And when the .Save() method throws an exception the RolesDataGridView has no data to show its content.
But the RolesDataGridView control have to show itself to user and also it
thinks that it is not empty because the rolesBindingSource didn't say to it about that fact
(because we set rolesBindingSource.RaiseListChangedEvents to false ).
And the RolesDataGridView control continues to fetch data for its cells
and, of cource, an exceptions are thrown.


So what can we do with all of that?


The first solution I'd checked was to allow the rolesBindingSource to say
to the RolesDataGridView that it is unbinded.
To do this we only need to swap two first calls in the SaveButton_Click() method.

The code for the first solution is here:

    private void SaveButton_Click( object sender, EventArgs e )
    {
      // commit edits in memory and unbind
      UnbindBindingSource( this.rolesBindingSource, true, true );

      // stop the flow of events
      this.rolesBindingSource.RaiseListChangedEvents = false;

      try
      {
        _roles = _roles.Save();
        this.Close();
      }
      catch ( Csla.DataPortalException ex )
      {
        MessageBox.Show( ex.BusinessException.ToString(),
          "Error saving", MessageBoxButtons.OK,
          MessageBoxIcon.Exclamation );
      }
      catch ( Exception ex )
      {
        MessageBox.Show( ex.ToString(), "Error saving",
          MessageBoxButtons.OK, MessageBoxIcon.Exclamation );
      }
      finally
      {
        this.rolesBindingSource.DataSource = _roles;
        this.rolesBindingSource.RaiseListChangedEvents = true;
        this.rolesBindingSource.ResetBindings( false );
      }
    }

But there is a small issue with this solution.
While we gaze at an exception messagebox the RolesDataGridView control shows
an empty grid on the background.


To solve this issue we must not assign rolesBindingSource.DataSource to null at all.
I think we can do this without any aftereffects. This is my assumption.

To do this we can call UnbindBindingSource() with isRoot=false.
I think it doesn't look pretty because of our list is a root object by design.

But we stil need to flush any pending edits from the rolesBindingSource to the underlying _roles object.

So I introduce another method of the WinPart class and the code of the second solution:

WinPart.cs:

    protected void FlushBindingSource( BindingSource source, bool apply )
    {
      // Call methods for every editing row in BindingSource. not only for current one.
      // I think there will be grid controls with multilines editing capabilities
      // available in the future...

      if ( apply )
        source.EndEdit(); // This calls .IEditableObject.EndEdit()
      else
        source.CancelEdit(); // This calls .IEditableObject.CancelEdit()
    }

RolesEdit.cs:

    private void SaveButton_Click( object sender, EventArgs e )
    {
      // stop the flow of events
      this.rolesBindingSource.RaiseListChangedEvents = false;

      try
      {
        // commit edits in memory
        FlushBindingSource( this.rolesBindingSource, true );

        _roles = _roles.Save();
        this.Close();
      }
      catch ( Csla.DataPortalException ex )
      {
        MessageBox.Show( ex.BusinessException.ToString(),
          "Error saving", MessageBoxButtons.OK,
          MessageBoxIcon.Exclamation );
      }
      catch ( Exception ex )
      {
        MessageBox.Show( ex.ToString(), "Error saving",
          MessageBoxButtons.OK, MessageBoxIcon.Exclamation );
      }
      finally
      {
        if ( _roles != null )
          this.rolesBindingSource.DataSource = _roles;
        else
          this.rolesBindingSource.DataSource = "";

        this.rolesBindingSource.RaiseListChangedEvents = true;
        this.rolesBindingSource.ResetBindings( false );
      }
    }

Notice a little trick with the .DataSource set to "".
If the .Save() method was so unsuccessful so it returns null, then I prefer to show to user
an absolutely empty grid. With a columns headers only, but without a row for adding
a new data. (Recall this.AllowNew = true; in Roles() constructor.)
The rolesBindingSource.DataSource is set to typeof(ProjectTracker.Library.RoleList)
at design time (RolesEdit.Designer.cs) and so can we do.
But an empty string "" is enough for me here. It does the same and I don't need to remember
about any 'typeof(ProjectTracker.Library.RoleList)' code.

Remember also if you use the LocalProxy then you should add this string to the <appSettings>
section of the app.config file in the PTWin project:
    <add key="CslaAutoCloneOnUpdate" value="true">

Or you'll be required to clone an object manually:
        Roles temp = _roles.Clone();
        _roles = temp.Save();

 

That's all about these solutions. And now I have two questions:

1. Are there any case when we must set xxxBindingSource.DataSource to null before cloning an object?

2. Why there is used the BindingSource.Current property instead of the whole BindingSource in the UnbindBindingSource() ?


---
Regards,
Oleg

so24wo replied on Monday, January 07, 2008

There is the bug in this code.

If the _roles object was initialized to non null value with Roles.GetRoles()
then there is no way to revert _roles back to null because the .Save() method
must never return null, instead it must throw an exception on any error.

So the code for finally block can be little lighter:

      finally
      {
        if ( _roles != null )
          this.rolesBindingSource.DataSource = _roles;

        this.rolesBindingSource.RaiseListChangedEvents = true;
        this.rolesBindingSource.ResetBindings(false);
      }

Have fun ;)

---
Regards,
Oleg

Copyright (c) Marimer LLC