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(() => {}));
Related
I'm trying to use serial communication in WPF with vb.net and the only successful thing that I can do is sending data. When it comes to receiving data I have received different errors, and this one is the hardest for me to get over with. I tried to apply the windows form code when we want to receive data by using the Control.InvokeRequired, but since this is WPF this was transferred to Dispatcher.CheckAccess(). Everytime I load the program the transmission is ok but reception gives me error of System.InvalidOperation:'The calling thread cannot acces this object because a different thread owns it.' I looked at other solutions but none have worked.
Are there other solutions/process for receiving data from the other side?
I tried various examples from both windows document and other sites, but none was successful in WPF application.
Imports System.IO.Ports
Imports System.Text
Imports System.Threading
Imports System.Windows
Class MainWindow
Private WithEvents serport As New SerialPort
Private Delegate Sub SetTextCallback(ByVal [text] As String)
Private Sub onLoad()
Dim ports As String() = SerialPort.GetPortNames()
Dim port As String
For Each port In ports
comPortCombo.Items.Add(port)
Next port
End Sub
Private Sub ConButton_Click(sender As Object, e As RoutedEventArgs) Handles conButton.Click
'serport = New SerialPort()
If (comPortCombo.Text = "") Then
MessageBox.Show("Please select COM port!")
Exit Sub
End If
serport.PortName = comPortCombo.Text
serport.BaudRate = baudCombo.Text
serport.Open()
transButton.IsEnabled = True
conButton.IsEnabled = False
disconButton.IsEnabled = True
End Sub
Private Sub DisconButton_Click(sender As Object, e As RoutedEventArgs) Handles disconButton.Click
serport.Close()
disconButton.IsEnabled = False
transButton.IsEnabled = False
conButton.IsEnabled = True
End Sub
Private Sub TransButton_Click(sender As Object, e As RoutedEventArgs) Handles transButton.Click
serport.Write(transTextBox.Text & vbCrLf)
transTextBox.Clear()
End Sub
Private Sub serport_datareceived(sender As Object, e As SerialDataReceivedEventArgs) Handles serport.DataReceived
'recTextBox.Text = receiveserialdata()
'with serForm's function???
ReceivedText(serport.ReadLine())
End Sub
Private Sub ReceivedText(ByVal [text] As String)
If recTextBox.Dispatcher.CheckAccess() Then
Dim x As New SetTextCallback(AddressOf ReceivedText)
Dispatcher.Invoke(x, New Object() {(text)})
Else
recTextBox.Text &= [text]
End If
End Sub
'Function receiveserialdata() As String
' ' receive strings from a serial port.
' Dim returnstr As String = ""
' Try
' Do
' Dim incoming As String = serport.ReadExisting()
' If incoming Is Nothing Then
' Exit Do
' Else
' returnstr &= incoming & vbCrLf
' End If
' Loop
' Catch ex As TimeoutException
' returnstr = "error: serial port read timed out."
' Finally
' If serport IsNot Nothing Then serport.Close()
' End Try
' Return returnstr
'End Function
End Class
System.InvalidOperationException: 'The calling thread cannot access this object because a different thread owns it.'
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
In this scenario when a user changes access to a different business (multi business accounting) it runs through a sub that closes all open tabs and changes the status of any that are still held open in editing mode.
Everything runs really well, the tabs are closed and then the ID for the new business is loaded.
The trouble is the Page.Unloaded event is running after the entire sub has run and is therefore updating the incorrect DB if any pages were left open in the edit mode.
Is there any way to force any pages that were closed as part of the sub to run unloaded before completing the rest of the code?
Thanks
Edit re comment from Pragmateek
This is an exampled of the unloaded event
Private Sub Website_WebPage_Page_Unloaded(sender As Object, e As System.Windows.RoutedEventArgs) Handles Me.Unloaded
Try
Dim SaveUpdateButton As Button = Website_WebPage_Grid.FindName("WebsiteWebPage_SaveUpdateButton")
Dim vScrollViewer As ScrollViewer = Website_WebPage_Grid.FindName("WebsiteWebPage_ScrollViewer")
If NewRecord = True Then
RemoveHandler SaveUpdateButton.Click, AddressOf Website_WebPage_DB_Insert
Else
'Edited record
Dim EditButton As Button = Website_WebPage_Grid.FindName("WebsiteWebPage_EditButton")
Dim EditWebPageButton As Button = Website_WebPage_Grid.FindName("Website_WebPage_EditWebPageButton")
RemoveHandler EditButton.Click, AddressOf Website_WebPage_ToggleEditMode_Click
RemoveHandler vScrollViewer.MouseDoubleClick, AddressOf Website_WebPage_ToggleEditMode_Click
RemoveHandler Me.MouseDown, AddressOf Website_WebPage_MouseDown
RemoveHandler SaveUpdateButton.Click, AddressOf Website_WebPage_DB_Update
RemoveHandler EditWebPageButton.Click, AddressOf WebsiteWebPage_BrowseToClick
End If
If OpenForEdit = True Then
If DB_Functions_ReleaseDT("HOA3_Pages", Page_ID, "Page_ID") = False Then
EditForm_Error()
Else
OpenForEdit = False
End If
End If
Catch ex As Exception
EmailError(ex)
End Try
End Sub
If the tab was left in edit mode OpenForEdit will be true and it will call DB_Functions_ReleaseDT
Public Function DB_Functions_ReleaseDT(ByVal TableName As String, ByVal FileID As Integer, ByVal PKey As String) As Boolean
Try
UpdateOpenForEdit(False, TableName)
vService = New Service1Client
strSQL = "UPDATE " & TableName & " SET Open_Editing = 0, Editing_Name = 'System' WHERE " & PKey & " = " & FileID
If vService.InsertDataHOA(strSQL, "3 DB_Functions 43", Current_HOA_ID) = False Then
Return False
Else
Return True
End If
Catch ex As Exception
EmailError(ex)
Return False
Finally
If Not vService Is Nothing Then
vService.Close()
vService = Nothing
End If
End Try
End Function
The problem is Unloaded is running far too late and the variable Current_HOA_ID has changed
Turned out the answer was quite simple - the sub was being run twice, so adding...
e.Handled = True
.. as the first line of code sorted the problem out
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
Good evening,
Following is the code I used for reading the files and folders from a drive etc.
Public Class LoadingBox
Public counter As ULong
Public OpenRecords As New Dictionary(Of String, MainWindow.records)
Public Path As String
Public Diskname As String
Private WithEvents BKWorker As New BackgroundWorker()
Public Sub New(ByVal _Path As String, ByVal _Diskname As String)
' This call is required by the designer.
InitializeComponent()
' Add any initialization after the InitializeComponent() call.
Path = _path
Diskname = _diskname
End Sub
Private Sub GetStructure(ByVal tempdir As String, ByVal ParentID As String, ByVal DiskName As String)
Dim maindir As DirectoryInfo = My.Computer.FileSystem.GetDirectoryInfo(tempdir)
For Each Dir As DirectoryInfo In maindir.GetDirectories
Try
Dim d As New MainWindow.records
d.Filename = Dir.Name
d.Folder = True
d.Rowid = Date.UtcNow.ToString() + counter.ToString()
d.Size = 0
d.ParentID = ParentID
d.DiskName = DiskName
d.DateCreated = Dir.CreationTimeUtc
d.DateModified = Dir.LastWriteTimeUtc
OpenRecords.Add(d.Rowid, d)
'Label1.Content = "Processing: " + Dir.FullName
BKWorker.ReportProgress(0, Dir.FullName)
counter = counter + 1
GetStructure(Dir.FullName, d.Rowid, DiskName)
Catch ex As Exception
End Try
Next
For Each fil As FileInfo In maindir.GetFiles
Try
Dim d As New MainWindow.records
d.Filename = fil.Name
d.Folder = False
d.Rowid = Date.UtcNow.ToString() + counter.ToString()
d.Size = fil.Length
d.ParentID = ParentID
d.DiskName = DiskName
d.DateCreated = fil.CreationTimeUtc
d.DateModified = fil.LastWriteTimeUtc
OpenRecords.Add(d.Rowid, d)
'Label1.Content = "Processing: " + fil.FullName
BKWorker.ReportProgress(0, fil.FullName)
counter = counter + 1
Catch ex As Exception
End Try
Next
End Sub
Private Sub Window_Loaded(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs) Handles MyBase.Loaded
counter = 0
BKWorker.WorkerReportsProgress = True
AddHandler BKWorker.DoWork, AddressOf BKWorker_Do
AddHandler BKWorker.ProgressChanged, AddressOf BKWorker_Progress
AddHandler BKWorker.RunWorkerCompleted, AddressOf BKWorker_Completed
BKWorker.RunWorkerAsync()
'GetStructure(Path, "0", Diskname)
End Sub
Private Sub BKWorker_Do(ByVal sender As Object, ByVal e As DoWorkEventArgs)
'Throw New NotImplementedException
GetStructure(Path, "0", Diskname)
End Sub
Private Sub BKWorker_Progress(ByVal sender As Object, ByVal e As ProgressChangedEventArgs)
'Throw New NotImplementedException
Label1.Content = "Processing: " + e.UserState.ToString()
If ProgressBar1.Value = 100 Then
ProgressBar1.Value = 0
End If
ProgressBar1.Value = ProgressBar1.Value + 1
End Sub
Private Sub BKWorker_Completed(ByVal sender As Object, ByVal e As RunWorkerCompletedEventArgs)
'Throw New NotImplementedException
MessageBox.Show("Completed")
Me.Close()
End Sub
End Class
However the problem is that, the background thread is able to read files very fast, but the UI thread is not able to keep up the speed with it, could you please advice me on how I can solve this issue.
You almost never want to report progress on every single item when you're iterating through that many items.
I would suggest finding some reasonable number of files to wait for before reporting progress. Every 5th or every 10th or so on. You probably want to take a look at what your normal number of files is. In other words, if you're normally processing only 25 files, you probably don't want to only update every 10 files. But if you're normally processing 25000 files, you could maybe even only update every 100 files.
One quick answer would be to only report the progress when a certain amount of time has passed that way if 10 files were processed in that time the UI isn't trying to update one each one. If things are processing that fast then you really don't need to update the user on every single file.
Also on a quick side note if your ProgressBar isn't actually reporting progress from 0 to 100% you might want to just set its IsIndeterminate property to true instead of increasing the percent and then resetting it back to 0.