How to do a backgroundworker about SAPI SPEAK - vb.net

I want to create a SAPI speaking background worker in Visual Basic .NET in order to allow my client to continue doing something while listens to the SAPI talk.
I've reached that point, but the problem is if I want to reproduce another speaking, I cannot cancel the current speaking and occurs an exception.
I have the following code:
'MODULE IMPORTED IN THE MAIN WORK: argsBackgroundWorker.vb
Public Class argsBackgroundWorker
Public text_to_speak As String
End Class
Private talk As argsBackgroundWorker = New argsBackgroundWorker()
Private Sub sapitalk_background_DoWork(sender As Object, e As System.ComponentModel.DoWorkEventArgs) Handles sapitalk_background.DoWork
If (My.Settings.help_voice = True) Then 'PASS TRUE
Dim reproduceText
Dim text As argsBackgroundWorker = e.Argument
'I have put this code to cancel... So? :(
If sapotalk_background.CancellationPending Then Exit Sub
reproduceText = CreateObject("Sapi.spvoice")
reproduceText.speak(talk.text_to_talk)
Else
sapitalk_background.CancelAsync()
End If
End Sub
Private Sub btn_saysomething_Click(sender As Object, e As EventArgs) Handles btn_saysomething.Click
'Support in order to cancel tasks.
sapitalk_background.WorkerSupportsCancellation = True
talk.text_to_speak = "SOMETHING SOOOOOO SOO SOOOOOO LONG..."
'Cancel another text being spoken.
sapitalk_background.CancelAsync()
'Then, talk the new text.
sapitalk_background.RunWorkerAsync(talk)
End Sub
Private Sub principal_Load(sender As Object, e As EventArgs) Handles MyBase.Load
'Support in order to cancel tasks.
sapitalk_background.WorkerSupportsCancellation = True
talk.text_to_speak = "SOMETHING SOOOOOO SOO SOOOOOO LONG..."
'Cancel another text being spoken.
sapitalk_background.CancelAsync()
'Then, talk the new text.
sapitalk_background.RunWorkerAsync(talk)
End Sub
It works great as background speaking but notice that when I compile the app, it starts speaking a long text. And if I click a button to cancel the current speaking and speak another text, it fails and shows me that it is running a current background worker.

The only idea I have is
While Not sapitalk_background.IsBusy
sapitalk_background.RunWorkerAsync(talk)
End While
If it gets stuck in this loop until the voice stops then I am thinking that you cant stop the voice once its running on a bg thread.

Related

CrystalReportViewer.RefreshReport hangs when running from BackgroundWorker

I'm trying to "enhance" my reporting code by adding a loading screen while the Crystal Report is being prepared/loaded. Before I started trying to add the loading screen, all of my reports would come up just fine, but the cursor change just wasn't "enough" of an indication that the application was still working on pulling the report - some of them can take a while - so I wanted to provide a more "obvious" visual cue.
In order to accomplish this, I've put the report creation method calls into a BackgroundWorker that exists in the loading screen itself (I haven't gotten around to learning how to use Async/Await well enough yet to feel comfortable using that instead). The loading screen comes up correctly and everything appears to work as expected until it actually attempts to display the report on screen. At that point, the "Please wait while the document is processing." box comes up (in the CrystalReportViewer control in the form used to display reports), but it just sits there, not even spinning. Eventually, my IDE throws an error about receiving a ContextSwitchDeadlock and I pretty much just have to cancel execution.
Here's my dlgReportLoading "splash screen" with a PictureBox control that contains an animated GIF:
Imports System.Windows.Forms
Public Class dlgReportLoading
Private DisplayReport As Common.CRReport
Private WithEvents LoadReportWorker As System.ComponentModel.BackgroundWorker
Public Sub New(ByRef Report As Common.CRReport)
InitializeComponent()
DisplayReport = Report
End Sub
Private Sub dlgReportLoading_Load(sender As Object, e As EventArgs) Handles Me.Load
Me.Cursor = Cursors.WaitCursor
Me.TopMost = True
Me.TopMost = False
LoadReportWorker = New System.ComponentModel.BackgroundWorker
LoadReportWorker.RunWorkerAsync()
End Sub
Private Sub dlgReportLoading_FormClosed(sender As Object, e As FormClosedEventArgs) Handles Me.FormClosed
Me.Cursor = Cursors.Default
End Sub
Private Sub LoadReport_DoWork(ByVal sender As Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles LoadReportWorker.DoWork
If Not DisplayReport.ReportOption = Common.CRReport.GenerateReportOption.None Then
Select Case DisplayReport.ReportOption
Case Common.CRReport.GenerateReportOption.DisplayOnScreen
'-- This is the method I'm currently testing
DisplayReport.ShowReport()
Case Common.CRReport.GenerateReportOption.SendToPrinter
DisplayReport.PrintReport()
Case Common.CRReport.GenerateReportOption.ExportToFile
DisplayReport.ExportReport()
End Select
End If
DisplayReport.ReportOption = Common.CRReport.GenerateReportOption.None
'--
'-- This code was in use before trying to generate the reports in the background
'If Not DisplayReport.CrystalReport Is Nothing Then
' DisplayReport.CrystalReport.Dispose()
'End If
'--
End Sub
Private Sub LoadReport_Complete(ByVal sender As Object, ByVal e As System.ComponentModel.RunWorkerCompletedEventArgs) Handles LoadReportWorker.RunWorkerCompleted
Me.DialogResult = DialogResult.OK
Me.Close()
End Sub
End Class
As noted in the code above, I'm currently testing the ShowReport() method as defined here:
Protected Friend Sub ShowReport()
Dim ReportViewer As frmReportPreview
Me.PrepareReport()
ReportViewer = New frmReportPreview(Me)
With ReportViewer
.WindowState = FormWindowState.Maximized
.Show()
End With
End Sub
And the frmReportPreview is this:
Imports System.ComponentModel
Public Class frmReportPreview
Private DisplayReport As Common.CRReport
Private ReportToDisplay As CrystalDecisions.CrystalReports.Engine.ReportDocument
Public Sub New(ByRef Report As Common.CRReport)
InitializeComponent()
DisplayReport = Report
PrepareReportForDisplay()
Me.rptViewer.ReportSource = Nothing
Me.rptViewer.ReportSource = ReportToDisplay
' SET ZOOM LEVEL FOR DISPLAY:
' 1 = Page Width
' 2 = Whole Page
' 25-100 = zoom %
Me.rptViewer.Zoom(1)
Me.rptViewer.Show()
End Sub
Private Sub frmReportPreview_Shown(sender As Object, e As EventArgs) Handles Me.Shown
'-- HANGS HERE
Me.rptViewer.RefreshReport()
End Sub
Private Sub frmReportPreview_Closing(sender As Object, e As CancelEventArgs) Handles Me.Closing
ReportToDisplay.Dispose()
Me.rptViewer.ReportSource = Nothing
End Sub
'...CODE FOR PREPARING THE REPORT TO BE DISPLAYED
End Class
The dlgReportLoading form pops up correctly and the animation plays until the frmReportPreview pops up in front of it (it doesn't close). The little box that has what is normally an animated spinning circle indicating the report data is being loaded appears, but almost immediately freezes in place.
I have a breakpoint in the LoadReport_DoWork() method of my dlgReportLoading form after the call to the ShowReport() method, but it never gets to that point. I also have one in the LoadReport_Complete() method of that form that it never hits either and that dialog never actually closes.
I put another breakpoint at the end of the frmReportPreview_Shown method, right after the Me.rptViewer.RefreshReport() call, but it never hits that either, so it seems clear that this is where things are getting stuck, but only when the report is being generated through the BackgroundWorker. If I just call the ShowReport() method without sending it through the "splash screen" and BackgroundWorker, everything generates and displays normally.
I've tried putting the RefreshReport() method into its own BackgroundWorker with no change in the behavior. I've tried making the frmReportPreview object display modally with ShowDialog() instead of just Show(). None of this seems to help the issue.
I have a feeling something is being disposed of too early somewhere, but I can't figure out what that would be. I can provide the rest of the report preparation code from frmReportPreview if required, but that all seems to be working without error, as far as I can tell. I'm not averse to trying alternate methods of accomplishing my goal of showing the user a loading screen while all the report preparation is taking place - e.g., Async/Await or other multi-threading methods - so any suggestions are welcome. Please let me know if any additional clarification is needed.
ENVIRONMENT
Microsoft Windows 10 Pro 21H1 (OS build 19043.1348)
Microsoft Visual Studio Community 2017 (v15.9.38)
Crystal Reports for .NET Framework v13.0.3500.0 (Runtime version 2.0.50727)
EDIT: I forgot to mention that this whole mess is being called from a GenerateReport() method in my CRReport class defined as:
Public Sub GenerateReport(ByVal ReportGeneration As GenerateReportOption)
Me.ReportOption = ReportGeneration
If Me.ReportOption = GenerateReportOption.None Then
'...CODE FOR REQUESTING A GENERATION OPTION FROM THE USER
End If
Dim ReportLoadingScreen As New dlgReportLoading(Me)
ReportLoadingScreen.ShowDialog()
End Sub
Which, in turn, is being called from my main form like this:
Private Sub PrintMyXMLReport(ByVal XMLFile As IO.FileInfo)
Dim MyXMLReport As New IO.FileInfo("\\SERVER\Applications\Reports\MyXMLReport.rpt")
Dim Report As New Common.CRReport(MyXMLReport, XMLFile)
Report.GenerateReport(Common.CRReport.GenerateReportOption.DisplayOnScreen)
End Sub
You should separate the heavy lifting and UI operations into distinct methods in order to put them into the appropriate BackgroundWorker events:
Protected Friend Sub PrepareReport()
' perform long-running background work
End Sub
Protected Friend Sub ShowReport()
Dim ReportViewer = New frmReportPreview(Me) With {.WindowState = FormWindowState.Maximized}
ReportViewer.Show()
End Sub
Private DisplayReport As Common.CRReport
Private Sub LoadReport_DoWork(ByVal sender As Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles LoadReportWorker.DoWork
DisplayReport.PrepareReport()
End Sub
Private Sub LoadReport_Complete(ByVal sender As Object, ByVal e As System.ComponentModel.RunWorkerCompletedEventArgs) Handles LoadReportWorker.RunWorkerCompleted
DisplayReport.ShowReport()
Me.DialogResult = DialogResult.OK
Me.Close()
End Sub
because LoadReport_DoWork actually runs on a new non-UI thread, and LoadReport_Complete runs on the caller thread, which is a UI thread. Only there can you interact with the UI and show Forms etc.

Running a BackGroundWorker more than once

I wonder if you can help?, please bear in mid I’m an absolute novice.
I trying to update a program I made a few years ago for booking in serial numbers into a CRM application.
Currently it runs the following command for each of the 100 textboxes and has worked a treat booking in more than 81000 serial numbers.
If TextBox1.Text.Length > 1 Then
Clipboard.SetText(TextBox1.Text)
RetBat1 = Shell("C:\Windows\BookIN.exe", , True)
Endif
The new version of the app I’ve added a listbox1 with the serial numbers in, I’m then running the below For Each loop.
The For Each loop copies each Item into the clipboard and the BookIN.exe tabs to the right location in the CRM and pastes the information, then clicks a button in the CRM for a new Line and then runs again. This works fine, but I want to add a stop button or a stop checkbox.
For Each items In ListBox1.Items
Clipboard.SetText(items)
RetBat1 = Shell("C:\Windows\BookIN.exe", , True)
Next
I have tried adding the Retbat1 to a backgroundworker, which checks if Checkbox1.checked then exit the for each loop.
The first serial number works, but when the backgroundworker runs more than once I get the following error.
If CheckBox1.Checked = True Then
BackgroundWorker1.CancelAsync()
Else
Dim RetBat1 As String
RetBat1 = Shell("C:\Windows\BookIN.exe", , True)
End If
System.InvalidOperationException: 'This BackgroundWorker is currently busy and cannot run multiple tasks concurrently.'
Sorry if this makes not sense, thanks James
The way it would go is that you would run the BackgroundWorker and have your loop in the DoWork event handler and check whether a cancellation has been requested within that loop. As you've described it, you would then handle the CheckedChanged even of your CheckBox and request the cancellation when the event is raised. I would not use a CheckBox though, because that implies that you can uncheck it to uncancel the work. I would suggest using a Button and handling its Click event, e.g.
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
'We must get the data from the UI here and pass it into the background thread.
Dim items = ListBox1.Items.Cast(Of String)().ToArray()
'Start the background work and pass in the data.
BackgroundWorker1.RunWorkerAsync(items)
'Enable the Cancel button.
Button2.Enabled = True
End Sub
Private Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click
'Disable the Cancel button so it cannot used again.
Button2.Enabled = False
'Request that background processing be cancelled.
BackgroundWorker1.CancelAsync()
End Sub
Private Sub BackgroundWorker1_DoWork(sender As Object, e As DoWorkEventArgs) Handles BackgroundWorker1.DoWork
'Get the data that was passed in.
Dim items = DirectCast(e.Argument, String())
'Process each item.
For Each item In items
'Check whether processing has been cancelled.
If BackgroundWorker1.CancellationPending Then
'Cancel processing.
e.Cancel = True
Exit For
End If
'Process the current item here.
Next
End Sub
Private Sub BackgroundWorker1_RunWorkerCompleted(sender As Object, e As RunWorkerCompletedEventArgs) Handles BackgroundWorker1.RunWorkerCompleted
If e.Cancelled Then
MessageBox.Show("The background operation was cancelled")
Else
'Disable the Cancel button.
Button2.Enabled = False
MessageBox.Show("The background operation completed successfully")
End If
End Sub
The Cancel Button should be disabled by default and notice that this code ensures that it is only enabled when background processing is in progress. Note that the other Button probably ought to be disabled while the processing is in progress too. If you don't do that, at least check IsBusy on the BackgroundWorker. The visual feedback to the user is better though.

Improve UI responsiveness on windows form application

I am currently working on a project and decided to create a user interface for it using visual studio with a windows forms application(Visual Basic).
The problem I'm facing is that the user interface doesn't respond as quickly and smoothly as I'd like it to.
Mainly, I am using pictures as buttons to make the user form look more modern.
However, when I hover my mouse over a "button" it takes a while until the "highlighted button" appears.
P1 is the picture of the "normal button" and P2 is the picture of the "highlighted button".
Here is the short code I have for now:
Public Class Main
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
End Sub
Private Sub PictureBox1_MouseHover(sender As Object, e As EventArgs) Handles P1.MouseHover
P1.Visible = False
P2.Visible = True
End Sub
Private Sub P2_MouseClick(sender As Object, e As MouseEventArgs) Handles P2.MouseClick
'Call cmdInit()
'Call cmdConnectRobot()
'Call cmdUnlock()
End Sub
Private Sub Main_MouseHover(sender As Object, e As EventArgs) Handles Me.MouseHover
If P2.Visible = True Then
P2.Visible = False
P1.Visible = True
End If
End Sub
Private Sub P4_Click(sender As Object, e As EventArgs) Handles P4.Click
End Sub
End Class
Another problem I'm facing is that when I call other subs, the user form becomes unresponsive while the sub is running.
I researched and found that I could implement multi threading or async tasks but I'm a bit lost and would be extremely grateful if someone could guide me or point me in the right direction.
Thanks in advance!!
In this case your UI is responsive, however the MouseHover event is only raised once the mouse cursor has hovered over the control for a certain amount of time (default is 400 ms), which is what is causing the delay.
What you are looking for is the MouseEnter event, which is raised as soon as the cursor enters ("touches") the control:
Private Sub P1_MouseEnter(sender As Object, e As EventArgs) Handles P1.MouseEnter
P1.Visible = False
P2.Visible = True
End Sub
You can then use that together with the MouseLeave event on the second picture box to switch back to the non-highlighted image:
Private Sub P2_MouseLeave(sender As Object, e As EventArgs) Handles P2.MouseLeave
P1.Visible = True
P2.Visible = False
End Sub
However switching picture boxes like this is not optimal. I recommend you to look into how you can use Application Resources, then modify your code to only switch the image that one picture box displays.
Here are the basic steps:
Right-click your project in the Solution Explorer and press Properties.
Select the Resources tab.
To add an image either:
a. Drag and drop the image onto the resource pane.
b. Click the arrow next to the Add Resource... button and press Add Existing File....
Now, in your code add this right below Public Class Form1:
Dim ButtonNormal As Image = My.Resources.<first image name>
Dim ButtonHighlighted As Image = My.Resources.<second image name>
Replace <first image name> and <second image name> with the names of your button images.
Now you only need one picture box for the button:
Private Sub Form1_Load(sender As System.Object, e As System.EventArgs) Handles MyBase.Load
P1.Image = ButtonNormal
End Sub
Private Sub P1_MouseEnter(sender As System.Object, e As System.EventArgs) Handles P1.MouseEnter
P1.Image = ButtonHighlighted
End Sub
Private Sub P1_MouseLeave(sender As System.Object, e As System.EventArgs) Handles P1.MouseLeave
P1.Image = ButtonNormal
End Sub
I'll start by saying i'm not a programmer by trade, and i'm sure someone will point out better ways of doing these things, but in regards to the threading question it's fairly simple to implement.
Imports System.Threading
Public Class Form1
Dim WorkerThread As New Thread(AddressOf DoWork)
'WorkerThread' can be any name you like, and 'DoWork' is the name of the sub you want to run in the new thread, and is launched by calling:
WorkerThread.start()
However there is a catch, the new thread is not able to interact directly with the GUI, so you cannot change textbox text etc... I find the easiest way to get changes made to the GUI is to drag a timer onto your form, and have the new thread change variables (pre-defined just below Public Class Form1), then use the Timer1 Tick event to monitor the variables and update the GUI if there are any changes.

Text to speech portion of code (VB.NET) works fine on windows 7 but not on windows 10

Public Class Form1
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
Dim SAPI As Object
Select Case TextBox1.Text
Case "1"
PictureBox1.Image = My.Resources.picture1
RichTextBox1.Text = "Information here."
SAPI = CreateObject("SAPI.spvoice")
SAPI.speak(RichTextBox1.Text)
End Select
End Sub
End Class
On two windows 7 machines the pictures and text appear first and then the TTS happens. While on two win10 machines the speech happens first and then the picture and text appear.
I've tried adding a try catch and finally statement and setting up a delay but the problem still persists!
Is there anything I can do to fix this?
Update for blackwood
Public Class Form1
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
Dim SAPI As Object
Select Case TextBox1.Text
Case "1"
PictureBox1.Image = My.Resources.picture1
RichTextBox1.Text = "Information here."
System.Threading.Thread.Sleep(100)
SAPI = CreateObject("SAPI.spvoice")
SAPI.speak(RichTextBox1.Text)
End Select
End Sub
End Class
Maybe try sticking an Application.DoEvents in there just before the SAPI = CreateObject call. Though it's not the solution, it's the easiest to implement. It'll cause the window get updated (the message loop is handled) before continuing.
If that works, go for the real solution: a background thread or async/await (whenever possible) for the playback of the TTS.

Vb.net button still allows click while disabled

My button is responding to clicks while disabled.
Private Sub btnGenerate_Click(sender As Object, e As EventArgs) Handles btnGenerate.Click
btnGenerate.Enabled = False
Me.Cursor = Cursors.WaitCursor
'Do a bunch of operations
Me.Cursor = Cursors.Default
btnGenerate.Enabled = True
End Sub
It takes about 5-10 seconds to process the stuff I'm doing in the background. During that 5-10 seconds the button is greyed out, but if I click it a second time, then it performs the operational stuff a second time after finishing the first.
I'm missing something here. How can I prevent button from allowing interaction until operations are finished?
Dim Working as boolean=false
Private Sub btnGenerate_Click(sender As Object, e As EventArgs) Handles btnGenerate.Click
if Working=true then exit sub
' Your Work Process
Work()
End Sub
sub Work()
Working=True
' Work code
Working=False
end sub
this should prevent the double click
With VS2012, Async Work is very easy to use (compared to previous versions...).
The problem is the UI thread is not letting go. Without seeing what 'work' is actually going on, I can not explain why. I am hoping nothing that re enables the button...
However, Async will allow release of the UI thread and the enabled = false should take effect. Try something like this:
Private Async Sub btnGenerate_Click(sender As Object, e As EventArgs) Handles btnGenerate.Click
btnGenerate.Enabled = False
Dim t As New Task(Sub() MyWorkLoad())
t.Start()
Await t
btnGenerate.Enabled = True
End Sub
Private Sub MyWorkLoad()
'do your work here
'for testing
Dim time As Date = Now
Do While True
If DateAdd(DateInterval.Second, -5, Now) > time Then Exit Do
Loop
End Sub
This did work for me...