Unwanted Retriggering of Textbox Events - vb.net

This is one of those "seems obvious" as how to do, but came across interesting side effect in implementing. I'm trying to keep two text boxes syncronized when information is updated. In this example, I will be using txtStartDate and txtEndDate. If the txtStartDate is changed, then the txtEndDate should should be updated. Likewise, if the txtEndDate changes, I want the txtSartDate to be updated. The side effect I'm comming across is that when I set them up under the TextChanged event for both, the events seem to retrigger against each other indefinately (endless loop?). Am I using the wrong event? Is this a task for a delegate?

You need an extra condition. Depending on your setup, that could be "only change other value when I have focus".
A more general solution (I hope you can read C#):
private bool changingTextBox1 = false;
void textBox1TextChanged(object sender, EventArgs e)
{
if (! changingTextBox1)
{
changingTextBox1 = true;
try
{
// do stuff
}
finally
{
changingTextBox1 = false;
}
}
}

A very basic way of solving it would be that you create an UpdateInProgress boolean member variable in the Form and at the start of each of the event handlers you check if it's true and if so you just ignore the event, otherwise set it to true and then set it to false at the end of the event.

Dim bUpdating As Boolean = False
Private Sub txtStartDate_TextChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles txtStartDate.TextChanged
If Not bUpdating = True Then
bUpdating = True
Try
'do stuff
Catch ex As Exception
Finally bUpdating = False
End Try
End If
End Sub

Related

How do I prevent mouse wheel from incrementing numericupdown without over loading?

How do I prevent mouse wheel from incrementing numericupdown without over loading?
I had previously inherited numericupdwon to overload the MouseWheel event with an empty event. This worked for a while, but something happened when I switched to x64 that made the whole inherited class periodically show not found. Not sure because even if I switched back to x86 it was still a problem.
This worked for me..
Private Sub NumericUpDown1_MouseWheel(sender As Object, e As MouseEventArgs) Handles NumericUpDown1.MouseWheel
Dim MW As HandledMouseEventArgs = CType(e, HandledMouseEventArgs)
MW.Handled = True
End Sub
That HandledMouseEventArgs usage does look weird though.. but it works.
https://msdn.microsoft.com/en-us/library/system.windows.forms.handledmouseeventargs(v=vs.110).aspx
I came up with a different solution, using the following code to prevent the increment
Private Sub nup_cores_MouseWheel(sender As Object, e As MouseEventArgs) Handles nup_cores.MouseWheel
nup_cores.Increment = 0
End Sub
And then change it back. In the specific case of a numericupdown, the cursor blinking seems to trigger gotfocus. The same principle could be applied with a short duration timer
Private Sub nup_cores_GotFocus(sender As Object, e As EventArgs) Handles nup_cores.GotFocus
nup_cores.Increment = 1
End Sub
For C#:
For some reason the MouseWheel doesn't show up in the list of events from the GUI. So I had to programmatically add the event in my form load. In the MouseWheel event I do something similar as the above selected answer (but a little different).
private void Form1_Load(object sender, EventArgs e)
{
numericUpDownX.MouseWheel += new MouseEventHandler(handle_MouseWheel);
}
void handle_MouseWheel(object sender, MouseEventArgs e)
{
((HandledMouseEventArgs)e).Handled = true;
}

VB: How to manually call or delay events?

Background: I'm new to vb, coming from javascript.
My assignment is to use the txtBox.LostFocus event to do some validation. The problem is I need to cancel the validation step if the user intends to press either two of three buttons.
Illustration:
Code
Dim lblInputBox As Label
lblInputBox.Text = "Name:"
Dim txtInputBox As TextBox
Dim btnClear As Button
btnClear.Text = "Clear"
Dim btnSayName As Button
btnSayName.Text = "Say Name"
Dim btnExit As Button
btnExit.Text = "Exit"
' Some boolean to determine what the next action is
Dim UserIntentDetected = False
' When the user moves focus away from the textbox
Private Sub txtInputBox_LostFocus(sender As Object, e As EventArgs) _
Handles txtIputBox.LostFocus
' I want to be able to detect the next focus, but this is where I'm going wrong
If btnExit.GotFocus Or btnClear.GotFocus Then
UserIntentDetected = True
Else
UserIntentDetected = False
End If
' Only call validate if there is no intent to quit or clear
If Not UserIntentDetected Then
Call validate()
End If
' Reset the intent boolean
UserIntentDetected = False
End Sub
' Validate subroutine
Private Sub validate()
' **Fixed description**
' User moved focus away from txtbox and doesn't intend to clear or exit
Console.WriteLine("You're NOT INTENDING to clear or exit")
End Sub
I've tried to add the two button's GotFocus event to the input box's LostFocus event handler, but there were bugs with the event firing multiple times in a loop.
Eg:
Private Sub txtInputBox_LostFocus(sender As Object, e As EventArgs) _
Handles txtIputBox.LostFocus, btnExit.GotFocus, _
btnClear.GotFocus
... (code follows)
These attempts are entirely wrong, but from a javascript background, although also entirely wrong, I could hack something like this..
... (assume same element structure from vb)
var eventQueue = [];
var userIntentDetected = false;
txtInputBox.addEventListener("blur", function(event){
// Set a timeout to manually trigger the last event's click method in the eventQueue
setTimeout(function(){
if (eventQueue.length > 0 && userIntetDetected)
runIntendedHandler(eventQueue[eventQueue.length -1]);
}, 500);
})
// Both event listeners listen for click and stop default actions,
// set intent boolean to true, and add it's event object to a queue
btnExit.addEventListener("click", function(event){
event.preventDefault();
userIntentDetected = true;
eventQueue.push(event);
});
btn.addEventListener("click", function(event){
event.preventDefault();
userIntentDetected = true;
eventQueue.push(event);
});
// Validation should occur only if the user moves focus to an element
// that IS NOT either the btnExit or btnClear
function runIntendedHandler(event){
if (event.target.id = "btnExit")
// run exit functions
code...
else if (event.target.id = "btnClear")
// run clear functions
code..
userIntentDetected = false;
}
What is the proper way to work with events in vb and how would I go about detecting the next event in the queue before triggering an action? could the RaiseEvent statement help?
UPDATE 3: The answer was a lot easier than I made it seem. Apparently, you can use the btn.Focused property to check the next focus of an element from within the txtInputBox.LostFocus event handler... Go figure!
UPDATE 2: There's been a lot of confusion as to what exactly was needed, and a lot of that was my fault in describing the validation subroutine. I've changed some of the element names and added an image to sum up all of the information that was given to me by my instructor.
UPDATE 1: #TnTinMn has provided the closest working answer that can be used with a minor alteration.
Example follows:
Private LastActiveControl As Control = Me ' initialize on form creation
Protected Overrides Sub UpdateDefaultButton()
' Just added an IsNot condition to stay inline with the requirements
If (LastActiveControl Is txtNumberOfDrinks) AndAlso
((ActiveControl Is btnClear) OrElse (ActiveControl Is btnExit)) Then
Console.WriteLine("Your intent to press either btnClear or btnExit has been heard...")
' Validation happens only if the user didn't intend to click btnClear or btnExit
ElseIf (LastActiveControl Is txtNumberOfDrinks) AndAlso
((ActiveControl IsNot btnClear) OrElse (ActiveControl IsNot btnExit)) Then
Console.WriteLine("You didn't press either btnClear or btnExit.. moving to validation")
validateForm()
End If
LastActiveControl = ActiveControl ' Store this for the next time Focus changes
End Sub
Thank you all!
Winform's event order is confusing at best. For a summary, see Order of Events in Windows Forms.
Assuming I have interpreted your goal correctly, I would not respond to the txtInputBox.LostFocus event but rather override a little known Form method called UpdateDefaultButton. This method is called when the Form.ActiveControl property changes and effectively gives you an ActiveControl changed pseudo-event. If the newly ActiveControl is a Button, this method also executes before the Button.Click event is raised.
Since you want to call your validation code only when the focus changes from txtInputBox to either btnPromptForName or btnExit, you can accomplish that with something like this.
Public Class Form1
Private LastActiveControl As Control = Me ' initialize on form creation
Protected Overrides Sub UpdateDefaultButton()
If (LastActiveControl Is tbInputBox) AndAlso
((ActiveControl Is btnPromptForName) OrElse (ActiveControl Is btnExit)) Then
ValidateInput()
End If
LastActiveControl = ActiveControl ' Store this for the next time Focus changes
End Sub
Private Sub ValidateInput()
Console.WriteLine("You're either intending to prompt for name or exit")
End Sub
Private Sub btnPromptForName_Click(sender As Object, e As EventArgs) Handles btnPromptForName.Click
Console.WriteLine("btnPromptForName_Click clicked")
End Sub
Private Sub btnExit_Click(sender As Object, e As EventArgs) Handles btnExit.Click
Console.WriteLine("btnExit clicked")
End Sub
End Class
Edit: I forgot to mention, that since UpdateDefaultButton runs before the Button.Click event is raised, it is possible wire-up the click event handler only if validation succeeds. You would then remove the event handler as part of the Button.Click handler code.
It looks like the solution was a lot easier than I thought. The intended (instructor's) way to do this is by checking for alternate "btn.Focused" properties within the txtInputBox.LostFocus event handler eg:
' When the user moves focus away from the textbox
Private Sub txtInputBox_LostFocus(sender As Object, e As EventArgs) _
Handles txtIputBox.LostFocus
' Check if the clear or exit button is focused before validating
' Validate only if btnClear and also btnExit IS NOT focused
If Not btnExit.Focused AndAlso Not btnClear.Focused Then
Call validateForm()
End If
End Sub
Thank you #TnTinMn for the UpdateDefaultButton and showing me an alternate way to track events. This was very helpful too.

How to prevent datagridview from selecting the first cell as currentcell on opening

I'm trying to find a way to prevent my datagridview from selecting the first cell as default cell. Right now I have code that turns the backcolor of the cells in my datagridview to red if negative numbers are in the cells on import. However this won't work properly in my first cell since its already highlighted by default on import. If anyone can find out how to turn the selecting of the cell off I would greatly appreciate it! :)
I know it must be something simple like DataGridView1.CurrentCell.Selected = False
Handle the DataBindingComplete event of the DataGridView as so:
private void myGrid_DataBindingComplete(object sender, DataGridViewBindingCompleteEventArgs e)
{
myGrid.ClearSelection();
}
Try this, works for me. Put this code anywhere within your form code with the datagrid in it.
Private Sub YourDataGridName_DataBindingComplete(ByVal sender As System.Object, _
ByVal e As System.Windows.Forms.DataGridViewBindingCompleteEventArgs) _
Handles YourDataGridName.DataBindingComplete
Dim DGV As DataGridView
DGV = CType(sender, DataGridView)
DGV.ClearSelection()
End Sub
(source)
try
datagridview.currentrow.selected = true
this would make the whole row selected so the code that change the curent back color wont be effected.
I had a code for creating focus but i forget it. to set the selection of the grid you need to cahnge the direction
This one works (and with differents selectionmode gridviews)
Public Function Grdvw_Cell_Unselect(ByVal Grdvw As DataGridView) As Boolean
' cancel all defaut selection in a datagridview
Grdvw_Cell_Unselect = False
Try
Grdvw.ClearSelection()
Grdvw.Item(0, 0).Selected = False
Grdvw_Cell_Unselect = True
Catch ex As Exception
End Try
End Function
then use as this:
Grdvw_Cell_Unselect(your_datagridview) ...
What worked for me was to clear the selection and then set the row based on the row index. Hope this helps.
GridView.ClearSelection()
GridView.Rows(RowIndex).Selected = True
GridView.DataSource = DataTable
GridView.Refresh()
Bind event for Paint for datagridview.
Click datagridview > Properties > Paint > double click on space near Paint > It will create method similar to this: datagridview1_Paint(object sender, PaintEventArgs e)
Write these two lines in that method as displayed:enter code here
private void datagridview1_Paint(object sender, PaintEventArgs e) {
this.datagridview1.ClearSelection();
this.datagridview1.CurrentCell = null;
}

How can I sort a WinForms DataGridView on a CheckBox column?

So I had a DataGridView with autogenerated columns, some of which were checkbox columns. When I clicked on the check box column's header, it didn't sort. I researched it and it turns out that Microsoft didn't include automatic sorting for checkbox columns... Which I think is absurd--how hard is it to sort checked / not checked?
How can you get a DataGridView to sort check box columns for you?
Here's what I came up with:
You could also simply do this:
DataGridView.Columns("ColumnOfChoice").SortMode = DataGridViewColumnSortMode.Automatic
Works in vb.net 4.0
You only need to add next lines to the code of the form (tested in VB.NET 2013)
Private Sub DataGridView1_ColumnAdded(sender As Object, e As System.Windows.Forms.DataGridViewColumnEventArgs) Handles DataGridView1.ColumnAdded
If e.Column.GetType Is GetType(DataGridViewCheckBoxColumn) Then
e.Column.SortMode = DataGridViewColumnSortMode.Automatic
End If
End Sub
First you need to hook into two events, the column added event and the column header click event:
AddHandler dg.ColumnAdded, AddressOf dgColumnAdded
AddHandler dg.ColumnHeaderMouseClick, AddressOf dgSortColumns
Then, enable programmatic sorting for each check box column:
Private Sub dgColumnAdded(ByVal sender As Object, _
ByVal e As System.Windows.Forms.DataGridViewColumnEventArgs)
If e.Column.GetType Is GetType(DataGridViewCheckBoxColumn) Then
e.Column.SortMode = DataGridViewColumnSortMode.Programmatic
End If
End Sub
Then, create a handler that will sort a checkbox column, but do nothing for columns that will handle their own sorting:
Private Sub dgSortColumns(ByVal sender As Object, _
ByVal e As System.Windows.Forms.DataGridViewCellMouseEventArgs)
Dim dg As DataGridView = sender
Dim c As DataGridViewColumn = dg.Columns(e.ColumnIndex)
If c.SortMode = DataGridViewColumnSortMode.Programmatic Then
If dg.SortedColumn IsNot Nothing _
AndAlso dg.SortedColumn.Name <> c.Name Then
dg.Sort(c, System.ComponentModel.ListSortDirection.Ascending)
Else
Select Case dg.SortOrder
Case Windows.Forms.SortOrder.None
dg.Sort(c, System.ComponentModel.ListSortDirection.Ascending)
Case Windows.Forms.SortOrder.Ascending
dg.Sort(c, System.ComponentModel.ListSortDirection.Descending)
Case Windows.Forms.SortOrder.Descending
dg.Sort(c, System.ComponentModel.ListSortDirection.Ascending)
End Select
End If
End If
End Sub
And there you go! Now was it really that hard, Microsoft? ;-)
I'm not sure about VB, but for C# in VS2012 in the designer you can also set the SortMode.
Right-click on the DataGridView and go to "Edit Columns".
There's a drop-down for SortMode with a choice of NotSortable, Automatic, and Programmatic.
It appears that the default for most columns is Automatic, but for checkboxes (boolean) columns the default is NotSortable.
I have created an extension method that you can reuse, you just need to use it during the form load event.
------ Be sure that your DataSource is sortable. ------
If you are binding the DataGridView to a simple List it WONT WORK, you need to use something else, I recommend you to use this SortableBindingList; You can pass directly your original List IEnumerable to the SortableBindingList's constructor.
Load:
private void frmWithTheDataGrid_Load(object sender, EventArgs e)
{
this.yourDataGridView.SortabilizeMe();
}
Then add this into a static class to use it as an ExtensionMethod..
public static void SortabilizeMe(this DataGridView dgv)
{
dgv.ColumnAdded+= delegate(object sender, DataGridViewColumnEventArgs args)
{
args.Column.SortMode = DataGridViewColumnSortMode.Programmatic;
};
dgv.ColumnHeaderMouseClick += delegate(object sender, DataGridViewCellMouseEventArgs args)
{
var col = dgv.Columns[args.ColumnIndex];
if (dgv.SortedColumn != null && dgv.SortedColumn.Name != col.Name)
{
dgv.Sort(row, ListSortDirection.Ascending);
}
else
{
switch (dgv.SortOrder)
{
case SortOrder.None:
dgv.Sort(col, ListSortDirection.Ascending);
break;
case SortOrder.Ascending:
dgv.Sort(col, ListSortDirection.Descending);
break;
case SortOrder.Descending:
dgv.Sort(col, ListSortDirection.Ascending);
break;
}
}
};
}
Then magic will happen :)

How to require CheckedListBox to have at least one item selected

With the CheckListBox in VB.NET in VS2005, how would you make it compulsory that at least one item is selected?
Can you select one of the items at design time to make it the default?
What would be best way to handle the validation part of this scenario? When should the user be required to tick one of the boxes?
Either idea -- preventing the user from unchecking the last checked item, or validating that at least one item is checked before proceeding -- is fairly straightforward to implement.
How to prevent the user from unchecking the last checked item
1. Make sure at least one item is checked to begin with (e.g., in your form's Load event):
Private Sub frm_Load(ByVal sender As Object, ByVal e As EventArgs)
clb.SetItemChecked(0, True) ' whatever index you want as your default '
End Sub
2. Add some simple logic to your ItemCheck event handler:
Private Sub clb_ItemCheck(ByVal sender As Object, ByVal e As ItemCheckEventArgs)
If clb.CheckedItems.Count = 1 Then ' only one item is still checked... '
If e.CurrentValue = CheckState.Checked Then ' ...and this is it... '
e.NewValue = CheckState.Checked ' ...so keep it checked '
End If
End If
End Sub
How to validate that at least one item is checked
Private Sub btn_Click(ByVal sender As Object, ByVal e As EventArgs)
' you could also put the below in its own method '
If clb.CheckedItems.Count < 1 Then
MsgBox("You must check at least one item.")
Return
End If
' do whatever you need to do '
End Sub
You can, but the user can always uncheck it.
The way I would do this would be on submit, loop through the checkbox items and make sure at least one of them was checked (break the loop after as your requirement is met, no need to process the rest of the list.)
If the check fails, make a custom validator visible. Required field validators don't work with Check List Boxes, but you can see how they implemented here:
http://www.codeproject.com/KB/webforms/Validator_CheckBoxList.aspx
This solution should be close. Not 100%, there is no easy way to find out that items were added to an empty list. Add a new class to your project and paste the code shown below. Compile. Drop the new control from the top of your toolbox onto your form. No additional code is needed.
Public Class MyCheckedListBox
Inherits CheckedListBox
Protected Overrides Sub OnEnter(ByVal e As System.EventArgs)
REM Ensure at least one item is checked
MyBase.OnEnter(e)
If Me.CheckedIndices.Count = 0 AndAlso Me.Items.Count > 0 Then
Me.SetItemChecked(0, True)
End If
End Sub
Protected Overrides Sub OnItemCheck(ByVal e As ItemCheckEventArgs)
REM Prevent unchecking last item
If Me.CheckedIndices.Count <= 1 AndAlso e.NewValue = CheckState.Unchecked Then e.NewValue = CheckState.Checked
MyBase.OnItemCheck(e)
End Sub
End Class
Optional additional override that ensures an item is checked when the form first shows up:
Protected Overrides Sub OnHandleCreated(ByVal e As System.EventArgs)
MyBase.OnHandleCreated(e)
If Me.CheckedIndices.Count = 0 AndAlso Me.Items.Count > 0 Then
Me.SetItemChecked(0, True)
End If
End Sub
Trap ItemCheck event and verify if last checkbox is unchecked:
private void Form1_Load(object sender, EventArgs e)
{
checkedListBox1.SetItemChecked(0, true);
checkedListBox1.ItemCheck += checkedListBox1_ItemCheck;
}
private void checkedListBox1_ItemCheck(object sender, ItemCheckEventArgs e)
{
if (CountChecked() == 1 &&
e.NewValue == CheckState.Unchecked &&
e.CurrentValue == CheckState.Checked)
{
checkedListBox1.SetItemChecked(0, true);
}
}
private int CountChecked()
{
int count = 0;
for (int i = 0; i < checkedListBox1.Items.Count; i++)
{
if (checkedListBox1.GetItemChecked(i) == true)
count++;
}
return count;
}
Updated: Then you have to make async call to set item check state back.
private delegate void SetItemCallback(int index);
private void checkedListBox1_ItemCheck(object sender, ItemCheckEventArgs e)
{
if (checkedListBox1.CheckedIndices.Count == 1 &&
e.NewValue == CheckState.Unchecked &&
e.CurrentValue == CheckState.Checked)
{
int index = checkedListBox1.CheckedIndices[0];
// Initiate the asynchronous call.
SetItemCallback d = new SetItemCallback(this.SetItem);
d.BeginInvoke(index, null, null);
}
}
private void SetItem(int index)
{
if (this.checkedListBox1.InvokeRequired)
{
SetItemCallback d = new SetItemCallback(SetItem);
this.Invoke(d, new object[] { index });
}
else
{
checkedListBox1.SetItemChecked(index, true);
}
}
Validation Scenario Suggestion :
If your winform has an Submit/Save button, I would like the application to show an error when the button is cliked (provided there is a label saying that there should be atleast one item selected). The error message need not be a MessageBox, it could a red label showing the error description in the form itself
If there are no buttons you can hold on to, then do the validation as when users click on the items. Using ItemCheck event, check if there is atleast one item checked. If not, show the error label.
Code :
private void checkedListBox1_ItemCheck(object sender, ItemCheckEventArgs e)
{
if (checkedListBox1.CheckedIndices.Count == 1 &&
e.NewValue == CheckState.Unchecked &&
e.CurrentValue == CheckState.Checked)
{
//Show error label in red
}
}
I would use the ItemCheck event to set the Button.Enabled = false when there is not at least one item checked, and Button.Enabled = true when there is at least one item checked (assuming an Ok button or something alike).