I have been working very hard on figuring this out and just can't understand the events. Can someone help me understand the event process in my code? Or tell me why my image won't switch when it runs the code?
Declarations of Class and members
Partial Public Class Name
Implements IChat
Private member As String
Private instanceContext As InstanceContext
Private participant As IChatChannel
Private ostat As IOnlineStatus
Private factory As DuplexChannelFactory(Of IChatChannel)
Inside my Connect Sub
'Construct InstanceContext to handle messages on callback interface.
' An instance of ChatApp is created and passed to the InstanceContext.
instanceContext = New InstanceContext(Me)
' Create the participant with the given endpoint configuration
' Each participant opens a duplex channel to the mesh
' participant is an instance of the chat application that has opened a channel to the mesh
factory = New DuplexChannelFactory(Of IChatChannel)(instanceContext, "ChatEndpoint")
participant = factory.CreateChannel()
' Retrieve the PeerNode associated with the participant and register for online/offline events
' PeerNode represents a node in the mesh. Mesh is the named collection of connected nodes.
ostat = participant.GetProperty(Of IOnlineStatus)()
AddHandler ostat.Online, AddressOf Me.OnOnline
AddHandler ostat.Offline, AddressOf Me.OnOffline
Sub routines that are supposed to change image
Public Sub Join(ByVal member As String) Implements IChat.Join
instanceShellProp.imgP2P.Image = Namespace.My.Resources.Offline
MsgBox("JOINED OFFLINE")
End Sub
Public Sub Leave1(ByVal member As String) Implements IChat.Leave
instanceShellProp.imgP2P.Image = Namespace.My.Resources.Disconnected
MsgBox("NOT CONNECTED")
End Sub
Public Sub OnOnline(ByVal sender As Object, ByVal e As EventArgs)
instanceShellProp.imgP2P.Image = Namespace.My.Resources.Online
MsgBox("JOINED ONLINE")
End Sub
Public Sub OnOffline(ByVal sender As Object, ByVal e As EventArgs)
instanceShellProp.imgP2P.Image = Namespace.My.Resources.Offline
MsgBox("JOINED OFFLINE")
End Sub
instanceShellProp returns the instance of the Shell that is a MDI container.
All of the images are in the Resources and properly spelled and referenced to. The MessageBox will pop up but the images won't change, EXCEPT on Join.
I'm not trying to Code Dump, just trying to make sure that you can see what I am looking at and allow you to give better advice.
All help is appreciated!
EDIT
Okay, I find this weird... I feel like I am getting close. When the messagebox is not commented out the image will change, when it is commented out the image doesn't change.
Any better suggestions on how to get this to work?
Public Sub OnOnline(ByVal sender As Object, ByVal e As EventArgs)
With instanceShellProp.imgP2P
.Image = Nothing
.Visible = True
End With
'MsgBox("JOINED ONLINE")
With instanceShellProp.imgP2P
.Image = Namespace.My.Resources.Online
.Visible = True
End With
End Sub
Added Application.DoEvents() and it allowed it to work. Not sure as to why but if someone could explain that would be awesome! Thanks!
Try to call instanceShellProp.imgP2P.Refresh() after changing the image.
The documentation of Microsoft for Refresh says:
"Forces the control to invalidate its client area and immediately redraw itself and any child controls."
Related
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.
I have two classes.
Public Class MainForm
Private Project As clsProject
Private Sub btnDo_Click
...
Backgroundworker.RunWorkerAsync()
End Sub
Private Sub BackgroundWorker1_DoWork(ByVal sender As System.Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork
Project = New clsProject
End Sub
and two methods inside MainForm
Public Shared Sub setLabelTxt(ByVal text As String, ByVal lbl As Label)
If lbl.InvokeRequired Then
lbl.Invoke(New setLabelTxtInvoker(AddressOf setLabelTxt), text, lbl)
Else
lbl.Text = text
End If
End Sub
Public Delegate Sub setLabelTxtInvoker(ByVal text As String, ByVal lbl As Label)
end class
I want to update the labels of MainForm from the clsProject constructor.
MainForm.setLabelTxt("Getting prsadasdasdasdasdry..", MainForm.lblProgress)
but it does not update them.
What am I doing wrong?
The problem is that you are using the global MainForm instance to access the label in a background thread here:
Public Class clsProject
Public Sub New()
' When accessing MainForm.Label1 on the next line, it causes an exception
MainForm.setLabelTxt("HERE!", MainForm.Label1)
End Sub
End Class
It's OK to call MainForm.setLabelTxt, since that is a shared method, so it's not going through the global instance to call it. But, when you access the Label1 property, that's utilizing VB.NET's trickery to access the global instance of the form. Using the form through that auto-global-instance variable (which always shares the same name as the type) is apparently not allowed in non-UI threads. When you do so, it throws an InvalidOperationException, with the following error message:
An error occurred creating the form. See Exception.InnerException for details. The error is: ActiveX control '8856f961-340a-11d0-a96b-00c04fd705a2' cannot be instantiated because the current thread is not in a single-threaded apartment.
I'm guessing that the reason you are not seeing the error is because you are catching the exception somewhere and you are simply ignoring it. If you stop using that global instance variable, the error goes away and it works. For instance, if you change the constructor to this:
Public Class clsProject
Public Sub New(f As MainForm)
' The next line works because it doesn't use the global MainForm instance variable
MainForm.setLabelTxt("HERE!", f.Label1)
End Sub
End Class
Then, in your MainForm, you would have to call it like this:
Private Sub BackgroundWorker1_DoWork(ByVal sender As System.Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork
Project = New clsProject(Me) ' Must pass Me
End Sub
Using the global instance from the background thread is not allowed, but when we use the same label from the background thread, without going through that global variable it works.
So it's clear that you cannot use the global MainForm variable from a background thread, but what may not be clear is that it's a bad idea to use it ever. First, it's confusing because it shares the same name as the MainForm type. More importantly, though, it is a global variable, and global state of any kind is almost always bad practice, if it can be avoided.
While the above example does solve the problem, it's still a pretty poor way of doing it. A better option would be to pass the setLabelTxt method to the clsProject object or even better have the clsProject simply raise an event when the label needs to be changed. Then, the MainForm can simply listen for those events and handle them when they happen. Ultimately, that clsProject class is probably some sort of business class which shouldn't be doing any kind of UI work anyway.
You cannot execute any action on GUI-elements from the BackgroundWorker directly. One way to "overcome" that is by forcing the given actions to be performed from the main thread via Me.Invoke; but this is not the ideal proceeding. Additionally, your code mixes up main form and external class (+ shared/non-shared objects) what makes the whole structure not too solid.
A for-sure working solution is relying on the specific BGW methods for dealing with GUI elements; for example: ProgressChanged Event. Sample code:
Public Class MainForm
Private Project As clsProject
Public Shared bgw As System.ComponentModel.BackgroundWorker
Private Sub Form1_Load(sender As System.Object, e As System.EventArgs) Handles MyBase.Load
bgw = BackgroundWorker1 'Required as far as you want to called it from a Shared method
BackgroundWorker1.WorkerReportsProgress = True
BackgroundWorker1.RunWorkerAsync()
End Sub
Private Sub BackgroundWorker1_DoWork(sender As System.Object, e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork
Project = New clsProject
End Sub
Public Shared Sub setLabelTxt(ByVal text As String)
bgw.ReportProgress(0, text) 'You can write any int as first argument as far as will not be used anyway
End Sub
Private Sub BackgroundWorker1_ProgressChanged(sender As Object, e As System.ComponentModel.ProgressChangedEventArgs) Handles BackgroundWorker1.ProgressChanged
Me.Label1.Text = e.UserState 'You can access the given GUI-element directly
Me.Label1.Update()
End Sub
End Class
Public Class clsProject
Public Sub New()
MainForm.setLabelTxt("Getting prsadasdasdasdasdry..")
End Sub
End Class
Try:
Me.Invoke(...)
instead of lbl.Invoke(.... I had to do this. This is my implementation:
Delegate Sub SetTextDelegate(ByVal args As String)
Private Sub SetTextBoxInfo(ByVal txt As String)
If txtInfo.InvokeRequired Then
Dim md As New SetTextDelegate(AddressOf SetTextBoxInfo)
Me.Invoke(md, txt)
Else
txtInfo.Text = txt
End If
End Sub
And this worked for me.
I need some help getting my head wrapped about instances and classes. In the code below I have a main_form, and I am also loading a user_control into the main_form. I have a property that resides inside main_form that I will be setting data called obj. My question is when I am doing work inside of the user_control, how can I reach in and set the obj property of main_form. I tried main_form.obj but I keep getting the error "object reference not set to an instance of an object". So, I'm not sure how to do it. Here's the dumbed down code
Public Class FormControlClass
Private _obj As New objCollection
Public Property obj As objCollection
Get
Return _obj
End Get
Set(ByVal value As objCollection)
_obj = value
End Set
End Property
'Load User Control Into Form from here.
me.controls.add('UserControl')
End Class
Public Class UserControlClass
'Access the obj property in the form control class from here.
FormControlClass.obj = 1
End Class
Even if you could do what you are trying to do, it would be a bad idea, because you would be coupling your user control to this particular form, making it useless outside of this form.
Instead you need to have your user control generate an event that the form can subscribe to and handle itself. In other words, you want to make the user control create a message that can be delivered to the form, like this:
Public Class UserControlClass
' Define event that will be raised by user control to anyone interested in handling the event
Public Event UC_Button1Click()
' Mechanism to allow event to be raised by user control
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
RaiseEvent UC_Button1Click()
End Sub
End Class
Now in your form class, you need to add a handler for the event raised by the user control, like this:
AddHandler userControl1.UC_Button1Click, AddressOf Button1_Click
Finally, you would create the method that is referenced in the AddressOf syntax, like this:
Public Sub Button1_Click(ByVal sender As Object, ByVal args As EventArgs)
' Do something here
End Sub
maybe I am being stooped... but the fact is that I am a bit of a n00b concerning threading...
I am making use of a serial port in a class. I am raising an event from that class to my form calling the class. Event contains data received...
I wish to simply populate a textbox from the raised event.
Now I am not specifically creating a seperate thread, but I get the normal crossthreading error when trying to update my textbox on the UI, so my assumption is that the serial port and its internal methods probably creates its own threads...
Regardless, I am a bit confused as to how to properly implement an invoke, from my main form, pointing to the thread in the instantiated class...
I hope this makes sense...
Dim WithEvents tmpRS232 As New clsRS232
Private Sub but_txt_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles but_txt.Click
tmpRS232.Set_com_port("COM8", 38400)
tmpRS232.Transmit_data(txt_tx.Text)
End Sub
Private Sub tmprs232_rx_data_returned(ByVal str_data As String) Handles tmpRS232.rx_data_returned
txt_rx.Text = str_data 'Cross threading error
MsgBox(str_data) 'Fires without errors
End Sub
Can someone please provide a code example based on this code?
thanks.
You are correct, the issue here is that you are attempting to update a UI element from a non-UI thread (in this case the serial port handler). What you need to do is check if the InvokeRequired flag is set on the control that you are trying to access from the callback. If so that means that you need to marshall your call to the UI thread. You can achieve this by using either Invoke or BeginInvoke from System.Windows.Forms.Control.
Private Delegate Sub SetRxTextCallback(ByVal [text] As String)
Private Sub SetRxText(ByVal [text] As String)
txt_rx.Text = [text]
End Sub
Private Sub tmprs232_rx_data_returned(ByVal str_data As String) Handles tmpRS232.rx_data_returned
If (txt_rx.InvokeRequired) Then
Dim d As New SetRxTextCallback(AddressOf Me.SetRxText)
Me.BeginInvoke(d, New Object() {[str_data]})
End If
'txt_rx.Text = str_data 'Cross threading error
'MsgBox(str_data) 'Fires without errors
End Sub
Here's a link to the MSDN documentation that explains it in detail.
Or simply...
Private Sub tmprs232_rx_data_returned(ByVal str_data As String) Handles tmpRS232.rx_data_returned
If InvokeRequired Then
Invoke(Sub()txt_rx.Text = str_data)
Else
txt_rx.Text = str_data
End If
End Sub
I have a loop (BackgroundWorker) that is changing a PictureBox's Location very frequently, but I'm getting an error -
Cross-thread operation not valid: Control 'box1' accessed from a thread other than the
thread it was created on.
I don't understand it at all, so I am hoping someone can help me with this situation.
Code:
box1.Location = New Point(posx, posy)
This exception is thrown when you try to access control from thread other than the thread it was created on.
To get past this, you need to use the InvokeRequired property for the control to see if it needs to be updated and to update the control you will need to use a delegate. i think you will need to do this in your backgroundWorker_DoWork method
Private Delegate Sub UpdatePictureBoxDelegate(Point p)
Dim del As New UpdatePictureBoxDelegate(AddressOf UpdatePictureBox)
Private Sub UpdatePictureBox(Point p)
If pictureBoxVariable.InvokeRequired Then
Dim del As New UpdatePictureBoxDelegate(AddressOf UpdatePictureBox)
pictureBoxVariable.Invoke(del, New Object() {p})
Else
' this is UI thread
End If
End Sub
For other people which coming across this error:
Try the dispatcher object: MSDN
My code:
Private _dispatcher As Dispatcher
Private Sub ThisAddIn_Startup(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Startup
_dispatcher = Dispatcher.CurrentDispatcher
End Sub
Private Sub otherFunction()
' Place where you want to make the cross thread call
_dispatcher.BeginInvoke(Sub() ThreadSafe())
End Sub
Private Sub ThreadSafe()
' here you can make the required calls
End Sub