How to set tooltips on ListView Subitems in .Net - vb.net

I am trying to set the tool tip text for some of my subitems in my listview control. I am unable to get the tool tip to show up.
Anyone have any suggestions?
Private _timer As Timer
Private Sub Timer()
If _timer Is Nothing Then
_timer = New Timer
_timer.Interval = 500
AddHandler _timer.Tick, AddressOf TimerTick
_timer.Start()
End If
End Sub
Private Sub TimerTick(ByVal sender As Object, ByVal e As EventArgs)
_timer.Enabled = False
End Sub
Protected Overrides Sub OnMouseMove(ByVal e As System.Windows.Forms.MouseEventArgs)
If Not _timer.Enabled Then
Dim item = Me.HitTest(e.X, e.Y)
If Not item Is Nothing AndAlso Not item.SubItem Is Nothing Then
If item.SubItem.Text = "" Then
Dim tip = New ToolTip
Dim p = item.SubItem.Bounds
tip.ToolTipTitle = "Status"
tip.ShowAlways = True
tip.Show("FOO", Me, e.X, e.Y, 1000)
_timer.Enabled = True
End If
End If
End If
MyBase.OnMouseMove(e)
End Sub

You can use the MouseMove event:
private void listview1_MouseMove(object sender, MouseEventargs e)
{
ListViewItem item = listview1.GetItemAt(e.X, e.Y);
ListViewHitTestInfo info = listview1.HitTest(e.X, e.Y);
if((item != null) && (info.SubItem != null))
{
toolTip1.SetToolTip(listview1, info.SubItem.Text);
}
else
{
toolTip1.SetToolTip(listview1, "");
}
}

Assuming .NET 2.0 or later, you can also set ListView.ShowItemToolTips to true. If you need to customize the tooltip text for a given item, set ListViewItem.ToolTipText to the string you want displayed.

ObjectListView (an open source wrapper around .NET WinForms ListView) has builtin support for cell tooltips (and, yes, it does work with VB). You listen for a CellToolTip event and you can do things like this (which is admittedly excessive):
If you don't want to use ObjectListView, you need to subclass ListView, listen for WM_NOTIFY messages, and then within those, respond to TTN_GETDISPINFO notifications, in a manner similar to this:
case TTN_GETDISPINFO:
ListViewHitTestInfo info = this.HitTest(this.PointToClient(Cursor.Position));
if (info.Item != null && info.SubItem != null) {
// Call some method of your own to get the tooltip you want
String tip = this.GetCellToolTip(info.Item, info.SubItem);
if (!String.IsNullOrEmpty(tip)) {
NativeMethods.TOOLTIPTEXT ttt = (NativeMethods.TOOLTIPTEXT)m.GetLParam(typeof(NativeMethods.TOOLTIPTEXT));
ttt.lpszText = tip;
if (this.RightToLeft == RightToLeft.Yes)
ttt.uFlags |= 4;
Marshal.StructureToPtr(ttt, m.LParam, false);
return; // do not do normal processing
}
}
break;
Obviously, this is C#, not VB, but you get the idea.

The original code in the question does not work since it creates a New ToolTip inside OnMouseMove. I guess that the ToolTip.Show method is asynchronous and thus the function exits immediately after invoking it, destroying the temporary ToolTip. When Show gets to execute, the object does not exist anymore.
The solution would be to create a persistent ToolTip object, by:
a ToolTip control on the form; or
a private ToolTip class field (disposed in the Finalize or Dispose method of the class); or
a Static object inside the function.
Also, there is no need to GetItemAt() since ListViewHitTestInfo already contains both the item and subitem references.
Improving Colin's answer, here is my code:
Private Sub ListView_MouseMove(sender As Object, e As MouseEventArgs) _
Handles MyList1.MouseMove
Static prevMousePos As Point = New Point(-1, -1)
Dim lv As ListView = TryCast(sender, ListView)
If lv Is Nothing Then _
Exit Sub
If prevMousePos = MousePosition Then _
Exit Sub ' to avoid annoying flickering
With lv.HitTest(lv.PointToClient(MousePosition))
If .SubItem IsNot Nothing AndAlso Not String.IsNullOrEmpty(.SubItem.Text) Then
'AndAlso .Item.SubItems.IndexOf(.SubItem) = 1
'...when a specific Column is needed
Static t As ToolTip = toolTip1 ' using a form's control
'Static t As New ToolTip() ' using a private variable
t.ShowAlways = True
t.UseFading = True
' To display at exact mouse position:
t.Show(.SubItem.Tag, .Item.ListView, _
.Item.ListView.PointToClient(MousePosition), 2000)
' To display beneath the list subitem:
t.Show(.SubItem.Tag, .Item.ListView, _
.SubItem.Bounds.Location + New Size(7, .SubItem.Bounds.Height + 1), 2000)
' To display beneath mouse cursor, as Windows does:
' (size is hardcoded in ugly manner because there is no easy way to find it)
t.Show(.SubItem.Tag, .Item.ListView, _
.Item.ListView.PointToClient(Cursor.Position + New Size(1, 20)), 2000)
End If
prevMousePos = MousePosition
End With
End Sub
I've made the code as general as possible so that the function could be assigned to multiple ListViews.

If you set ShowItemTooltips for the ListView control in "details" mode and do nothing else, the ListView control will automatically provide tooltips for items and subitems that exceed their column's widths. This turns out to work even if the FullRowSelect property is set to true. If ToolTipText has been set for a ListViewItem and FullRowSelect is true, then the tooltip will appear for the whole row; that's the case where tooltips won't be displayed for subitems.

Related

TabPage selection, move the Focus to the previous ActiveControl when a TabPage is reselected

I need some help to focus a particular control when a TabPage is revisited. I followed many other blogs, but I wasn't able to solve the problem myself.
I created the TabPages inside a MDIForm:
Public Sub Tab_Open(Of T As {Form, New})(name As String, NameofTab As String, Tabnumber As String)
Dim _formByName As New Dictionary(Of String, Form)
Dim Frm As Form = Nothing
If Not _formByName.TryGetValue(name, Frm) OrElse _formByName(name).IsDisposed Then
Frm = New T()
_formByName(name) = Frm
End If
Dim childTab As TabPage = New TabPage With {
.Name = NameofTab & " : " & Tabnumber,
.Text = NameofTab & " : " & Tabnumber,
.Tag = Frm.Name
}
Form1.tabForms.TabPages.Add(childTab)
Frm.TopLevel = False
Frm.FormBorderStyle = FormBorderStyle.None
Frm.Parent = Form1.tabForms.TabPages(Form1.tabForms.TabCount - 1)
Frm.Dock = DockStyle.Fill
Frm.Show()
Form1.tabForms.SelectedTab = childTab
Form1.tabForms.Visible = True
End Sub
Let's assume that in first TabPage the Focus was on a TextBox (with TabIndex = 4), now I may be click on the second TabPage.
After some calculations, when I select the previous TabPage, the Focus should be set to the TextBox with TabIndex = 4 again, but that's not happening.
I tried to create a Dictionary in the MDIForm as:
Public Tab_Last_Focus_info As New Dictionary(Of String, String())
and in SelectedIndexChanged I have this code:
Private Sub tabForms_SelectedIndexChanged(sender As Object, e As EventArgs) Handles tabForms.SelectedIndexChanged
If Tab_Last_Focus_info.ContainsKey(tabForms.SelectedTab.Name) Then
Dim FullTypeName1 As String = String.Format("{0}", Tab_Last_Focus_info.Item(tabForms.SelectedTab.Name))
Dim Indxval As String = String.Format("{1}", Tab_Last_Focus_info.Item(tabForms.SelectedTab.Name))
Dim FullTypeName As String = Application.ProductName & "." & FullTypeName1
Dim FormInstanceType As Type = Type.GetType(FullTypeName, True, True)
Dim frm As Form = CType(Activator.CreateInstance(FormInstanceType), Form)
Dim Focus_on As Integer = Integer.Parse(Indxval)
frm.Controls(Focus_on).Focus()
' Not working too =>
' frm.Controls(Focus_on).Select()
' Invisible or disabled control cannot be activated =>
' ActiveControl = frm.Controls(Focus_on) 'System.ArgumentException:
End If
End Sub
In the Form, which is opened via a Menu, I have this code for the Control that's focused:
Private Sub All_Got_Focus(sender As Object, e As EventArgs) Handles TB_ImageLoc.GotFocus, TB_CompWebsite.GotFocus,
TB_CompPinCD.GotFocus, TB_CompPAN.GotFocus, TB_CompName.GotFocus, TB_CompMobile.GotFocus,
TB_CompMD.GotFocus, TB_CompLL.GotFocus, TB_CompGSTIN.GotFocus, TB_CompFax.GotFocus, TB_CompEmail.GotFocus,
TB_CompCD.GotFocus, TB_CompAreaCity.GotFocus, RTB_CompADD.GotFocus, PB_Logo.GotFocus, DTP_CompEst.GotFocus, DGV_CompList.GotFocus,
CHKB_CompIsRegTrans.GotFocus, CB_CompStateID.GotFocus, CB_CompDistrictID.GotFocus, But_Upd.GotFocus, But_SelectLogo.GotFocus,
But_Search.GotFocus, But_Reset.GotFocus, But_Refresh.GotFocus, But_GridSelect.GotFocus, But_Exit.GotFocus, But_Edit.GotFocus,
But_Del.GotFocus, But_Add.GotFocus
If Form1.Tab_Last_Focus_info.ContainsKey(Form1.tabForms.SelectedTab.Name) Then
Form1.Tab_Last_Focus_info.Remove(Form1.tabForms.SelectedTab.Name)
End If
Form1.Tab_Last_Focus_info.Add(Form1.tabForms.SelectedTab.Name, New String() {Me.Name, Me.ActiveControl.TabIndex})
End Sub
Now in TabIndexChange I'm getting a correct value from the Dictionary, but I'm not able to focus on the required tab.
Kindly help and let me know what I am missing or what need to taken care for this issue or please let me know any other better idea for the same.
First thing, a suggestion: test this code in a clean Project, where you have a MDIParent and one Form with a TabControl with 2 o more TabPages, containing different types of Controls. Test the functionality, then apply to the Project that is meant to use it.
You need to keep track of the selected Control in a TabPage - the current ActiveControl - switch to other TabPages, restore the previous ActiveControl in a TabPage when it's brought to front again.
The procedure is simple, implemented as follows:
To keep track of the current ActiveControl - the Control that has the Focus, you need to know when a Control becomes the ActiveControl. This Control of course must be child of a TabPage.
The ContainerControl class (the class from which Form derives) has a protected virtual method, UpdateDefaultButton(), that's overridden in the Form class. It's used to determine which child Button is activated when a User presses the Enter Key.
This method is called each time a new Control becomes the ActiveControl: overriding it, we can be informed when this happens, so we can check whether the new ActiveControl is one we're interested in, because it's child of a TabPage of our TabControl.
When the new ActiveControl is one we need to keep track of, we can store the reference of this Control and the Index of the TabPage it belongs to in a collection, so we can then use this reference, when the selected TabBage changes, to set it again as the ActiveControl in its TabPage.
Here, to store the state, I'm using a Dictionary(Of Integer, Control), where the Key is the Index of the TabPage and the Value is the reference of its ActiveControl.
When the TabControl.Selected event is raised - after a TabPage has been selected - we can lookup the Dictionary and restore the previous ActiveControl of that TabPage if one was stored.
► Here, BeginInvoke() is used to defer the action of setting the new ActiveControl, because this also causes a call to UpdateDefaultButton() and this method is called before the TabControl.Selected event handler completes.
Public Class SomeMdiChildForm
Private tabPagesActiveControl As New Dictionary(Of Integer, Control)()
' This method is called each time a Control becomes the ActiveControl
Protected Overrides Sub UpdateDefaultButton()
MyBase.UpdateDefaultButton()
If TypeOf ActiveControl.Parent Is TabPage Then
Dim tabPageIdx = CType(CType(ActiveControl.Parent, TabPage).Parent, TabControl).SelectedIndex
If tabPagesActiveControl.Count > 0 AndAlso tabPagesActiveControl.ContainsKey(tabPageIdx) Then
tabPagesActiveControl(tabPageIdx) = ActiveControl
Else
tabPagesActiveControl.Add(tabPageIdx, ActiveControl)
End If
End If
End Sub
Private Sub TabControl1_Selected(sender As Object, e As TabControlEventArgs) Handles TabControl1.Selected
Dim ctrl As Control = Nothing
If tabPagesActiveControl.TryGetValue(e.TabPageIndex, ctrl) Then
BeginInvoke(New Action(Sub() Me.ActiveControl = ctrl))
End If
End Sub
End Class
C# Version:
(assume tabControl1 is the name of the TabControl instance)
public partial class SomeForm : Form
{
private Dictionary<int, Control> tabPagesActiveControl = new Dictionary<int, Control>();
// [...]
// This method is called each time a Control becomes the ActiveControl
protected override void UpdateDefaultButton()
{
base.UpdateDefaultButton();
if (ActiveControl.Parent is TabPage tp) {
var tabPageIdx = (tp.Parent as TabControl).SelectedIndex;
if (tabPagesActiveControl.Count > 0 && tabPagesActiveControl.ContainsKey(tabPageIdx)) {
tabPagesActiveControl[tabPageIdx] = ActiveControl;
}
else {
tabPagesActiveControl.Add(tabPageIdx, ActiveControl);
}
}
}
private void tabControl1_Selected(object sender, TabControlEventArgs e)
{
if (tabPagesActiveControl.TryGetValue(e.TabPageIndex, out Control ctrl)) {
BeginInvoke(new Action(() => ActiveControl = ctrl));
}
}
}
As mentioned previously Tab_Open sub is used to create a form as tab.
In Main form (MDI) created Dictionary as
Public tabPagesActiveControl As New Dictionary(Of String, Integer)
In each form when the control is focused the value has been added to dictionary as
Private Sub DateTimePicker1_Leave(sender As Object, e As EventArgs) Handles RadioButton1.GotFocus,
DateTimePicker1.GotFocus, ComboBox1.GotFocus, CheckBox1.GotFocus, Button1.GotFocus, TextBox3.GotFocus, TextBox4.GotFocus, RichTextBox1.GotFocus
If Form1.tabPagesActiveControl.ContainsKey(Form1.TabControl1.SelectedTab.Name) Then
Form1.tabPagesActiveControl(Form1.TabControl1.SelectedTab.Name) = Me.ActiveControl.TabIndex
Else
Form1.tabPagesActiveControl.Add(Form1.TabControl1.SelectedTab.Name, Me.ActiveControl.TabIndex)
End If
End Sub
And when the tab is focused:
Private Sub TabControl1_SelectedIndexChanged(sender As Object, e As EventArgs) Handles TabControl1.SelectedIndexChanged
If tabPagesActiveControl.ContainsKey(Me.TabControl1.SelectedTab.Name) Then
Dim Indxval As String = String.Format(tabPagesActiveControl.Item(Me.TabControl1.SelectedTab.Name))
SendKeys.Send("{TAB " & Indxval & "}")
End If
End Sub
As mentioned in the comments it has flaws. Kindly please check and help or do let me know what can be tried.
Finally I solved the issue after struggling for 8 Days :)
As I mentioned earlier I Open the forms as tabs using the Sub Tab_Open mentioned in the question.
Defined or created a new dictionary in MDI form as
Public tabPagesActiveControl As New Dictionary(Of String, Control)
and defined a control variable as
Dim Sel_Control As Control
Now in each form when the control is focused I have the below code to assign the current control alone to the dictionary:
Private Sub All_Focus(sender As Object, e As EventArgs) Handles TBox_Reg_website.GotFocus,
TBox_Reg_To.GotFocus, TBox_Reg_State.GotFocus, TBox_Reg_PinCD.GotFocus, TBox_Reg_PAN.GotFocus, TBox_Reg_office_num.GotFocus,
TBox_Reg_mobile_num.GotFocus, TBox_Reg_GSTIN.GotFocus, TBox_Reg_fax_no.GotFocus, TBox_Reg_email.GotFocus, TBox_Reg_country.GotFocus,
TBox_Reg_Company.GotFocus, TBox_Reg_City.GotFocus, TBox_Reg_Add2.GotFocus, TBox_Reg_Add1.GotFocus, TB_Curr_website.GotFocus,
TB_Curr_state.GotFocus, TB_Curr_RegTo.GotFocus, TB_Curr_Pincd.GotFocus, TB_Curr_Pan.GotFocus, TB_Curr_office_num.GotFocus,
TB_Curr_Mobile_num.GotFocus, TB_Curr_Gstin.GotFocus, TB_Curr_fax_no.GotFocus, TB_Curr_email.GotFocus, TB_Curr_country.GotFocus,
TB_Curr_Company.GotFocus, TB_Curr_city.GotFocus, TB_Curr_add2.GotFocus, TB_Curr_add1.GotFocus,
PICBox_Reg_Logo.GotFocus, MSP_Reg.GotFocus, Label9.GotFocus, Label8.GotFocus, Label7.GotFocus, Label6.GotFocus, Label5.GotFocus,
Label4.GotFocus, Label3.GotFocus, Label2.GotFocus, Label15.GotFocus, Label14.GotFocus, Label13.GotFocus, Label12.GotFocus,
Label11.GotFocus, Label10.GotFocus, Label1.GotFocus,
ChkBx_Upd_Logo.GotFocus, Chkbox_NoLogo.GotFocus
If Form1.tabPagesActiveControl.ContainsKey(Form1.TabControl1.SelectedTab.Name) Then
Form1.tabPagesActiveControl.Remove(Form1.TabControl1.SelectedTab.Name)
End If
Form1.tabPagesActiveControl.Add(Form1.TabControl1.SelectedTab.Name, Me.ActiveControl)
End Sub
and in the MDI form when tab select index changes having the below code:
Private Sub TabControl1_SelectedIndexChanged(sender As Object, e As EventArgs) Handles TabControl1.SelectedIndexChanged
If tabPagesActiveControl.ContainsKey(Me.TabControl1.SelectedTab.Name) Then
Sel_Control = tabPagesActiveControl.Item(Me.TabControl1.SelectedTab.Name)
Sel_Control.Focus()
End If
End Sub
Thanks :)

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.

Dynamically assign reference of a control

I have a desktop application where I have many textboxes inside a tab control and four tabs, as each of the tabs is exact copy.
Currently I'm assigning line by line controls to corresponding control array so I can easily access control of active tab. Each tab represents a shopping basket. And I have four tabs for now. lvBasket1 is on the fist tab, lvBasket2 is on the second etc...
Part of my code:
Private Sub InitControlsArrayAndEventHandlers()
lvBasket(0) = lvBasket1
lvBasket(1) = lvBasket2
lvBasket(2) = lvBasket3
lvBasket(3) = lvBasket4
btnSave(0) = btnSave1
btnSave(1) = btnSave2
btnSave(2) = btnSave3
btnSave(3) = btnSave4
For i As Integer = 0 To 3
AddHandler lvBasket(i).MouseDoubleClick, AddressOf lvBasket_MouseDoubleClick
AddHandler btnSave(i).Click, AddressOf btnSave_Click
Next
End Sub
Question is; Is it possible somehow to assign control reference to its array, inside the for loop. Like eval in javascript:
lvBasket(i) = Eval("lvBasket" & i)
btnSave(i) = Eval("btnSave" & i)
It looks like you already have 3 references to the controls:
The one in the controls collection
the one in the array
Possibly the lvBasketN variables shown
Since it is pretty easy to get a control from the controls collection, you really do not need a separate collection of them. To hook up a set of newly added controls to event handlers (given the name, I am assuming Listviews):
For Each lv As ListView In TabPage8.Controls.OfType(Of ListView)()
AddHandler lv.MouseDoubleClick, AddressOf lv_MouseDoubleClick
Next
And I have four tabs for now. lvBasket1 is on the fist tab, lvBasket2 is on the second etc... [an edit not in the original post]
To keep track of controls scattered across different controls collections, use a List(of T) and just add them when they are created. If you hook up event handlers as part of creating the control, you dont need a loop at all.
Private baskets As New List(of Listview)
...
Dim lv As New ListView ' e.g lvbasjket1
lv.Name = "ziggy"
... many props
AddHandler lv.MouseDoubleClick, AddressOf lvBasket_MouseDoubleClick
baskets.Add(lv) ' add to secondary collection
BasketTab1.Controls.Add(lv) ' add to controls collection
lv = New ListView ' ie lvBasket2
...
baskets.Add(lv)
BasketTab2.Controls.Add(lv)
The handlers were added as the control was created, so there is no need for any loop, though you could create them in a loop and add them to the list.
Lists are easier to work with than arrays, but baskets(0) will refer to the first one created, baskets(1) to the second etc. You can do the same thing for buttons, textboxes etc, but these are all still grouped together in each TabPage's control collection making it easy to get them without creating additional references:
' do something to basket one on tabpage 1
Dim n = 1
Dim lv = TabControl2.TabPages(n - 1).Controls().OfType(Of ListView)().FirstOrDefault()
If lv IsNot Nothing Then
' do something wonderful
End If
Yes, use Me.Controls.Find as follows:
Option Strict On
Public Class Form1
Sub New()
' This call is required by the designer.
InitializeComponent()
' Add any initialization after the InitializeComponent() call.
Call InitControlsArrayAndEventHandlers()
End Sub
Private lvBasket(-1) As ListView
Private btnSave(-1) As Button
Private Sub InitControlsArrayAndEventHandlers()
Dim i As Integer = 1
Do
Dim ctl() As Control = Me.Controls.Find("lvBasket" & i.ToString, True)
If ctl.GetUpperBound(0) = -1 Then Exit Do 'finished
ReDim Preserve lvBasket(i - 1)
lvBasket(i - 1) = DirectCast(ctl(0), ListView)
ctl = Me.Controls.Find("btnSave" & i.ToString, True)
ReDim Preserve btnSave(i - 1)
btnSave(i - 1) = DirectCast(ctl(0), Button)
i += 1
Loop
For i = 0 To lvBasket.GetUpperBound(0)
AddHandler lvBasket(i).MouseDoubleClick, AddressOf lvBasket_MouseDoubleClick
AddHandler btnSave(i).Click, AddressOf btnSave_Click
Next
End Sub
Private Sub btnSave_Click(sender As Object, e As EventArgs)
MsgBox(DirectCast(sender, Button).Name)
End Sub
Private Sub lvBasket_MouseDoubleClick(sender As Object, e As MouseEventArgs)
MsgBox(DirectCast(sender, ListView).Name)
End Sub
End Class
Here is how I've implemented a solution. My actual controls are named without underscore, like lvBasket1.
Imports System.Linq.Expressions
Private _txtNameSurname(BasketCount), _txtPhone(BasketCount) As TextBox
Private _btnSelectCustomer(BasketCount), _btnAddProduct(BasketCount) As Button
Private _lblInfo(BasketCount) As Label
Private _lvBasket(BasketCount) As ListView
Private Sub AssignControl(Of ControlType)(index As Integer, controlArrayFunc As_
Expression(Of Func(Of ControlType())))
Dim controlArray = controlArrayFunc.Compile()()
Dim keyword = CType(controlArrayFunc.Body, MemberExpression).Member.Name.TrimStart("_"c)
controlArray(index) =_
Me.Controls.Find(keyword & (index + 1), True).OfType(Of ControlType).Single()
End Sub
Private Sub InitControlsArrayAndEventHandlers()
For i As Integer = 0 To BasketCount
AssignControl(i, Function() _lvBasket)
AssignControl(i, Function() _txtNameSurname)
AssignControl(i, Function() _txtPhone)
AssignControl(i, Function() _btnSelectCustomer)
AssignControl(i, Function() _btnAddProduct)
AssignControl(i, Function() _lblInfo)
AddHandler _lvBasket(i).MouseDoubleClick, AddressOf vlBasket_Click
AddHandler _btnSelectCustomer(i).Click, AddressOf btnSelectCustomer_Click
Next
End Sub

How do I get a radio box that permits no selections?

I have a pair of radio buttons on top of a panel to function as a radio box, and it is mostly fine, but I need the user to have the option of leaving both of them unchecked (I wish the user to select zero or one of the options). They are both unchecked when the form is displayed, but after the user checks one of them, it is impossible to revert to the original state. I would expect clicking the selected radio box would clear it (I vaguely remember this from my X/Motif days) but that doesn't seem to happen.
How do I program the radio buttons to allow both of them the be unchecked? Or should I be using some other control for this purpose?
There is no built in way to do what you're asking for. It is not that hard to do it our-self. Here's the sample code which shows how to do that.
C# version:
private void Initialize()
{
radioButton1.AutoCheck = false;
radioButton2.AutoCheck = false;
radioButton1.Click += radioButton_Click;
radioButton2.Click += radioButton_Click;
}
void radioButton_Click(object sender, EventArgs e)
{
RadioButton radio = (RadioButton)sender;
radio.Checked = !radio.Checked;
if (!radio.Checked)
{
return;
}
var otherRadios = radio.Parent
.Controls
.OfType<RadioButton>()
.Where(r => r != radio);
foreach (var r in otherRadios)
{
r.Checked = !radio.Checked;
}
}
Vb.net version:(Converted using http://converter.telerik.com/)
Private Sub Initialize()
radioButton1.AutoCheck = False
radioButton2.AutoCheck = False
AddHandler radioButton1.Click, AddressOf radioButton_Click
AddHandler radioButton2.Click, AddressOf radioButton_Click
End Sub
Private Sub radioButton_Click(sender As Object, e As EventArgs)
Dim radio As RadioButton = DirectCast(sender, RadioButton)
radio.Checked = Not radio.Checked
If Not radio.Checked Then
Return
End If
Dim otherRadios = radio.Parent.Controls.OfType(Of RadioButton)().Where(Function(r) r <> radio)
For Each r As var In otherRadios
r.Checked = Not radio.Checked
Next
End Sub
It should be simple if you think hatke.. Create your custom control
Imports System.ComponentModel
Public Class MyRadio
Inherits RadioButton
Private _permitted As Boolean = True
<DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)> _
<Browsable(True)> _
Public Property Permitted() As Boolean
Get
Return _permitted
End Get
Set(ByVal value As Boolean)
_permitted = value
End Set
End Property
Protected Overrides Sub OnClick(ByVal e As System.EventArgs)
If _permitted Then MyBase.OnClick(e)
End Sub
End Class
HOW TO USE IT?
myRadio1.Permitted = False 'Assigning it to false will not allow you to select it.
You can use checkboxes, they can be checked and unchecked.
If you really want to uncheck a radio button then use the code below:
Private Sub RadioButton1_MouseDown(sender As Object, e As MouseEventArgs) Handles RadioButton1.MouseDown
If RadioButton1.Checked = True Then
Dim thr As New Threading.Thread(AddressOf unticker)
thr.Start()
End If
End Sub
Private Sub unticker()
Threading.Thread.Sleep(100)
Do Until RadioButton1.Checked = False
Invoke(Sub() RadioButton1.Checked = False)
Loop
End Sub
Use this for each radiobutton individually

How to reset the close reason when close is cancelled

Question
Is it possible to reset the CloseReason provided by the FormClosingEventArgs in the FormClosing event of a modal dialog?
Symptoms
Setting the DialogResult of a modal dialog can result in an "incorrect" CloseReason if the close event have previously been cancelled.
Details
(The following code is just sample code to highlight the inconvenience)
Imagine I have a form with two buttons, OK and Cancel, displayed as a modal dialog.
Me.btnOk = New Button With {.DialogResult = Windows.Forms.DialogResult.OK}
Me.btnCancel = New Button With {.DialogResult = Windows.Forms.DialogResult.Cancel}
Me.AcceptButton = Me.btnOk
Me.CancelButton = Me.btnCancel
Any attempts to close the form will be cancelled.
If I click each button (including the [X] - close form button) in the following order, the close reasons will be as following:
Case 1
btnOk::::::::::: None
btnCancel::: None
X::::::::::::::::::: UserClosing
Now, if I repeat the steps you'll see that the UserClosing reason will persist:
btnOk::::::::::: UserClosing
btnCancel::: UserClosing
X::::::::::::::::::: UserClosing
Case 2
X::::::::::::::::::: UserClosing
btnCancel::: UserClosing
btnOk::::::::::: UserClosing
Same here. Once you click the X button the close reason will always return UserClosing.
Sample application
Public Class Form1
Public Sub New()
Me.InitializeComponent()
Me.Text = "Test"
Me.FormBorderStyle = Windows.Forms.FormBorderStyle.FixedDialog
Me.MinimizeBox = False
Me.MaximizeBox = False
Me.ClientSize = New Size(75, 25)
Me.StartPosition = FormStartPosition.CenterScreen
Me.btnOpenDialog = New Button() With {.TabIndex = 0, .Dock = DockStyle.Fill, .Text = "Open dialog"}
Me.Controls.Add(Me.btnOpenDialog)
End Sub
Private Sub HandleOpenDialog(sender As Object, e As EventArgs) Handles btnOpenDialog.Click
Using instance As New CustomDialog()
instance.ShowDialog()
End Using
End Sub
Private WithEvents btnOpenDialog As Button
Private Class CustomDialog
Inherits Form
Public Sub New()
Me.Text = "Custom dialog"
Me.ClientSize = New Size(400, 200)
Me.StartPosition = FormStartPosition.CenterParent
Me.FormBorderStyle = Windows.Forms.FormBorderStyle.FixedDialog
Me.MinimizeBox = False
Me.MaximizeBox = False
Me.tbOutput = New RichTextBox() With {.TabIndex = 0, .Bounds = New Rectangle(0, 0, 400, 155), .ReadOnly = True, .ScrollBars = RichTextBoxScrollBars.ForcedBoth, .WordWrap = True}
Me.btnExit = New Button With {.TabIndex = 3, .Text = "Exit", .Bounds = New Rectangle(10, 165, 75, 25), .Anchor = (AnchorStyles.Bottom Or AnchorStyles.Left)}
Me.btnOk = New Button With {.TabIndex = 1, .Text = "OK", .Bounds = New Rectangle(237, 165, 75, 25), .Anchor = (AnchorStyles.Bottom Or AnchorStyles.Right), .DialogResult = Windows.Forms.DialogResult.OK}
Me.btnCancel = New Button With {.TabIndex = 2, .Text = "Cancel", .Bounds = New Rectangle(315, 165, 75, 25), .Anchor = (AnchorStyles.Bottom Or AnchorStyles.Right), .DialogResult = Windows.Forms.DialogResult.Cancel}
Me.Controls.AddRange({Me.tbOutput, Me.btnExit, Me.btnOk, Me.btnCancel})
Me.AcceptButton = Me.btnOk
Me.CancelButton = Me.btnCancel
End Sub
Private Sub HandleExitDialog(sender As Object, e As EventArgs) Handles btnExit.Click
Me.exitPending = True
Me.Close()
End Sub
Protected Overrides Sub OnFormClosing(e As FormClosingEventArgs)
If (Not Me.exitPending) Then
e.Cancel = True
Me.tbOutput.Text += (String.Format("DialogResult={0}, CloseReason={1}{2}", Me.DialogResult.ToString(), e.CloseReason.ToString(), Environment.NewLine))
Me.DialogResult = Windows.Forms.DialogResult.None
End If
MyBase.OnFormClosing(e)
End Sub
Private exitPending As Boolean
Private WithEvents btnExit As Button
Private WithEvents btnCancel As Button
Private WithEvents btnOk As Button
Private WithEvents tbOutput As RichTextBox
End Class
End Class
Update
I was of the impression that if either the Form.AcceptButton or Form.CancelButton (IButtonControl) was clicked the close reason would be set to UserClosing, but this is not the case. In the following code you'll see that all it do is setting the DialogResult of the owning form to that of its own DialogResult.
Protected Overrides Sub OnClick(ByVal e As EventArgs)
Dim form As Form = MyBase.FindFormInternal
If (Not form Is Nothing) Then
form.DialogResult = Me.DialogResult
End If
MyBase.AccessibilityNotifyClients(AccessibleEvents.StateChange, -1)
MyBase.AccessibilityNotifyClients(AccessibleEvents.NameChange, -1)
MyBase.OnClick(e)
End Sub
The Control class do have a property named CloseReason but it's defined as Friend, thus not accessible.
I also thought that setting the forms DialogResult would result in a WM message being sent, but all it does is setting a private field.
So I delved into reflector and followed the stack. The following image is a highly simplified illustration.
This is how the CheckCloseDialog method looks like:
Friend Function CheckCloseDialog(ByVal closingOnly As Boolean) As Boolean
If ((Me.dialogResult = DialogResult.None) AndAlso MyBase.Visible) Then
Return False
End If
Try
Dim e As New FormClosingEventArgs(Me.closeReason, False)
If Not Me.CalledClosing Then
Me.OnClosing(e)
Me.OnFormClosing(e)
If e.Cancel Then
Me.dialogResult = DialogResult.None
Else
Me.CalledClosing = True
End If
End If
If (Not closingOnly AndAlso (Me.dialogResult <> DialogResult.None)) Then
Dim args2 As New FormClosedEventArgs(Me.closeReason)
Me.OnClosed(args2)
Me.OnFormClosed(args2)
Me.CalledClosing = False
End If
Catch exception As Exception
Me.dialogResult = DialogResult.None
If NativeWindow.WndProcShouldBeDebuggable Then
Throw
End If
Application.OnThreadException(exception)
End Try
If (Me.dialogResult = DialogResult.None) Then
Return Not MyBase.Visible
End If
Return True
End Function
As you can see the modal message loop checks the DialogResult in every cycle and if the conditions are met it will use the stored CloseReason (as observed) when creating the FormClosingEventArgs.
Summary
Yes, I know that the IButtonControl interface have a PerformClick method which you can call programmatically, but still, IMO this smells like a bug. If clicking a button is not a result of a user action then what is?
It is pretty important to understand why this is behaving the way it does, you are liable to get yourself into trouble when you rely in the CloseReason too much. This is not a bug, it is a restriction due to the way Windows was designed. One core issue is the way the WM_CLOSE message is formulated, it is the one that sets the train in motion, first firing the FormClosing event.
This message can be sent for lots of reasons, you are familiar with the common ones. But that's not where it ends, other programs can send that message as well. You can tell the "flaw" from the MSDN Library article I linked to, the message is missing a WPARAM value that encodes the intent of the message. So there isn't any way for a program to provide a reasonable CloseReason back to you. Winforms is forced to guess at a reason. It is of course an entirely imperfect guess.
That's not where it ends, the DialogResult property is a problem as well. It will force a dialog to close when any code assigns that property. But again the same problem, there isn't any way for such code to indicate the intent of the assignment. So it doesn't, it leaves in internal Form.CloseReason property at whatever value it had before, None by default.
This was "properly" implemented in .NET 1.0, there was only the Closing event and it didn't give a reason at all. But that didn't work out so well either, apps that used it chronically prevented Windows from shutting down. They just didn't know that it was inappropriate to, say, display a message box. The .NET 2.0 FormClosing event was added as a workaround for that. But it needs to work with the imperfect guess.
It is important to rate the CloseReason values, some are very accurate and some are just guesses:
CloseReason.WindowsShutdown - reliable
CloseReason.ApplicationExitCall - reliable
CloseReason.MdiFormClosing - reliable, not very useful
CloseReason.FormOwnerClosing - reliable, not very useful
CloseReason.TaskManagerClosing - complete guess, will be returned when any program sends a WM_CLOSE message, not just Task Manager
CloseReason.UserClosing - complete guess, will also be returned when your program calls the Close() method for example
CloseReason.None - it just doesn't know.
Yes, Winforms not setting the CloseReason back to None when your FormClosing event handler cancels is arguably a bug. But it isn't the kind of bug that actually really matters. Since you can't treat UserClosing and None differently anyway.
I would probably call that a bug.
As you mentioned, the CloseReason property is marked internal (or Friend in VB.Net terms) so one work-around to the problem is using Reflection to reset that value yourself:
Protected Overrides Sub OnFormClosing(e As FormClosingEventArgs)
If Not exitPending Then
e.Cancel = True
tbOutput.AppendText(String.Format("DialogResult={0}, CloseReason={1}{2}", _
Me.DialogResult.ToString(), e.CloseReason.ToString(), _
Environment.NewLine))
Dim pi As PropertyInfo
pi = Me.GetType.GetProperty("CloseReason", _
BindingFlags.Instance Or BindingFlags.NonPublic)
pi.SetValue(Me, CloseReason.None, Nothing)
End If
MyBase.OnFormClosing(e)
End Sub
No guarantee that this code would work on future versions of WinForms, but I'm guessing it's a safe bet these days. :-)
Private Const WM_SYSCOMMAND As Int32 = &H112
Private Const SC_CLOSE As Int32 = &HF060
'Private Const SC_MAXIMIZE As Int32 = &HF030
'Private Const SC_MINIMIZE As Int32 = &HF020
'Private Const SC_RESTORE As Int32 = &HF120
Private _commandClose As Boolean = False
Protected Overrides Sub WndProc(ByRef m As Message)
If CInt(m.Msg) = WM_SYSCOMMAND Then
If (m.WParam.ToInt32 And &HFFF0) = SC_CLOSE Then _commandClose = True
End If
MyBase.WndProc(m)
End Sub
Private Sub baseClick(ByVal sender As Object, ByVal e As EventArgs) Handles Me.Click
Close()
End Sub
Protected Overrides Sub OnFormClosing(ByVal e As FormClosingEventArgs)
If _commandClose Then DialogResult = ' ...
MyBase.OnFormClosing(e)
End Sub
reference: MSDN - WM_SYSCOMMAND message
hmm, actually, this does work. but unlike the official docs, SC_CLOSE fires for Alt+F4, etc... as well, even though not specifically mentioned.
It does not fire when calling the Form.Close() method. therefore, working as intended.
however, it will still return UserClosing if you call the Close() method, which is by design.
note: SC_SCREENSAVE can be used to detect/prevent screensavers, along with SC_MONITORPOWER. the documentation on that seems a bit vague.