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
Related
I am kinda new to visual basic and was wondering if it is possible to trigger events in a class, based on action with in form.
So the following is my problem:
I have a form called Main, with a picture box called picPID
And a class called add_new
What I want to do is:
when the right mousebutton is clicked, the code for the event handling is placed within the class called add_new
I thought i could just declare it in the following way:
Sub meMouseDown(sender As Object, e As MouseEventArgs) Handles Main.picPID.MouseDown
But I get an error saying i have to declare it withEvents, I tried to declare the picturebox as:
Public shared withEvents as picturebox
but it dident help, any sugestions? It is not a problem having the code within the Main form, but it would result in a lot of code, so I was hoping to split it up.
So you have the class with an eventhandler:
Public Class add_new
Public Sub PictureBoxEventHandler(sender As Object, e As MouseEventArgs)
'Your Implementation
End Sub
End Class
Then you need an instance of this class within the form containing the picturebox:
Public Class Form1
Private add_New_Command As New add_new ' hold a reference to the command
'constructor
Public Sub New()
InitializeComponent()
' and add the handler to the event of the picture box ... hook it up
AddHandler PictureBox1.MouseDown, AddressOf add_New_Command.PictureBoxEventHandler
End Sub
End Class
I have a BackgroundWorker that is used to carry a time consuming process while a form is shown. The form and the BackgroundWorker are in separate classes, and when the BackgroundWorker has finished what it has to do, I need to carry out some basic actions on the form.
However, the below does not work and produces the warning Reference to non-shared member requires an object reference.
Private Sub bw_RunWorkerCompleted(ByVal sender As Object, ByVal e As RunWorkerCompletedEventArgs)
mainForm.btnCancel.Enabled = False
mainForm.btnFinish.Enabled = True
End Sub
I researched the warning and it suggested that I had to ensure the object mainForm was declared, which for this scenario seems odd from the get go. Regardless, I changed my code to this, and the warning disappeared, but as suspected, it doesn't work. It seems that a new instance of the form would be referenced, which is not what I require.
Private Sub bw_RunWorkerCompleted(ByVal sender As Object, ByVal e As RunWorkerCompletedEventArgs)
Dim objForm As New mainForm
objForm.btnCancel.Enabled = False
objForm.btnFinish.Enabled = True
End Sub
Can someone please tell me how I can interact with a form from a BackgroundWorker? Thanks.
The problem is not that you need to declare a new mainForm object. The problem is that you need a reference to the right mainForm object. Since it is possible to create any number of mainForm objects, you need a reference to the particular mainForm object that you want to modify. Remember, mainForm is the class (a type of object). It is not, itself, an object.
The simplest way fix this would be to give a reference to the mainForm object to the class that is performing the work, like this:
Public Class MyBusiness
Public Property TheMainForm As mainForm
' ...
Private Sub bw_RunWorkerCompleted(ByVal sender As Object, ByVal e As RunWorkerCompletedEventArgs)
TheMainForm.btnCancel.Enabled = False
TheMainForm.btnFinish.Enabled = True
End Sub
End Class
Then, before starting the work, you need to make sure you set the TheMainForm property. For instance, something like this:
Dim business As New MyBusiness
business.TheMainForm = Me
business.DoWork()
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 have a class A which contains 2 user controls declared as
Friend WithEvents CustInfo1 As WindowsApplication1.CustInfo
Friend WithEvents ServiceLocation1 As WindowsApplication1.ServiceLocation
Both have textBoxes. If value of textBoxA in custInfo1 changes then how can I make value of textBoxB in SeviceLocation1 also change
I will be most thankful if anyone can help me.
Thanks
You need to do the following:
Inside the CustInfo user control, you need to write code inside the textBoxA Changed event that raises an event from the CustInfo user control (e.g. TextBoxChanged event). RaiseEvent statement
Inside the ServiceLocation user control, create a public property getter and setter for whatever your textBoxB.Text is
On the form that contains both user controls, create code in the new CustInfo TextBoxChanged event and set the new property on the ServiceLocation1 user control.
All controls (also custom controls) have the property Controls, through which you can access the (sub) controls of that control. Now you can retrieve your textbox by calling the .Item(key) method on it. Then you can assign a event handler to it in your form or class.
Dim key As String = "textBoxA" 'Or simply the name of that TextBox in your CustInfo
Dim textboxA As TextBox = CustInfo1.Controls.Item(key)
AddHandler textBoxA.TextChanged, AddressOf mytextchangedhandler
Where that mytextchangedhandler handles the TextChanged event for that TextBox.
Personally I don't like this method too much, as you are relying on knowing either the name of the TextBox or the index in the Controls list of your usercontrol.
I would definitely go for the option to create your own event on your usercontrol. It is quite easy to do even! Below how to do it. In the code behind of your usercontrol, you'll have to add an event declaration:
Event MyTextBoxChanged(sender As Object, e As EventArgs)
Now we'll have to raise it, we do this by implementing the TextChanged event of the TextBoxA in your usercontrol (as you explained you wanted to do):
Private Sub TextBoxA_TextChanged(sender As System.Object, e As System.EventArgs) Handles TextBoxA.TextChanged
RaiseEvent MyTextBoxChanged(Me, EventArgs.Empty)
End Sub
Now we can simply implement this event (MyTextBoxChanged) in your Form as follows:
Private Sub CustInfo1_MyTextBoxChanged(sender As System.Object, e As System.EventArgs) Handles CustInfo1.MyTextBoxChanged
' Do something
End Sub
Obviously we still need to get the updated text, now we can create our own EventArgs that will give us the new (and/or old value) as you will want to have. We simply can inherit the System.EventArgs class and add some properties (for example a property OldText that holds the old text value and a property NewText that holds the new text value):
Public Class MyEventArgs
Inherits System.EventArgs
Private _OldText As String
Public ReadOnly Property OldText() As String
Get
Return _OldText
End Get
End Property
Private _NewText As String
Public ReadOnly Property NewText() As String
Get
Return _NewText
End Get
End Property
Public Sub New(oldText As String, newText As String)
_OldText = oldText
_NewText = newText
End Sub
End Class
Now we have to change the event definition and raising to use the MyEventArgs:
Event MyTextBoxChanged(sender As Object, e As MyEventArgs)
Private Sub TextBoxA_TextChanged(sender As System.Object, e As System.EventArgs) Handles TextBoxA.TextChanged
RaiseEvent MyTextBoxChanged(Me, New MyEventArgs(TextBoxA.Text))
End Sub
And also change the implementation in your Form:
Private Sub CustInfo1_MyTextBoxChanged(sender As System.Object, e As MyEventArgs) Handles CustInfo1.MyTextBoxChanged
MessageBox.Show(e.Text)
End Sub
More information about events can be found on our favorite spot MSDN.
I have a form in VB.NET that is used as a dialog in a mainform. Its instances are always locally defined, there's no field for it. When the user clicks the OK button in the dialog, it will fire an event with exactly one argument, an instance of one of my classes.
Since it is always a local variable, how can I add an event handler for that event? I've searched for myself and found something but I can't really figure it out...
Code for the event, a field in MyDialog:
public Event ObjectCreated(ByRef newMyObject as MyObject)
Code for the main form to call dialog : (never mind the syntax)
Dim dialog As New MyDialog()
dialog.ShowDialog(Me)
AddHandler ObjectCreated, (what do I put here?) //Or how do I add a handler?
As you can see I'm stuck on how to add a handler for my event. Can anyone help me? Preferrably with the best way to do it...
It's recommended, for consistency, that you use the same source and event args model as all system event handlers.
Create your own class inheriting from EventArgs, as:
Public Class MyObjectEventArgs
Inherits EventArgs
Public Property EventObject As MyObject
End Class
Then declare your event, and a handler method, like:
Public Event ObjectCreated As EventHandler(Of MyObjectEventArgs)
Private Sub Container_ObjectCreated(ByVal sender As Object, ByVal e As MyObjectEventArgs)
' Handler code here
End Sub
Then attach the handler to your event using:
AddHandler ObjectCreated, AddressOf Container_ObjectCreated
Additionally, you can use the Handles to attach to the event raised from your main form (assuming the name MainForm), as below:
Private Sub MainForm_ObjectCreated(ByVal sender As Object, ByVal e As MyObjectEventArgs) Handles MainForm.ObjectCreated
' Handler code here
End Sub
You need to write the subroutine that actually executes when the event is generated:
public Sub OnObjectCreated(ByRef newMyObject as MyObject)
...
End Sub
Then the handler is added:
AddHandler ObjectCreated, AddressOf OnObjectCreated
As a side note, ByRef does nothing here. All objects in VB are passed by reference. Only primitave variables (string, int, etc) by default use ByVal and can be set to ByRef