I have a DGV in VB.Net 2008 connected to an Access DB table. The DGV is not Read Only, but is full of read-only columns except for one, which contains a combo box. The combo box allows the user to select an outcome for that particular row, and then the program copies in a pre calculated value into the "Profit" column depending upon the item selected in the combobox. Then the user hits the Save button and the DB updates (currently via SQL methods in the XSD).
Easy enough so far.
Here is the code.
Private Sub DGUserBets_EditingControlShowing(ByVal sender As Object, ByVal e As System.Windows.Forms.DataGridViewEditingControlShowingEventArgs) Handles DGUserBets.EditingControlShowing
Dim combo As ComboBox = CType(e.Control, ComboBox)
If (combo IsNot Nothing) Then
// Remove an existing event-handler, if present, to avoid
// adding multiple handlers when the editing control is reused.
RemoveHandler combo.SelectedIndexChanged, _
New EventHandler(AddressOf DGUBStake_SelectedIndexChanged)
// Add the event handler.
AddHandler combo.SelectedIndexChanged, _
New EventHandler(AddressOf DGUBStake_SelectedIndexChanged)
End If
End Sub
Private Sub DGUBStake_SelectedIndexChanged(ByVal sender As Object, ByVal e As EventArgs)
Dim myStatus As ComboBox = CType(sender, ComboBox)
Dim row = DGUserBets.CurrentRow
Select Case myStatus.SelectedIndex
Case 0
row.Cells("DGUBProfit").Value = 0
// pending. no action
Case 1
row.Cells("DGUBProfit").Value = row.Cells("DGUBIfWin").Value
// win
Case 2
// loses
row.Cells("DGUBProfit").Value = row.Cells("DGUBIfLose").Value
Case 3
// void
row.Cells("DGUBProfit").Value = 0
End Select
End Sub
The problem I have is that it would seem that if a user selects the desired outcome from the combobox but does NOT hit Enter, and simply mouses on to a different combobox to again select the outcome for a different row, the first eventhandler is not disconnected and thus the events fire multiple times. This then causes various default MsgBox errors and brings up problems when the user tries to commit all changes to the DB/exit program etc etc.
What do I need to do? Do I need to .EndEdit somewhere appropriate to force the row to save the changes? And where should I call this?
Thank you.
A quick glance at the code brings up this question:
If you create a new EventHandler when removing the existing one is it the same one?
I have had a similar issue, add a handler for CellLeave if the cell being exited is the cell you are looking for (IE e.ColumnIndex = myEditableColumn.Index) then call gv.EndEdit()
Also I would recommend making the handlers member variables for assignment and removal because it seems nicer then always saying Remove New and Add New.
CKRet/Quintin , thank you for the fast responses.
A quick try with this code seems better and breakpointing and stepping through the code seems to be firing the events correctly. I'm fairly new to .NET as the last real VB programming I did was VB6 so I'm not sure if this is the most elegant way to solve the problem.
Also a note that when LastEventHandler = Nothing, calling the RemoveHandler does not throw an exception, which is quite nice.
Maybe I should suggest to MS they should update that article.
Private Sub DGUserBets_EditingControlShowing(ByVal sender As Object, ByVal e As System.Windows.Forms.DataGridViewEditingControlShowingEventArgs) Handles DGUserBets.EditingControlShowing
Dim combo As ComboBox = CType(e.Control, ComboBox)
Static LastEventHandler As EventHandler
If (combo IsNot Nothing) Then
// Remove an existing event-handler, if present, to avoid
// adding multiple handlers when the editing control is reused.
RemoveHandler combo.SelectedIndexChanged, _
LastEventHandler
LastEventHandler = New EventHandler(AddressOf DGUBStake_SelectedIndexChanged)
// Add the event handler.
AddHandler combo.SelectedIndexChanged, _
LastEventHandler
End If
End Sub
Simpler code which also appears to work well, as suggested by CKRet:
Dim combo As ComboBox = CType(e.Control, ComboBox)
If (combo IsNot Nothing) Then
RemoveHandler combo.SelectedIndexChanged, AddressOf DGUBStake_SelectedIndexChanged
AddHandler combo.SelectedIndexChanged, AddressOf DGUBStake_SelectedIndexChanged
End If
I know this is an archaic post, but after buggering around with this same problem for half a day I've found a way to solve this another way, so I thought it would be worth sharing.
Adding a second handler to handle the leave event of the combobox which then removes the handler of selectedvalue changed. Appears to work quite slick, and unlike another option i found gives the desired resulting action (unlike removing the value changed handler on the actual handling event which then won't fire if you re-select from the same combobox)
Private LastEventHandler As EventHandler = AddressOf Me.ComboBoxValueChanged
Private Sub dgvThisDatagrid_EditingControlShowing(sender As Object, e As System.Windows.Forms.DataGridViewEditingControlShowingEventArgs) Handles dgvOutstandingReminders.EditingControlShowing
If TypeOf (e.Control) Is ComboBox Then
Dim cboThisComboBox = DirectCast(e.Control, ComboBox)
AddHandler cboThisComboBox.SelectedValueChanged, LastEventHandler
AddHandler cboThisComboBox.Leave, AddressOf RemoveValueChangedHandler
End If
End Sub
Private Sub ComboBoxValueChanged(ByVal sender As Object, ByVal e As System.EventArgs)
If TypeOf (sender) Is ComboBox Then
Dim cboThisComboBox = DirectCast(sender, ComboBox)
MessageBox.Show("Value = " & cboThisComboBox.SelectedValue.ToString() & Environment.NewLine & "Text = " & cboThisComboBox.Text) ' Display index
End If
End Sub
Private Sub RemoveValueChangedHandler(ByVal sender As Object, ByVal e As System.EventArgs)
If TypeOf (sender) Is ComboBox Then
Dim cboThisCombobox = DirectCast(sender, ComboBox)
RemoveHandler cboThisCombobox.SelectedValueChanged, LastEventHandler
End If
End Sub
Related
I've created a UserControl and added it to a FlowLayoutPanel. The UserControl is being used to allow a user to enter a material, cost and delivery status to the form. When the user has filled it in, I want another UserControl to appear underneath it in the FlowLayoutPanel
The UserControl simply generates a string based on the text entered into two TextBox controls and the status of two Checkbox controls. it also has a property that indicates when the user has filled in enough information. I want to use this property to signal generating a new UserControl.
At the moment I have my first UserControl on the FlowLayoutPanel, it is successfully passing the String and CreateNew property back.
The problems I am encountering are:
How do I monitor to see if CreateNew has changed to True?
How do I add a control to the form and +1 to the controls name for future referencing
Once the new control is added, I need to monitor to see if the new CreateNew state changes to repeat the cycle
Can anyone point me in the right direction here, there's a lot of information out there on this, but I can't seem to find anything useful from other's problems/questions.
UPDATE 1
Thanks to user Zaggler for the comment, I have now managed to get the control to create a new instance of itself on the FlowLayoutPanel. But now I'm faced with a new problem of it only creating one new usercontrol, then it stops.
Here's the code I'm using:
UserControl Code
Public Class Alv_Product_Order_Control
Public OutString As String
Public Event CreateNew()
Dim CreateNewRaised As Boolean
Private Sub OutputString(sender As Object, e As EventArgs) Handles tbMaterial.TextChanged, tbCost.TextChanged,
cbDelivered.CheckedChanged, cbOrderPlaced.CheckedChanged
OutString = "¦¦" & tbMaterial.Text & "¦" & tbCost.Text & "¦"
If cbOrderPlaced.Checked = True Then
OutString = OutString & "Yes¦"
Else
OutString = OutString & "No¦"
End If
If cbDelivered.Checked = True Then
OutString = OutString & "Yes¦"
Else
OutString = OutString & "No¦"
End If
If tbCost.Text = "" Or tbMaterial.Text = "" Then
Else
If CreateNewRaised = False Then
RaiseEvent CreateNew() 'Raise the event that's used to signal adding a new control to the layout
CreateNewRaised = True 'Create A Latched Boolean that cannot change again in the future
End If
End If
End Sub
Public ReadOnly Property Alv_Product_Order_Control As String
Get
Return OutString 'Pass string back to main form
End Get
End Property
Main Form Code
Private Sub CreateSecondPOC() Handles POC1.CreateNew
FlowLayoutPanel1.Controls.Add(New Alveare_VB.Alv_Product_Order_Control)
End Sub
I'm guessing here that the problem is the CreateSecondPOC only handles the event for the the first POC1
How do I create a new Alveare_VB.Alv_Product_Order_Control, name it as POC2 and also add a handler to handle POC2.CreateNew and add another control?
Edit 2
I know I've found the answer, but i'd like to look at this memory leak that has been mentioned. I've changed the code supplied in the answer below to this:
Private Sub CreateSecondPOC(ByVal sender As Object, ByVal e As System.EventArgs) Handles POC1.CreateNew
Try
Dim oldPoc = DirectCast(sender, Alveare_VB.Alv_Product_Order_Control)
RemoveHandler oldPoc.CreateNew, AddressOf CreateSecondPOC
Catch ex As Exception
Debug.Print(ex.Message)
End Try
Dim newPoc As New Alveare_VB.Alv_Product_Order_Control
AddHandler newPoc.CreateNew, AddressOf CreateSecondPOC
FlowLayoutPanel1.Controls.Add(newPoc)
End Sub
And I get the following error on the "RemoveHandler" line:
Unable to cast object of type 'System.Windows.Forms.TextBox' to type 'Alveare_VB.Alv_Product_Order_Control'.
The CreateNew event is raised when a textbox is written in, this is getting passed back as the sender I assume? Not really sure where to go with this now.
Edit 3
The error was in my UserControl, I was sending the incorrect object back (in this case the textbox). I have now changed the RaiseEvent to return the UserControl as an object. Now all is working correctly.
You could change your handler to something like this
Private Sub CreateSecondPOC() Handles POC1.CreateNew
Dim newPoc As New Alveare_VB.Alv_Product_Order_Control
AddHandler newPoc.CreateNew, AddressOf CreateSecondPOC
FlowLayoutPanel1.Controls.Add(newPoc)
End Sub
I'm not sure if you want to keep handling the event, even after it has been populated once, i.e. can it be depopulated, then repopulated, then raise the event again? Maybe you want to lock it once it's populated, but this isn't clear.
You could also keep all your POC controls in a container and only create a new one when they are all populated.
Edit:
According to comments below, you should remove the event handler when you no longer need it in order to avoid memory leaks. Here is one way
Private Sub CreateSecondPOC(sender As Object) Handles POC1.CreateNew
Dim oldPoc = DirectCast(sender, Alveare_VB.Alv_Product_Order_Control)
RemoveHandler oldPoc, AddressOf CreateSecondPOC
Dim newPoc As New Alveare_VB.Alv_Product_Order_Control
AddHandler newPoc.CreateNew, AddressOf CreateSecondPOC
FlowLayoutPanel1.Controls.Add(newPoc)
End Sub
But the last POC control's event handler would never be unsubscribed. So you could also do something like this when closing the form
For Each poc In Me.FlowLayoutPanel1.Controls.OfType(Of Alveare_VB.Alv_Product_Order_Control)()
RemoveHandler poc.CreateNew, AddressOf CreateSecondPOC
Next poc
I mentioned above that You could also keep all your POC controls in a container, which would be a better way to keep track of your controls, instead of using the FlowLayoutPanel as logical container. Just do what works for you and consider removing the handlers.
I've spent a day trying to figure out how to solve my problem, which is identical to
this related unanswered question
On the first combobox, my code works fine, no problem. When I try to change the combobox it throws an error Null Reference Exception
This is my code:
Private Sub dgvSurveyQuestions_EditingControlShowing(ByVal sender As Object, ByVal e As DataGridViewEditingControlShowingEventArgs) Handles dgvSurveyQuestions.EditingControlShowing
Dim editingComboBox As ComboBox = TryCast(e.Control, ComboBox)
If Not editingComboBox Is Nothing Then
'Add the handle to your IndexChanged Event
RemoveHandler editingComboBox.SelectedIndexChanged, AddressOf editingComboBox_SelectedIndexChanged
AddHandler editingComboBox.SelectedIndexChanged, AddressOf editingComboBox_SelectedIndexChanged
End If
'Prevent this event from firing twice, as is normally the case.
'RemoveHandler dgvSurveyQuestions.EditingControlShowing, AddressOf dgvSurveyQuestions_EditingControlShowing
End Sub
Private Sub editingComboBox_SelectedIndexChanged(ByVal sender As Object, ByVal e As System.EventArgs)
Dim editingComboBox As ComboBox = TryCast(sender, ComboBox)
If editingComboBox Is Nothing Then Exit Sub
'Show your Message Boxes
MessageBox.Show(editingComboBox.SelectedValue.ToString) ' throws error here
End Sub
I'm still working on a different workaround, like adding a handler while the datagridview is populated or whatever.. I don't even know if this is possible but I need to do this.
I'm really stuck here, can someone shed some light and advice me on what to do? Thanks
Simply check if editingComboBox.SelectedValue is Nothing.
The DataGridView reuses only one ComboBox instance, and internally resets the DataSource of the ComboBox when the user selects another ComboBox cell.
Then the event SelectedIndexChanged raises and SelectedValue will be Nothing at that time.
I do know how to handle event of textboxes in my form. But want to make this code shorter because I will 30 textboxes. It's inefficient to use this:
Private Sub TextBox1_TextChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles TextBox1.TextChanged, TextBox2.TextChanged, TextBox3.TextChanged, TextBox4.TextChanged, TextBox5.TextChanged, TextBox6.TextChanged, TextBox7.TextChanged, TextBox8.TextChanged, TextBox9.TextChanged, TextBox10.TextChanged
Dim tb As TextBox = CType(sender, TextBox)
Select Case tb.Name
Case "TextBox1"
MsgBox(tb.Text)
Case "TextBox2"
MsgBox(tb.Text)
End Select
End Sub
Is there a way to shorten the handler?
You can use Controls.OfType + AddHandler programmatically. For example:
Dim textBoxes = Me.Controls.OfType(Of TextBox)()
For Each txt In textBoxes
AddHandler txt.TextChanged, AddressOf txtTextChanged
Next
one handler for all:
Private Sub txtTextChanged(sender As Object, e As EventArgs)
Dim txt = DirectCast(sender, TextBox)
Select Case txt.Name
Case "TextBox1"
MsgBox(txt.Text)
Case "TextBox2"
MsgBox(txt.Text)
End Select
End Sub
If you have created very Textbox with the Designer, I don't think there is a better method.
But, if you have created the Textboxes dynamically, you should AddHandler in this way:
For i = 0 to 30
Dim TB as New Texbox
AddHandler TB.TextChanged, TextBox1_TextChanged
'Set every Property that you need
Me.Controls.Add(TB)
Next
Say If you are having that 30 textboxes inside a panel(PnlTextBoxes), Now you can create handler for your textboxes dynamically like this below
For each ctrl in PnlTextBoxes.controls
If TypeOf ctrl is TextBox then
AddHandler ctrl.TextChanged, AddressOf CommonClickHandler
end if
Next
Private Sub CommonHandler(ByVal sender As System.Object, _
ByVal e As System.EventArgs)
MsgBox(ctype(sender,TextBox).Text)
End Sub
The best way would be to inherit from TextBox, override its OnTextChanged method to add your custom handling code, and then use that on your form(s) instead of the built-in TextBox control.
That way, all of the event handling code is in one single place and you increase abstraction. The behavior follows and is defined within the control class itself, not the form that contains the control. And of course, it frees you from having a bunch of ugly, hard-to-maintain Handles statements, or worse, slow and even uglier For loops.
For example, add this code defining a new custom text box control to a new file in your project:
Public Class CustomTextBox : Inherits TextBox
Protected Overridable Sub OnTextChanged(e As EventArgs)
' Do whatever you want to do here...
MsgBox(Me.Text)
' Call the base class implementation for default behavior.
' (If you don't call this, the TextChanged event will never be raised!)
MyBase.OnTextChanged(e)
End Sub
End Class
Then, after you recompile, you should be able to replace your existing TextBox controls with the newly-defined CustomTextBox control that has all of your behavior built in.
I have a Data grid with DatagridComboBoxColumn , and i want to Fire Event SelectionChanged when user Select any thing From the ComboBox , Do Some operations ,
how can i do that any advice ,
thanks
You can handle your DataGridView's EditingControlShowing event and cast the editing control to the ComboBox being displayed and then wire up its SelectionChangeCommitted event. Use the SelectionChangeCommitted handler do you what you need to do.
See the sample code in the MSDN article I linked for details.
Two important notes:
Despite the MSDN article's sample code it's best to use the
ComboBox SelectionChangeCommitted event, as discussed here and in the
comments of the linked MSDN article.
If you have more than one DatagridComboBoxColumn in your
DataGridView you might want to determine which fired either your
EditingControlShowing or the ComboBox's SelectionChangeCommitted
event. You can do this by checking your DGV
CurrentCell.ColumnIndex property value.
I reworked the MSDN sample code a bit to show what I mean:
Private Sub DataGridView1_EditingControlShowing(ByVal sender As System.Object, ByVal e As System.Windows.Forms.DataGridViewEditingControlShowingEventArgs) Handles DataGridView1.EditingControlShowing
' Only for a DatagridComboBoxColumn at ColumnIndex 1.
If DataGridView1.CurrentCell.ColumnIndex = 1 Then
Dim combo As ComboBox = CType(e.Control, ComboBox)
If (combo IsNot Nothing) Then
' Remove an existing event-handler, if present, to avoid
' adding multiple handlers when the editing control is reused.
RemoveHandler combo.SelectionChangeCommitted, New EventHandler(AddressOf ComboBox_SelectionChangeCommitted)
' Add the event handler.
AddHandler combo.SelectionChangeCommitted, New EventHandler(AddressOf ComboBox_SelectionChangeCommitted)
End If
End If
End Sub
Private Sub ComboBox_SelectionChangeCommitted(ByVal sender As System.Object, ByVal e As System.EventArgs)
Dim combo As ComboBox = CType(sender, ComboBox)
Console.WriteLine("Row: {0}, Value: {1}", DataGridView1.CurrentCell.RowIndex, combo.SelectedItem)
End Sub
So yes I'm very new to creating my own custom events. I can do the basics when I put controls on a form but this one is a little more complex. I have an application that reads in a .TSV and populates a form with controls based on the number of objects it "reads." So for instance: I have a file that contains 10 people objects and my code populates a form with controls for each person. Easy stuff!
Now lets say I have a ComboBox with the items: "Alive", "Deceased", "Unborn". Right next to this I have a textbox for age. Now originally, this textbox is not enabled because the default value for the ComboBox is "Unborn". But lets say when the user selects "Alive", I want that textbox to become enabled so an age can be entered.
Obviously from me asking this and the title of this question, I don't know how to go about doing this. I don't really understand events and I learn by example but the MSDN examples don't quite cut it.
Any help (especially an awesome Step-by-Step guide) would be greatly appreciated.
From what I gather from the comments, you want to add events to a form object that is created at runtime. Use the AddHandler command to the object. Something to the effect of:
AddHandler NameOfFormObject.TypeOfAction, AddressOf HowToHandle
Private Sub HowToHandle(ByVal sender as System.Object, ByVal e As System.EventArgs)
DropDownMenu.enabled = True
End Sub
Doing it this way, you will be able to modify the events of an object created at runtime. In your case, it sounds like you'll want to use the action that Josaph recommended, and end up incorporating both solutions offered, like so
AddHandler ComboBox1.SelectedIndexChanged, AddressOf HowToHandle
Private Sub HowToHandle(ByVal sender as System.Object, ByVal e As System.EventArgs)
If DirectCast(sender, ComboBox).SelectedIndex = 0 'Alive
DirectCast(DirectCast(sender, ComboBox).Tag, TextBox).enabled = True
Else
DirectCast(DirectCast(sender, ComboBox).Tag, TextBox).enabled = False
End If
End Sub
You will want to use the ComboBox_SelectedIndexChanged() event to capture that the combo box item has been changed. At that point, you will need to check to see which combo box item has been selected and make a decision as to whether the TextBox should be enabled or not. Here is an example. Note: This example assumes that "Alive" is the first item in your combobox at the 0 index.
Private Sub ComboBox1_SelectedIndexChanged(ByVal sender As System.Object, ByVal e As System.EventArgs)
If ComboBox1.SelectedIndex = 0 Then 'Alive
TextBox1.Enabled = True
Else
TextBox1.Enabled = False
End If
End Sub
Dynamically generate the combobox and add handler.
Dim cmb as New ComboBox
AddHandler cmb.SelectedIndexChanged, AddressOf ComboBox1_SelectedIndexChanged
Me.Controls.Add(cmb)
I suppose that as you will have 10 comboboxes... in the same way you'll have 10 textboxes.
In that case... once you attach and handle the event as AndyPerfect and Joseph... in that method you will need code something to know which of the Textboxs you need to enable/disable.
First you need to know which combobox triggered the event: this is done using the "sender" parameter. ctype(sender, Combobox) to access the methods and properties of the ComboBox.
Once you know which combo, you need to activate/deactivate the correct textbox.
To accomplish this, you'll need to add a reference to the TextBox in the "TAG" property of the Combobox at the moment of creating it.
Dim txt as new TextBox
Dim cmb as new ComboBox
cmb.Tag = txt
Then... you simple use:
ctype(ctype(sender, Combobox).Tag, TextBox).Enable = true
Here is how I ended up writing it in the end. I appreciate all the help! Thank you!
If DirectCast(sender, ComboBox).SelectedIndex = 2 Then
DirectCast(Me.Controls.Item(DirectCast(sender, ComboBox).Tag), TextBox).Enabled = True
Else
DirectCast(Me.Controls.Item(DirectCast(sender, ComboBox).Tag), TextBox).Enabled = False
End If