views:

59

answers:

4

I have a datagridview bound to a binding source and a couple buttons on a form. One button adds an item to the binding source, the other removes the currently selected item. There's also an event handler that listens to the CurrentChanged event and updates the Enabled status of the Remove button.

Everything is hunky dory until I go to remove the last item from the datagridview. Then I see a very ugly exception:

   at System.Windows.Forms.CurrencyManager.get_Item(Int32 index)
   at System.Windows.Forms.CurrencyManager.get_Current()
   at System.Windows.Forms.DataGridView.DataGridViewDataConnection.OnRowEnter(DataGridViewCellEventArgs e)
   at System.Windows.Forms.DataGridView.OnRowEnter(DataGridViewCell& dataGridViewCell, Int32 columnIndex, Int32 rowIndex, Boolean canCreateNewRow, Boolean validationFailureOccurred)
   at System.Windows.Forms.DataGridView.SetCurrentCellAddressCore(Int32 columnIndex, Int32 rowIndex, Boolean setAnchorCellAddress, Boolean validateCurrentCell, Boolean throughMouseClick)
   at System.Windows.Forms.DataGridView.SetAndSelectCurrentCellAddress(Int32 columnIndex, Int32 rowIndex, Boolean setAnchorCellAddress, Boolean validateCurrentCell, Boolean throughMouseClick, Boolean clearSelection, Boolean forceCurrentCellSelection)\r\n   at System.Windows.Forms.DataGridView.MakeFirstDisplayedCellCurrentCell(Boolean includeNewRow)
   at System.Windows.Forms.DataGridView.OnEnter(EventArgs e)
   at System.Windows.Forms.Control.NotifyEnter()
   at System.Windows.Forms.ContainerControl.UpdateFocusedControl()
   at System.Windows.Forms.ContainerControl.AssignActiveControlInternal(Control value)
   at System.Windows.Forms.ContainerControl.ActivateControlInternal(Control control, Boolean originator)
   at System.Windows.Forms.ContainerControl.SetActiveControlInternal(Control value)
   at System.Windows.Forms.ContainerControl.SetActiveControl(Control ctl)
   at System.Windows.Forms.ContainerControl.set_ActiveControl(Control value)
   at System.Windows.Forms.Control.Select(Boolean directed, Boolean forward)
   at System.Windows.Forms.Control.SelectNextControl(Control ctl, Boolean forward, Boolean tabStopOnly, Boolean nested, Boolean wrap)
   at System.Windows.Forms.Control.SelectNextControlInternal(Control ctl, Boolean forward, Boolean tabStopOnly, Boolean nested, Boolean wrap)
   at System.Windows.Forms.Control.SelectNextIfFocused()
   at System.Windows.Forms.Control.set_Enabled(Boolean value)
   at Bug3324.Form1.HandleBindingSourceCurrentChanged(Object _sender, EventArgs _e) in D:\\Dev\\TempApps\\Bug3324\\Bug3324\\Form1.cs:line 41
   at System.Windows.Forms.BindingSource.OnCurrentChanged(EventArgs e)
   at System.Windows.Forms.BindingSource.CurrencyManager_CurrentChanged(Object sender, EventArgs e)
   at System.Windows.Forms.CurrencyManager.OnCurrentChanged(EventArgs e)

I've isolated the problem in a small scenario:

    private BindingSource m_bindingSource = new BindingSource();

    public Form1()
    {
        InitializeComponent();

        m_bindingSource.CurrentChanged += HandleBindingSourceCurrentChanged;
        m_bindingSource.DataSource = new BindingList<StringValue>();

        dataGridView1.DataSource = m_bindingSource;

        btnAdd.Click += HandleAddClick;
        btnRemove.Click += HandleRemoveClick;
    }

    private void HandleRemoveClick(object _sender, EventArgs _e)
    {
        m_bindingSource.RemoveCurrent();
    }

    private void HandleAddClick(object _sender, EventArgs _e)
    {
        m_bindingSource.Add(new StringValue("Some string"));
    }

    private void HandleBindingSourceCurrentChanged(object _sender, EventArgs _e)
    {
        // this line throws an exception when the last item is removed from
        // the datagridview
        btnRemove.Enabled = (m_bindingSource.Current != null);

    }
}

public class StringValue
{
    public string Value { get; set; }

    public StringValue(string value)
    {
        Value = value;
    }
}

Through pure experimentation, I've found that if I don't alter the button state in the CurrentChanged event handler, then everything works fine. So I suspect some sort of order of operations issue. But what? Why does attempting to make a change completely unrelated to the datagridview cause issues?

To make things even more interesting, the exception is usually harmless (or not showing up at all) if the program is started within VS with a debugger attached. But if it's executed on its own, there's a message box popping up with exception details.

I've tried handling the RowEnter event on the datagridview and found that in this scenario, it still thinks it has a row and attempts to retrieve the Current item from the binding source, but m_bindingSource.Current is already null. Why is this only an issue when the CurrentChanged event is handled?

Any and all help would be greatly appreciated. Thanks.

+2  A: 

Maybe not a real answer but I remember BindingSource and Datagrid being picky and brittle in this department. My general advice would be not to use RemoveCurrent but to delete the record from the underlying datastore.

Henk Holterman
That does seem to do the trick. Thanks!
Anna Lear
... Or not. It might be time to bite the bullet and rewrite this to eliminate binding source entirely.
Anna Lear
While you're biting, consider WPF and MVVM (:
Henk Holterman
I don't think I can quite swing that one by my boss. :)
Anna Lear
A: 

Try replacing the CurrentChanged handler with:

private void HandleBindingSourceCurrentChanged(object _sender, EventArgs _e)
    {
        if (m_bindingSource.Position < 0) return;

        btnRemove.Enabled = (m_bindingSource.Current != null);

    }
Ivan Ferić
Can't return -- that'll leave the button enabled. But I will try going by position instead of checking Current. Thanks!
Anna Lear
+1  A: 

After some figgling, I've discovered some good news and some bad news for you:

The good news is that the (m_bindingSource.Current != null); part isn't the problem. That runs just fine.

The bad news is that the error is being caused by btnRemove.Enabled = false;

Do see what I mean, change: btnRemove.Enabled = (m_bindingSource.Current != null); To:

btnRemove.Enabled = false; 
if(m_bindingSource.Current != null)
   btnRemove.Enabled = true;

The code will die on the first line.

I'm not 100% sure why, but if you move btnRemove.Enabled = false up to the first line of the HandleRemoveClick method everything works as planned.

Hope that's helpful to you.

Tyanna
That is helpful. See my answer for the final solution. Thanks!
Anna Lear
A: 

I ended up resolving it like this:

private void HandleRemoveClick(object _sender, EventArgs _e)
{
    btnRemove.Enabled = false;
    m_bindingSource.RemoveCurrent();
}

private void HandleBindingSourceCurrentChanged(object _sender, EventArgs _e)
{
    if(m_bindingSource.Current != null)
        btnRemove.Enabled = true;
}

It's a little weird, but seems to be working fine.

Anna Lear