I'm having trouble wrapping my head around Events and their Handlers in general. I was working from some sample code that used them, and I can't understand why use an event rather than simply using a sub. I'm absolutely sure I'm missing the bigger picture here.
Abbreviated code example:
Public Class MenuEntry
Public Event selected as EventHandler(of EventArgs)
Protected Friend Overridable Sub onSelectEntry(e as EventArgs)
RaiseEvent selected(Me, e)
End Sub
End Class
Public Class Menu
Private menuSelect as New inputAction(Keys.Enter)
Private menuEntry as New List(of MenuEntry)
'keeps track of which menu item we're currently on
Private _selectedEntry as Integer
Public Sub Update()
If menuSelect.evaluate Then
onSelectEntry(_selectedEntry)
End If
Protected Overridable Sub onSelectEntry(ByVal entryIndex as Integer)
menuEntry(entryIndex).onSelectEntry(New EventArgs())
End Sub
End Sub
End Class
Public Class OptionsMenu
Inherits Menu
Private arbitraryOne as Integer
Private arbitraryTwo as Integer
Public Sub New()
Dim entryOne as New MenuEntry(String)
Dim entryTwo as New MenuEntry(String)
AddHandler entryOne.selected, AddressOf entryOneSelected
AddHandler entryTwo.selected, AddressOf entryTwoSelected
MenuEntry.add(entryOne)
MenuEntry.add(entryTwo)
End Sub
Private Sub entryOneSelected(ByVal entryIndex as Integer)
arbitraryOne += 1
End Sub
Private Sub entryTwoSelected(ByVal entryIndex as Integer)
arbitraryTwo += 1
End Sub
End Class
And I was right, I was missing the bigger picture. Writing out all the code in the same place helped me to see exactly what was going on. Hopefully I'm correct:
The Event allows a class to say 'Do something when this happens' in a very ambiguous way, leaving the class which created the Object to define a Handler; what that action should be. That Handler can, and very likely will, be unique to each instance of the class.
It seems to me that this would likely be achievable (on a basic level) through indexing and enumeration, but that would get messy and become a lot of code to write rather quickly. This is probably a much more flexible and extensible way of handling things.
I'm going to post this anyway, in the hopes that I'll get someone to tell me whether I am correct in my observations or totally off base, and that it helps someone else who is having trouble with this concept as they dip their toes into OOP and event driven objects.
Being able to have arbitrary code being run when the event is raised is an important aspect. But there's a much bigger benefit, it strongly reduces the coupling between classes. Note that your MenuEntry class has no reference to the Menu class at all. You can completely redesign the Menu class and not have a need to make any changes at all in the MenuEntry class. That makes code highly composable.
The technical term is the Observer Pattern. The Gang of Four book is essential reading for programmers.
Yes, your observations are correct. Events are used to let all interested parties know that something has occurred and, if they are interested, they can then perform some additional actions that are not necessarily intrinsic to the class that raised the event.
Related
I'm using VB .NET to create a planning, and I got a little problem with events.
In the main form, I put a panel in which I add programatically rows and boxes in those rows. I have inside the form a TextBox and the panel that contains all the boxes. I want to change a the text of the TextBox when I click on a box, so I use the AddHandler statement but it doesn't work. I tried to debug it and I realised that it actually calls the sub and inside it, I can see the changes it makes (TextBox.Text becomes what I want), but when it exits the sub, it is like nothing has changed.
I don't know if I was clear enough.
Thanks
Here is a simplified code (I removed all the graphics functions to resize the controls...)
Public Class frmPrinc
Public actEditing As Object
Private Class boxAct
Inherits Label
Public act As Integer
Public Sub New(ByVal a As Integer)
act = a
AddHandler Me.Click, AddressOf clickBox
End Sub
Private Sub clickBox(sender As Object, e As EventArgs)
Dim boxact As boxAct = DirectCast(sender, boxAct)
frmPrinc.actEditing = boxact
boxact.Text = "Clicked"
End Sub
End Class
Private Sub showPlanning()
pan_plan.Controls.Clear()
Dim plan As New Control ' Control that will be used as a row
For i As Integer = 0 To 10
plan.Controls.Add(New boxAct(i))
Next
Panel1.Controls.Add(plan)
End Sub
End Class
When I run that, the text of the box changes but actEditing is still Nothing...
Instead of boxAct trying to directly update frmPrinc of the current "box" being clicked, it should instead raise a Custom Event that frmPrinc subscribes to. frmPrinc can use that information as it then sees fit. Below I've added the custom event and raise it in class boxAct. The form subscribes to that event using AddHandler when each instance of boxAct is created. All together, this looks something like:
Public Class frmPrinc
Public actEditing As boxAct
Public Class boxAct
Inherits Label
Public act As Integer
Public Event BoxClicked(ByVal box As boxAct)
Public Sub New(ByVal a As Integer)
act = a
End Sub
Private Sub boxAct_Click(sender As Object, e As EventArgs) Handles Me.Click
Me.Text = "Clicked"
RaiseEvent BoxClicked(Me)
End Sub
End Class
Private Sub showPlanning()
pan_plan.Controls.Clear()
Dim plan As New Control ' Control that will be used as a row
For i As Integer = 0 To 10
Dim box As New boxAct(i)
AddHandler box.BoxClicked, AddressOf box_BoxClicked
plan.Controls.Add(box)
Next
Panel1.Controls.Add(plan)
End Sub
Private Sub box_BoxClicked(box As boxAct)
actEditing = box
Debug.Print("Box Clicked: " & actEditing.act)
End Sub
End Class
From the comments:
Thanks man, it worked! I'd like to know though why I need to make such
a structure to raise a simple event that modifies the main form...
Just to not do the same mistake again – Algor Frile
This a design decision everyone must make: "Loosely Coupled" vs. "Tightly Coupled". The approach I gave above falls into the Loosely Coupled category. The main benefit to a loosely coupled solution is re-usability. In your specific case, we have Class boxAct being reused multiple times, albeit all within the same form. But what if that wasn't the case? What if you wanted to use boxAct on multiple forms (or even have multiple "groups" of them)? With your original approach, you had this line:
frmPrinc.actEditing = boxact
which means that if wanted to use Class boxAct with a different form you'd have to make a copy of Class boxAct, give it a new name, and then manually change that one line to reference the new form:
Public Class boxAct2
Private Sub clickBox(sender As Object, e As EventArgs)
Dim boxact As boxAct = DirectCast(sender, boxAct)
frmSomeOtherForm.actEditing = boxact
boxact.Text = "Clicked"
End Sub
End Class
This shows the disadvantage of the Tightly Coupled approach, which uses references to specifics types (the Form in this case) to communicate. The Tightly coupled approach might be initially easier to implement when you're coding fast and furious, but then it suffers from re-usability down the line. There are scenarios in which a tightly coupled solution make sense, but only you can make that decision; those scenarios usually involve some kind of "sub-control" that will only ever get used within some kind of custom container/control and will never be used on its own somewhere else.
Conversely, with the loosely coupled approach, if we wanted to re-use Class boxAct in a different form, then no changes to it would be required at all (though at that point you'd probably not want it declared within your original Form!). In the new form you'd simply add a handler for the BoxClicked() event and then do what you need to do. Each form would receive the events for its respective instances of boxAct.
Final thoughts...your original approach could actually work, but most likely was failing at this line (same as above):
frmPrinc.actEditing = boxact
Here you were referencing to frmPrinc using what is known as the Default Instance of that form. This would have worked if frmPrinc was the "Startup Object" for your application. I'm guessing it wasn't, however, and you were creating an instance of frmPrinc from somewhere else. To make the original approach work you would have had to pass a reference to your ACTUAL instance of frmPrinc into Class boxAct (usually via the Constructor in tightly coupled solutions).
I know there is a lot of information regarding the above, but I do not grasp how to do this correctly, so I thought using a real life problem may help to click it for me and others.
So in class A I have defined an event method
Public Sub textChangedMethod(ByVal textedChanged As Boolean)
' do some code on properties of this class only
End Sub
What I need to happen is I need some other class to raise this method,
I have a concept but its totally wrong.
At the moment I pass an instance of class A to the another class so it can reference the event (this must be wrong)
Dim UI As New newClassDialog(Me) 'class A
In this new class I have the event handler
Public Event textChanged(ByVal textedChanged As Boolean)
So in the constructor of the new class I can now add the handler
Public Sub New(ByRef classA As Class A)
' This call is required by the designer.
InitializeComponent()
AddHandler textChanged, AddressOf classA.textChangedMethod
End Sub
Now of course I can raise the event like so
RaiseEvent textChanged(True)
Basically passing in the class seems ridiculous in my eyes, so using this example is there a 'proper' way of doing this?
Thanks
It seems that you are inverting the roles. In this context the class that raises the event shouldn't know who handles the event. It is the responsability of class that instantiate the newClassDialog to add the event handler for the events raised by the called class
Dim UI As New newClassDialog(Me)
AddHandler UI.textchanged, AddressOf Me.textChangedMethod
I have been banging my head against the wall all day trying to figure this one out.
I am finishing up a program to simply delete files in specific temp folders. I have read that it is sometimes good practice to create separate classes for methods and variables. So I have created a separate class for a couple methods to delete files and folders in a specified directory. I am using a Background Worker in my Form1 class and am calling my deleteFiles() method from my WebFixProcesses class in the DoWork event in the Form1 class. I am using a Background Worker so that I can easily report progress back to a progress bar on my main form.
The files get deleted without an issue but I just can't get the label on my main form to reflect the current file being deleted. the label doesn't change in any way.
I know the formula is correct as I can get this working if the method is in the Form1 class. and I simply use:
Invoke(Sub()
lblStatus.Text = File.ToString
lblStatus.Refresh()
End Sub)
here is my method that I am calling from the WebFixProcesses class:
Public Shared Sub deleteFiles(ByVal fileLocation As String)
For Each file As String In Directory.GetFiles(fileLocation)
Try
fileDisplay.Add(file)
For i = 1 To fileDisplay.Count
file = fileDisplay(i)
Form1.BackgroundWorker1.ReportProgress(CInt(i / fileDisplay.Count) * 100)
Next
IO.File.Delete(file)
Form1.labelText(file)
Form1.labelRefresh()
Catch ex As Exception
MessageBox.Show(ex.Message)
End Try
Next
End Sub
labelText() and labelRefresh() are methods from my main form which are using delegates to try to pass information to the control:
Public Sub labelText(ByVal file As String)
If lblStatus.InvokeRequired Then
Dim del As New txtBoxDelegate(AddressOf labelText)
Me.Invoke(del, file)
Else
lblStatus.Text = file.ToString()
End If
End Sub
Public Sub labelRefresh()
If lblStatus.InvokeRequired Then
Dim del As New txtBoxRefDelegate(AddressOf labelRefresh)
Me.Invoke(del)
Else
lblStatus.Refresh()
End If
End Sub
If anyone can help me out to inform me what I may be doing wrong it would be immensely appreciated as my head is in a lot of pain from this. And maybe I am going at it all wrong, and just being stubborn keeping my methods in their own class. But any help would be awesome. Thanks guys!
What Hans wrote on the question comment is true: Form1 is a type, not an instance, but to make things easier to newbye programmes (coming from VB6), M$ did a "mix", allowing you to use the form name as the instance of the form in the main thread.
This however works only if you are on that thread.
If you reference Form1 from another thread, a new instance of Form1 is created.
To solve the issue, add this code to the form:
Private Shared _instance As Form1
Public ReadOnly Property Instance As Form1
Get
Return _instance
End Get
End Property
We will use this property to store the current instance of the form. To do so, add this line to the Load event:
Private Sub Form1_Load(sender As Object, e As System.EventArgs) Handles Me.Load
_instance = Me
'other code here
End Sub
Now, from every class, in any thread, if you use
Form1.Instance
...you get the actual form. Now you can Invoke, even from the same form:
Me.instance.Invoke(Sub()
Me.lblStatus.Text = "Hello World"
End Sub)
To start with I have a fairly unique situation in that I am dealing with large amounts of data - multiple series of about 500,000 points each. The typical plot time is about 1s which is perfectly adequate.
The chart is created 'WithEvents' in code and the plot time doesn't change.
However, when I add the sub with the handler for the click event ..
Private Sub Chart_Main_Click(ByVal sender As Object, _
ByVal e As MouseEventArgs) Handles Chart_Main.Click
Dim y As Integer = Chart_Main.ChartAreas(0).AxisX.PixelPositionToValue(e.X)
'MsgBox(y)
End Sub
the plot time blows out to 3min. Even having no code in the sub, the result is the same. There is no reference to the click event in any of the code so I am at a loss as to why this is occurring. I suspect it has something to do with the number of points being added but not knowing the cause is frustrating.
Is anyone able to explain what is going on?
Ok, i don't know if the explanation in the comments was sufficient, so here some example code...
Also i wanted to try this myself!
Essencially, what you do is take control on when you want Windows to check the events.
For that, i suggested two wrappers on AddHandler and RemoveHandler that can safely be called from worker threads.
So, what you have to do, is:
Initialize the Handler in the constructor
Call RemoveClickHandler on your control, each time you want it to be left alone by the EventHandler
But don't forget to reinitialize the handler afterwards via AddClickHandler
Also, your handler method should not have the 'Handles' keyword anymore...
Public Class MainForm
Public Sub New()
' This call is required by the designer.
InitializeComponent()
m_pPictureClickHandler = New MouseEventHandler(AddressOf hndPictureClick)
AddClickHandler(pbxFirst, m_pPictureClickHandler)
End Sub
' Have a persistent local instance of the delegate (for convinience)
Private m_pPictureClickHandler As MouseEventHandler
Public Sub AddClickHandler(obj As Control, target As [Delegate])
If Me.InvokeRequired Then
Me.Invoke(New Action(Of Control, [Delegate])(AddressOf AddClickHandler), obj, target)
Else
AddHandler obj.MouseClick, target
End If
End Sub
Public Sub RemoveClickHandler(obj As Control, target As [Delegate])
If Me.InvokeRequired Then
Me.Invoke(New Action(Of Control, [Delegate])(AddressOf RemoveClickHandler), obj, target)
Else
RemoveHandler obj.MouseClick, target
End If
End Sub
' Here your Plot is done
Public Sub LockedPlot()
RemoveClickHandler(pbxFirst, m_pPictureClickHandler)
' do something on your handler free control ...
AddClickHandler(pbxFirst, m_pPictureClickHandler)
End Sub
' This is your handler (note without a 'Handles' keyword)
Private Sub hndPictureClick(sender As Object, e As MouseEventArgs)
' do something with the click
MessageBox.Show(String.Format("Yeah! You clicked at: {0}x{1}", e.X.ToString(), e.Y.ToString()))
End Sub
End Class
I suppose an even better design would be to create a child class of your chart that has an LPC style method called, say 'SafePlot', with folowing features:
It accepts a pointer (delegate) to a procedure
It will remove all the event handler before invoking the procedure
Finally it would reinitialize the handlers on it's own after the job is done.
It may require a collection to all handler refering to it's events.
-> For that reason i'd let the class manage the handlers entiraly...
Alternativly you could put the 'SafePlot' idea in your main class. then you could manage the event handler there... but that is disputable
Well i can think of a few other ways to do this, but i'm cutting the brainstorming now!
If interested in one of these design solutions, give me a poke.
I have created a Form with Objects like Progressbar and Button.
I have also created Public library code outside my Form
I want to modify the Progressbar Control or the Button's Text from a Sub that is written in a library (I try to pass my Form as a parameter):
Public Shared Sub ModifyItems(ByRef _WhichForm As Form)
_WhichForm.MyProgressBar1.visible = True
End sub
Unfortunately, the code Is not recognizing the Progressbar name MyProgressBar1
Is there a way to modify the Progressbar Control on the Form directly in a Sub or Function that is written in a class library, not directly in the Form Code ?
This type of approach would generally fall under the umbrella of "bad practice". Having objects modifying each others members directly introduces tight coupling that makes for difficult code to extend, debug, and maintain.
Rather than trying to modify something from within the library, better to think in terms of notifying that something within the library has changed. You could, for example, raise an event within the library. Form1 could then listen for that event and make any changes to its components that are appropriate. This way Form1 is solely responsible for modifying its components and the library is solely responsible for announcing changes to its internal state.
To give a good example - if, say, you were to change your Form's progress bar component (maybe you find a better one out there) you suddenly introduce a breaking change into your library.
To use an event you might do something like :
Public Class MyLibrary
Event OnSomethingHappened()
Private Sub SomethingHappened()
RaiseEvent OnSomethingHappened()
End Sub
End Class
And then in your form :
Public Class Form1
Private WithEvents _myLibrary as New MyLibrary
Private Sub LibraryDidSomething() Handles _myLibrary.OnSomethingHappened
MyProgressBar1.Visible = True
End Sub
End Class
This nicely decouples any dependence on Form1 - the library exists as a self-contained entity and doesn't care who listens to its events.
If you want to do this with a shared class you can also use shared events - these must be connected programmatically as :
Public Class MyLibrary
Shared Event OnSomething()
Public Shared Sub DoSomething()
RaiseEvent OnSomething()
End Sub
End Class
in the form :
Public Class Form1
Private Sub Form1_Load(sender As System.Object, e As System.EventArgs) _
Handles MyBase.Load
AddHandler MyLibrary.OnSomethingHappened, AddressOf LibDidSomething
End Sub
Private Sub Form1_FormClosed(sender As System.Object, e As _
System.Windows.Forms.FormClosedEventArgs) Handles MyBase.FormClosed
RemoveHandler MyLibrary.OnSomethingHappened, AddressOf LibDidSomething
End Sub
Private Sub LibDidSomething()
MyProgressBar1.Visible = True
End Sub
End Class
When programmatically adding events you must take care to remove them before disposing of the objects that have subscribed to them. In this case I have added the handler when the form loads and have removed it when the form closes. If you fail to remove an added handler then the form would not be garbage collected and this would cause a memory leak.
See here for more reading : Raising Events and Responding to Events (MSDN)