Erratic InvalidOperationException on datagridview - c#

My Winform app is logging AppDomain.CurrentDomain.UnhandledException and Application.ThreadException at the root level, and I got this exception:
System.InvalidOperationException: Operation is not valid due to the current state of the object. at System.Windows.Forms.DataGridView.DataGridViewDataConnection.ProcessListChanged(ListChangedEventArgs e) at System.Windows.Forms.DataGridView.DataGridViewDataConnection.currencyManager_ListChanged(Object sender, ListChangedEventArgs e) at System.Windows.Forms.CurrencyManager.OnListChanged(ListChangedEventArgs e) at System.Windows.Forms.CurrencyManager.List_ListChanged(Object sender, ListChangedEventArgs e) at System.ComponentModel.BindingList1.OnListChanged(ListChangedEventArgs e) at System.ComponentModel.BindingList1.FireListChanged(ListChangedType type, Int32 index) at System.ComponentModel.BindingList1.InsertItem(Int32 index, T item) at System.Collections.ObjectModel.Collection1.Add(T item) at System.ComponentModel.BindingList1.AddNewCore() at System.ComponentModel.BindingList1.System.ComponentModel.IBindingList.AddNew() at System.Windows.Forms.CurrencyManager.AddNew() at System.Windows.Forms.DataGridView.DataGridViewDataConnection.AddNew() at System.Windows.Forms.DataGridView.DataGridViewDataConnection.OnNewRowNeeded() 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.ProcessDownKeyInternal(Keys keyData, Boolean& moved) at System.Windows.Forms.DataGridView.ProcessEnterKey(Keys keyData) at System.Windows.Forms.DataGridView.ProcessDialogKey(Keys keyData) at System.Windows.Forms.Control.ProcessDialogKey(Keys keyData) at System.Windows.Forms.TextBoxBase.ProcessDialogKey(Keys keyData) at System.Windows.Forms.Control.PreProcessMessage(Message& msg) at System.Windows.Forms.Control.PreProcessControlMessageInternal(Control target, Message& msg) at System.Windows.Forms.Application.ThreadContext.PreTranslateMessage(MSG& msg)
This is the result of ex.ToString(), and it returns no custom code of my app, only internal System.Windows.Forms methods.
The exception is raised time to time on some customer machine, I'm even not able to reproduce it myself.
This smell not good, and my assumption was something when I change the datasource bounding of the datagridview. But in this case I should see at least my class in the exception stack, but here nothing.
Any clue to find the root cause, or to debug that?
Many thanks

If you investigate the stack trace, you'll see the root of the problem: customer is trying to add a new record on the grid, so the event handler tries to add the record to the datasource, which lead to another event handler, which tries to add a record to the binding list, which lead to event currencyManager_listChanged firing up, which fails due to wrong state of the object.
Either you dispose your list or you do not unsubscribe from the events of the disposed control.

I've experienced exactly the same exception, and it occurs after the user deletes multiple rows (multi select enabled), and includes the last row in the selection. The last row is for adding new items. Everything works fine, until the user then goes to edit remaining rows, and the exception occurs.
When the user doesn't include the last/add new item row in the selection, everything works fine after the delete.
I haven't had time to investigate exactly why this behavior occurs, but the work around was to not allow the add new item row to be included when multi-selecting items, which can be detected by the IsNewRow property of DataGridViewRow and then calling ClearSelection() on the DataGridView if this row is in the selection.
private void DataGridView1_SelectionChanged(object sender, EventArgs e)
{
foreach (DataGridViewRow row in dataGridView1.SelectedRows)
{
if (row.IsNewRow)
{
dataGridView1.ClearSelection();
return;
}
}
}

Related

How to know if ToolStripButton click was fired?

In my Windows Forms project I have a BindingNavigator control in a user control (It is a data-binding scenario). Pressing one of the buttons, First, Previous, Next or Last throws exception, crashing the application entirely. To tackle this I have written following code to handle the exception, in the user control.
protected override void OnLoad(EventArgs e)
{
Application.ThreadException += Application_ThreadException;
base.OnLoad(e);
}
private void Application_ThreadException(object sender, ThreadExceptionEventArgs e)
{
if (bindingNavigatorMovePreviousItem.Pressed)
{
MessageBox.Show(e.Exception.Message);
bsRestrictions.CancelEdit();
}
}
How do I know if the exception thrown is the result of ToolStripButton click event?
The code above is helpless as Pressed property is false in the method, so the message box will not pop out.
Following is from my Program.cs file
[STAThread]
static void Main()
{
Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException);
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new AppForm());
}
Experienced developers must have understood that I am trying to solve the well known issue with BindingNavigator. This control simply manages BindingSource component. In scenarios where strongly typed data sets are used, navigating through records throws exception crashing the application.
Just debug your application. If you have an idea where the exception is thrown, put a breakpoint there.
When the exception is thrown, you can go back in the call stack.
In the call stack, you should be able to see where the exception came from.
From the code that you are providing, seems hard to help more.
When you click on BindingNavigator buttons, the exception is not thrown from your code, it's thrown in framework classes, so you cannot set a breakpoint. You need to avoid the exception, or if you want to handle it in Application.ThreadException, you need to check stack trace.
This post answers two questions:
How to avoid exception when clicking on navigation buttons or add/remove/save buttons?
How to know if ToolStripButton was involved in the actions which resulted in throwing an exception?
1 - How to avoid exception when clicking on navigation buttons or add/remove/save buttons?
Disable constraints in the form load. Then in the Save button click, enable them, handle the exceptions (if necessary) and disable them again:
private async void Form1_Load(object sender, EventArgs e)
{
dataSet1.EnforceConstraints = false;
}
private void dataTable1BindingNavigatorSaveItem_Click(object sender, EventArgs e)
{
try
{
dataSet1.EnforceConstraints = true;
}
catch (Exception ex)
{
//process errors if necessary
}
dataSet1.EnforceConstraints = false;
}
Any other option to avoid the exception?
Yes, as another option to avoid the error, you can just remove the default functionalities from buttons; for example select the binding navigator, and in properties find the AddNewItem and select (none) as its value. Then double click on the button to handle its click event and handle it like this:
private void bindingNavigatorAddNewItem_Click(object sender, EventArgs e)
{
try
{
myBindingNavigator.BindingSource.AddNew();
}
catch (Exception)
{
//process the error, for example show a message
}
}
2 - How to know if ToolStripButton was involved in the actions which resulted in throwing an exception?
This exception is not thrown from your code, but thrown in framework classes. Looking into the e.Exception.StackTrace of the exception that you have handled in Application.ThreadException, you see all the necessary details including class names and method names:
at System.Data.DataColumn.CheckNullable(DataRow row)
at System.Data.DataTable.RaiseRowChanging(DataRowChangeEventArgs args, DataRow eRow, DataRowAction eAction, Boolean fireEvent)
at System.Data.DataTable.SetNewRecordWorker(DataRow row, Int32 proposedRecord, DataRowAction action, Boolean isInMerge, Boolean suppressEnsurePropertyChanged, Int32 position, Boolean fireEvent, Exception& deferredException)
at System.Data.DataTable.InsertRow(DataRow row, Int64 proposedID, Int32 pos, Boolean fireEvent)
at System.Data.DataView.FinishAddNew(Boolean success)
at System.Data.DataRowView.EndEdit()
at System.Windows.Forms.CurrencyManager.EndCurrentEdit()
at System.Windows.Forms.BindingSource.EndEdit()
at System.Windows.Forms.BindingSource.AddNew()
at System.Windows.Forms.BindingNavigator.OnAddNew(Object sender, EventArgs e)
at System.Windows.Forms.ToolStripItem.RaiseEvent(Object key, EventArgs e)
at System.Windows.Forms.ToolStripButton.OnClick(EventArgs e)
at System.Windows.Forms.ToolStripItem.HandleClick(EventArgs e)
at System.Windows.Forms.ToolStripItem.HandleMouseUp(MouseEventArgs e)
at System.Windows.Forms.ToolStripItem.FireEventInteractive(EventArgs e, ToolStripItemEventType met)
at System.Windows.Forms.ToolStripItem.FireEvent(EventArgs e, ToolStripItemEventType met)
at System.Windows.Forms.ToolStrip.OnMouseUp(MouseEventArgs mea)
at System.Windows.Forms.Control.WmMouseUp(Message& m, MouseButtons button, Int32 clicks)
at System.Windows.Forms.Control.WndProc(Message& m)
at System.Windows.Forms.ScrollableControl.WndProc(Message& m)
at System.Windows.Forms.ToolStrip.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.Callback(IntPtr hWnd, Int32 msg, IntPtr wparam, IntPtr lparam)
You can also get the stack frames yourself:
var frames = new StackTrace(e.Exception, true).GetFrames();
And then you can search between the frames, for example by checking:
Where(x => x.GetMethod().DeclaringType == typeof(ToolStripButton))

C# System.Web.HttpUnhandledExcpetion when changing page of table

I have a table in my web app that displays a list of users from my SQLExpress DB but for some reason when I navigate to page 8 I get this stacktrace in my logs:
ERROR Global Exception Logged in Global.asax.cs: Exception of type 'System.Web.HttpUnhandledException' was thrown.Data is Null. This method or property cannot be called on Null values. at System.Data.SqlClient.SqlBuffer.get_String()
at BusinessLayer.Appl.UserDetailMgr.PopulateObjectFromReader(UserDetailMgr obj, IDataReader rdr)
at BusinessLayer.Appl.UserDetailMgr..ctor(IDataReader dr)
at BusinessLayer.Appl.UserDetailMgr.GetUserList(Int32 startIndex, Int32 recordsPerPage, Int32 colNo, Int32 order)
at PresentationLayer.Pages.User.BSLeadListing.Display(String sortBycolumnNo)
at PresentationLayer.Pages.User.BSLeadListing.gdPager_ItemCommand(Object source, RepeaterCommandEventArgs e)
at System.Web.UI.WebControls.Repeater.OnItemCommand(RepeaterCommandEventArgs e)
at System.Web.UI.WebControls.Repeater.OnBubbleEvent(Object sender, EventArgs e)
at System.Web.UI.Control.RaiseBubbleEvent(Object source, EventArgs args)
at System.Web.UI.WebControls.RepeaterItem.OnBubbleEvent(Object source, EventArgs e)
at System.Web.UI.Control.RaiseBubbleEvent(Object source, EventArgs args)
at System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint)
2018-08-02 14:56:26,853 [6008] ERROR Global Request URL: http://mednet.butterflyscheme.org.uk/WebEvaluation/Pages/User/BSLeadListing.aspx
2018-08-02 14:56:26,853 [6008] ERROR Global Exception is not null & type of exception is 'HttpUnhandledException'.
Literally every page of the table works excpet page 8.
I have no clue where to start looking since I write the original code. Any thoughts would be much appreciated
When I look in my logs, it crashes just after this function is complete:
/// <summary>
/// Repeater event
/// </summary>
/// <param name="source"></param>
/// <param name="e"></param>
protected void gdPager_ItemCommand(object source, RepeaterCommandEventArgs e)
{
logger.Debug("gdPager_ItemCommand - Start");
try
{
ViewState["UserListPageIndex"] = Convert.ToInt32(e.CommandArgument);
Display(Convert.ToString(ViewState["ColumnNo"]));
PopulatePager();
}
finally
{
logger.Debug("gdPager_ItemCommand - End");
}
}
UPDATE:
I searched for every value that would exist on page 8 using the search feature of the table while on the site. Every result displayed. So now I'm left wondering what is null?
The error says
Data is Null
It looks like the query returns no data, on page 8, and that causes the problem. you can use it in a try-catch.
I think you have not correctly calculated the number of pages, so page 7 should be the last page, however, still you have do add error handling there.

How to access the Index of a changed Selection in a Datagridview

I am working on a windows form. Currently I am geting quite a headache over a seemingly simple prolbem.
I have a datagridview and want to allow right click selection. Therefore I created a function called dataGridViewJobs_MouseDown that is raised on MouseDown on the datagridview:
private void dataGridViewJobs_MouseDown(object sender, MouseEventArgs e)
{
int curRowIdx = dataGridViewJobs.HitTest(e.Location.X, e.Location.Y).RowIndex;
if (curRowIdx != -1 && curRowIdx < dataGridViewJobs.Rows.Count)
{
dataGridViewJobs.CurrentCell = dataGridViewJobs[0, curRowIdx];
}
}
A hitTest is executed to find the row-index of the clicked cell. Afterwards the currentCell of the datagridview is set to the first cell in said row.
This causes the SelectionChanged-event to be raised. This is connected to the following function:
private void dataGridViewJobs_SelectionChanged(object sender, EventArgs e)
{
if (dataGridViewJobs.Rows.Count > 0)
{
Console.WriteLine(dataGridViewJobs.CurrentCell.RowIndex);
}
}
This writes the old index to the console. Why is that?
I am currently working with a workaround, which means I save the result of the hitTest in a global variable. But that can not be the right way to do this.
Am I doing something wrong? Thanks in advance!
From MSDN
When you change the value of the CurrentCell property, the
SelectionChanged event occurs before the CurrentCellChanged event. Any
SelectionChanged event handler accessing the CurrentCell property at
this time will get its previous value.
Use CurrentCellChanged event for printing current value

DataGridView binding problem: "Index -1 does not have a value."

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.
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.
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.
I ran into the same problem today and found the workaround in this thread. Unfortunately I didn't like to split up the enable/disable code of the buttons. So I did some more research and found another solution, which worked for me.
All I did to resolve the IndexOutOfRangeException was to reset the bindings before I set the enable/disable of the buttons. (To optimize the performance you may check if the datasource count is zero or the position of the currency manager is -1. In other cases the reset isn't necessary.)
private void HandleBindingSourceCurrentChanged(object _sender, EventArgs _e)
{
if(m_bindingSource.Count == 0) // You also can check position == -1
{
m_bindingSource.ResetBindings(false);
}
btnRemove.Enabled = (m_bindingSource.Current != null);
}
Hope that's helpful.
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.
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);
}
I think the problem occurs because you are disabling a button that currently has the focus. There should be nothing wrong with disabling the focused control, but in certain circumstances it produced the described problem. If you the set the focus to some other control first I think you will see the problem go away. I was having the same problem and it worked for me.
Dim bCurrent As Boolean = CredentialTypeBindingSource.Current IsNot Nothing
'set focus to the New button which is never disabled
NewBtn.Focus()
'enable/disable the other buttons
EditBtn.Enabled = bCurrent
DeleteBtn.Enabled = bCurrent

DataGridView capturing user row selection

I am having trouble handling the selections in DataGridView.
My grid view contains an amount column. There is a textbox on the form which should display the total amount of the selected grid view rows. Hence I need to capture events when the user selects/ deselects the gridview rows and calculate (add/ subtract) the amount accordingly. I have found two methods of doing it:
Using the RowEnter and RowLeave events.
These work fine when user selects/ deselects a single row. However, when the user is selecting multiple rows at one go, the event gets fired only for the last row. Hence, from my total amount only the amount in the last row gets added/ subtracted. Thus making my result erroneous.
Using the RowStateChanged event.
This works for multiple rows. However, the event gets fired event if the user scrolls through the datagrid.
Has anyone handled such a scenario. I would like to know which datagrid event I should be using, so that my code executes only when user selects/ deselects rows including multiple rows.
Found the solution. I can use RowStateChanged and run my code only if StateChanged for the row is Selected...
private void dgridv_RowStateChanged(object sender, DataGridViewRowStateChangedEventArgs e)
{
// For any other operation except, StateChanged, do nothing
if (e.StateChanged != DataGridViewElementStates.Selected) return;
// Calculate amount code goes here
}
I use SelectionChanged event or CellValueChanged event:
dtGrid.SelectionChanged += DataGridView_SelectionChanged;
this.dtGrid.DataSource = GetListOfEntities;
dtGrid.CellValueChanged += DataGridView_CellValueChanged;
private void DataGridView_CellValueChanged(object sender, DataGridViewCellEventArgs e)
{
DataGridViewRow row = dtGrid.Rows[e.RowIndex];
SetRowProperties(row);
}
private void DataGridView_SelectionChanged(object sender, EventArgs e)
{
var rowsCount = dtGrid.SelectedRows.Count;
if (rowsCount == 0 || rowsCount > 1) return;
var row = dtGrid.SelectedRows[0];
if (row == null) return;
ResolveActionsForRow(row);
}
I think you can consider the SelectionChanged event:
private void DataGridView1_SelectionChanged(object sender, EventArgs e) {
textbox1.Text = DataGridView1.SelectedRows.Count.ToString();
}
You can use SelectionChanged event and retrieve the row index of the selected cell(s):
private void dataGridView1_SelectionChanged(object sender, EventArgs e)
{
if (dataGridView1.SelectedCells.Count > 0)
Do_Something_With_It(dataGridView1.SelectedCells[0].RowIndex);
}
You can simply capture this in following way, but it is restricted to single row selection only:
private void dataGridView1_CellClick(object sender, DataGridViewCellEventArgs e)
{
MessageBox.Show("Selected row is=" + e.RowIndex);
// you can perform (any operation) delete action on selected row like
dataGridView1.Rows.RemoveAt(e.RowIndex);
dataGridView1.Refresh();
}
You can use your first method (row enter row leave) along with the SelectedRows property. Meaning, when you detect those events, and you need to calculate, instead of using the row from the event args, loop through the SelectedRows and get your total.

Categories

Resources