I have Parent VB.Net form that has to create some MDI childs.
Each MDI has to connect to a database to do some staff which may take some time...
I'm trying to create the MDI child asynchronously using the following code but it stills execute synchronously :
ParentForm.BeginInvoke(Sub()
CreateMDIChildForm()
End sub)
This code executes but it's still synchronous, and the parent form freezes.
I tried to create the MDI in a separate thread but then I can't join the form created to the parent form.
Does anyone have an idea please ?
Thanks.
I can reproduce this with the following code:
Public Class Form1
Private Sub Open(sender As System.Object, e As System.EventArgs) Handles Button1.Click
Me.BeginInvoke(Sub() Me.CreateMDIChildForm())
End Sub
Private Sub CreateMDIChildForm()
Dim f As New Form2()
MsgBox("OK")
End Sub
Public Class Form2
Inherits Form
Sub New()
Thread.Sleep(5000)
End Sub
End Class
End Class
And the fix would be something like this:
Public Class Form1
Private Sub Open(sender As System.Object, e As System.EventArgs) Handles Button1.Click
If (Me.operationCompleted Is Nothing) Then
Me.operationCompleted = New SendOrPostCallback(AddressOf Me.CreateMDIChildFormCompleted)
End If
Me.asyncOperation = AsyncOperationManager.CreateOperation(Nothing)
Dim thread As New Thread(AddressOf Me.CreateMDIChildForm)
thread.Start()
End Sub
Private Sub CreateMDIChildForm()
Dim f As New Form2()
Me.asyncOperation.PostOperationCompleted(Me.operationCompleted, f)
End Sub
Private Sub CreateMDIChildFormCompleted(args As Object)
Dim f As Form = DirectCast(args, Form)
'TODO: Set mdi child, show window etc.
MsgBox("OK")
End Sub
Private asyncOperation As AsyncOperation
Private operationCompleted As SendOrPostCallback
Public Class Form2
Inherits Form
Sub New()
Thread.Sleep(5000)
End Sub
End Class
End Class
The recommended method to fix issues like this is not to try to load forms in separate threads, but, rather, to do only the time-consuming work in a separate thread.
In this circumstance, what that means is that you would load the MDI child forms normally, all on the same UI thread, but inside the child forms, they would each start a separate thread to perform the DB-related work which is causing the hangups. That way, the DB stuff won't cause the form-loading to hang up. The form will load and show itself quickly and then the database work will be done after the form is already visible.
You may need to disable some, or all, of your controls on the child form until the DB work is complete. You may also want the child form to display some sort of spinning animation to show that it is still loading. The easiest way to implement separate threads in your UI, for things like this, is to use a BackgroundWorker component. You will find it in the Components section of your form designer tool box.
Solution :
Dim Thread as new Thread(sub()
ParentForm.BeginInvoke(sub()
CreateMDIChild()
end sub)
end sub)
Thread.Start()
With this, the MDI child form is still created by the parent form but everything is done in a separate thread.
Related
I'm relatively new to Windows Forms development and my first real application has reached a point where a lot of code starts to build up in my main Form file, so I decided to restructure my project using the MVC pattern.
One major problem I have is dealing with the different control events of the form. I have several buttons, textfields, comboboxes and also a tabcontroll element which again contains different input elements and so far, every procedure for handling clicks, updates and other changes is defined in my main class.
For example:
Private Sub btnOk_Click(sender As Object, e As EventArgs) Handles btnOk.Click
some code...
End Sub
So my question is: what would be the best way to handle these events outside of my main form? I'm more familiar with building GUIs in Java where you can use ActionListeners to achieve this but I have found nothing similar for my work with Windows Forms.
To subscribe to a Control event outside of your main form class, make your control public, so you can access from another class). This can be done using the Modifier property at design-time. Then, use the AddHandler keyword to subscribe to any event programmatically.
After researching a bit more, I found that there is probably not THE correct answer to this problem but I found 2 approaches which provide a solution in the way I was looking for. In both cases, I use a controller class which is responsible for handling any user interaction from my main form.
The first approach makes use of what DmitryBabich suggested, adding a handler to the object and referencing it to a method of my controller class:
in Form1:
Dim ctrl as new Controller(Me)
AddHandler Button1.Click, AddressOf ctrl.doSomething
Controller class:
Public Class Controller
Private myForm As Form1
Public Sub New(ByVal f As Form1)
myForm = f
End Sub
Public Sub doSomething(sender As Object, e As EventArgs)
MsgBox("Button clicked.")
End Sub
End Class
For an example this simple it is not necessary to pass an instance of Form1 over to the controller but if for example I'd like to access the values of other control elements as well, I can address them by using this instance of Form1.
For example:
Public Sub doSomething(sender As Object, e As EventArgs)
MsgBox("You clicked the button, by the way: The value of TextField1 is " & myForm.TextField1.text)
End Sub
The other approach is almost identical except that here the controller knows all the relevant user control objects of the form and can handle their events directly, meaning that in the main form I have to do nothing more than create an instance of the controller. In the controller however, I have to assign every user control I want to access to its own variable as soon as the main form is loaded:
in Form1:
Dim ctrl as new Controller(Me)
Controller class:
Public Class Controller
WithEvents myForm As Form1
WithEvents button1 As Button
WithEvents button2 As Button
Public Sub New(ByVal f As Form1)
myForm = f
End Sub
Public Sub formLoad() Handles myForm.Load
button1 = myForm.Button1
button2 = myForm.Button2
End Sub
Private Sub b1Click() Handles button1.Click
MsgBox("You clicked button1!")
End Sub
Private Sub b2Click() Handles button2.Click
MsgBox("Button #2 was clicked!")
End Sub
End Class
i have my form class, and a second module with some special functions,
when i click a button on my form i run a public function from the second module(which run later other public functions from the second module) in a separated thread, i set SetApartmentState(ApartmentState.STA), and i tried using deletage sub and CheckForIllegalCrossThreadCalls = False, but the problem stays the same, my thread functions (which are on the second module) can't access my form controls, but when i move the functions to the form class everything work again, what do you suggest to solve this issue?
Public Class Form1
Dim T0 As Thread
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
CheckForIllegalCrossThreadCalls = False
End Sub
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
T0 = New Thread(AddressOf sub1)
T0.SetApartmentState(ApartmentState.STA)
T0.start()
End Sub
End Class
Module Module1
Public Sub Sub1()
msgbox(form1.textbox1.text) 'even if the textbox contains content it returns ""
Function2()
End Sub
Public Function Function1()
'SomeInstructions
msgbox(form1.textbox1.text) 'same problem here
End Function
End module
PS: it dosn't give any error or stop the code while debugging, and i tried to put the sub1 on the form class and the other functions in the module, but then, only he sub1 can access the controls, i tried delegate but i don't know if i have done it right, can anyone make any suggestions please
There are two issues at play here.
One of them is due to the way default forms work in VB.NET. See, in C# you need a concrete instance of type Form1 in order to access its non-static members, whereas in VB.NET Form1 looks like an instance of the form allowing you to write things like Form1.TextBox1.Text. This works fine while you're accessing members of Form1 from the UI thread, but when you try to get it from a background thread, a new instance of Form1 is created, and Form1.TextBox1 seen by that thread actually points to a completely different instance of TextBox.
Another way saying this is that default form instances in VB.NET are thread static.
A way to get around this is to keep a concrete reference to Form1 so that you can pass it around.
When you go
Dim myForm As New Form1
... or
Dim myForm As Form1 = Me
... the 'myForm' variable pointing to that specific Form1 instance can then be passed between threads like any other reference and will not change its meaning.
This, however, brings us to issue #2:
You should not be accessing a UI control (which means any type derived from Control, and that includes Form , from a thread other than the thread that it was created on.
If you absolutely have to, you have to marshal the calls accessing the Control to the thread that it was created on, for example by using Invoke/BeginInvoke (there are other ways too).
Here's a modification of your code to achieve what you want, plus a more complex example which demonstrates multiple thread "switches": gathering interesting state on the UI thread, performing work with it on the background thread, then displaying results on the UI thread.
Imports System.Threading
Public Class Form1
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
' We don't need this anymore.
' We'll do things right and access
' the UI on the UI thread only.
' CheckForIllegalCrossThreadCalls = False
End Sub
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
' Note that we're passing a reference
' to THIS instance of Form1 to Sub1 and Sub2.
Dim form As Form1 = Me
' Let's spin up some threads.
Dim T0 As New Thread(Sub() Module1.Sub1(form))
T0.Start()
Dim T1 As New Thread(Sub() Module1.Sub2(form))
T1.Start()
End Sub
End Class
Module Module1
' Note that this sub now accepts
' a reference to an instance of Form1.
Public Sub Sub1(form As Form1)
' This is what we want to do:
Dim action As New Action(Sub() MsgBox(form.TextBox1.Text))
' See if we're on the right thread.
If form.InvokeRequired Then
' Invoke on the thread which created this Form1 instance.
form.Invoke(action)
Else
' Invoke on the current thread.
action.Invoke()
End If
End Sub
' This is a more complex example.
Public Sub Sub2(form As Form1)
' This function will get the text from TextBox1 when invoked.
' It still needs to be invoked on the UI thread though.
Dim getText As New Func(Of String)(Function() form.TextBox1.Text)
Dim text As String
If form.InvokeRequired Then
text = CStr(form.Invoke(getText))
Else
text = getText() ' Shorthand syntax.
End If
' Now that we have the text, let's do some
' intensive work with it while we're on
' the background thread.
For i = 0 To 5
text &= i
Thread.Sleep(100)
Next
' Now we want to show the message box - again, on the UI thread.
Dim showMessageBox As New Action(Sub() MsgBox(text))
If form.InvokeRequired Then
form.Invoke(showMessageBox)
Else
showMessageBox()
End If
End Sub
End Module
I am trying to create a customized form class (CustomBorderlessForm) in VB.NET.
My Progress So Far
I created a new Class and named it CustomBorderlessForm.vb
I then proceeded to write the following code:
CustomBorderlessForm.vb
Public Class CustomBorderlessForm
Inherits Form
Dim _form As Form = Nothing
Public Sub New(form As Form)
_form = form
MsgBox("Testing: New()")
End Sub
Protected Overrides Sub OnMouseMove(e As MouseEventArgs)
MyBase.OnMouseMove(e)
MsgBox("Testing OnMouseMove()")
End Sub
End Class
Form1.vb
Public Class Form1
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
Dim form As New CustomBorderlessForm(Me)
End Sub
End Class
Results of progress
A message box displays "Testing: New()" on load
Nothing shows on mouse move
As you can see, my problem lies with the events
Questions
Is it possible to create a form object and use that instead of the pre-populating form?
If so, can I give this form custom properties, such as, a border and some boolean values (shadow...etc), just like any other custom object/class?
What am I doing wrong in my current approach?
Why isn't the OnMouseMove being overridden?
Am I initialising the class wrong?
Can it even be done this way?
After creating a form you also need to show it. Change your logic to:
Dim form As New CustomBorderlessForm(Me)
form.Show()
Before you do that, I'd recommend changing from MsgBox to Console.WriteLine(), otherwise you can run into a fun/frustrating little cat and mouse game.
EDIT
Based on the comments, if, from VS you did a "Add New, Windows Form" you can just right-click the project, select property and on the Application tab change the Startup object to your new form. VS only allows you to do this with forms it creates for you (by default, more on this later).
If you wrote that file by hand (which is absolutely fine) you can perform the Show() like I did above and call Me.Hide() to hide the "parent" form. Unfortunately the Load event is fired before the Show event so if you place this in Form1_Load() it won't work. Instead you can use the Shown event like this:
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
Dim form As New CustomBorderlessForm(Me)
form.Show()
End Sub
Private Sub Form1_Shown(sender As Object, e As EventArgs) Handles Me.Shown
Me.Hide()
End Sub
Another option has to do with "Application framework". You can read about it here however it basically handles application events that other languages have to manually implement. If you go into your project properties you can uncheck the "Enable application framework" checkbox. This will give you more option in the "Startup object" dropdown. If you add the following code to your project one of the items in the Startup object dropdown menu should now be "Loader"
Public Module Loader
<STAThread()>
Public Sub Main()
Dim form As New CustomBorderlessForm(Nothing)
form.ShowDialog()
End Sub
End Module
You'll notice that the above bypasses Form1 completely. Also, instead of Show() I'm using ShowDialog() because otherwise the form shows and then the program ends.
I have a little problem here. I'm trying to transfer/pass/raise the events of an owned form to his parent. Lets look at my example:
Lets say i have a form that initialize a CustomPanel (simply a class that inherits from System.Windows.Forms.Panel). It also have an event handler (it could be an other event, not necessarily a click event):
Public Sub New()
Me.Size = New Size(1000,1000)
Dim pnl1 As New CustomPanel()
pnl1.Location = New Point(0,0)
pnl1.size = New Size(100,100)
Me.Controls.Add(pnl1)
End Sub
Private Sub form1_Click(sender As Object, e As EventArgs) Handles Me.Click
MsgBox("I got it!")
End Sub
I did something similar and when I clicked on the CustomPanel (pnl1) the parent container (form1) did not receive a click event ... which is understandable. I tried to look in the properties of the CustomPanel (pnl1) if i could find something interesting like "click through" or "raise event to parent" (I was desperate here) but without success. I said alright, I will handle the events that I need to pass to parent in the CustomPanel class but I cant find a solution here neither:
Imports System.Windows.Forms
Public Class CustomPanel
Inherits Panel
Public Sub New()
End Sub
Private Sub CustomPanel_Click(sender As Object, e As EventArgs) Handles Me.Click
'What to put here?
'Me.Parent.?
End Sub
End Class
I just want to know if its possible to throw/raise/pass events to the parent. One thing is sure, its that i shouldn't have to and i cannot add anything else to the parent form. The reason is simple, i could have over 100 controls in this parent form and they could be added dynamically. And on top of that, these controls could also have their own controls inside! So i could have something like:
pnl99 call parent click -> pnl98 call parent click -> ... until the parent of the control really handle the click event ... -> form1 perform click event
Maybe its hard to understand but if you can help me I would appreciate.
Using a custom event, that the form owning the panel subscribes to. Raise Event
Public Sub New()
Me.Size = New Size(1000,1000)
Dim pnl1 As New CustomPanel()
pnl1.Location = New Point(0,0)
pnl1.size = New Size(100,100)
Addhandler pnl1.MyClickEvent, AddressOf pl_Click
Me.Controls.Add(pnl1)
End Sub
Private Sub pl_Click()
MsgBox("I got it!")
End Sub
Custom panel:
Public Class CustomPanel
Inherits Panel
Public Event MyClickEvent
Private Sub CustomPanel_Click(sender As Object, e As EventArgs) Handles Me.Click
RaiseEvent MyClickEvent()
End Sub
End Class
On the child form
Imports System.Windows.Forms
Imports STAS_PLC_Link_Lib
Public Class ChildForm
Public Event MyClick()
'.....rest of code
On the parent form
Public Class ParentForm
Private Sub GetSomeClick() Handles ChildFor.MyClick
System.Console.WriteLine("test")
End Sub
end Class
I have a sub in a child form that resets settings for my application. One of which is the start location of my parent form. I want the parent form location to be updated by the reset_click event. Partly this is because the Parents closing event stores the forms last location, so I need to move it before it gets closed.
Child forms code:
Public Sub Reset_Click(sender As System.Object, e As System.EventArgs) Handles Reset.Click
My.Settings.Reset()
My.Settings.Save()
TextBox2.Text = My.Settings.FilePath
TextBox1.Text = My.Settings.VerticalExaggeration
Dim frm As New Global.JJsGEControlPanel.Form1()
frm.MoveFrm()
End Sub
The MoveForm Sub in the Parent:
Public Sub MoveFrm()
Me.Location = My.Settings.MainFormLocation
Me.Refresh()
End Sub
The MoveFrm Sub works if executed from within the Parent Form but wont if executed from the child click event? I'm stumped!
This line creates a new instance of the first form. An instance that is never shown. The command is executed by this hidden instance and thus you don't see your original form move anywhere.
Dim frm As New Global.JJsGEControlPanel.Form1()
frm.MoveFrm()
To simplest way to solve your problem is passing the original instance of the Form1 to the child form, save this passed instance and then use it when you need to move the original form
In your Form1 (when you create the instance of the child form) you write
Dim child = new frmChild(Me)
in your child form constructor
Private callerForm as Global.JJsGEControlPanel.Form1
Public Sub New(ByVal callerInstance as Global.JJsGEControlPanel.Form1)
callerForm = callerInstance
InitializeComponent()
End sub
and finally when you need to move the main form
callerForm.MoveFrm()