Error when pressing escape key on a filtered list bound datagridview

Error when pressing escape key on a filtered list bound datagridview

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


Helius posted on Friday, January 11, 2013

In my Windows Form Application i have a datagridview bound to an Editable Child List representing a list of tariffs. I have some custom filters implemented through comboboxes and based on the values i select i build a filtered list for the tariffs.

        Dim mFiltered As New Csla.FilteredBindingList(Of ApTariff)(oApmailItemCharacteristic.ApTariffs)

        mFiltered.FilterProvider = AddressOf FilterTariff
        mFiltered.ApplyFilter("", oCriteria)
        Dim mSortedTariff As New SortedBindingList(Of ApTariff)(mFiltered)
        mSortedTariff.ApplySort("WeightFrom", ListSortDirection.Ascending)
        ApTariffsBindingSource.DataSource = mSortedTariff

The save for the object graph is working ok when editing existing rows on the tariffs grid, but when i try to add new rows i got an error of type "Edit Level Mismatch"

Also if the datagridview is empty because of the selection from comboboxes  and i try to add a new row and press the escape key i got the following error:

System.ArgumentOutOfRangeException was unhandled
  Message=Index was out of range. Must be non-negative and less than the size of the collection.
Parameter name: index
  Source=mscorlib
  ParamName=index
  StackTrace:
       at System.ThrowHelper.ThrowArgumentOutOfRangeException()
       at System.Collections.Generic.List`1.get_Item(Int32 index)
       at Csla.FilteredBindingList`1.OriginalIndex(Int32 filteredIndex)
       at Csla.FilteredBindingList`1.System.ComponentModel.ICancelAddNew.CancelNew(Int32 itemIndex)
       at Csla.SortedBindingList`1.System.ComponentModel.ICancelAddNew.CancelNew(Int32 itemIndex)
       at System.Windows.Forms.BindingSource.System.ComponentModel.ICancelAddNew.CancelNew(Int32 position)
       at System.Windows.Forms.CurrencyManager.CancelCurrentEdit()
       at System.Windows.Forms.DataGridView.DataGridViewDataConnection.CancelRowEdit(Boolean restoreRow, Boolean addNewFinished)
       at System.Windows.Forms.DataGridView.CancelEditPrivate()
       at System.Windows.Forms.DataGridView.CancelEdit(Boolean endEdit)
       at System.Windows.Forms.DataGridView.ProcessEscapeKey(Keys keyData)
       at System.Windows.Forms.DataGridView.ProcessDataGridViewKey(KeyEventArgs e)
       at System.Windows.Forms.DataGridView.OnKeyDown(KeyEventArgs e)
       at System.Windows.Forms.Control.ProcessKeyEventArgs(Message& m)
       at System.Windows.Forms.DataGridView.ProcessKeyEventArgs(Message& m)
       at System.Windows.Forms.Control.ProcessKeyMessage(Message& m)
       at System.Windows.Forms.Control.WndProc(Message& m)
       at System.Windows.Forms.DataGridView.WndProc(Message& m)
       at System.Windows.Forms.Control.ControlNativeWindow.OnMessage(Message& m)
       at System.Windows.Forms.Control.ControlNativeWindow.WndProc(Message& m)
       at System.Windows.Forms.NativeWindow.DebuggableCallback(IntPtr hWnd, Int32 msg, IntPtr wparam, IntPtr lparam)
       at System.Windows.Forms.UnsafeNativeMethods.DispatchMessageW(MSG& msg)
       at System.Windows.Forms.Application.ComponentManager.System.Windows.Forms.UnsafeNativeMethods.IMsoComponentManager.FPushMessageLoop(IntPtr dwComponentID, Int32 reason, Int32 pvLoopData)
       at System.Windows.Forms.Application.ThreadContext.RunMessageLoopInner(Int32 reason, ApplicationContext context)
       at System.Windows.Forms.Application.ThreadContext.RunMessageLoop(Int32 reason, ApplicationContext context)
       at System.Windows.Forms.Application.Run(ApplicationContext context)
       at Microsoft.VisualBasic.ApplicationServices.WindowsFormsApplicationBase.OnRun()
       at Microsoft.VisualBasic.ApplicationServices.WindowsFormsApplicationBase.DoApplicationModel()
       at Microsoft.VisualBasic.ApplicationServices.WindowsFormsApplicationBase.Run(String[] commandLine)
       at MHS.Desktop.My.MyApplication.Main(String[] Args) in 17d14f5c-a337-4978-8281-53493378c1071.vb:line 81
       at System.AppDomain._nExecuteAssembly(RuntimeAssembly assembly, String[] args)
       at System.AppDomain.ExecuteAssembly(String assemblyFile, Evidence assemblySecurity, String[] args)
       at Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly()
       at System.Threading.ThreadHelper.ThreadStart_Context(Object state)
       at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean ignoreSyncCtx)
       at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
       at System.Threading.ThreadHelper.ThreadStart()
  InnerException:

 

 

 

 

JonnyBee replied on Sunday, January 13, 2013

Which version of CSLA do you use? 

In windows forms it is essential to bind/unbind properly before calling Save. This is the most common reason for EditLevelMismatch Exception.

See my post: http://jonnybekkum.wordpress.com/2009/10/20/forms-databinding-the-magic-sequence-of-binduiunbindui/

Helius replied on Sunday, January 13, 2013

Hi Jonny, 

I have tried with the latest version of CSLA 4.5,  and the problem persists. I am using CSLAExtender to manage the  bind/unbind of bindingsources. The save is working ok when i filter, edit data in the grid, filter, edit again and save in the end. The problem occurs only when i try to add any new rows in the filtered datagrid. 

Also there is that problem when adding a new row in the empty datagrid, and directly pressing ok. I think may be these two errors are related, and probably i am missing something there.

 

Cymro replied on Tuesday, May 14, 2013

I get this too...but I don't think it is an "Edit Level Mismatch" type exception.  I believe that the issue is in the FilteredBindingList (and posibly the SortedBindingList) implementation of ICancelAddNew.CancelNew (which is called by the DataGridView binding).  

For some reason the DataGridView calls CancelNew with an index of -1 when the list is empty. and this causes an issue in the FilteredBindingList.OriginalIndex function.  It attempts to find the OriginalIndex based on an index of -1.

 void ICancelAddNew.CancelNew(int itemIndex)
    {
      ICancelAddNew can = _list as ICancelAddNew;
      if (can != null)
        can.CancelNew(OriginalIndex(itemIndex));
      else
        _list.RemoveAt(OriginalIndex(itemIndex));
    }

  private int OriginalIndex(int filteredIndex)
    {
      if (_filtered)
        return _filterIndex[filteredIndex].BaseIndex;
      else
        return filteredIndex;
    }

 This can be replicated very easily by binding a FilteredBindingList to a DataGridView with DataGridView.AllowUserToAddRows = True.  Apply a filter to the list which will cause the grid to be empty, click in the new row and then press escape.  You should get a System.ArgumentOutOfRangeException.

Cymro replied on Tuesday, May 14, 2013

I just checked the implementation of CancelNew in the SortedBindingList and this is Ok...I believe the missing line of code needs adding to the FilteredBindingList implementation...

void ICancelAddNew.CancelNew(int itemIndex)
    {
      //if (itemIndex > -1) return;
      if (itemIndex <= -1) return;

      ICancelAddNew can = _list as ICancelAddNew;
      if (can != null)
        can.CancelNew(OriginalIndex(itemIndex));
      else
        _list.RemoveAt(OriginalIndex(itemIndex));
    }

Edit: I don't think the SortedBindingList implementation was right either! I've switched the condition and this seems to be Ok now.

tiago replied on Tuesday, May 14, 2013

Hi Cymro,

I feel a bit lost after your edit comment. The fix below applies to SortedBindingList.

Right?

The fixed line is missing on FilteredBindingList.

Right?

Cymro

void ICancelAddNew.CancelNew(int itemIndex)
    {
      //if (itemIndex > -1) return;
      if (itemIndex <= -1) return;

      ICancelAddNew can = _list as ICancelAddNew;
      if (can != null)
        can.CancelNew(OriginalIndex(itemIndex));
      else
        _list.RemoveAt(OriginalIndex(itemIndex));
    }

 

 

Cymro replied on Wednesday, May 15, 2013

Hi Tiago,

Yes in our current version of CSLA - 4.0, the line was completely missing from FilteredBindingList and incorrect in the SortedBindingList.  The implementation of this method should be the same in both classes.

Cymro replied on Thursday, May 16, 2013

I have tested this further and it appears that there are other issues around the CancelNew behaviour with a DataGridView.  The reason I was looking at this was because we are attempting to use a SortedBindingList and FilteredBindingList with a DynamicBindingListBase derived class.  The issue here is that when the list is Sorted (or filtered) the cancel new fails and leaves the invalid new item in the list.

I have tracked this down and found the issue but not a satisfactory resolution Tongue Tied

When a new row is added to a DataGridView the AddNew adds the object to the end of the list.  Even if the sorted is list is sorted it still goes at the end of the sorted list (which is what we want).

The issue is with the way that the DataGridView's DataBinding handles the cancelling. 

  1. The DataGridView calls CancelEdit on the child object through (IEditableObject.CancelEdit). 
  2. The changes get rolled back in the child object, but this has the side effect of raising OnUnknownPropertyChanged.
  3. PropertyChanged is handled by the parent BindingList and raises the IBindingList.ListChanged event with a ListChangeType of "Reset". 
  4. The SortedBindingList handles the ListChanged of the source list and calls DoSort, resorting all the items in the list (including the new item).  This is where the problem lies!!!
  5. The DataGridView then calls ICancelAddNew.CancelNew passing the last row index - but this is no longer where our item is, because it has been re-sorted into the middle of the list somewhere.

We do not see the effect of this issue in a BusinessBindingListBase because as part of the CancelEdit on the child, it calls to the parent list to remove it (when the EditLevel < EditLevelAdded) using IParent.RemoveChild(T child).  With a BusinessBindingListBase the implementation of this method removes the child, whereas with a DynamicBindingList does not.  Therefore, with a BusinessBindingListBase the new child is removed from the list before the CancelNew is even called (as part of step 2 above) which masks the issue with the CancelNew implementation of the SortedBindingList.

I am unhappy with this "fix" for two reasons. Firstly, the original comment in the method...

...suggests that this was thought abount and decided this should not be done here.  And secondly because this just masks the real issue of the list getting re-sorted during the whole cancel process.

Now the obvious "fix" is to have the DynamicBindingList remove the child as part of the IParent.RemoveChild implementation as follows...

    /// <summary>
    /// This method is called by a child object when it
    /// wants to be removed from the collection.
    /// </summary>
    /// <param name="child">The child object to remove.</param>
    void Core.IEditableCollection.RemoveChild(Csla.Core.IEditableBusinessObject child)
    {
      Remove((C)child);
    }

I am unhappy with this "fix" for two reasons. Firstly, the original comment in the method...

      // do nothing, removal of a child is handled by
      // the RemoveItem override

...suggests that this was thought abount and decided this should not be done here. And secondly because this just masks the real issue of the list getting re-sorted during the whole cancel process.  Does anyone have any thoughts?

tiago replied on Thursday, June 13, 2013

Hy Cymro,

Two comments:

1) On a dynamic list we can't just delete the child as one must also delete it from the database. So I can understand IParent.RemoveChild isn't deleting the child.

2) It looks like this bad behavior (aka bug) has been there forever. It works all right provided you dont wrap it in a SortedBindingList. I guess it wasn't tested on this scenario.

How about use the IParent.RemoveChild to remove the object, only when it's new?

void Csla.Core.IParent.RemoveChild(Core.IEditableBusinessObject child)
{
  if (child.IsNew)
    Remove((T)child);
}

Did you try it? Any side effects when it's not wrapped in a SortedBindingList?

Cymro replied on Thursday, June 20, 2013

Hi Tiago,

Thank you very much for taking the time to look at this.

Yes I can see how it was missed - the reason the SortedBindingList (and FilteredBindingList) get messed up is because the indexes get mixed up before databinding gets chance to call the ICancelAddNew.CancelNew method (which would otherwise sucessfully remove the item).

This really is a workaround fix, whereby the CancelEdit of the child object would result in the child being removed from the list (before it even gets to the ICancelAddNew.CancelNew on the list).

I guess the additional check does add a little more protection as it does check that the item has not yet been written to the Db (and thus does not need deleting).  From an internal Csla point of view it is not really required, as the child will only call the IParent.RemoveChild if it IsNew and _uncommitted (this is the only place that I can see it is called from within Csla).

    void System.ComponentModel.IEditableObject.CancelEdit()
    {
      if (!_disableIEditableObject && BindingEdit)
      {
        CancelEdit();
        BindingEdit = false;
        if (IsNew && _neverCommitted && EditLevel <= EditLevelAdded)
        {
          // we're new and no EndEdit or ApplyEdit has ever been
          // called on us, and now we've been cancelled back to
          // where we were added so we should have ourselves
          // removed from the parent collection
          if (Parent != null)
            Parent.RemoveChild(this);
        }
      }
    }

 

If we were to implement this fix how can we get Rocky to look at it to fix it in the latest version of Csla?

tiago replied on Thursday, June 20, 2013

Cymro

I guess the additional check does add a little more protection as it does check that the item has not yet been written to the Db (and thus does not need deleting).  From an internal Csla point of view it is not really required, as the child will only call the IParent.RemoveChild if it IsNew and _uncommitted (this is the only place that I can see it is called from within Csla).

(...)

If we were to implement this fix how can we get Rocky to look at it to fix it in the latest version of Csla?

Hi Cymro,

I'm finishing a test project to test for this fix. If the fix is ok, I guess Rocky won't object to include it in 4.5.40.

tiago replied on Sunday, June 23, 2013

Hi Cymro,

I tested your suggested fixes and it works all right. I sent you a personal message you should find on your Conversations/Friends area on this forum.

tiago replied on Sunday, June 30, 2013

Hi Stephen,

See https://github.com/MarimerLLC/csla/issues/169

Cymro replied on Monday, July 01, 2013

Thank you Tiago,

Much appreciated.

Copyright (c) Marimer LLC