TDD with DataGridView - winforms

I'm relatively new to TDD, and still trying to learn to apply some of the concepts. Here's my situation.
I've got a WinForm with a DataGridView. I'm trying to write a test for the routine to be called by a button click that will perform some operations on the selected rows of the grid.
So I will be passing in the DataGridViewSelectedRowCollection object (i.e, the dgv.SelectedRows property at the time the button is clicked).
The DataGridViewSelectedRowCollection object has no constructor, so the only way I can figure to create it is to put together a DataGridView in my test project, then select some rows and pass in the SelectedRows property. But clearly, I don't want to re-create the whole form there.
So I do a DataGridView dgv = new DataGridView(), and gin up a BindingList (actually a SortableBindingList) just like the grid is bound to in the real application. The test list has 3 rows in it. And I do a dgv.DataSource = myList.
Now, at that point in the real application, the grid view is bound. If I look at dgv.Rows.Count, it's equal to the number of rows in the list. However, in my test, setting the DataSource property to the list still results in zero rows in the grid.
I'm thinking there's something missing in the creation of the gridview that normally gets done when it's added to the control list of the form. It probably initializes the handler for the OnDataSourceChanged event or something, and that isn't being done in my test code, but I'm really at a loss as to how to fix it, again, without re-creating a whole form object in my test fixture.
Here's the relavant code form my test method:
DataGridView residueGrid = new DataGridView();
List<Employee> baseListToGrid = new List<Employee>();
SortableBindingList<Employee> listToGrid = new SortableBindingList<Employee>(baseListToGrid);
residueGrid.DataSource = listToGrid;
for (int ix = 1; ix < 4; ix++)
{
listToGrid.Add(ObjectMother.GetEmployee(ix));
}
Assert.AreEqual(3, listToGrid.Count, "SortableBindingList does not have correct count");
Assert.AreEqual(3, residueGrid.Rows.Count, "DataGrid is not bound to list");
Thanks for any help you can give me.

DataGridView residueGrid = new DataGridView();
List<Employee> baseListToGrid = new List<Employee>();
SortableBindingList<Employee> listToGrid = new SortableBindingList<Employee>(baseListToGrid);
// residueGrid.DataSource = listToGrid; <-- move this line...
for (int ix = 1; ix < 4; ix++)
{
listToGrid.Add(ObjectMother.GetEmployee(ix));
}
// residueGrid.DataSource = listToGrid; <-- ...to here!
Assert.AreEqual(3, listToGrid.Count, "SortableBindingList does not have correct count");
Assert.AreEqual(3, residueGrid.Rows.Count, "DataGrid is not bound to list");
A useful structure for writing test is the following:
public void MyTest()
{
// Arrange
// Act
// Assert
}
In this case, Arrange would be instantiating all the objects, and filling the list. Act is where you set the data source of the gridview, and Assert is where you check that everything went OK. I usually write out those three comment lines each time I start writing a test.

Well, I solved the problem, and pretty much confirmed that it is something being done in the initialization of the control when added to the form that makes the DataSource binding work.
It suddenly dawned on me that that the "target" created by the MS testing framework is a private accessor to the Form itself. So I changed the line
DataGridView residueGrid = new DataGridView();
in the above code to, instead of creating a new DGV object, just reference the one on the target form:
DataGridView residueGrid = target.residueGrid;
That change made everything work as expected.

Related

getting error when trying to read from DataGridView with event handler winform

I have a winform that has a gridview that I am applying data to via a Dataset. When the data binds, it calls the SelectionChanged event handler. I researched that and found a way around it by adding an if clause to see if the DGV has focus (all other resolutions did not work that I found). That part is working as planned. When I step through the program, the event handler tries to go through the code 3 times when it binds the data. The if clause stops it from reaching the code. My issue is after the data binds and I then choose a row in the DGV, the event handler then throws "An unhandled exception of type 'System.ArgumentOutOfRangeException' occurred in mscorlib.dll". When stepping through the code, the DGV is returning the proper row index to my 'int row' variable, but the code I use to get the row/cell info throws the error before it applies it to the 'loadtableID' variable. I need help. You can ignore the second DGV in the top. It takes the selected row's info and gets another DB table info. Also, if it helps, I did not apply a datasource to the program or create datasets for each individual dataset that is returned, I am using the system's generic Dataset when returning data.
private void gvMainSelectResults_SelectionChanged(object sender, EventArgs e)
{
if (gvMainSelectResults.Focused)
{
gvMainArchiveResults.DataSource = null; //second DGV that is populated later and everytime is cleared with a new selection
loadTableID = 0;
orgID = 0;
dbFileName = "";
sourceType = "";
int row = gvMainSelectResults.CurrentCell.RowIndex;
loadTableID = Convert.ToInt32(gvMainSelectResults.SelectedRows[row].Cells["LoadTableID"].Value); //this is where I get the error, even if the "int row" has the correct index number
orgID = Convert.ToInt32(gvMainSelectResults.SelectedRows[row].Cells["OrganizationID"].Value);
dbFileName = Convert.ToString(gvMainSelectResults.SelectedRows[row].Cells["FileName"].Value);
sourceType = Convert.ToString(gvMainSelectResults.SelectedRows[row].Cells["SourceType"].Value);
more code here...
You are using the RowIndex value to get your text from the SelectedRows collection.
But this collection contains only
Gets the collection of rows selected by the user.
This means that the collection contains only a subset of the rows present in your grid. When RowIndex is 2 and you have just one row in the SelectedRows collection you get the OutOfRange exception.
With the RowIndex value you should refer to the Rows collection instead
loadTableID = Convert.ToInt32(gvMainSelectResults.Rows[row].Cells["LoadTableID"].Value);
orgID = Convert.ToInt32(gvMainSelectResults.Rows[row].Cells["OrganizationID"].Value);
dbFileName = Convert.ToString(gvMainSelectResults.Rows[row].Cells["FileName"].Value);
sourceType = Convert.ToString(gvMainSelectResults.Rows[row].Cells["SourceType"].Value);

DataGridViewComboBoxColumn setting selected index

I just cant get this to work.
I have a datagridview in winforms and in this one of my columns is a DataGridViewComboBoxColumn.
In my constructor I set it up like so
DataGridViewComboBoxColumn column = (DataGridViewComboBoxColumn)RectangleGrid.Columns["Material"];
DataTable data = new DataTable();
data.Columns.Add(new DataColumn("Value", typeof(int)));
data.Columns.Add(new DataColumn("Description", typeof(string)));
foreach (Materials M in DataStructure.Active.Active_Materials)
{
data.Rows.Add(M.MaterialNr, (M.MaterialNr + 1).ToString() + " " + M.Material.Name);
}
column.DataSource = data;
column.ValueMember = "Value";
column.DisplayMember = "Description";
And it actually works well except that nothing is selected in the drop down box which I want. I have googled this and for instance tried this approach: http://goo.gl/kBy8W but with no go because EditingControlShowing only happens when i click the box and not when it first comes up (so I can set selected index once it's clicked but thats no good).
The CellFormatting version at least changes the value but it just puts a string there rather than my first index from my data source.
I also tried this
column.DefaultCellStyle.NullValue = data.Rows[0]["Description"];
column.DefaultCellStyle.DataSourceNullValue = data.Rows[0]["Value"];
and that seemed to work but then when i selected the first index in the dropdown (so drop the dropdown down and then select the first index and then deselct the cell) I got an error from ParseFormattedValue where it says it cannot convert "value" to system.String.
There was this which seemed to be on the right track but i could not get it to work: http://goo.gl/VevA3
I ended up "solving" it in a very dirty solution that I don't like but it sort of works.
I had my datagridview bound to a datatable (not database) so what i did was i connected an event handler to the TableNewRow event in the datatable.
then i did this in that event handler
private void NewRectangleInserted(Object sender, DataTableNewRowEventArgs e)
{
if (e.Row[0].ToString() == "")
{
e.Row[0] = 0;
}
}
So basically when a new row is created I set the value of the combobox to 0 unless the user created the row by adding a value in that particular cell (why i check if it equals "")
The result is that as soon as you highlight a new line or any cell in a line the combobox is filled in. however the new line at the botton still has a blank combobox until you highlight it.
not perfect but a lot better. to bad it had to be done with a hack!

How Do I get a Bindingsource to see a record added with the TableAdapter.Update() method without using .fill?

I have a large dataset that load in with a fill method on page load.
Then, a record can be added to the dataset.
All of that works fine, but the only way that I can get the bindingsource to recognize the new record is to do a fill method. This also works but is a perfomance problem. Why does the binding source not see the new record in the dataset?
Mainform Code. Works Great.
DialogResult returnFormVal;
Schedulers.DataSets.SchedOneFetch.WOMainFetchRow newRow = schedOneFetch.WOMainFetch.NewWOMainFetchRow();
Schedulers.Forms.NewWorkOrder genReport = new Schedulers.Forms.NewWorkOrder(ref newRow);
Int32 picNumber;
returnFormVal = genReport.ShowDialog();
schedOneFetch.WOMainFetch.Rows.Add(newRow);
wOMainFetchBindingSource.EndEdit();
wOMainFetchTableAdapter.Adapter.Update(schedOneFetch.WOMainFetch);
Int32 passBackVal = newRow.DISID;
SubForm code. Also works great.
passBackRow.DISDueDate = monthCalendar1.SelectionStart;
passBackRow.DISID = 99999999;
if (ckbEqpt.Checked == true & lbProcNum.Items.Count > 0)
{
passBackRow.DISEquip = Convert.ToInt32(lbProcNum.SelectedValue.ToString());
}
else
{
passBackRow.DISEquip = 0;
}
passBackRow.DISLineNumber = Convert.ToInt32(lbLineName.SelectedValue.ToString());
passBackRow.DISManHours = Convert.ToInt32(nudEstTotTime.Value);
passBackRow.DISNumberAss = Convert.ToInt32(nudEstTM.Value);
passBackRow.DISOpenDate = DateTime.Now;
passBackRow.DISOriginator = userID.DBUserID;
passBackRow.DISRequestor = 0;
passBackRow.DISResponsible = Convert.ToInt32(lbRespons.SelectedValue.ToString());
passBackRow.DISType = Convert.ToInt32(lbType.SelectedValue.ToString());
passBackRow.DISWorkAccomp = "";
passBackRow.DISWorkRequired = rtbWorkReq.Text;
passBackRow.MLID = 0;
passBackRow.LIID = 0;
passBackVal = 0;
this.Close();
Return control to main form. The new record has been added to the database.
wOMainFetchBindingSource.Position = wOMainFetchBindingSource.Find("DISID", passBackVal);
DataRowView dtaRow = (DataRowView)wOMainFetchBindingSource.Current;
String woID = dtaRow["DISID"].ToString();
FAIL! The bindingsource wont find the the new record, returns a -1 on the find and defaults to the first record in the dataset.
If I put the .fill method in between the dialog and the main page then it all works fine, but takes a loooonnng time to do the fill... seven or eight seconds.
I guess my understanding of the binding source is disfunctional, I had assumed that if the underlying dataset was updated then the bindingsource would see it.
So, first if someone has a suggestion on how to refresh the binding source without the fill I would appreciate it, and if someone can explain why this works the way it does I might be able to find a workaround.
Thanks
My understanding of the Bindingsource was correct, it was seeing the new record added. The issue is that the Dataset gets its information from a view. The add new method only populates the fields in the base table. The other fields, the ones assembled by the View, arent populated until the tableadapter re-reads by using the fill method. I dont see a way around this other than filling each of the fields of the new record, big drawback is that whenever the view or any of the tables assembled in the view are changed you would have to make sure to change the code. I will instead reduce the number of records being loaded on each fill.

How to set the ColumnIndex of a newly added DataGridViewButton column

I have a really annoying issue with a button cell in a DataGridView control. I'm binding the grid to a dataset at runtime. Some of the rows in the grid will be linked to pdf documents. I create a button column and add it to the grid, then I loop through the rows and based on the value of a certain column I set the text of the cell in the button column. When I step through the code I can see the ColumnIndex of the button column is 10. However when the form appears, the button text values for the rows I want are blank.
When I click the button I check in the CellContentClick event to see if the ColumnIndex is 10 (which is the button column) it tells me the ColumnIndex is 0, even though it's the last column. Then when I reload the grid I call the BindHistoryGrid method again which drops the column if it exists and re-adds it. This time it sets the button text correctly. Is there some strange behavior going on that I can't see? How do I set the button ColumnIndex to 10 the first time I add it (even though it tells me that it's 10)?
private DataGridViewButtonColumn PDFButtonColumn;
private void BindHistoryGrid()
{
dataGridViewStmt.DataSource = ah.getAccountHistory(0, dateTimePicker1.Value, dateTimePicker2.Value);
if (dataGridViewStmt.Columns["GetPDFFile"] != null)
dataGridViewStmt.Columns.Remove("GetPDFFile");
dataGridViewStmt.Columns[0].DisplayIndex = 0;
dataGridViewStmt.Columns[0].AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
dataGridViewStmt.AutoResizeColumns(DataGridViewAutoSizeColumnsMode.AllCells);
dataGridViewStmt.Columns[0].Visible = false;
dataGridViewStmt.Columns[1].Visible = false;
dataGridViewStmt.Columns.Add(PDFButtonColumn);
dataGridViewStmt.RowHeadersVisible = false;
dataGridViewStmt.ReadOnly = true;
dataGridViewStmt.AllowUserToAddRows = false;
foreach (DataGridViewRow row in dataGridViewStmt.Rows)
{
//if (((string)row.Cells[5].Value).Contains("Invoice"))
if (((int)row.Cells[9].Value) > 0)
{
((DataGridViewButtonCell)(row.Cells[10])).Value = "Get Invoice";
}
else
{
((DataGridViewButtonCell)(row.Cells[10])).Value = "";
}
}
}
private void dataGridViewStmt_CellContentClick(object sender, DataGridViewCellEventArgs e)
{
if (e.ColumnIndex == 10 && dataGridViewStmt.CurrentRow.Cells[6].Value != System.DBNull.Value)
{
string pdfFile = "";
int docID = 0;
pdfFile = (string)dataGridViewStmt.CurrentRow.Cells[5].Value + ".pdf";
docID = (int)dataGridViewStmt.CurrentRow.Cells[9].Value;
if (docID > 0)
{
getPDFFile(docID, pdfFile, "pdf");
}
else
{
MessageBox.Show("No invoice available for this item"; }
}
}
I called my bindGrid() method from the two place one after the InitializeComponent() in form's constructor as well as from form1_load(). it works for me.
hope this will also helps you.
I didn't get any replies here so I posted on another forum. I eventually got an answer of sorts, but the whole thing is still pretty vague. The answer I got stated that in order to preserve resources, the grid doesn't always refresh itself. An example is if you have a form with a tab control that has 2 tabs, place a grid on the 1st tab and set column properties after binding in Form Load. This will work. However, when you place the grid on the 2nd tab, using the same binding won't work:
http://social.msdn.microsoft.com/Forums/en-US/csharpgeneral/thread/99ab9fbf-9eaa-4eef-86b8-8f4e49fa81c5
I still haven't found out how or when it decides to preserve resources, if there's a way to bypass this behaviour, if this behaviour is documented anywhere etc. If anyone can throw more light on it I'm all ears.
I had the very same issue. I originally had a DataGridView on a separate form and it worked perfectly with the button column - which I add in code after setting the datasource. However, when I decided to move the grid onto another form with a Tabbed Control (on to the Tab(2) page as it happens), the button column index kept reverting to zero. It looked perfectly OK on the grid of course, i.e. in the correct physical location, and if I stepped through the code in debug mode the Index didn't change, but when I ran the program it did change! Very frustrating.
I solved it by setting the tab page to the page that my grid was located BEFORE setting the datasource.
My simple process was like this (I use VB10):
TabControl1.SelectedIndex = 2 ' this is where the datagridview is
MyGrid.DataSource = Nothing
MyGrid.Columns.Clear
' I execute an Sql command into a DataReader, then fill a DataTable and then assign it to the grid
MyGrid.DataSource = MyDataTable
' Now add button column
Dim btnCol as New DatGridViewButtonColumn
MyGrid.Columns.Add(btnCol)

Refreshing BindingSource after insert (Linq To SQL)

I have a grid bound to a BindingSource which is bound to DataContext table, like this:
myBindingSource.DataSource = myDataContext.MyTable;
myGrid.DataSource = myBindingSource;
I couldn't refresh BindingSource after insert. This didn't work:
myDataContext.Refresh(RefreshMode.OverwriteCurrentValues, myBindingSource);
myBindingSource.ResetBinding(false);
Neither this:
myDataContext.Refresh(RefreshMode.OverwriteCurrentValues, myDataContext.MyTable);
myBindingSource.ResetBinding(false);
What should I do?
I have solved the problem but not in a way I wanted.
Turns out that DataContext and Linq To SQL is best for unit-of-work operations. Means you create a DataContext, get your job done, discard it. If you need another operation, create another one.
For this problem only thing I had to do was recreate my DataContext like this.dx = new MyDataContext();. If you don't do this you always get stale/cached data. From what I've read from various blog/forum posts that DataContext is lightweight and doing this A-OK. This was the only way I've found after searching for a day.
And finally one more working solution.
This solution works fine and do not require recreating DataContext.
You need to reset internal Table cache.
for this you need change private property cachedList of Table using reflection.
You can use following utility code:
public static class LinqDataTableExtension
{
public static void ResetTableCache(this ITable table)
{
table.InternalSetNonPublicFieldValue("cachedList", null);
}
public static void ResetTableCache(this IListSource source)
{
source.InternalSetNonPublicFieldValue("cachedList", null);
}
public static void InternalSetNonPublicFieldValue(this object entity, string propertyName, object value)
{
if (entity == null)
throw new ArgumentNullException("entity");
if(string.IsNullOrEmpty(propertyName))
throw new ArgumentNullException("propertyName");
var type = entity.GetType();
var prop = type.GetField(propertyName, BindingFlags.NonPublic | BindingFlags.Instance);
if (prop != null)
prop.SetValue(entity, value);
// add any exception code here if property was not found :)
}
}
using something like:
var dSource = Db.GetTable(...)
dSource.ResetTableCache();
You need to reset your BindingSource using something like:
_BindingSource.DataSource = new List();
_BindingSource.DataSource = dSource;
// hack - refresh binding list
Enjoy :)
Grid Data Source Referesh by new query instead just Contest.Table.
Simple Solution < But Working.
Whre is eg.
!!!!! Thanks - Problem Solved after no of days !!! but with so simple way ..
CrmDemoContext.CrmDemoDataContext Context = new CrmDemoContext.CrmDemoDataContext();
var query = from it in Context.Companies select it;
// initial connection
dataGridView1.DataSource = query;
after changes or add in data
Context.SubmitChanges();
//call here again
dataGridView1.DataSource = query;
I have the same problem. I was using a form to create rows in my table without saving the context each time. Luckily I had multiple forms doing this and one updated the grid properly and one didn't.
The only difference?
I bound one to the entity similarly (not using the bindingSource) to what you did:
myGrid.DataSource = myDataContext.MyTable;
The second I bound:
myGrid.DataSource = myDataContext.MyTable.ToList();
The second way worked.
I think you should also refresh/update datagrid. You need to force redraw of grid.
Not sure how you insert rows. I had same problem when used DataContext.InsertOnSubmit(row), but when I just inserted rows into BindingSource instead BindingSource.Insert(Bindingsource.Count, row)
and used DataContext only to DataContext.SubmitChanges() and DataContext.GetChangeSet(). BindingSource inserts rows into both grid and context.
the answer from Atomosk helped me to solve a similar problem -
thanks a lot Atomosk!
I updated my database by the following two lines of code, but the DataGridView did not show the changes (it did not add a new row):
this.dataContext.MyTable.InsertOnSubmit(newDataset);
this.dataContext.SubmitChanges();
Where this.dataContext.MyTable was set to the DataSource property of a BindingSource object, which was set to the DataSource property of a DataGridView object.
In code it does looks like this:
DataGridView dgv = new DataGridView();
BindingSource bs = new BindingSource();
bs.DataSource = this.dataContext.MyTable; // Table<T> object type
dgv.DataSource = bs;
Setting bs.DataSource equals null and after that back to this.dataContext.MyTable did not help to update the DataGridView either.
The only way to update the DataGridView with the new entry was a complete different approach by adding it to the BindingSource instead of the corresponding table of the DataContext, as Atomosk mentioned.
this.bs.Add(newDataset);
this.dataContext.SubmitChanges();
Without doing so bs.Count; returned a smaller number as this.dataContext.MyTable.Count();
This does not make sense and seems to be a bug in the binding model in my opinion.

Resources