Silverlight: Get first TextBox in DataTemplate - vb.net

So far I have looked at several questions and answers for how to get a TextBox within a DataTemplate, but none of it is working for me.
I have xaml like so (minimal example). The data template is in my section for static resources, and the ItemsControl is in the content section:
<DataTemplate x:Key="GridTemplate">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="140" />
</Grid.ColumnDefinitions>
<sdk:IntegerTextBox DataField="Model.DataField" Width="90" SelectAllOnFocus="True" />
</Grid>
</DataTemplate>
<ItemsControl x:Name="MyControl" ItemsSource="{Binding MyList}" ItemTemplate="{StaticResource GridTemplate}" />
I need to be able set focus to the first IntegerTextBox in the Grid in the code behind. IntegerTextBox inherits the TextBox class.
I first tried writing my own method to recursively search through all UIElements for the first TextBox, but I found that the content within the DataTemplate isn't searchable in this way. The ItemsControl's children always return Nothing:
Private Function FirstTextBox(ByVal uiElement As UIElement) As TextBox
Dim textBox As TextBox = TryCast(uiElement, TextBox)
If textBox IsNot Nothing Then Return textBox
Dim panel As Panel = TryCast(uiElement, Panel)
If panel IsNot Nothing Then
For Each child As UIElement In panel.Children
textBox = FirstTextBox(child)
If textBox IsNot Nothing Then Return textBox
Next
End If
Dim itemsControl As ItemsControl = TryCast(uiElement, ItemsControl)
If itemsControl IsNot Nothing Then
For i As Integer = 0 To itemsControl.Items.Count
textBox = FirstTextBox(CType(itemsControl.ItemContainerGenerator.ContainerFromIndex(i), UIElement))
If textBox IsNot Nothing Then Return textBox
Next
End If
Return textBox
End Function
I have tried this, similar to here and here, but the ContentPresenter is Nothing:
Dim contentPresenter = CType(MyControl.ItemContainerGenerator.ContainerFromIndex(0), ContentPresenter)
Dim textbox As TextBox = CType(CType(contentPresenter.ContentTemplate.LoadContent(), Panel).Children.First(Function(c) TypeOf c Is TextBox), TextBox)
I tried getting the DataTemplate as shown here, then loading the content and searching the children for a TextBox as shown here, but it never finds a TextBox.
I have worked a couple days on this, but I cannot see what I am doing wrong. Is it some obvious mistake, or am I approaching the problem incorrectly? Thanks.
EDIT - This is how I got it to work by adding 100 ms delay:
Private Function FindDescendant(Of TDescendant As DependencyObject)(ByVal obj As DependencyObject) As TDescendant
Dim all = VisualTreeExtensions.GetVisualDescendants(obj)
Dim first = all.OfType(Of TDescendant)().FirstOrDefault()
Return first
End Function
Private Sub bw_DoWork(ByVal sender As Object, ByVal e As ComponentModel.DoWorkEventArgs)
System.Threading.Thread.Sleep(100)
End Sub
Private Sub bw_RunWorkerCompleted(ByVal sender As Object, ByVal e As ComponentModel.RunWorkerCompletedEventArgs)
Dim firstTextBox = FindDescendant(Of IntegerTextBox)(MyControl)
If firstTextBox IsNot Nothing Then firstTextBox.Focus()
End Sub
Private Sub SetFocus()
Dim bw As New ComponentModel.BackgroundWorker
bw.WorkerReportsProgress = True
bw.WorkerSupportsCancellation = True
AddHandler bw.DoWork, AddressOf bw_DoWork
AddHandler bw.RunWorkerCompleted, AddressOf bw_RunWorkerCompleted
bw.RunWorkerAsync()
End Sub

You are approaching the problem incorrectly. Accessing the DataTemplate and searching for a TextBox will get you nowhere.
The template is only a blueprint, only when it is used somewhere (like for each item in your ItemsControl) its content is instantiated (once for each item).
One of several possible solutions to set focus to the first item's textbox:
In your code-behind add an eventhandler to the itemsControl:
MyControl.GotFocus += (sender, args) =>
{
var firstItemTextBox = MyControl.FindDescendant<IntegerTextBox>();
if ( firstItemTextBox != null ) firstItemTextBox.Focus();
};
some helper code:
//you need System.Windows.Controls.Toolkit.dll from the SilverlightToolkit for the class VisualTreeExtensions
using System.Windows.Controls.Primitives;
public static class ControlExtensions
{
public static TDescendant FindDescendant<TDescendant>(this DependencyObject element)
{
return element.GetVisualDescendants().OfType<TDescendant>().FirstOrDefault();
}
}
And btw: why do you wrap the IntergerTextBox in a separate Grid for each item?

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 :)

Keep the values in page when navigating to other pages

I am developing a UWP app where there is a "page" and in that page, there are several "textbox"es which values are entered by a user (numbers). When it is navigated to other pages to take other values and came back to this page the values entered in "textbox"es are all lost. I used "NavigationCacheMode", no success.
Could you please help how to maintain values in UI elements (textboxes) in current page when navigating to other pages.
I would appreciate if the solution were in VB.NET.
Thank you very much.
You would have to save the date by yourself in this scenario. For example, you could choose the ApplicationData.LocalSettings to save the data when navigating to other page.
You could save the data on the page's OnNavigatedFrom method and get the data in its OnNavigatedTo method like the following:
<StackPanel>
<TextBox x:Name="textbox1"></TextBox>
<TextBox x:Name="textbox2"></TextBox>
<Button Content="navigate" Click="Button_Click"></Button>
</StackPanel>
Public NotInheritable Class MainPage
Inherits Page
Dim localSettings As Windows.Storage.ApplicationDataContainer = Windows.Storage.ApplicationData.Current.LocalSettings
Protected Overrides Sub OnNavigatedTo(e As NavigationEventArgs)
MyBase.OnNavigatedTo(e)
Dim value1 As Object = localSettings.Values(textbox1.Name)
Dim value2 As Object = localSettings.Values(textbox2.Name)
If value1 IsNot Nothing And value2 IsNot Nothing Then
textbox1.Text = value1.ToString()
textbox2.Text = value2.ToString()
End If
End Sub
Protected Overrides Sub OnNavigatedFrom(e As NavigationEventArgs)
MyBase.OnNavigatedFrom(e)
localSettings.Values(textbox1.Name) = textbox1.Text
localSettings.Values(textbox2.Name) = textbox2.Text
End Sub
Private Sub Button_Click(sender As Object, e As RoutedEventArgs)
Frame.Navigate(GetType(BlankPage1))
End Sub
End Class

Maintain listview row selected

I already check this question - prevent listview to lose selected item .
but I need this code for VB.Net. I already used c# to vb converter and the output is this.
Private Sub ListView_SelectionChanged(sender As Object, e As SelectionChangedEventArgs)
Dim listView As ListView = TryCast(sender, ListView)
If listView.SelectedItems.Count = 0 Then
For Each item As Object In e.RemovedItems
listView.SelectedItems.Add(item)
Next
End If
End Sub
And the error is : 'SelectionChangedEventArgs' is not defined.
I replaced the SelectionChangedEventArgs to ByVal e As System.EventArgs
and now the error is:
'RemovedItems' is not a member of 'System.EventArgs'.
Property: HideSelection = False also not working for me.
How will I able to maintain the row selected?

Getting value of a dynamically generated text box in a listbox, Windows Phone 8

In my Windows Phone 8 app, I have a ListBox control. This list box contains a grid, which contains a pair of TextBlock (title of the field) and a TextBox (user input) controls. This list is generated based on the results of a service the app connects to. What I need to do is access each textbox within the list, and bring back the value of it. To do this, I have bound a unique ID of each item to the Tag property of the TextBox, and I am using the LostFocus event to capture the user input. Once this has been captured and added to a collection in the code behind, the data is processed when the user clicks a button under the list. This works fine for every item except the last one.
The problem is that the LostFocus doesn't work if the button is clicked. The button click method seems to take precedence over the textbox LostFocus method. So if there is only 1 item in the list, the value isn't recorded. Here is the code:
<ItemsControl x:Name="itmScreen4">
<TextBlock Margin="0,10,0,10">Fields</TextBlock>
<ListBox x:Name="lstCustom" ItemsSource="{Binding}" Visibility="Visible">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid x:Name="grdCustom">
<Grid.RowDefinitions>
<RowDefinition></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" x:Name="txtCustTitle" Text="{Binding Name}" Foreground="#4C4C4C" FontSize="18" Margin="0,15,0,0"></TextBlock>
<TextBox Tag="{Binding DataID}" x:Name="txtCust" Grid.Row="1" Style="{StaticResource TextBox}" Width="450" LostFocus="txtCust_LostFocus"></TextBox>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Button x:Name="btnSubmit" Content="Submit" Background="#404040"></Button>
</ItemsControl>
For the final item in the list (or if there's only one item in the list), the txtCust_LostFocus method doesn't get called when the btnSubmit method is called. Any ideas of how I can capture that final textbox value?
I have tried some other methods (casting a ListBoxItem and performing a FindName on it, for example), but haven't found anything that works. Google hasn't been much help either. Thanks in advance.
EDIT:
Here is the code behind. I am binding a custom class to the list as below.
Class definition here (I have removed some properties for readabality purposes):
Public Class CustomDataRequest
Public Sub New()
End Sub
Public Property ID As Integer
Public Property Name As String
End Class
Use in the code here:
Public Sub ShowCustomData()
Dim CustomDataList As New List(Of CustomDataRequest)()
For Each item In _CustomDataRequestList
If item.ID= _CurrentID Then
CustomDataList.Add(item)
End If
Next
lstCustom.ItemsSource = CustomDataList.ToArray
End Sub
The txtCust_LostFocus method is just capturing the fields at the minute. Once I can actually get it called, I can then add the data to the collection:
Private Sub txtCust_LostFocus(sender As Object, e As RoutedEventArgs)
Dim elem = DirectCast(sender, FrameworkElement)
Dim txt = DirectCast(elem.FindName("txtCust"), TextBox)
Dim text As String = txt.Text
Dim tag As String = txt.Tag
End Sub
The problem is that it never gets called once the button has been tapped:
Protected Sub btnSubmit_Tap(sender As Object, e As Input.GestureEventArgs) Handles btnSubmit.Tap
ValidateData()
End Sub
Here is the answer to this. The TextBox should have TwoWay binding mode set, to bind the text value to a corresponding Value property in the class. It should also have UpdateSourceTrigger set to Explicit for binding efficiency, as detailed here.
Public Class CustomDataRequest
Public Sub New()
End Sub
Public Property ID As Integer
Public Property Name As String
'New property here
Public Property Value As String
End Class
Textbox code:
<TextBox Text="{Binding Value, Mode=TwoWay, UpdateSourceTrigger=Explicit}" x:Name="txtCust" Grid.Row="1" BorderBrush="#BBBBBB" Style="{StaticResource TextBox}" Width="450" TextChanged="Textbox_Changed"></TextBox>
In the code behind, the following method should be called from the Textbox TextChanged (translated into VB):
Private Sub Textbox_Changed(sender As Object, e As TextChangedEventArgs)
Dim txt As TextBox = TryCast(sender, TextBox)
Dim bindingExpr As BindingExpression = txt.GetBindingExpression(TextBox.TextProperty)
bindingExpr.UpdateSource()
End Sub
This solves the problem of the textbox value not binding when the focus is still on the textbox. Thanks to venerik for the answer to this question.

How to set tooltips on ListView Subitems in .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.