Using Delegates
I have a form with a datagrid and a class that loads usercounts from a database and returns them as a dataset.
I use threading to load the dataset so it will not 'freeze' my form. But I can not bind the dataset I get back from the thread to the datagrid, says can not bind to parent control if created in another thread. After some reading, I found out I should use invoke.
I`m pretty much stuck on using delegates... can you please look at the code I have so far and give me hint/tips/answers?
Code snippets:
Private WithEvents myGrid As New AccountGrid()
Private AccountGridThread As New Thread(AddressOf myGrid.FillDataSet)
Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
DataGrid1.Enabled = False
AccountGridThread.Start()
StatusBar1.Text = "Loading accounts..."
End Sub
Delegate Function BindGrid(ByVal DS As DataSet) As DataSet
Private GridDelegate As BindGrid
Private Sub GetAccountsCompleted(ByVal DS As DataSet) Handles myGrid.ThreadComplete
MsgBox(DS.GetXml.ToString)
DataGrid1.Enabled = True
Dim DataBindDelegate As BindGrid = AddressOf myGrid. '--What to do here?
'--DataGrid1.SetDataBinding(DS, "Accounts")
' Gives error: Cannot bind control created in another thread...
' this is the problem i`m trying to solve.
StatusBar1.Text = "Done loading accounts"
End Sub
Not actually following what your grid is doing, and not having enough code to actually compile and play with your demo, it's hard to know what to tell you.
OTOH, if your grid provides a procedure that matches the procedure you've specified in the BindGrid delegate, then you should be able to say
Dim DataBindDelegate As New BindGrid(AddressOf myGrid.YourProcedure)
Me.Invoke(DataBindDelegate, {YourDataSet})
where {yourDataSet} is an array containing a reference to your DataSet variable to be filled.
Obviously, since I can't run your code, all I can do is show you the syntax for getting started. This may or may not actually solve your problem.
Ok, i`m having a hard time with your advice so i`ll post all the code here:
Imports System.Data.SqlClient
AccountGrid.vb:
Public Class AccountGrid
Private _DataSet As New Dataset()
Private Const ConnStr As String = SqlConnStringHere
Public Event ThreadComplete(ByVal DataSet As DataSet)
ReadOnly Property Dataset() As Dataset
Get
Return _DataSet
End Get
End Property
Public Sub FillDataSet()
Dim myConnection As SqlConnection = New SqlConnection(ConnStr)
Try
Dim myAdapter As New SqlDataAdapter("tsp_get_Accounts", myConnection)
myConnection.Open()
myAdapter.Fill(_DataSet, "Accounts")
myConnection.Close()
Catch ex As Exception
RecordError.Record(ex)
End Try
RaiseEvent ThreadComplete(_DataSet)
End Sub
End Class
Main.vb
Imports System.Data.SqlClient
Imports System.Threading
Public Class FormMain
Inherits System.Windows.Forms.Form
Private WithEvents myGrid As New AccountGrid()
Private AccountGridThread As New Thread(AddressOf myGrid.FillDataSet)
#Region " Windows Form Designer generated code "
Public Sub New()
MyBase.New()
'This call is required by the Windows Form Designer.
InitializeComponent()
'Add any initialization after the InitializeComponent() call
End Sub
'Form overrides dispose to clean up the component list.
Protected Overloads Overrides Sub Dispose(ByVal disposing As Boolean)
If disposing Then
If Not (components Is Nothing) Then
components.Dispose()
End If
End If
MyBase.Dispose(disposing)
End Sub
'Required by the Windows Form Designer
Private components As System.ComponentModel.IContainer
'NOTE: The following procedure is required by the Windows Form Designer
'It can be modified using the Windows Form Designer.
'Do not modify it using the code editor.
Friend WithEvents MenuAbout As System.Windows.Forms.MenuItem
<System.Diagnostics.DebuggerStepThrough()> Private Sub InitializeComponent()
Me.MainMenu1 = New System.Windows.Forms.MainMenu()
Me.MenuItem1 = New System.Windows.Forms.MenuItem()
Me.MenuItem4 = New System.Windows.Forms.MenuItem()
Me.MenuItem5 = New System.Windows.Forms.MenuItem()
Me.MenuItem2 = New System.Windows.Forms.MenuItem()
Me.MenuItem3 = New System.Windows.Forms.MenuItem()
Me.MenuAbout = New System.Windows.Forms.MenuItem()
Me.DataGrid1 = New System.Windows.Forms.DataGrid()
Me.StatusBar1 = New System.Windows.Forms.StatusBar()
CType(Me.DataGrid1, System.ComponentModel.ISupportInitialize).BeginInit()
Me.SuspendLayout()
'
'MainMenu1
'
Me.MainMenu1.MenuItems.AddRange(New System.Windows.Forms.MenuItem() {Me.MenuItem1, Me.MenuItem2})
'
'MenuItem1
'
Me.MenuItem1.Index = 0
Me.MenuItem1.MenuItems.AddRange(New System.Windows.Forms.MenuItem() {Me.MenuItem4, Me.MenuItem5})
Me.MenuItem1.Text = "Commands"
'
'MenuItem4
'
Me.MenuItem4.Index = 0
Me.MenuItem4.Text = "Add Email"
'
'MenuItem5
'
Me.MenuItem5.Index = 1
Me.MenuItem5.Text = "Exit"
'
'MenuItem2
'
Me.MenuItem2.Index = 1
Me.MenuItem2.MenuItems.AddRange(New System.Windows.Forms.MenuItem() {Me.MenuItem3, Me.MenuAbout})
Me.MenuItem2.Text = "Help"
'
'MenuItem3
'
Me.MenuItem3.Index = 0
Me.MenuItem3.Text = "Help"
'
'MenuAbout
'
Me.MenuAbout.Index = 1
Me.MenuAbout.Text = "About"
'
'DataGrid1
'
Me.DataGrid1.DataMember = ""
Me.DataGrid1.Dock = System.Windows.Forms.DockStyle.Fill
Me.DataGrid1.HeaderForeColor = System.Drawing.SystemColors.ControlText
Me.DataGrid1.Name = "DataGrid1"
Me.DataGrid1.ReadOnly = True
Me.DataGrid1.Size = New System.Drawing.Size(520, 294)
Me.DataGrid1.TabIndex = 0
'
'StatusBar1
'
Me.StatusBar1.Location = New System.Drawing.Point(0, 272)
Me.StatusBar1.Name = "StatusBar1"
Me.StatusBar1.Size = New System.Drawing.Size(520, 22)
Me.StatusBar1.TabIndex = 1
'
'FormMain
'
Me.AutoScaleBaseSize = New System.Drawing.Size(5, 13)
Me.ClientSize = New System.Drawing.Size(520, 294)
Me.Controls.AddRange(New System.Windows.Forms.Control() {Me.StatusBar1, Me.DataGrid1})
Me.Menu = Me.MainMenu1
Me.Name = "FormMain"
Me.Text = "Mail to Database Importer"
CType(Me.DataGrid1, System.ComponentModel.ISupportInitialize).EndInit()
Me.ResumeLayout(False)
End Sub
#End Region
Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
DataGrid1.Enabled = False
AccountGridThread.Start()
StatusBar1.Text = "Loading accounts..."
End Sub
Delegate Function BindGrid(ByVal DS As DataSet) As DataSet
Private GridDelegate As BindGrid
Private Sub GetAccountsCompleted(ByVal DS As DataSet) Handles myGrid.ThreadComplete
MsgBox(DS.GetXml.ToString)
DataGrid1.Enabled = True
'Dim DataBindDelegate As BindGrid = (AddressOf )Dim DataBindDelegate As New BindGrid(AddressOf myGrid.Dataset)
'DataGrid1.SetDataBinding(myGrid.Dataset, "Accounts")
StatusBar1.Text = "Done loading accounts"
End Sub
Friend WithEvents MenuItem1 As System.Windows.Forms.MenuItem
Friend WithEvents MainMenu1 As System.Windows.Forms.MainMenu
Private Sub MenuAbout_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MenuAbout.Click
Dim AboutFrm As New AboutForm()
AboutFrm.Show()
End Sub
Friend WithEvents MenuItem3 As System.Windows.Forms.MenuItem
Friend WithEvents MenuItem2 As System.Windows.Forms.MenuItem
Private Sub MenuItem3_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MenuItem3.Click
Dim HelpForm As New HelpForm()
HelpForm.Show()
End Sub
Private Sub MenuItem5_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MenuItem5.Click
Application.Exit()
End Sub
Friend WithEvents MenuItem5 As System.Windows.Forms.MenuItem
Friend WithEvents MenuItem4 As System.Windows.Forms.MenuItem
Private Sub MenuItem4_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MenuItem4.Click
Dim AddNewAccount As New AddAccount()
AddNewAccount.Show()
End Sub
Friend WithEvents StatusBar1 As System.Windows.Forms.StatusBar
Friend WithEvents DataGrid1 As System.Windows.Forms.DataGrid
End Class
Well, that didn't help much. Still couldn't run it--too many "bits" missing.
So I created my own demo, and worked through it.
Here's the deal: you WANT to be able to set the data source from the event handler of the ThreadComplete event, but you can't because it's in a different thread than the form's thread. What you WANT to do is this:
Me.DataGrid1.SetDataBinding(ds, "Accounts")
But you can't. So, first step is to create a procedure that does that for you:
Private Sub SetDataSource(ByVal Grid As DataGrid, ByVal ds As DataSet, ByVal DataMember As String)
Grid.SetDataBinding(ds, DataMember)
End Sub
and call that procedure from your event handler:
Private Sub myGrid_ThreadComplete(ByVal DS As System.Data.DataSet) Handles myGrid.ThreadComplete
DataGrid1.Enabled = True
SetDataSource(Me.DataGrid1, DS, "Accounts")
End Sub
Of course, that doesn't really help, because it's still running on the wrong thread.
Then, create a delegate type that matches the procedure, and add it to the class, at the top near your other class-level declarations:
Private Delegate Sub SetDataSourceDelegate(ByVal Grid As DataGrid, ByVal ds As DataSet, ByVal DataMember As String)
Finally, modify your event handler to create a new instance of this delegate type, and invoke it instead of the current method call. That is, replace:
SetDataSource(Me.DataGrid1, DS, "Accounts")
with this:
Dim myProc As New SetDataSourceDelegate(AddressOf SetDataSource)
Me.Invoke(myProc, New Object() {Me.DataGrid1, DS, "Accounts"})
That's it! Now, you're setting the datagrid's data source from the form's thread. Note that you have to pass an array of Objects containing the method's parameters, as shown in this demo.