VB.net PrintForm Not Working in New Thread - vb.net

I am developing a e-filing app and I need to print an adhesive label with some info to attach to the physical folder.
I already designed the label as a Form put the logo and everything that I need there. Then on the Form.Shown event I put the command to print:
Me.PrintLabelForm.Print() (This is VisualStudio PowerPack Control)
And here is where I bump into a problem. The print out is totally empty (I already changed margins setup the printer, etc). The issue is that the form is not actually fully loaded, I switch the method to the print preview and the controls are there but they are empty.
I tried several approaches but I have been not able to do this automatically. One solution that I found was to have a button to do the Me.PrintLabelForm.Print() then it works because the form is already fully loaded and displayed but this is not an option. I need the form to open automatically, print and close.
An option that I think it should work will be to have a new thread with a timer then printing so I did this:
Private Sub LabelPrint_Load(sender As Object, e As EventArgs) Handles MyBase.Load
End Sub
Private Sub LabelPrint_Shown(sender As Object, e As EventArgs) Handles Me.Shown
PrintLabelForm.PrinterSettings.DefaultPageSettings.Margins.Left = 0.1
PrintLabelForm.PrinterSettings.DefaultPageSettings.Margins.Right = 0.1
PrintLabelForm.PrinterSettings.DefaultPageSettings.Margins.Top = 0.1
PrintLabelForm.PrinterSettings.DefaultPageSettings.Margins.Bottom = 0.1
PrintLabelForm.PrinterSettings.DefaultPageSettings.Landscape = True
Dim PrintThread As New System.Threading.Thread(AddressOf PrintSub)
PrintThread.Start()
End Sub
Private Sub PrintSub()
Threading.Thread.Sleep(1000)
Me.PrintLabelForm.Print()
Me.Close()
End Sub
The idea was to have the PrintSub to give the app enough time to finish to render the whole thing then print but I am getting this error:
**An unhandled exception of type 'System.Exception' occurred in Microsoft.VisualBasic.PowerPacks.dll
Additional information: The window being printed must be visible and contain focus.**
So I wonder how to make this thread have the window form in focus in order to be able to print.
That is all. Thanks for all the help.

Always work with the form only from main thread.
You found it right – form printing will not run from new thread.
When you do any actions on forms, you must perform all the work from Dispatcher thread. It is the thread on which all event methods run. If you fail doing so, you can encounter many side effects. (Not only problem with printing. I've been there and this advice from senior Windows programmer helped me to get things back to normal.) So do not use form printing from any other thread.
If you want a workaround for this, print form to the image (in main thread) and then you can print the image using new thread.
This has nothing to do with .NET, this is related to internals of Windows Forms technology. Welcome to Windows programming.

I manage to solve it putting this line in the Form.Shown
PrintLabelForm.Print(Me, PrintForm.PrintOption.ClientAreaOnly)
I don't know why or how but it works.
Thanks to all of you guys for your help. Let's hope I don't find myself trying to do stuff when the form is fully displayed.
This is my full code let's hope it works for someone else:
Imports Microsoft.VisualBasic.PowerPacks.Printing
Public Class PrintAdhesiveLabel
Private Sub LabelPrint_Load(sender As Object, e As EventArgs) Handles MyBase.Load
End Sub
Private Sub LabelPrint_Shown(sender As Object, e As EventArgs) Handles Me.Shown
PrintLabelForm.PrinterSettings.DefaultPageSettings.Margins.Left = 0.1
PrintLabelForm.PrinterSettings.DefaultPageSettings.Margins.Right = 0.1
PrintLabelForm.PrinterSettings.DefaultPageSettings.Margins.Top = 0.1
PrintLabelForm.PrinterSettings.DefaultPageSettings.Margins.Bottom = 0.1
PrintLabelForm.PrinterSettings.DefaultPageSettings.Landscape = True
PrintLabelForm.Print(Me, PrintForm.PrintOption.ClientAreaOnly)
Me.Close()
End Sub
End Class

Perhaps this is relevant:
Only the form that currently has focus can be printed by using this
method. If you have set the Form property to another form before
calling this method, the image of the form may not be rendered as
expected. To avoid this, call the Focus method of the form before you
call Print.
So call Me.PrintLabelForm.Focus() before calling Me.PrintLabelForm.Print():
Private Sub PrintSub()
Threading.Thread.Sleep(1000)
Me.PrintLabelForm.Focus()
Me.PrintLabelForm.Print()
Me.Close()
End Sub

Related

Most efficient way to programmatically update and configure forms and controls

I am looking for a way to prevent user form controls from appearing one by one when I'm programmatically adding them and for ways to enhance application performance and visual appeal.
Say, I have a Panel_Top in which I programmatically add comboboxes. What is happening is that they appear one by one as they are created and I am looking for a way to suspend the refreshing of the panel and or user form to make all of those programmatically added comboboxes to appear at the same time and faster than it happens right now.
I've tried suspendlayout which doesn't do anything for me or maybe I'm doing it wrong.
MyForm.PanelTop.SuspendLayout = true
And also I've tried to set the Panel_Top to invisible like:
MyForm.Top_Panel.visible = false
Which kind of sorta looks and performs better, or it might be a placebo.
What is the correct approach to this problem?
PS: I do have form set to doublebuffer = true, if that matters
What I tend to do is create a loading modal to appear on top of the form rendering the controls that need to be created/made visible, this can optionally have a progress bar that gets incremented as the control is created/shown. With the loading modal running, the container that needs to add the controls starts with SuspendLayout, adds the controls, and then finished with ResumeLayout.
This makes it so that controls are added/shown while giving the user a visual indicator that something is going on behind the scenes.
Here is a phenomenal example of a loading modal: https://www.vbforums.com/showthread.php?869567-Modal-Wait-Dialogue-with-BackgroundWorker and here is an example of using it:
Private ReadOnly _controlsToAdd As New List(Of Control)()
Private Sub MyForm_Show(sender As Object, e As EventArgs) Handles MyBase.Shown
Using waitModal = New BackgroundWorkerForm(AddressOf backgroundWorker_DoWork,
AddressOf backgroundWorker_ProgressChanged,
AddressOf backgroundWorker_RunWorkerCompleted)
waitModal.ShowDialog()
End Using
End Sub
Private Sub backgroundWorker_DoWork(sender As Object, e As DoWorkEventArgs)
Dim worker = DirectCast(sender, BackgroundWorker)
For index = 1 To 100
_controlsToAdd.Add(New ComboBox() With {.Name = $"ComboBox{index}"})
worker.ReportProgress(index)
Threading.Thread.Sleep(100) ' Zzz to simulate a long running process
Next
End Sub
Private Sub backgroundWorker_ProgressChanged(sender As Object, e As ProgressChangedEventArgs)
Dim percentageCompleted = e.ProgressPercentage / 100
' do something with the percentageCompleted value
End Sub
Private Sub backgroundWorker_RunWorkerCompleted(sender As Object, e As RunWorkerCompletedEventArgs)
PanelTop.SuspendLayout()
PanelTop.Controls.AddRange(_controlsToAdd.ToArray())
PanelTop.ResumeLayout()
End Sub
SuspendLayout() is the correct way to handle this with WinForms.
But first of all, this is a function you call, and not a flag you set.
Secondly, don't forget to call ResumeLayout() at the end of the changes.
Finally, you need to ensure you only call them once when you start to change around the controls in the panel and then again at the very end. If you use them with every control you won't get any benefit.
So the pattern might look something like this:
Public Sub SomeMethod()
PanelTop.SuspendLayout() ' Prevent the panel from updating until you've finished
' Make a bunch of changes
PanelTop.Controls.Clear()
For Each ...
PanelTop.Controls.Add( ... )
Next
PanelTop.ResumeLayout() ' Allow the panel to show all the changes in the same WM_PAINT event
End Sub
You also need to ensure you don't have anything in there like DoEvents()/Invalidate() that might invoke the windows message loop and cause the form to redraw itself.

Making sure async tasks complete before vb.net application terminates

I'm creating a vb.net desktop application. This application includes some asynchronous functions. When the user closes the application via the red X in the upper-right corner, there is some logic to possibly run one or more of these async functions. The problem is, the program terminates before they are complete. I figured using "Await" in my call would do that, but apparently not.
I found this thread that talks about using ManualResetEvent, but I'm having trouble understanding all of it, especially since the question is in the context of a console app, and the MSDN documentation the answer links to is about specifying threads, not simply using async tasks. As an attempt at using it anyway, I tried adding this to my main form:
Public resetEvent As ManualResetEvent = New ManualResetEvent(False)
And immediately after the call to one of these functions, I added this (quote includes the call):
Await activeCount.SerializeAsync(activeCount)
resetEvent.WaitOne()
And at the end of my async function itself, before returning the Task, added this:
frmMain.resetEvent.Set()
I don't think I'm using that right, though. The program still terminates before it's complete anyway.
Even before that, I figured the best place for such a thing would be in ApplicationEvents MyApplication_Shutdown, but I'm not sure how to know if such a function is still running at that point.
So what is the best way to make sure all my async functions complete before the application terminates in this situation?
Thank you!
UPDATE AFTER ACCEPTED ANSWER:
Though F0r3v3r-A-N00b's answer worked, I realized I need to use a dialog in certain cases. I couldn't call that within the background worker because the dialog is on the GUI thread, not the background thread. I tried moving things around so I'd call the dialog first, then make the background worker and all that, but for whatever reason I couldn't get it to work.
Long story short, I got around it by simply making a synchronous version of my functions, and so I could say 'if the user terminated the program and I need to call any of these functions before closing, call the synchronous versions instead'. That works. Thanks!
Try this. Create a new project. Add 1 label and backgroundworker to your form. Paste this in your form's code area:
Public Class Form1
Dim taskCompleted As Boolean = False
Dim taskIsrunning As Boolean = False
Private Sub BackgroundWorker1_DoWork(sender As System.Object, e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork
Threading.Thread.Sleep(5000)
End Sub
Private Sub BackgroundWorker1_RunWorkerCompleted(sender As Object, e As System.ComponentModel.RunWorkerCompletedEventArgs) Handles BackgroundWorker1.RunWorkerCompleted
taskCompleted = True
taskIsRunning = False
Label1.Text = "Background task completed."
Me.Close()
End Sub
Private Sub Form1_FormClosing(sender As Object, e As System.Windows.Forms.FormClosingEventArgs) Handles Me.FormClosing
If taskIsRunning Then
e.Cancel = True
Exit Sub
End If
If Not taskCompleted Then
taskIsRunning = True
Label1.Text = "Starting background task."
BackgroundWorker1.RunWorkerAsync()
Label1.Text = "Background task is running."
e.Cancel = True
End If
End Sub
End Class

Working with passed data between forms

Currently in my windows form, I have a few WinForms to work with. One WinForm acts as a main menu and is supposed to call another form as a secondary window on its own.
Private Sub btnMainGame_Click(sender As Object, e As EventArgs) Handles btnMainGame.Click
' This is the button to call up the main game controller. So simply hide this form aned then open the new form.
Dim frmController As New frmControllerScreen
frmController.Show()
Me.Hide() ' Happens on .Close as well
End Sub
The above code invokes another WinForm which is used to handle more options. When the user clicks on a particular button, a sub form is created again.
Dim OpenNewGameWindow As New frmGameConfig
OpenNewGameWindow.ShowDialog(Me)
Me.DialogResult = DialogResult.None ' Used to prevent the subform from closing the main form when it catches a dialog result.
Now in the frmGameConfig, the program is supposed to take data and pass it back to the form that called it.
Private Sub btnNewGameStartGame_Click(sender As Object, e As EventArgs) Handles btnNewGameStartGame.Click
' ... Skipped code...
frmControllerScreen.MasterQuestionList = QuestionList
frmControllerScreen.blnBankedTime = cbBankedTime.Checked
' ... Skipped code...
End Sub
However, when the frmController tries to reference MasterQuestionList... it returns a nullreference error as if it was not set.
Here's where things get funny...
When I made this code, frmControllerScreen was actually the startup form. Now when I change this form back to frmMainMenu, I get NullReference errors constantly.
My question: How am I supposed to pass information from one form to the next form if it was instantiated from a parent form. (Note I even moved the declartion to Public as a "module-wide" variable... and nothing happens but the same result.) The same error happens even if I go ahead and declare frmController.MasterQuestionList as well.
Instead of trying to pass data back from the called form to the caller, you can reference the called form's controls from the calling code after .ShowDialog.
Dim OpenNewGameWindow As New frmGameConfig
If OpenNewGameWindow.ShowDialog() Then
MasterQuestionList = OpenNewGameWindow.QuestionList
blnBankedTime = OpenNewGameWindow.cbBankedTime.Checked
End If
In OpenGameWindow button click:
Private Sub btnNewGameStartGame_Click(sender As Object, e As EventArgs) Handles btnNewGameStartGame.Click
Me.DialogResult = True
End Sub

Timer does not work

I try to run a timer from my winform application. For some reason the function that should run on the timer's tick (IsTimeOffsetValid) is not called nor stopped on break point, and basically nothing happens. I attached a code sample below.
I appreciate the help.
Module Module1
Sub main()
Dim OutputForm As New Form17
Application.Run(OutputForm)
End Sub
End Module
Public Class Form17
Private TimerServerOffset As New System.Timers.Timer
Private Sub Form17_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
AddHandler TimerServerOffset.Elapsed, New System.Timers.ElapsedEventHandler(AddressOf IsTimeOffsetValid)
TimerServerOffset.Interval = 1
TimerServerOffset.Start()
End Sub
Private Sub IsTimeOffsetValid()
MsgBox("IsTimeOffsetValid")
End Sub
End Class
Apart from errors in the code that you posted there are other issues with the design.
Read this question: System.Timers.Timer vs System.Threading.Timer
The callback is called on a worker thread (not the UI thread) so displaying a message box could be a big problem.
then switch to a more fitting timer. If all you want to do is validate the inputs every second, switch to the System.Windows.Forms.Timer. The tick handler runs on the UI thread so you can change the UI in the handler.
Then consider changing the interval a message box popping up every millisecond is not possible and not user friendly.
Finally, I would suggest NOT using a timer for this: just handle changes to the input fields and respond to changed inputs or use the standard validation events of the WinForms controls. This is much cheaper (on the CPU) and will not mess with the focus.

Showing form from form_load event handler and showing progressbar

Here are some things which I don't know how to do properly in .Net but I'm sure that some solution surely exists.
I have form which is open like dialog and under (that) form_load I have some checks of data and after that I call a procedure which may take 10 seconds to process.
During this time my form is not showed until my process don't finish and I can see some 'garbage of menu' in my main form instead of progressbar in those new form which is in process.
Second thing I see and which may be close to first problem is that in some of my forms progressbar don't comes to end when results of some process is showed but program (for progressbar) is maked properly. It seem's like progressbar work in some asynchronous task.
How to fix those problems and get my form showed before my procedure starts?
How to get progressbar to show a value reliable? Or I can say beter like it works in VB6?
For first problem I try:
Me.Activate
or
Me.Refresh
Call myProcedureWithProgressBar(myArgs)
From Form_Load but without results.
Form is showed when my procedure finishes but should be showed before.
Event Form_Load executes the code before showing the Form. You can create a Timer and use this to automatically execute code after form load:
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
Timer1.Start()
End Sub
Private Sub OnFormLoaded(sender As Object, e As EventArgs) Handles Timer1.Tick
Timer1.Enabled = False
'...
End Sub
And make sure you stop the timer on the method like I did, and make sure you prevent user to do something that could interfiere with the execution (clicking buttons, closing form...). Hope it helps.
And about the ProgressBar issue, I don't know what could be the problem without more information or code. Maybe you can try something like this:
Public Sub UpdateProgressBar(ByVal cont As Integer, ByVal max As Integer, ByRef objTarget As ProgressBar)
Dim dProgress As Double = cont * 100 / max
objTarget.Value = CInt(dProgress)
objTarget.Refresh()
End Sub