I'm using Observal.subscribe to run a sub that changes a value of label
with this code
Dim observ As IObservable(Of Long) = System.Reactive.Linq.Observable.Interval(TimeSpan.FromSeconds(10))
Dim source As New System.Threading.CancellationTokenSource()
Dim action As Action = (Sub() ChangeLed())
Dim resumeAction As Action = (Sub() ChangeLed())
observ.Subscribe(Sub(x)
Dim task As New Task(action)
task.Start()
task.ContinueWith(Sub(c) resumeAction())
End Sub, source.Token)
and the changeled() sub mentioned above is by example
private sub Changeled()
If PingHost("myhostIP") Then
label1.Content = "Connected"
Else
label1.Content = "Not Connected"
End If
end sub
I have an error says : "The calling thread cannot access this object because a different thread owns it."
I know i have to use invoke By dispatcher here but i don't know where.
ps:the pingHost is a function that returns true or false based on pinging a host
You're using two different concurrency models: Task Parallel Library (TPL) and Rx. Ideally you would use only one (Rx) like so:
observ.
Select(Function(i) Pinghost("myHostIP")).
ObserveOnDispatcher().
Subscribe(Sub(pingResult)
If pingResult Then
label1.Content = "Connected"
Else
label1.Content = "Not Connected"
End If
End Sub, source.Token)
ObserveOnDispatcher comes from nuget package System.Reactive.Windows.Threading.
If you want to keep your TPL code in there, then I would update ChangeLed as follows:
Private Sub Changeled()
If PingHost("myhostIP") Then
this.Dispatcher.Invoke(Sub() label1.Content = "Connected")
Else
this.Dispatcher.Invoke(Sub() label1.Content = "Not Connected")
End If
End Sub
Related
I have a whole lot of stored procedures in a SQL Server database that need to be executed in sequence.
I have them setup in a table that have multiple sequences each containing a max of 5 stored procedures - so basically 5x threads:
I would like loop through each sequence and kick off each sequence's stored procedures at the same time. Then when all the stored procedures have completed, the next sequence of stored procedures can be kicked off.
I will be using SQL commands in .NET to kick of each stored procedure, but I need a way for them to all start at the same time and have the process wait for all to finish before moving on to the next sequence.
The idea of this is so that I can save time on processing data. If each stored procedure runs 2 minutes, running 5 at a time will take 2 minutes instead of 10 minutes.
Now I was told TPL would be a good approach. This is my first time working with multi-threading / TPL.
Below is a class that contain the properties and a function to execute a SQL Server stored procedure:
Public Class clsSQLStoredProcedure
Private mConnection As SqlClient.SqlConnection
Private mCommand As SqlClient.SqlCommand
Private mSQLStoredProcedure As String
Private mName As String
Private mStatus As String = "Pending"
Private mFeedback As DataTable
Public Property Connection() As SqlClient.SqlConnection
Set(ByVal o As SqlClient.SqlConnection)
mConnection = o
End Set
Get
Return mConnection
End Get
End Property
Public Property SQLStoredProcedure() As String
Set(ByVal o As String)
mSQLStoredProcedure = o
End Set
Get
Return mSQLStoredProcedure
End Get
End Property
Public Property Name() As String
Set(ByVal o As String)
mName = o
End Set
Get
Return mName
End Get
End Property
Public ReadOnly Property Status() As String
Get
Return mStatus
End Get
End Property
Public ReadOnly Property Feedback() As DataTable
Get
Return mFeedback
End Get
End Property
Public Function ExecuteSQLStoredProcedure()
On Error GoTo Errorhandler
Debug.Print(mName & " Start # " & Now().ToString)
mStatus = "Running"
If mConnection.State <> ConnectionState.Open Then mConnection.Open()
mCommand = New SqlClient.SqlCommand(mSQLStoredProcedure, mConnection)
mCommand.CommandTimeout = 0
mCommand.ExecuteNonQueryAsync()
mStatus = "Completed"
Debug.Print(mName & " Completed # " & Now().ToString)
Exit Function
Errorhandler:
mStatus = "Failed"
Debug.Print(mName & " Failed # " & Now().ToString & " - " & Err.Description)
End Function
End Class
This is the code I use to perform tasks (TPL) to start 3 stored procedures at the same time:
Imports System.Threading
Imports System.Threading.Tasks
Module modThreading
Public Sub TestThreading()
Dim oSP1 As New clsSQLStoredProcedure
oSP1.SQLStoredProcedure = "EXEC CURRO_DW.conform.usp_ETLFactCollectorActivity -99"
oSP1.Connection = gGDM.Database.Connection
oSP1.Name = "usp_ETLFactCollectorActivity"
'oSP1.ExecuteSQLStoredProcedure()
Task.Run(action:=oSP1.ExecuteSQLStoredProcedure())
Dim oSP2 As New clsSQLStoredProcedure
oSP2.SQLStoredProcedure = "EXEC CURRO_DW.conform.usp_ETLDimPerson -99"
oSP2.Connection = gGDM.Database.Connection
oSP2.Name = "usp_ETLDimPerson"
'oSP2.ExecuteSQLStoredProcedure()
Task.Run(action:=oSP2.ExecuteSQLStoredProcedure())
Dim oSP3 As New clsSQLStoredProcedure
oSP3.SQLStoredProcedure = "SELECT 1"
oSP3.Connection = gGDM.Database.Connection
oSP3.Name = "TEST"
'oSP3.ExecuteSQLStoredProcedure()
Task.Run(action:=oSP3.ExecuteSQLStoredProcedure())
MsgBox("Done")
End Sub
End Module
However they still seem to run one after the other even though some are suppose to run instant while other take about 1min.
Don't keep your connection open for the life of your application. You should open and close a database connection as late and early as possible, respectively. A database connection runs on a single thread so the first task, although being run asynchronously, will block the second, et cetera.
So create a new connection for each stored procedure and perhaps WaitAll at the end. You can put the Tasks in a collection to do this.
Dim gGDM1 As New DatabaseContext()
Dim gGDM2 As New DatabaseContext()
Dim gGDM3 As New DatabaseContext()
Try
Dim oSP1 As New clsSQLStoredProcedure
oSP1.SQLStoredProcedure = "EXEC CURRO_DW.conform.usp_ETLFactCollectorActivity -99"
oSP1.Connection = gGDM1.Database.Connection
oSP1.Name = "usp_ETLFactCollectorActivity"
Dim oSP2 As New clsSQLStoredProcedure
oSP2.SQLStoredProcedure = "EXEC CURRO_DW.conform.usp_ETLDimPerson -99"
oSP2.Connection = gGDM2.Database.Connection
oSP2.Name = "usp_ETLDimPerson"
Dim oSP3 As New clsSQLStoredProcedure
oSP3.SQLStoredProcedure = "SELECT 1"
oSP3.Connection = gGDM3.Database.Connection
oSP3.Name = "TEST"
Dim tasks As New List(Of Task)()
tasks.Add(Task.Run(oSP1.ExecuteSQLStoredProcedure))
tasks.Add(Task.Run(oSP2.ExecuteSQLStoredProcedure))
tasks.Add(Task.Run(oSP3.ExecuteSQLStoredProcedure))
Task.WaitAll(tasks.ToArray())
Finally
gGDM1.Dispose()
gGDM2.Dispose()
gGDM3.Dispose()
MsgBox("Done")
End Try
You need to modify the Dim gGDM As New DatabaseContext() lines to create a new context, however you have done it globally, but instead for each call. They are immediately disposed of when you're done.
So I am working with blobs in visual basic, and I'm saving image files to a database. When I save them I have their name.jpg showing up in a combobox, but when I save another image and it refreshes the list, the name of the previous image shows twice. I'm not sure how I managed that. I am new to this so don't look down on me too much!
When the button on my form is clicked:
Private Sub btnSaveBlob_Click(sender As Object, e As EventArgs) Handles btnSaveBlob.Click
SaveBlobToDatabase()
refreshBlobList()
End Sub
My methods:
Private Sub SaveBlobToDatabase()
GetCompleteFilePath()
Dim BLOB() As Byte
Dim FileStream As New IO.FileStream _
(CompleteFilePath, IO.FileMode.Open, IO.FileAccess.Read)
Dim reader As New IO.BinaryReader(FileStream)
BLOB = reader.ReadBytes(CInt(My.Computer.FileSystem.GetFileInfo(CompleteFilePath).Length))
FileStream.Close()
reader.Close()
Dim SaveDocCommand As New SqlCommand
SaveDocCommand.Connection = conn
SaveDocCommand.CommandText = "INSERT INTO DocumentStorage" &
"(FileName, DocumentFile)" &
"VALUES (#FileName, #DocumentFile)"
Dim FileNameParameter As New SqlParameter("#FileName", SqlDbType.NChar)
Dim DocumentFileParameter As New SqlParameter("#DocumentFile", SqlDbType.Binary)
SaveDocCommand.Parameters.Add(FileNameParameter)
SaveDocCommand.Parameters.Add(DocumentFileParameter)
FileNameParameter.Value =
CompleteFilePath.Substring(CompleteFilePath.LastIndexOf("\") + 1)
DocumentFileParameter.Value = BLOB
Try
SaveDocCommand.Connection.Open()
SaveDocCommand.ExecuteNonQuery()
MessageBox.Show(FileNameParameter.Value.ToString &
"saved to database.", "BLOB Saved!", MessageBoxButtons.OK,
MessageBoxIcon.Information)
Catch ex As Exception
MessageBox.Show(ex.Message, "Save Failed",
MessageBoxButtons.OK, MessageBoxIcon.Error)
Finally
SaveDocCommand.Connection.Close()
End Try
End Sub
Private Sub refreshBlobList()
Dim GetBlobListCommand As New SqlCommand("SELECT FileName FROM DocumentStorage", conn)
Dim reader As SqlDataReader
GetBlobListCommand.Connection.Open()
reader = GetBlobListCommand.ExecuteReader
While reader.Read
lstBlob.Items.Add(reader(0))
End While
reader.Close()
GetBlobListCommand.Connection.Close()
If lstBlob.Items.Count > 0 Then
lstBlob.SelectedIndex = 0
End If
End Sub
Objects in .net that show you a .Dispose method should be disposed. They have unmanaged code and need to properly release things. Connections, Commands, Streams, and Readers fall into this group. Luckily .net has provided Using...End Using blocks that handle this for us even if there is an error. For this reason, these objects should be kept local to the methods where they are used.
As much as possible, methods should perform a single task. I pulled out the code to create the byte array to a separate Function. I also split the data access code from the user interace code. You may want to put the data access code in a separate class. This would come in handy if you want to change to a web app, for example.
In the data access code:
You can pass the connection string directly to the constructor of the connection. Likewise, pass the CommandText and Connection directly to the constructor of the command. The .Add method of the .Parameters collection will create a new parameter from the name and datatype passed to it. You can also set the value on the same line.
Private ConStr As String = "Your connection string"
Private Sub Button3_Click(sender As Object, e As EventArgs) Handles Button3.Click
Dim RowsAdded As Integer
Try
RowsAdded = SaveBlobToDatabase()
Catch ex As Exception
MessageBox.Show(ex.Message, "Save Failed", MessageBoxButtons.OK, MessageBoxIcon.Error)
End Try
If RowsAdded = 1 Then
MessageBox.Show("Saved to database.", "BLOB Saved!", MessageBoxButtons.OK, MessageBoxIcon.Information)
End If
End Sub
Private Function SaveBlobToDatabase() As Integer
Dim RowsAdded As Integer
Dim CompleteFilePath As String = GetCompleteFilePath()
Using conn As New SqlConnection(ConStr),
SaveCommand As New SqlCommand("INSERT INTO DocumentStorage(FileName, DocumentFile) VALUES(#FileName, #DocumentFile);", conn)
SaveCommand.Parameters.Add("#FileName", SqlDbType.NChar).Value = Path.GetFileName(CompleteFilePath) 'Requires Imports System.IO
SaveCommand.Parameters.Add("#DocumentFile", SqlDbType.Binary).Value = GetByteArrayFromFile(CompleteFilePath)
conn.Open()
RowsAdded = SaveCommand.ExecuteNonQuery()
End Using
Return RowsAdded
End Function
Private Function GetByteArrayFromFile(FullPath As String) As Byte()
Dim Blob() As Byte
Using FileStream As New IO.FileStream(FullPath, IO.FileMode.Open, IO.FileAccess.Read),
reader As New IO.BinaryReader(FileStream)
Blob = reader.ReadBytes(CInt(My.Computer.FileSystem.GetFileInfo(FullPath).Length))
End Using
Return Blob
End Function
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
lstBlob.Items.Clear()
lstBlob.DataSource = refreshBlobList()
lstBlob.DisplayMember = "FileName"
If lstBlob.Items.Count > 0 Then
lstBlob.SelectedIndex = 0
End If
End Sub
Private Function refreshBlobList() As DataTable
Dim dt As New DataTable
Using conn As New SqlConnection(ConStr),
GetBlobListCommand As New SqlCommand("SELECT FileName FROM DocumentStorage", conn)
conn.Open()
dt.Load(GetBlobListCommand.ExecuteReader)
End Using
Return dt
End Function
I'm not an experienced VBA programmer but I've been trying to create an Excel Spreadsheet that is able to manage a basketball team.
In it I've got a primary userform where I have declared an array, 'selectedPlayers'.
This primary userform has a for loop that starts up the secondary userform 'i' times.
I have not been able to access the primary userform's 'i' and 'selectedPlayers' from the secondary one.
I've been able to find a workaround the 'i' by creating a non-visible textbox in the first userform, that I'm able to reference from the second one.
I've tried declaring both of them as public, but yet I'm not able to call upon it from the second userform.
part of the code for the first userform:
i = 0
Do While Not i = Int(txtNumberPlayers)
frmGameDataSecondary.Show
i = i + 1
Loop
second userform:
Private Sub cmdDone_Click()
frmGameData.selectedPlayers(frmGameData.i) = lbxPlayer.Value
Unload Me
End Sub
Private Sub UserForm_Initialize()
With Me.lbxPlayer
For Each LR In LO.ListRows
exitSequence = False
For k = 1 To Int(frmGameData.txtNumberPlayers)
If frmGameData.selectedPlayers(k) = blablabla.Value Then
exitSequence = True
End If
Next k
If !exitSequence Then
.AddItem blablabla.Value
End If
Next LR
End With
End Sub
The main problem is that array contents are cleared after the sub is finished.
I was also messing around with this idea and there is a really good thread I started with tons of great information from various awesome people
Calling an Array Upon User Form Terminate/Close VBA
Forms in VBA are Objects, and can be treated like any other Class module. This means that you can add properties to them. If you need to pass information back from a form, all you need to do is grab a reference to it, then Hide it instead of Unload it. Treat it like a dialog and let the calling code handle it's create and destruction (I'm assuming from your code that it is modal).
Something like this:
In the first UserForm:
For i = 0 To 1
Dim second As frmGameDataSecondary
Set second = New frmGameDataSecondary
second.Show
'Execution suspends until the second form is dismissed.
selectedPlayers(i) = second.Player
Unload second
Next i
In the second UserForm:
Private mPlayer As String
'This is where your returned information goes.
Public Property Get Player() As String
Player = mPlayer
End Property
Private Sub cmdDone_Click()
mPlayer = lbxPlayer.Value
'Control passes back to the caller, but the object still exists.
Me.Hide
End Sub
You can declare properties inside of parent form which will manipulate the array from outside. The child form needs to have a reference to parent so it can call this properties. HTH
Parent form
Option Explicit
' I have not been able to access the primary userform's
' 'i' and 'selectedPlayers' from the secondary one
Private selectedPlayers As Variant
Public Function GetMyArrayValue(index) As Variant
GetMyArrayValue = selectedPlayers(index)
End Function
Public Sub SetMyArrayValue(index, newValue)
selectedPlayers(index) = newValue
End Sub
Private Sub UserForm_Click()
Dim i
i = 0
Do While Not i = Int(txtNumberPlayers)
With New secondaryUserForm
Set .ParentForm = Me
.SetIndex = i
.Show
End With
i = i + 1
Loop
End Sub
Private Sub UserForm_Initialize()
selectedPlayers = Array("A", "B", "C")
End Sub
Child form
Option Explicit
Private m_parent As primaryUserForm
Private m_index As Integer
Public Property Let SetIndex(ByVal vNewValue As Integer)
m_index = vNewValue
End Property
Public Property Set ParentForm(ByVal vNewValue As UserForm)
Set m_parent = vNewValue
End Property
Private Sub cmdDone_Click()
' frmGameData.selectedPlayers(frmGameData.i) = lbxPlayer.Value
m_parent.SetMyArrayValue m_index, "lbxPlayer.Value"
Unload Me
End Sub
I have a program that uploads files to our server when they are dropped over the form, to make it easy for our customers to get large files to us.
I have it mostly working, but I want to have a progress bar so that the user knows it's working, instead of having it just sit there for 5 minutes while the files upload quietly in the background.
I would be happy to just have the progress bar pulse so it looks the program is working, and not frozen. If I can show actual status then that would be better.
My code:
Private Sub Grid1_Drop(sender As System.Object, e As System.Windows.DragEventArgs) Handles Grid1.Drop
Dim sFileInfo As System.IO.FileInfo
Dim sStatus As String = ""
If e.Data.GetDataPresent("FileDrop") Then
Try
Dim theFiles() As String = CType(e.Data.GetData("FileDrop", True), String())
For Each file As String In theFiles
sFileInfo = New System.IO.FileInfo(file)
If UploadFile(txtUsername.Text, sFileInfo) Then
lstFileList.Items.Add(file & " - Uploaded")
Else
lstFileList.Items.Add(file & " - Upload Failed")
End If
Next
Catch ex As Exception
MsgBox(ex.Message)
End Try
End If
End Sub
Public Function UploadFile(ByVal User As String, ByVal oFile As FileInfo) As Boolean
Dim ftpRequest As FtpWebRequest
Dim ftpResponse As FtpWebResponse
Try
ftpRequest = CType(FtpWebRequest.Create(Base + User + "/" + oFile.Name), FtpWebRequest)
ftpRequest.Method = WebRequestMethods.Ftp.UploadFile
ftpRequest.Proxy = Nothing
ftpRequest.UseBinary = True
ftpRequest.Credentials = Cred
ftpRequest.KeepAlive = KeepAlive
ftpRequest.EnableSsl = UseSSL
If UseSSL Then ServicePointManager.ServerCertificateValidationCallback = New RemoteCertificateValidationCallback(AddressOf ValidateServerCertificate)
Dim fileContents(oFile.Length) As Byte
Using fr As FileStream = oFile.OpenRead
fr.Read(fileContents, 0, Convert.ToInt32(oFile.Length))
End Using
Using writer As Stream = ftpRequest.GetRequestStream
writer.Write(fileContents, 0, fileContents.Length)
End Using
ftpResponse = CType(ftpRequest.GetResponse, FtpWebResponse)
ftpResponse.Close()
ftpRequest = Nothing
Return True
Catch ex As WebException
Return False
End Try
End Function
Have a look at the background worker class. http://msdn.microsoft.com/en-us/library/cc221403(v=vs.95).aspx which will free up your ui so you can add in a progress bar control and have it animate while your files are being uploaded
I have a search box that works great in WinForms, but is giving me trouble in WPF.
It works by starting a search each time a letter is pushed, similar to Google.
If (txtQuickSearch.Text.Length >= 3) Or (e.Key = Key.Enter) Then
If SearchWorker.IsBusy Then
SearchWorker.CancelAsync()
Do While SearchWorker.IsBusy
'Application.DoEvents()
'System.Threading.Thread.Sleep(500)
Loop
End If
doSearchText = txtQuickSearch.Text
SearchWorker.RunWorkerAsync()
End If
Every time a key is pushed it cancels the current searchWorker then restarts it. In WinForms the Do while searchworker.isbusy doevents loop worked great, but since I don't have access to that anymore, I need to figure out a better way to do it. Sleep() deadlocks it, and I've tried just doing i+=1 as a way to pass time until it's not busy, but that doesn't work either...
What should I do?
Update: Here's what I changed it to. It works, but the cancel part doesn't seem to ever trigger, this doesn't seem to be running async...
Imports System.ComponentModel
Imports System.Collections.ObjectModel
Imports System.Threading
Imports System.Threading.Tasks
Public Class QuickSearch
Private doSearchText As String
Private canceled As Boolean
Private curSearch As String
Dim searchResults As New ObservableCollection(Of ocSearchResults)
'Task Factory
Private cts As CancellationTokenSource
Private searchtask As Task(Of ObservableCollection(Of ocSearchResults))
Private Sub txtQuickSearch_KeyDown(ByVal sender As System.Object, ByVal e As System.Windows.Input.KeyEventArgs) Handles txtQuickSearch.KeyDown
If e.Key = Key.Enter Then
curSearch = ""
End If
If ((txtQuickSearch.Text.Length >= 3) Or (e.Key = Key.Enter)) And Not txtQuickSearch.Text = curSearch Then
If Not cts Is Nothing Then
cts.Cancel()
ColorChecker.CancelAsync()
Try
' searchtask.Wait()
Catch ex As AggregateException
MsgBox(ex.InnerException.Message)
End Try
cts = New CancellationTokenSource
Else
cts = New CancellationTokenSource
End If
Dim cToken = cts.Token
Me.Height = 400
doSearchText = txtQuickSearch.Text
'This always completes fully before continuing on to tRunWorkerComplete(searchtask.Result) '
searchtask = Task(Of ObservableCollection(Of ocSearchResults)).Factory.StartNew(Function() tPerformSearch(cToken), cToken)
Try
tRunWorkerCompleted(searchtask.Result)
Catch ex As AggregateException
' MsgBox(ex.InnerException.Message)
End Try
Else
If Not cts Is Nothing Then
cts.Cancel()
End If
searchResults.Clear()
End If
End Sub
Function tPerformSearch(ByVal ct As CancellationToken) As ObservableCollection(Of ocSearchResults)
On Error GoTo sError
canceled = False
If curSearch = doSearchText Then
canceled = True
Return Nothing
End If
curSearch = doSearchText
Dim SR As New ObservableCollection(Of ocSearchResults)
Dim t As ocSearchResults
Dim rSelect As New ADODB.Recordset
Dim sSql As String = "SELECT DISTINCT CustomerID, CustomerName, City, State, Zip FROM qrySearchFieldsQuick WHERE "
Dim sWhere As String = "CustomerName Like '" & doSearchText & "%'"
SR.Clear()
With rSelect
.Open(sSql & sWhere & " ORDER BY CustomerName", MyCn, ADODB.CursorTypeEnum.adOpenStatic, ADODB.LockTypeEnum.adLockReadOnly)
Do While Not .EOF
If ct.IsCancellationRequested Then ' This never shows true, the process always returns a value, as if it wasn't async'
canceled = True
Return Nothing
End If
Do While IsDBNull(.Fields("CustomerID").Value)
.MoveNext()
Loop
t = New ocSearchResults(.Fields!CustomerID.Value, .Fields!CustomerName.Value.ToString.Trim, .Fields!City.Value.ToString.Trim, .Fields!State.Value.ToString.Trim, .Fields!Zip.Value.ToString.Trim)
If Not SR.Contains(t) Then
SR.Add(t)
End If
aMoveNext:
.MoveNext()
Loop
.Close()
End With
Return SR
Exit Function
sError:
MsgBox(ErrorToString, MsgBoxStyle.Exclamation)
End Function
Sub tRunWorkerCompleted(ByVal SR As ObservableCollection(Of ocSearchResults))
If canceled Then
Exit Sub
End If
If cts.IsCancellationRequested Then
Exit Sub
End If
searchResults.Clear()
For Each t As ocSearchResults In SR
searchResults.Add(t)
Next
ColorChecker = New BackgroundWorker
ColorChecker.WorkerReportsProgress = True
ColorChecker.WorkerSupportsCancellation = True
ColorChecker.RunWorkerAsync(searchResults)
lblRecordCount.Text = "(" & searchResults.Count & ") Records"
progBar.Value = 100
Exit Sub
sError:
MsgBox(ErrorToString)
End Sub
I don't know enough VB to give you any well written sample code, but if you're on .Net 4.0 I suggest switching to the System.Threading.Tasks namespace, which has cancellation abilities.
If (txtQuickSearch.Text.Length >= 3) Or (e.Key = Key.Enter) Then
If TokenSource Is Not Nothing Then
TokenSource.Cancel()
TokenSource = New CancellationTokenSource()
End If
Task.Factory.StartNew(SomeSearchMethod, txtQuickSearch.Text, TokenSource.Token)
End If
I am not sure a BackgroundWorker is flexible enough to provide an elegant solution for this type of background processing anyway. I think what I would do is to create a single dedicated thread for doing the searching. This thread would use the producer-consumer pattern for accepting work items and processing them.
The following code is a rough sketch of how I see this strategy working. You would call the SearchAsync method to request a new search. That method accepts a callback that gets invoked when and if the search operation found something. Notice that the consumer code (in the Run method) cancels its current search operation if another search request is queued. The effect is that the consumer only ever processes the latest request.
Public Class Searcher
Private m_Queue As BlockingCollection(Of WorkItem) = New BlockingCollection(Of WorkItem)()
Public Sub New()
Dim t = New Thread(AddressOf Run)
t.IsBackground = True
t.Start()
End Sub
Public Sub SearchAsync(ByVal text As String, ByVal callback As Action)
Dim wi = New WorkItem()
wi.Text = text
wi.Callback = callback
m_Queue.Add(wi)
End Sub
Private Sub Run()
Do While True
Dim wi As WorkItem = m_Queue.Take()
Dim found As Boolean = False
Do While Not found AndAlso m_Queue.Count = 0
' Continue searching using your custom algorithm here.
Loop
If found Then
wi.Callback()
End If
Loop
End Sub
Private Class WorkItem
Public Text As String
Public Callback As Action
End Class
End Class
Here is where the elegance happens. Look at how you can implement the logic from the UI thread now.
If (txtQuickSearch.Text.Length >= 3) Or (e.Key = Key.Enter) Then
searcher.SearchAsync(txtQuickSearch.Text, AddressOf OnSearchComplete)
End If
Note that OnSearchComplete will be executed on the worker thread so you will need to call Dispatcher.Invoke from within the callback if you want to publish the results to a UI control.
You can simulate a DoEvents in WPF by doing (in C#):
Dispatcher.Invoke(DispatcherPriority.Background, new Action(() => {}));