Okay so maybe i'm looking at this problem the wrong way and if I am please tell me but here goes.
I have a class lets call it newcycles.vb and that class has multiple events.
When I called in my program I say something like dim cyc1 as newcycles
I set up multiple eventhandlers to handle the events in the class in the program.
But I need to be able to dynamically create as many instances of the class as I need depending on the user. The only way I could think of was copy and paste the each declaration and event handler x amount of times. That seems ridicoulous and by copy and paste I mean like
Dim cy1 as newcycles
dim cy2 as newcycles
dim cyc3 as newcycles
etc etc
then the event handles
Public event bla handles cy1.bla
Public event bla2 handles cy2.bla
Is there a better way to do this? Oh and I'm doing this in vb.net.
You could use a generic list and a loop to store all your class initiations
Dim NoOfUsers As Integer = 10
Dim ClassList As List(Of newcycles) = New List(Of newcycles)(NoOfUsers)
For I As Integer = 1 To NoOfUsers
Dim c As newcycles = New newcycles()
c.ObjectIndex = I 'Property for storing the Object Index
AddHandler c.bla, AddressOf bla
ClassList.Add(c)
Next
' The event handler
Private Sub bla(ByVal sender As Object, ByVal e As EventArgs)
Dim c As newcycles = CType(sender, newcycles)
'Do something with c.ObjectIndex which identifies this object in particular
End Sub
And instead of creating multiple event handlers for each object you could handle events of all the objects in a single event handler. To differentiate one object from another you could use the objects unique identifier of some sorts.
Assuming that the same procedure handles all of the events you can do the following. Declare the event in newcycles, then do something like this...
Dim lstCycles As New List(of newcycles)
For X = 0 to 10
lstCycles.Add(New newcycle)
AddHandler lstCycles(X).YourEvent, AddressOf SubThatHandlesThisEvent
Next
Then to access...
For Each objCycle as newcycles In lstCycles
objCycle.SomeBooleanProperty = True
Next
How about creating a collection of NewCycle objects to which you can dynamically add new items, modify any of the existing items, and remove any of the existing items from code while your application is running? You easily implement this using a List(Of T).
Just change your variable declaration to:
Public cycles As List(Of NewCycle)
The documentation for the List(Of T) class contains a fairly comprehensive example of how to use this collection, and lists the methods you'll need to use in order to manipulate the objects in the list.
If you need to handle events for each of the items in the class, you could consider creating your own custom collection and overriding the Add and Remove methods to dynamically add and remove event handlers for the events raised by that object. In other words, each time a NewCycle item is added to your cycle collection, you can add an event handler for its bla method using AddHandler, and each time an item is removed, you can remove the event handler for its bla method using RemoveHandler. All of the objects' events can be handled by a single event handler method, rather than having one for each object. See the answers to this question for more on how to do that.
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'm trying to dynamically creating dropdownList boxes, and I want trying to add AddHandlers to them so that when an item is selected in them, it fires an event, but also need to pass another variable, and I don't know what to put as the parameter for system.EventArgs. Please look at the code below to see the problem I'm having.
AddHandler inputDrop.SelectedIndexChanged, AddressOf selOption(inputDrop, ???, var1)
Protected Sub selOption(ByVal sender As Object, ByVal e As System.EventArgs, ByVal tableCount As String)
End Sub
What do I put (???) right here.
The error:
is an event, and cannot be called directly. Use a 'RaiseEvent' statement to raise an event.
In addition what Mike C already explained, if the signature of the event handler does not match the event, you can always wrap the event handler in another method, for example an anonymous one:
Protected Sub selOption(ender As Object, e As System.EventArgs, somestring As String)
End Sub
...
For i = 1 To 10
Dim cbox = new ComboBox()
Dim number = i ' local copy to prevent capturing of i '
AddHandler cbox.SelectedIndexChanged, Sub(s, e) selOption(s, e, "Hi! I am Number " & number)
Next
Now, when the index of the last ComboBox changes, the somestring parameter passed to selOption will be Hi! I am Number 10, while it will be Hi! I am Number 1 for the first ComboBox etc.
When you register an event handler, you don't specify the arguments at that time. You're basically just setting a reference to a delegate that will handle the event when it is raised.
AddHandler inputDrop.SelectedIndexChanged, AddressOf selOption
The most important thing is that the method signature of the event handler matches up exactly with the method signature defined by the event. I'm not sure that your method would work because you have that extra tableCount parameter specified. You will need to modify your method signature to be:
Protected Sub selOption(ByVal sender As Object, ByVal e As System.EventArgs)
I'm basing that off the definition of SelectedIndexChanged for Winforms. This event could be defined differently in another technology, such as ASP.net or WPF. Or if this is some custom class, it could be an entirely different signature altogether. However, typically most event handlers have a similar structure of a sender (the instance that raises the event) and some event arguments.
Then when inputDrop fires it's event (when the selected item changes), your code will get automatically called. The arguments passed to this method will be passed directly from inputDrop, you do not have to specify them.
Also, your AddHandler statement must exist inside a method or code block, it can't just live in the class definition. It's a statement that must be executed like any other piece of code, it's not a declaration.
And there is yet another way of doing it. Inherit the control in question and add a property like this:
Public Class MyComboBox : Inherits ComboBox
Public Property tableCount As String
End Class
Then set your custom value and add a handler as you would for a regular ComboBox:
combo.tableCount = tableCount
AddHandler combo.Click, AddressOf combo_Click
Inside combo_Click, CType sender to your inherited type, and get the value you stored previously:
Private Sub combo_Click(sender As Object, e As System.EventArgs)
Debug.WriteLine(CType(sender, WorkflowActionBox).tableCount)
End Sub
You will need to replace current usages of ComboBox with those of MyComboBox, where you want the new property to be available. Simple as opening your designer file and doing find/replace.
Written a quick subroutine in a class to move controls from one Panel to another in VB.NET, which seemed simple enough:
Public Sub Move(ByRef OldPanel As System.Windows.Forms.Panel)
Dim panelControl As System.Windows.Forms.Control
For Each panelControl In OldPanel.Controls
MessageBox.Show(panelControl.Name) 'Debugging
OldPanel.Controls.Remove(panelControl) 'Fairly certain this line makes no difference
NewPanel.Controls.Add(panelControl)
Next
End Sub
The problem is, it only moves about half the controls. The other panels aren't picked up by the loop at all and remain bound to OldPanel. I have verified that the controls are definitely part of the OldPanel (and not just visually floated above it).
For example, if there are 6 controls on the panel, MessageBox.Show(panelControl.Name) only feeds back 3 of them, and only those 3 controls move. This is... baffling.
I wrote a similar debugging loop inside the form class _Load event itself and this correctly picks up all 6 controls on the panel:
Dim panelControl As System.Windows.Forms.Control
For Each panelControl In Me.Panel1.Controls
MessageBox.Show(panelControl.name)
Next
Any ideas?
A common solution to this type of problem is to loop backwards over the collection. Then when you remove items it doesn't affect the index of the items you haven't seen yet:
Public Class Form1
Private Sub Button1_Click(sender As System.Object, e As System.EventArgs) Handles Button1.Click
MoveControls(Panel1, Panel2)
End Sub
Public Sub MoveControls(ByVal OldPanel As Panel, ByVal NewPanel As Panel)
Dim ctlCount As Integer = OldPanel.Controls.Count - 1
For i As Integer = ctlCount To 0 Step -1
NewPanel.Controls.Add(OldPanel.Controls(i))
Next
End Sub
End Class
You are changing the collection while using for each to loop through it; that is asking for trouble: once the foreach is started and acquired the enumerator the enumerator is tied to the collection as it was at the start.
One way to solve this is by first looping and collecting a list of controls to be deleted.
Then loop the list and remove these controls.
Another way is to use for which doesn't create an enumerator.
Note that your code will not work if a control is nested within another control.
How can I use the event for a dimmed variable that is NOT a control.
This is my dimmed variable:
Dim engine As New Speech.Recognition.SpeechRecognitionEngine
I want to use the event "engine.SpeechRecognized".
You do it the same way you would for anything else where you wanted to add handlers explicitly:
AddHandler engine.SpeechRecognized, AddressOf HandleSpeechRecognized
See the documentation for the AddHandler statement for more information.
There are two ways to add error handlers in VB.NET. You can do so "manually" by using the AddHandler statement, such as:
Dim engine As New SpeechRecognitionEngine()
AddHandler engine.SpeechDetected, AddressOf OnSpeechDetected
With this approach, you would then need to manually implement the OnSpeechDetected event handler method, such as:
Private Sub OnSpeechDetected(ByVal sender As Object, ByVal e As SpeechDetectedEventArgs)
' Do something
End Sub
However, the second method is often easier. This second method is the way that events for controls are handled. However, it is only possible if your object variable is declared as a field (at the class level, outside of any method). All you need to do is add the keyword WithEvents before the variable name, such as:
Dim WithEvents engine As New SpeechRecognitionEngine()
Then, that variable name will show up in the left-side drop-down box at the top of your code window along with all your controls. When you select it in that drop-down box, you can then select any of its events in the right-side drop-down box and it will automatically create the event handler method for you:
Private Sub engine_SpeechDetected(ByVal sender As Object, ByVal e As SpeechDetectedEventArgs) Handles engine.SpeechDetected
End Sub
I keep running into situations where I don't know what event I have to listen to in order to execute my code at the correct time. Is there any way to get a log of all events that is raised? Any way to filter that log based on what object raised the event?
EDIT: Final solution:
Private Sub WireAllEvents(ByVal obj As Object)
Dim parameterTypes() As Type = {GetType(System.Object), GetType(System.EventArgs)}
Dim Events = obj.GetType().GetEvents()
For Each ev In Events
Dim handler As New DynamicMethod("", Nothing, parameterTypes, GetType(main))
Dim ilgen As ILGenerator = handler.GetILGenerator()
ilgen.EmitWriteLine("Event Name: " + ev.Name)
ilgen.Emit(OpCodes.Ret)
ev.AddEventHandler(obj, handler.CreateDelegate(ev.EventHandlerType))
Next
End Sub
And yes, I know this is not a good solution when you actually want to do real stuff that triggers off the events. There are good reasons for the 1 method - 1 event approach, but this is still useful when trying to figure out which of the methods you want to add your handlers to.
The only way that I can think of is to use Reflection to enumerate all of the events and wire up a generic handler which would be a PITA.
Is the problem with Framework events? If so, Microsoft does a pretty good job of giving event life-cycle/call order.
Edit
So here's a global event capture routine:
Private Sub WireAllEvents(ByVal obj As Object)
'Grab all of the events for the supplied object
Dim Events = obj.GetType().GetEvents()
'This points to the method that we want to invoke each time
Dim HandlerMethod = Me.GetType().GetMethod("GlobalHandler")
'Loop through all of the events
For Each ev In Events
'Wire in a handler for the event
ev.AddEventHandler(obj, [Delegate].CreateDelegate(ev.EventHandlerType, Me, HandlerMethod))
Next
End Sub
Public Sub GlobalHandler(ByVal sender As Object, ByVal e As EventArgs)
'Probably want to do something more meaningful here than just tracing
Trace.WriteLine(e)
End Sub
To wire it in just call WireAllEvents(Me.DataGridView1) supplying your object. Almost all MS events use sender/e (including DataGridView) format but if for some reason it doesn't I think this code will error out. But I just tested it with both a DataGridView and Form and it worked as expected.