Keep focus on row after datagridview update - vb.net

I'm creating an VB windows application. The point of the application is a simple DataGridView where I'm fetching a View from a SQL Server database.
The DataGridView is refreshed every second so I could see new data income in my GridView.
The problem is keeping focus on row after the refresh. I need the solution, where after I click a row, or a cell it keeps me on it even after the refresh.
Here is my code:
Public Class Form1
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
'Refresh every 1 sec
Dim timer As New Timer()
timer.Interval = 1000
AddHandler timer.Tick, AddressOf timer_Tick
timer.Start()
'TODO: This line of code loads data into the 'XYZDataSet.view1' table. You can move, or remove it, as needed.
Me.View1TableAdapter.Fill(Me.XYZDataSet.view1)
End Sub
Private Sub DataGridView1_CellContentClick(sender As Object, e As DataGridViewCellEventArgs) Handles DataGridView1.CellContentClick
End Sub
Private Sub DataGridView1_CellFormatting(ByVal sender As Object, ByVal e As DataGridViewCellFormattingEventArgs) Handles DataGridView1.CellFormatting
For i As Integer = 0 To Me.DataGridView1.Rows.Count - 1
If Me.DataGridView1.Rows(i).Cells("DayTillDelivery").Value <= 30 Then
Me.DataGridView1.Rows(i).Cells("DayTillDelivery").Style.ForeColor = Color.Red
End If
Next
End Sub
Private Sub timer_Tick(ByVal sender As Object, ByVal e As EventArgs)
'Calling refresh after 1 second and updating the data
Me.DataGridView1.Refresh()
Me.View1TableAdapter.Fill(Me.XYZDataSet.view1)
End Sub
End Class

I've solved a similar problem in the past by storing the indexes of the selected cell in a variable before doing the refresh, so I'm able to restore the selection by calling DataGridView.Rows(selRow).Cells(selCol).Selected = True after the update.
Edit - Sample Code:
To later readers:Please take a look at Edit#2 where I describe a better method to re-select the previous selected cell!
Sample Code:
' Variables for remembering the indexes of the selected cell
Dim selRow As Integer
Dim selCol As Integer
' Check if there is a selected cell to prevent NullPointerException
If DataGridView1.SelectedCells().Count > 0 Then
selRow = DataGridView1.CurrentCell.RowIndex
selCol = DataGridView1.CurrentCell.ColumnIndex
End If
' Dummy "update"
' don't forget to clear any existing rows before adding the new bunch (only if you always reloading all rows)!
DataGridView1.Rows.Clear()
For index = 1 To 20
DataGridView1.Rows.Add()
Next
' Check if there are "enough" rows after the update,
' to prevent setting the selection to an rowindex greater than the Rows.Count - 1 which would
' cause an IndexOutOfBoundsException
If (DataGridView1.Rows.Count - 1) > selRow Then
' Clear selection and then reselect the cell that was selected before by index
DataGridView1.ClearSelection()
' For the next line of code, there is a better solution in Edit #2!
DataGridView1.Rows(selRow).Cells(selCol).Selected = True
End If
Please note:
This procedure requires you to add the rows in the exact same order that you have added them before the update, as only the .Index of the selected row is stored in the variable. If you readding the rows in a different order, then not the same row but the row at the same position will be selected after the refresh.
You should add check if there is a selected row at all (to prevent a NullPointerException) and if there are "enough" rows in the DataGridView after the refresh, to prevent an IndexOutOfBoundsException.
This only works if the DataGridView1.SelectionMode is to something that actually selects rows, like FullRowSelect.
Don't forget to clear any existing rows before adding new ones by updating (only if you always reloading all rows).
Edit 2 - RowHeader triangle and accidental MultiSelect
As stated in the comments below, there was an odd behavior that would lead to an accidental MultiSelect, if the user holds down the mouse button past the refresh cycle. Also, the RowHeader triangle was not set to the correct row.
After some research I found a solution to this behavior. Instead of setting the .Selected-property of a given cell to True, set the .CurrentCell-property of the DataGridView to the cell you would like to select!
In code, this means changing
DataGridView1.Rows(selRow).Cells(selCol).Selected = True
to
DataGridView1.CurrentCell = DataGridView1.Rows(selRow).Cells(selCol)
and there you go. :-)

Before Fill, store the CurrentRow values and currenCell column:
Dim currentColumnIndex As Integer = 0 ;
Dim currentValues As List(Of Object) = If(DataGridView1.CurrentRow Is Nothing, Nothing, New List(Of Object)())
If currentValues IsNot Nothing Then
For i As Integer = 0 To DataGridView1.Columns.Count - 1
currentValues.Add(DataGridView1.CurrentRow.Cells(i).Value)
Next
currentColumnIndex = DataGridView1.CurrentCell.ColumnIndex;
End If
After Fill, search the row corresponding to stored values:
Dim i As Integer = 0
While i < DataGridView1.Rows.Count AndAlso currentValues IsNot Nothing
Dim areIdentical As Boolean = True
Dim j As Integer = 0
While j < DataGridView1.Columns.Count AndAlso areIdentical
areIdentical = DataGridView1.Rows(i).Cells(j).Value = currentValues(j)
j += 1
End While
If areIdentical Then
DataGridView1.CurrentCell = DataGridView1.Rows(i).Cells(currentColumnIndex)
currentValues = Nothing
End If
i += 1
End While
Note: the "For/While" loop coding is perhaps not optimal because it results from automatic conversion from C# to vb.net.

C# fix code , next reload pattern
if (dataGridView7.SelectedCells.Count > 0)
{
//MessageBox.Show(selcell + "------"+dataGridView7.CurrentCell.ColumnIndex.ToString());
if (selcell > 0 && dataGridView7.CurrentCell.ColumnIndex==0) { }else
{
selrow = dataGridView7.CurrentCell.RowIndex;
selcell = dataGridView7.CurrentCell.ColumnIndex;
}
}
loaddataJobsall();
dataGridView7.ClearSelection();
dataGridView7.Rows[selrow].Cells[selcell].Selected = true;

Related

DataGridView slow to refresh on screen

I have a program that regularly updates a few datagridviews with new data that is received via TCP. The problem I am having is that the screen refresh is quite slow. Bellow is a stripped back version of my code. This example takes 1.1s to update the screen each time the loop in StartButton_Click is iterated. How can I make this faster without reducing the amount of data that is shown?
I added a stopwatch to try and work out what lines of code were causing the biggest issue. From the tests, it seemed that the main issue was updating the datagridview cells with a new number.
I'm not sure how to make this faster as my program relies on the values being updated regularly. Is a datagridview not the right object for this application? Should i be using something else? Is there a way to get a datagridview to update faster?
Public Class Form1
Public DataTable1 As New DataTable
Private Sub Load_From(ByVal sender As Object, ByVal e As System.EventArgs) Handles MyBase.Load
DataGridView1.DataSource = DataTable1
Me.Height = 700
Me.Width = 1000
DataGridView1.Location = New Point(10, 10)
DataGridView1.Width = Me.Width - 10
DataGridView1.Height = Me.Height - 10
For c As Integer = 0 To 20
DataTable1.Columns.Add("col" & c)
If DataTable1.Rows.Count = 0 Then
DataTable1.Rows.Add()
End If
DataGridView1.Columns(c).AutoSizeMode = DataGridViewAutoSizeColumnMode.None '0%
DataTable1.Rows(0).Item(c) = "col" & c
DataGridView1.Columns(c).Width = 40
'Header
DataGridView1.Rows(0).Cells(c).Style.Alignment = DataGridViewContentAlignment.MiddleCenter
DataGridView1.Rows(0).Cells(c).Style.WrapMode = DataGridViewTriState.True
DataGridView1.Rows(0).Cells(c).Style.Font = New Font("Verdana", 8, FontStyle.Bold)
'Data
DataGridView1.Columns(c).DefaultCellStyle.Alignment = DataGridViewContentAlignment.MiddleRight
DataGridView1.Columns(c).DefaultCellStyle.WrapMode = DataGridViewTriState.False
DataGridView1.Columns(c).DefaultCellStyle.Font = New Font("Verdana", 8, FontStyle.Regular)
Next
For r As Integer = 1 To 25
DataTable1.Rows.Add()
Next
End Sub
Private Sub StartButton_Click(sender As Object, e As EventArgs) Handles StartButton.Click
Dim stpw As New Stopwatch
stpw.Reset()
stpw.Start()
For i As Integer = 0 To 10
Dim rand As New Random
Dim randnumber As Double = rand.Next(5, 15) / 10
UpdateDataTable(randnumber)
DataGridView1.Update()
Me.Text = i & "/100"
Next
stpw.Stop()
MsgBox(stpw.Elapsed.TotalMilliseconds)
End Sub
Private Sub UpdateDataTable(ByVal offset As Double)
For r As Integer = 1 To DataTable1.Rows.Count - 1 'loop through rows
For c As Integer = 0 To DataTable1.Columns.Count - 1 '89%
DataTable1.Rows(r).Item(c) = (r / c) * offset
Next
Next
End Sub
End Class
Edit:
I have to admit that I totally botched my original answer by erroneously believing that the call to DataGridView.Update was not needed to emulate the OP conditions. I am leaving my original text as it may be of use for someone in another situation.
A potential solution is to use a DoubleBuffered DataGridView. This can be accomplished by creating a class that inherits from DataGridView and enables DoubleBuffering.
Public Class BufferedDataGridView : Inherits DataGridView
Public Sub New()
MyBase.New()
Me.DoubleBuffered = True
End Sub
Protected Overrides Sub OnPaint(e As PaintEventArgs)
e.Graphics.Clear(Me.BackgroundColor)
MyBase.OnPaint(e)
End Sub
End Class
Doing this yields a change in appearance in that the client area is black until something is drawn on it. To alleviate this, the class overrides the OnPaint method to draw a background.
In my testing this reduced the bench-march time from approximately 2600 ms to approximately 600 ms.
End Edit
In addition to the highly pertinent suggestions of #Visual Vincent in the comments regarding eliminating unnecessary updating, I would recommend that you use a BindingSource to encapsulate the DataTable and use that as the DataGridview.DataSource.
Private bs As New BindingSource
Private Sub Load_From(ByVal sender As Object, ByVal e As System.EventArgs) Handles MyBase.Load
bs.DataSource = DataTable1
DataGridView1.DataSource = bs
This will allow you to temporary suspend change events raised through the DataTable that cause the DataGridView to repaint cells.
Private Sub UpdateDataTable(ByVal offset As Double)
' prevent each item change from raising an event that causes a redraw
bs.RaiseListChangedEvents = False
For r As Integer = 1 To DataTable1.Rows.Count - 1 'loop through rows
For c As Integer = 0 To DataTable1.Columns.Count - 1 '89%
DataTable1.Rows(r).Item(c) = (r / c) * offset
Next
Next
bs.RaiseListChangedEvents = True ' re-enable change events
bs.ResetBindings(False) ' Force bound controls to re-read list
End Sub
This way the will only repaint once to reflect all the changes to the underlying DataTable.

DataGridView loses cell formatting from DataTable.AcceptChanges

VB2010. I have researched this issue and cannot seem to find a reason for it or a workaround. What I have is a DataGridView that is bound to a DataTable. I allow the user to select Edit mode which turns ON/OFF the ReadOnly property. Once ReadMode=True I make sure to set the DataTable to AcceptChanges. When this property is set all my cell formatting disappears.
I do this on form load:
Private Sub frmMain_Load(sender As System.Object, e As System.EventArgs) Handles MyBase.Load
dgv.DataSource = Nothing
dgv.DataSource = GetTripData()
dgv.AutoResizeColumns()
dgv.ClearSelection()
dgv.ReadOnly = True
End Sub
Then the user can click on a menu item to go into Edit Mode:
Private Sub mnuEditMode_Click(sender As System.Object, e As System.EventArgs) Handles mnuEditMode.Click
If mnuEditMode.Checked Then
dgv.ReadOnly = False
dgv.AllowUserToAddRows = True
dgv.AllowUserToDeleteRows = True
Else
dgv.ReadOnly = True
dgv.AllowUserToAddRows = False
dgv.AllowUserToDeleteRows = False
'accept all changes. if we dont do this any row that is deleted will still exist in the DataTable.
Dim dt As DataTable = CType(dgv.DataSource, DataTable)
If dt IsNot Nothing Then
dt.AcceptChanges() 'note: this causes custom cell font to be cleared
End If
End If
End Sub
Once in edit mode they can dictate which cells to change. Two cells they put in the list to change are treated a such:
'update the proper cells via the DataGridView
dgv.Rows(2).Cells(5).Value = "HOME"
dgv.Rows(2).Cells(6).Value = 10
'bold the cell's font in the DataGridView
Dim styleUpdated As New DataGridViewCellStyle
styleUpdated.Font = New Font(dgv.Font, FontStyle.Bold)
dgv.Rows(2).Cells(6).Style = styleUpdated
dgv.Rows(2).Cells(6).Style = styleUpdated
'refresh the DGV
dgv.Refresh()
This works! I can see the changes in the DGV. Now they are done with editing the data so they click on the menu item to set Edit Mode Off and that sets dgv.ReadOnly=True and I also set dt.AcceptChanges. This last method AcceptChanges clears all the bold fonts on modified cells.
Is this expected behavior? If so what suggestions are there to keep my edited cell formatting?
This isn't really an answer but I want to post a significant piece of code so I'm posting it as an answer. I just tested the following code and it worked for me, in that the bold text remained as the two Buttons were clicked.
Public Class Form1
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
Dim table As New DataTable
With table.Columns
.Add("Id", GetType(Integer))
.Add("Name", GetType(String))
.Add("Age", GetType(Integer))
End With
With table.Rows
.Add(1, "Mary", 20)
.Add(2, "Paul", 30)
.Add(3, "Peter", 40)
End With
DataGridView1.DataSource = table
Dim style = DataGridView1.Rows(1).Cells(1).Style
style.Font = New Font(DataGridView1.Font, FontStyle.Bold)
DataGridView1.Rows(1).Cells(1).Style = style
End Sub
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
With DataGridView1
.ReadOnly = True
.AllowUserToAddRows = False
.AllowUserToDeleteRows = False
End With
End Sub
Private Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click
With DataGridView1
.ReadOnly = False
.AllowUserToAddRows = True
.AllowUserToDeleteRows = True
End With
End Sub
End Class
I'd suggest that you give that code a go and, if it works for you, you know that there's something else going on in your original project. You can then modify that test project slowly to make it more and more like your existing project and you should then be able to see where this functionality breaks.
I looked some more and I think the cell formatting clearing is expected behavior. So I came up with a small routine that will save each cell's formatting and then re-apply it after the AcceptChanges. Note that my DataTable is small so you may get a performance hit if you have a large dataset. Please provide any feedback if I have missed something:
'updated routine to implement AcceptChanges
'dt.AcceptChanges() 'note: this causes custom cell font to be cleared
DgvAcceptChanges(dgvMain)
''' <summary>
''' this routine will take a DataGridView and save the style for each cell, then it will take it's DataTable source and accept any
''' changes, then it will re-apply the style font to each DataGridView cell. this is required since DataTable.AcceptChanges will
''' clear any DataGridView cell formatting.
''' </summary>
''' <param name="dgv">DataGridView object</param>
''' <remarks>Could be extended to do other things like cell ReadOnly status or cell BackColor.</remarks>
Public Sub DgvAcceptChanges(dgv As DataGridView)
Dim dt As DataTable = CType(dgv.DataSource, DataTable)
If dt IsNot Nothing Then
'save the DataGridView's cell style font to an array
Dim cellStyle(dgv.Rows.Count - 1, dgv.Columns.Count - 1) As DataGridViewCellStyle
For r As Integer = 0 To dgv.Rows.Count - 1
'the DataGridViewRow.IsNewRow Property = Gets a value indicating whether the row is the row for new records.
'Remarks: Because the row for new records is in the Rows collection, use the IsNewRow property to determine whether a row
'is the row for new records or is a populated row. A row stops being the new row when data entry into the row begins.
If Not dgv.Rows(r).IsNewRow Then
For c As Integer = 0 To dgv.Columns.Count - 1
cellStyle(r, c) = dgv.Rows(r).Cells(c).Style
Next c
End If
Next r
'this causes custom cell font to be cleared in the DataGridView
dt.AcceptChanges()
're-apply the DataGridView's cell style font from an array
For r As Integer = 0 To dgv.Rows.Count - 1
If Not dgv.Rows(r).IsNewRow Then
For c As Integer = 0 To dgv.Columns.Count - 1
dgv.Rows(r).Cells(c).Style.Font = cellStyle(r, c).Font
Next c
End If
Next r
End If
End Sub

How to make cell readonly if the second cell is empty

The user should not be able to input a qty where the unit is empty in the datagridview.
To make it clear, I want to make the cell readonly = true if unit column is empty.
The colUOM4 is the name of the column that if the cell of this column is empty the olNewQty2 cell will be readonly.
I tried this code but it didn't work
Public Sub UnitEmpty()
For i As Integer = 0 To dgvCount.RowCount - 1
If dgvCount.Rows(i).Cells("colUOM4").Value Is Nothing Then
MessageBox.Show("It worked!")
dgvCount.Rows(i).Cells("colNewQty2").ReadOnly = True
Else
MessageBox.Show("Nothing happened!")
Exit For
End If
Next
End Sub
I'd recommend not using a loop because that will only set the state when you execute it and not react to any changes. I'd suggest working at the row and cell level, i.e. set the default state when a row is added and then react when a specific cell changes, e.g.
Private Sub DataGridView1_RowsAdded(sender As Object, e As DataGridViewRowsAddedEventArgs) Handles DataGridView1.RowsAdded
For i = e.RowIndex To e.RowIndex + e.RowCount - 1
'Make the first cell in each new row read-only by default.
DataGridView1(0, i).ReadOnly = True
Next
End Sub
Private Sub DataGridView1_CellValueChanged(sender As Object, e As DataGridViewCellEventArgs) Handles DataGridView1.CellValueChanged
'Check whether the change is in the second column.
If e.RowIndex >= 0 AndAlso e.ColumnIndex = 1 Then
Dim row = DataGridView1.Rows(e.RowIndex)
'Make the first cell in the row read-only if and only if the second cell is empty.
row.Cells(0).ReadOnly = (row.Cells(1).Value Is Nothing)
End If
End Sub

dgv add row number to headercell

I'm trying to show the rownumber of a datagridview in the headercell I was under the impression that this was possible but I can't get it to show the value.
What do I need to do?
** The dgv is bound with BindingSource **
My attempt:
Private Sub NumberRows()
For Each oRow As DataGridViewRow In dgvBodyOverview.Rows
oRow.HeaderCell.Value = (oRow.Index + 1).ToString
Next
End Sub
To reproduce your problem, I followed your comment:
I'm calling it from a sub that loads all rows and at the end the call is made.
Doing this, the numbers failed to show whether or not the grid had a DataSource. Instead, I've typically done this as Plutonix suggested above - in DataGridView.CellFormatting.
Private Sub dataGridView1_CellFormatting(sender As Object, e As DataGridViewCellFormattingEventArgs)
Dim header As DataGridViewRowHeaderCell = Me.dataGridView1.Rows(e.RowIndex).HeaderCell
If e.ColumnIndex = 0 Then
header.Value = [String].Format("{0}", e.RowIndex + 1)
End If
End Sub
I check for ColumnIndex = 0 to ensure the header cell value is only set once per row. See this C# answer for additional explanation.

Why is gridview.selectedColumns nothing?

I want to run a code like this, but it always jumps over the loop, so I see no line in the console.
That means that selectedColumns is empty. My assumption was that I (or the user) select a cell from a Column and then, selectedColummns are +1. But as it looks, it doesnt work. Then I tried to set proberties of selectionMode to select full columns, but then an exception is thrown:
"System.InvalidOperationException" Additional Information: the SortMode cannot be Automatic, if full Column selection is selected.
I don't know what SortMode is.
For Each col As DataGridViewColumn In datagridview2.SelectedColumns
Console.Write(datagridview2.SelectedColumns.Count)
Console.Write("1")
Next
Any Ideas how to get that the columns into selectedColumns?
Here my code that solved my problem, but I guess it is not the smartest one:
Private Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click
Dim body As String = ""
Dim myWriter As New StreamWriter("H:\downloads\test.csv", True)
Dim list As List(Of Integer) = New List(Of Integer)
For Each cell As DataGridViewCell In datagridview2.SelectedCells
If list.Contains(cell.ColumnIndex) = False Then
list.Add(cell.ColumnIndex)
End If
Next
For i = 0 To datagridview2.Rows.Count - 1
For ix = 0 To datagridview2.Columns.Count - 1
If list.Contains(ix) Then
If datagridview2.Rows(i).Cells(ix).Value IsNot Nothing Then
body = body + datagridview2.Rows(i).Cells(ix).Value.ToString + ";"
Else
body = body + ";"
End If
End If
Next
myWriter.WriteLine(body)
body = ""
Next
myWriter.Close()
End Sub
You need to do this after the selected changed event has fired.
For example:
Private Sub Mydg_ColSelected(sender As Object, e As SelectionChangedEventArgs) Handles datagridview2.SelectionChanged
For Each col As DataGridViewColumn In datagridview2.SelectedColumns
Console.Write(datagridview2.SelectedColumns.Count)
Console.Write("1")
Next
End Sub
Or if this isn't what you are after, then try handling the ColumnHeaderMouseClick event instead. I'm not sure what technology you are using, e.g Winforms, WPF, silverlight