Why does binding to a set of Winforms cascading combo boxes cause the root combo box to not set its value properly? - vb.net

I have a Windows form with two combo boxes. The SelectedValue property of each combo box is data bound to a property on a simple DTO. The options for each combo box are drawn from a list of model objects. I only require the controls on the form to update the DTO; I have no need to modify any of the DTO's properties programmatically and see the corresponding control being updated - i.e., I only need one-way (control -> source) data binding to work.
When the user changes the value of the first combo box, the options for the second combo box will change completely. However, I have run into two issues with this setup that I cannot figure out why they occur or how to solve them:
Whenever the first combo box is changed, an NRE is generated and swallowed by the data binding framework (I can see it thrown in the Immediate Window of the Visual Studio IDE), which tips me off that something isn't set up correctly. Changing the second combo box or any other unrelated, data bound control (combo box or otherwise) does not generate an NRE.
Also whenever the first combo box is changed, after generating the NRE mentioned above, the second combo box loads successfully, but the first combo box's selected index resets to -1. I suspect this is because the data binding's "push" event fires to update the controls, and for some reason, the value of my DTO's property backing the first combo box gets reset to NULL / Nothing.
Does anyone have any idea why these things occur? I mocked up my problem, which exhibits the two issues above. I also added a third combo box that has nothing to do with either of the first two, just as a sanity check to show that a combo box without any dependency on another combo box works fine.
This code replicates the issues - paste as the code for the default Form1 class of a Visual Basic Windows Forms project (3.5 Framework).
Imports System
Imports System.Collections.Generic
Imports System.Linq
Imports System.Windows.Forms
Public Class Form1
Inherits System.Windows.Forms.Form
'Form overrides dispose to clean up the component list.
<System.Diagnostics.DebuggerNonUserCode()> _
Protected Overrides Sub Dispose(ByVal disposing As Boolean)
Try
If disposing AndAlso components IsNot Nothing Then
components.Dispose()
End If
Finally
MyBase.Dispose(disposing)
End Try
End Sub
'Required by the Windows Form Designer
Private components As System.ComponentModel.IContainer
'NOTE: The following procedure is required by the Windows Form Designer
'It can be modified using the Windows Form Designer.
'Do not modify it using the code editor.
<System.Diagnostics.DebuggerStepThrough()> _
Private Sub InitializeComponent()
Me.cboA = New System.Windows.Forms.ComboBox()
Me.cboB = New System.Windows.Forms.ComboBox()
Me.cboC = New System.Windows.Forms.ComboBox()
Me.SuspendLayout()
'
'cboA
'
Me.cboA.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList
Me.cboA.FormattingEnabled = True
Me.cboA.Location = New System.Drawing.Point(120, 25)
Me.cboA.Name = "cboA"
Me.cboA.Size = New System.Drawing.Size(121, 21)
Me.cboA.TabIndex = 0
'
'cboB
'
Me.cboB.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList
Me.cboB.FormattingEnabled = True
Me.cboB.Location = New System.Drawing.Point(120, 77)
Me.cboB.Name = "cboB"
Me.cboB.Size = New System.Drawing.Size(121, 21)
Me.cboB.TabIndex = 1
'
'cboC
'
Me.cboC.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList
Me.cboC.FormattingEnabled = True
Me.cboC.Location = New System.Drawing.Point(120, 132)
Me.cboC.Name = "cboC"
Me.cboC.Size = New System.Drawing.Size(121, 21)
Me.cboC.TabIndex = 2
'
'Form1
'
Me.AutoScaleDimensions = New System.Drawing.SizeF(6.0!, 13.0!)
Me.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font
Me.ClientSize = New System.Drawing.Size(284, 262)
Me.Controls.Add(Me.cboC)
Me.Controls.Add(Me.cboB)
Me.Controls.Add(Me.cboA)
Me.Name = "Form1"
Me.Text = "Form1"
Me.ResumeLayout(False)
End Sub
Friend WithEvents cboA As System.Windows.Forms.ComboBox
Friend WithEvents cboB As System.Windows.Forms.ComboBox
Friend WithEvents cboC As System.Windows.Forms.ComboBox
Private _DataObject As MyDataObject
Private _IsInitialized As Boolean = False
Public Sub New()
' This call is required by the Windows Form Designer.
InitializeComponent()
' Add any initialization after the InitializeComponent() call.
_DataObject = New MyDataObject()
BindControls()
End Sub
Private Sub BindControls()
LoadComboA(cboA)
Dim cmbABinding As New Binding("SelectedValue", _DataObject, "ValueA", True, DataSourceUpdateMode.OnPropertyChanged)
cboA.DataBindings.Add(cmbABinding)
Dim cmbBBinding As New Binding("SelectedValue", _DataObject, "ValueB", True, DataSourceUpdateMode.OnPropertyChanged)
cboB.DataBindings.Add(cmbBBinding)
LoadComboC(cboC)
Dim cmbCBinding As New Binding("SelectedValue", _DataObject, "ValueC", True, DataSourceUpdateMode.OnPropertyChanged)
cboC.DataBindings.Add(cmbCBinding)
End Sub
Protected Overrides Sub OnLoad(ByVal e As System.EventArgs)
MyBase.OnLoad(e)
_IsInitialized = True
cboA.SelectedIndex = 0
cboC.SelectedIndex = 0
End Sub
Private Sub ComboA_SelectedValueChanged(ByVal sender As Object, ByVal e As EventArgs) Handles cboA.SelectedValueChanged
If _IsInitialized Then
LoadComboB(cboB, cboA.SelectedValue.ToString())
cboB.SelectedIndex = 0
End If
End Sub
Private Sub LoadComboA(ByVal cmbBox As ComboBox)
Dim someData As New Dictionary(Of String, String)()
someData.Add("Value1", "Text 1")
someData.Add("Value2", "Text 2")
someData.Add("Value3", "Text 3")
cmbBox.DataSource = someData.ToList()
cmbBox.DisplayMember = "Value"
cmbBox.ValueMember = "Key"
End Sub
Private Sub LoadComboB(ByVal cmbBox As ComboBox, ByVal selector As String)
Dim someSubData As New Dictionary(Of String, String)()
Select Case selector
Case "Value1"
someSubData.Add("SubValue1", "Value1 - Sub Text 1")
someSubData.Add("SubValue2", "Value1 - Sub Text 2")
someSubData.Add("SubValue3", "Value1 - Sub Text 3")
Case "Value2"
someSubData.Add("SubValue4", "Value2 - Sub Text 4")
someSubData.Add("SubValue5", "Value2 - Sub Text 5")
someSubData.Add("SubValue6", "Value2 - Sub Text 6")
Case "Value3"
someSubData.Add("SubValue7", "Value3 - Sub Text 7")
someSubData.Add("SubValue8", "Value3 - Sub Text 8")
someSubData.Add("SubValue9", "Value3 - Sub Text 9")
End Select
cmbBox.DataSource = someSubData.ToList()
cmbBox.DisplayMember = "Value"
cmbBox.ValueMember = "Key"
End Sub
Private Sub LoadComboC(ByVal cmbBox As ComboBox)
Dim someData As New Dictionary(Of String, String)()
someData.Add("Value100", "Text 100")
someData.Add("Value101", "Text 101")
cmbBox.DataSource = someData.ToList()
cmbBox.DisplayMember = "Value"
cmbBox.ValueMember = "Key"
End Sub
End Class
Public Class MyDataObject ' DTO class
Private _ValueA As String
Public Property ValueA() As String
Get
Return _ValueA
End Get
Set(ByVal value As String)
_ValueA = value
End Set
End Property
Private _ValueB As String
Public Property ValueB() As String
Get
Return _ValueB
End Get
Set(ByVal value As String)
_ValueB = value
End Set
End Property
Private _ValueC As String
Public Property ValueC() As String
Get
Return _ValueC
End Get
Set(ByVal value As String)
_ValueC = value
End Set
End Property
End Class

DataBinding can be difficult to debug when it misbehaves. There are two things going wrong here, enough to make it hard to diagnose. The first thing you didn't count on is that the SelectedValueChanged event fires before the currency manager updates the bound object. That normally isn't a problem but your event handler has a side-effect. The next thing you didn't count on is that changing one property of the bound object causes the binding of all other properties to be updated as well.
And there's the rub, _DataObject.ValueA is still Nothing when you update combo B. Which updates _DataObject.ValueB. So the currency manager updates combo A again, trying to make it match the value of Nothing in property ValueA. Which is what also produced the NullReferenceException.
One possible fix is to delay the side-effect in your SelectedValueChanged event handler and postpone it until the currency manager updated the bound object. Than can be cleanly done by using Control.BeginInvoke(), the target runs when the UI thread goes idle again. This fixed your problem:
Private Sub ComboA_SelectedValueChanged(ByVal sender As Object, ByVal e As EventArgs) Handles cboA.SelectedValueChanged
If _IsInitialized Then Me.BeginInvoke(New MethodInvoker(AddressOf LoadB))
End Sub
Private Sub LoadB()
LoadComboB(cboB, cboA.SelectedValue.ToString())
cboB.SelectedIndex = 0
End Sub
There's probably a cleaner fix, updating _DataObject instead of trying to update the combobox. But you made that a bit difficult by using a Dictionary, I didn't pursue it.

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

Is it possible to have things defined and controlled inside a class, without being assigned in the "form" (outside the class) in VB?

My problem:
I have a checkbox I use to control if certain textboxes are enabled or not, and I need to do this around 30+ times. I've named my textboxes numerically/sequentially (TB_name_1, TB_name_2, etc) so if I know the Checkbox name I know which textboxes are affected.
My question:
Can I make a class for my checkboxes that says "if this box is checked/unchecked, then enable/disable these 3 textboxes" without the class also having to be told which textboxes (finds them itself)?
Here's the copy/paste code I'm currently using (not a class, obviously). I change the first 2 values and the rest of the code solves itself. (PS - I see you laughing)
Private Sub T1_cb_c_1_CheckedChanged(sender As Object, e As EventArgs) Handles T1_cb_c_1.CheckedChanged
'change here for current checkbox
Dim b As CheckBox = T1_cb_c_1
'change here for start value of first textbox (of 3), the next 2 will be in sequence
Dim a As Integer = 1
'How much of the below code can be moved to, and controlled from, a class?
Dim a1 As Integer = a + 1
Dim a2 As Integer = a + 2
Dim TB_PtNum As TextBox = Me.Controls.Find("T1_tb_c_" & a, True).FirstOrDefault
Dim TB_Qty As TextBox = Me.Controls.Find("T1_tb_c_" & a1, True).FirstOrDefault
Dim TB_Seq As TextBox = Me.Controls.Find("T1_tb_c_" & a2, True).FirstOrDefault
If b.Checked = True Then
TB_PtNum.Enabled = True
TB_Qty.Enabled = True
TB_Seq.Enabled = True
Else
TB_PtNum.Enabled = False
TB_Qty.Enabled = False
TB_Seq.Enabled = False
End If
End Sub
Here's a design time only class that will do this. You only have to the AssociatedCheckbox property in the designer:
Public Class TextBoxWithCheckboxProperty
Inherits TextBox
Private m_CheckBox As CheckBox
Public Property AssociatedCheckBox As CheckBox
Get
Return m_CheckBox
End Get
Set(value As CheckBox)
If Not m_CheckBox Is Nothing Then
RemoveHandler m_CheckBox.CheckedChanged, AddressOf OnCheckBoxChanged
End If
m_CheckBox = value
If Not value Is Nothing Then
AddHandler m_CheckBox.CheckedChanged, AddressOf OnCheckBoxChanged
End If
OnCheckBoxChanged(m_CheckBox, Nothing)
End Set
End Property
Private Sub OnCheckBoxChanged(ByVal sender As Object, ByVal e As System.EventArgs)
If Not sender Is Nothing Then
Me.Enabled = CType(sender, CheckBox).Checked
Else
Me.Enabled = False
End If
End Sub
End Class
Here's a sample Form1 that uses it:
<Global.Microsoft.VisualBasic.CompilerServices.DesignerGenerated()> _
Class Form1
Inherits System.Windows.Forms.Form
'Form overrides dispose to clean up the component list.
<System.Diagnostics.DebuggerNonUserCode()> _
Protected Overrides Sub Dispose(ByVal disposing As Boolean)
Try
If disposing AndAlso components IsNot Nothing Then
components.Dispose()
End If
Finally
MyBase.Dispose(disposing)
End Try
End Sub
'Required by the Windows Form Designer
Private components As System.ComponentModel.IContainer
'NOTE: The following procedure is required by the Windows Form Designer
'It can be modified using the Windows Form Designer.
'Do not modify it using the code editor.
<System.Diagnostics.DebuggerStepThrough()> _
Private Sub InitializeComponent()
Me.CheckBox1 = New System.Windows.Forms.CheckBox()
Me.TextBoxWithCheckboxProperty1 = New WindowsApp4.TextBoxWithCheckboxProperty()
Me.SuspendLayout()
'
'CheckBox1
'
Me.CheckBox1.AutoSize = True
Me.CheckBox1.Location = New System.Drawing.Point(293, 131)
Me.CheckBox1.Name = "CheckBox1"
Me.CheckBox1.Size = New System.Drawing.Size(81, 17)
Me.CheckBox1.TabIndex = 0
Me.CheckBox1.Text = "CheckBox1"
Me.CheckBox1.UseVisualStyleBackColor = True
'
'TextBoxWithCheckboxProperty1
'
Me.TextBoxWithCheckboxProperty1.AssociatedCheckBox = Me.CheckBox1
Me.TextBoxWithCheckboxProperty1.Location = New System.Drawing.Point(428, 131)
Me.TextBoxWithCheckboxProperty1.Name = "TextBoxWithCheckboxProperty1"
Me.TextBoxWithCheckboxProperty1.Size = New System.Drawing.Size(100, 20)
Me.TextBoxWithCheckboxProperty1.TabIndex = 1
'
'Form1
'
Me.AutoScaleDimensions = New System.Drawing.SizeF(6.0!, 13.0!)
Me.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font
Me.ClientSize = New System.Drawing.Size(800, 450)
Me.Controls.Add(Me.TextBoxWithCheckboxProperty1)
Me.Controls.Add(Me.CheckBox1)
Me.Name = "Form1"
Me.Text = "Form1"
Me.ResumeLayout(False)
Me.PerformLayout()
End Sub
Friend WithEvents CheckBox1 As CheckBox
Friend WithEvents TextBoxWithCheckboxProperty1 As TextBoxWithCheckboxProperty
End Class
I would use the property Tag for the related controls.
Suppose to set this property to the value "line1" for the first set of textboxes and on the checkbox that controls them.
Next row of controls (checkbox+textboxes) will have the property set to "line2" and so on until the last row. (You can do this through the Winforms Designer or through code)
At this point you could have a single event handler for all your checkboxes
Private Sub onCheckedChanged(sender As Object, e As EventArgs) _
Handles T1_cb_c_1.CheckedChanged, T2_cb_c_2.CheckedChanged, _
..... add other checkbox events here .......
' Get whatever checkbox has been clicked and extract its tag
Dim b As CheckBox = DirectCast(sender, CheckBox)
Dim tag = b.Tag.ToString()
' Find the textbox controls in this form with the same Tag
Dim ctrls = Me.Controls.OfType(Of TextBox).Where(Function(x) x.Tag.ToString() = tag)
' Enabled status matches the status of the Checked property
For Each c as TextBox in ctrls
c.Enabled = b.Checked
Next
End Sub

How to pass a form, object or data to a second form

I have created 2 forms.
The first one is the button that you want to back up.
In the second there are paths that can be modified.
How to make a reference that after pressing the "backup" button will get a path of 2 forms.
The path is saved when I closed form2
I know how to do it in one form but unfortunately I can not refer to another form.
Source of Form 2:
Private Sub Browser_from1_Click(sender As Object, e As EventArgs) Handles Browser_from1.Click
Dim FolderBrowserDialog1 As New FolderBrowserDialog
FolderBrowserDialog1.ShowDialog()
TextBox1from.Text = FolderBrowserDialog1.SelectedPath
If Browser_from1.Text <> "" And TextBox1from.Text <> "" Then
Backup.StartCopy.Enabled = True
End If
End Sub
Private Sub Browser_to1_Click(sender As Object, e As EventArgs) Handles Browser_to1.Click
Dim FolderBrowserDialog1 As New FolderBrowserDialog
FolderBrowserDialog1.ShowDialog()
TextBox2to.Text = FolderBrowserDialog1.SelectedPath
If Browser_to1.Text <> "" And TextBox2to.Text <> "" Then
Backup.StartCopy.Enabled = True
End If
End Sub
Private Sub TextBox1from_TextChanged(sender As Object, e As EventArgs) Handles TextBox1from.TextChanged
End Sub
Private Sub save_settings_Click(sender As Object, e As EventArgs) Handles save_settings.Click
My.Settings.pathmem = TextBox2to.Text
My.Settings.pathmem1 = TextBox1from.Text
My.Settings.Save()
End Sub
Private Sub setting_Load(sender As Object, e As EventArgs) Handles MyBase.Load
TextBox1from.Text = My.Settings.pathmem1
TextBox2to.Text = My.Settings.pathmem
End Sub
End Class
You dont want to create a reference to a form - that would (or could) create a whole new form. You want to hold onto the form reference.
This is done by passing a reference to the forms, but the talk of one form fiddling with the controls on another form is a bad idea because it breaks encapsulation. But forms are classes (it says so at the top of each one), so you can add Properties and Methods (Sub and/or Functions) to facilitate passing information back and forth.
Method One - Passing a Form Reference
The simplest way is to pass whatever the other form needs in the constructor:
' form 1 / "main" form / form to return to
Dim frm As New Form6(Me)
frm.Show()
Me.Hide()
In order for this to work, you need to modify the constructor (Sub New) on the destination form:
Private frmReturnTo As Form
Public Sub New(f As Form)
' This call is required by the designer.
InitializeComponent()
frmReturnTo = f
End Sub
It is best not to create your own constructor until you are familiar with them. Use the drop downs at the top of the code window: from the left pick the form name; from the right, select New. The designer adds required code to them which must not be changed.
Do not add any code before the InitializeComponent() call at least until you are familiar with the life cycle of a form. The form and its controls do not exist until that runs.
To return to the "main" form:
If frmReturnTo IsNot Nothing Then
frmReturnTo.Show()
End If
You may want to remove some of the title bar buttons or add code to the form Closing event to handle when the user closes via the system menu or buttons.
Using the constructor is ideal for cases where there is some bit of data which the form must have in order to do its job.
Method Two - Passing Data
Thats all well and good, but what about passing data to another form? You can use the constructor for that too. In order to pass say, a string, integer and a Point:
' destination / second form:
Public Sub New(a As String, b As Int32, c As Point)
' This call is required by the designer.
InitializeComponent()
' Add any initialization after the InitializeComponent() call.
Label1.Text = a
Label2.Text = b.ToString
Label3.Text = c.ToString
End Sub
Call it like this:
' method two: pass data you want to share in the ctor
Dim frm As New frmData("hello", 6, New Point(150, 550))
frm.Show()
Result:
Method Three: Properties
Thats fine, but if there is a lots of data that way can get cumbersome. Plus, you may want to update some of the data from the calling/main form. For this you can create Properties on the form to handle the data:
Public Property Label1Text As String
Get
Return Me.Label1.Text
End Get
Set(value As String)
Me.Label1.Text = value
End Set
End Property
Rather than a private variable to act as the backing field, one of the controls is used. The name leaves a bit to be desired as it exposes implementation details. So, use names which describe what the data represents rather than where it displays.
Public Property SpecialValue As Integer
Get
Return Integer.Parse(Me.Label2.Text)
End Get
Set(value As Integer)
Me.Label2.Text = value.ToString
End Set
End Property
Public Property SomePoint As Point
Get
Dim data = Me.Label3.Text.Split(","c)
Return New Point(Convert.ToInt32(data(0)),
Convert.ToInt32(data(1))
)
End Get
Set(value As Point)
Me.Label3.Text = value.X.ToString & "," & value.Y.ToString
End Set
End Property
A point was used just to show that other data types can be used. Setting those values from the calling/original/source form:
Using frm As New Form6
frm.Label1Text = "Ziggy"
frm.SpecialValue = 42
frm.SomePoint = New Point(111, 222)
frm.ShowDialog()
' do stuff here with any changes
Dim theint = frm.SpecialValue
End Using ' dispose of dialog
The destination controls would well have been TextBoxes for the user to edit. The Property "wrappers" allow you to fetch those values back, so in this case, a Dialog was used.
Method Four: Methods
You can also use methods as a way to pass data to the second/helper form. Here a List(of T) collection will be passed. In the child/display form a method is added to receive the data which it then displays. The task represented is proofing or viewing a filtered list:
Public Sub UpdateDisplay(lst As List(Of SimpleItem), filter As String)
DataGridView1.DataSource = lst
Label1.Text = String.Format("{0} Total {1} Items", lst.Count, filter)
End Sub
In the main/calling form:
' form level variable
Private frmDV As frmDataView
elsewhere...perhaps in a Click event:
' myList is a simple list of items
' Users pick which color to filter on via a combo box
Dim filter As String
If cboListFilter.SelectedItem IsNot Nothing Then
'Dim frmDV As New frmDataView
If frmDV Is Nothing OrElse frmDV.IsDisposed Then
frmDV = New frmDataView
End If
filter = cboListFilter.SelectedItem.ToString()
' apply the filter
Dim tmpList = myList.Where(Function(w) w.Color = filter).ToList()
frmDV.UpdateDisplay(tmpList, filter)
frmDV.Show()
Else
Return
End If
Result:
With DataBased apps a modified version of this can allow for the case where you display DataGridView data in detail form on another form. You need not have the second form rung SQL to add or update the record, and then the main form running another query to "refresh" the display. If the DataSource is a DataTable backed up by a fully configured DataAdapter, pass the DataTable and have the child form add, change or delete using that. The data will automagically be in the DataTable and DataGridView`.
There are other ways to do this, but they generally all boil down to passing something from A to B. Which way is "best" depends on what the app does, the use-case and the nature of the data. There is no one right way or best way.
For instance, Properties and in many cases Functions allow the B Form to close the feedback loop. With DB items, a DataChanged property might tell the calling form that data was added or changed so that form knows to use the DataAdapter to update the db.
'SECOND FORM
Public class secondForm (blah blah)
Public overloads property owner as myMainForm
'Must be only the form you prepared for that
Private sub secondForm_load(blah blah) handles blah blah
Texbox1.text=Owner.customcontrol.text
End sub
End class
'MAIN FORM
public class myMainForm(blah blah)
Private sub button1_click(blah blah) handles blah blah
Dim NewSecondForm as secondForm = New secondForm
NewSecondForm.owner(me)
NewSecondForm.show(me)
NewSecondForm.dispose()
' so you can have bidirectional communication between the two forms and access all the controls and properties from each other
End sub
End Class

Update data bound control when data changes

I have a List(T) that is bound to some controls, a read-only DataGridView, a ComboBox and a few Labels. This works fine, the controls are all populated correctly when the form loads, the Label.Text and DataGridView row focus all change as the ComboBox selection is changed.
But if I change the data in an object on the List the data shown in the controls does not update to reflect the changed data.
My class T implements the INotifyChanged interface and the label control data bindings update mode is set to OnPropertychanged.
I can force the DataGridView to update by calling its Refresh() method, but trying the same for the labels seems to have no effect.
So how can I make changes to the data in the objects in my list update the data shown in the Label controls? Have I done something wrong?
My MRE so far:
Class Form1
' Form1 has a DataGridView, a ComboBox, a Label, a Button and a TextBox
Dim FooList As New List(Of Foo)(3)
Private Sub Form1_Load(sender As System.Object, e As System.EventArgs) Handles MyBase.Load
For Index As Integer = 0 To FooList.Capacity - 1
FooList.Add(New Foo() With {.Bar = Index, .Baz = 0})
Next
' Shows all Bar and Baz
DataGridView1.DataSource = FooList
' User selects Bar value
ComboBox1.DataSource = FooList
ComboBox1.DisplayMember = "Bar"
' Related Baz value shows
Label1.DataBindings.Add(New Binding("Text", FooList, "Baz", DataSourceUpdateMode.OnPropertyChanged))
End Sub
Private Sub Button1_Click(sender As System.Object, e As System.EventArgs) Handles Button1.Click
' This is not _actually_ how I'm selecting indexes and changing the data
' But for the MRE it changes the Baz property
'Change the Baz value on the List, should result in Label1 changing
FooList(ComboBox1.SelectedItem.Bar).Baz = TextBox1.Text.Convert.ToUInt16
' Should I even need this when my list objects have INotifyChanged?
DataGridView1.Refresh()
End Sub
End Class
Class Foo
Implements INotifyChanged
Private _bar As UInt16
Private _baz As UInt16
Public Event PropertyChanged As PropertyChangedEventHandler _
Implements INotifyPropertyChanged.PropertyChanged
Private Sub NotifyPropertyChanged(ByVal PropertyName As String)
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(PropertyName))
End Sub
Property Bar As UInt 16
Get
Return _bar
End Get
Set(value As Byte)
If Not (value = _bar) Then
_bar = Bar
NotifyPropertyChanged("Bar")
End If
End Set
End Property
Property Baz As UInt 16
Get
Return _baz
End Get
Set(value As Byte)
If Not (value = _baz) Then
_baz = Baz
NotifyPropertyChanged("Baz")
End If
End Set
End Property
End Class
One way to have changes in the collection reflected in bound controls, is to "reset" the DataSource:
FooList.Add(New Foo(...))
dgv1.DataSource = Nothing
dgv1.DataSource = FooList
If the control is something like a ListBox, you have to also reset the DisplayMember and ValueMember properties because they get cleared. This is a greasy way to notify controls of changes to the list because many things get reset. In a ListBox for instance, the SelectedItems collection is cleared.
A much better way to let changes to your collection flow thru to controls is to use a BindingList(Of T). Changes to the collection/list (adds, removes) will automatically and instantly be shown in your control.
INotifyPropertyChanged goes one step further. If a property value on an item in the list changes, the BindingList<T> will catch the PropertyChanged events your class raises and "forward" the changes to the control.
To be clear, the BindingList handlea changes to the list, while INotifyPropertyChanged handles changes to the items in the list.
FooList(13).Name = "Ziggy"
Using a List<T> the name change won't show up unless you "reset" the DataSource. Using a BindingList<T> alone, it wont show up right away - when the list changes, it should show up. Implementing INotifyPropertyChanged allows the change to show up right away.
Your code is mostly correct for it, except it is not INotifyChanged. But there is also a shortcut as of Net 4.5:
Private Sub NotifyChange(<CallerMemberName> Optional propname As String = "")
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propname))
End Sub
CallerMemberName is an attribute which allows you to forego actually passing the name; the named ends up being substituted at runtime:
Private _name As String
Public Property Name As String
Get
Return _name
End Get
Set(value As String)
If _name <> value Then
_name = value
NotifyChange()
End If
End Set
End Property
If nothing else it can cut down on copy/paste errors if there are lots of properties to raise events for (ie Bar property using NotifyChange("Foo") because you copied the code from that setter).
I figured I can push the data around the other way. That is, instead of updating the data in the list and then trying to update the controls, I update the control and the List data is updated via the binding.
E.g. My click event from above now becomes:
Private Sub Button1_Click(sender As System.Object, e As System.EventArgs) Handles Button1.Click
Label1.Text = TextBox1.Text
DataGridView1.Refresh()
End Sub
Though TBH Im not a fan of that and I'm still puzzled as to how I could better use the INotifyPropertyChanged interface .

How to change programmatically cell values of bound DataGridView without receiving exceptions?

I have problems trying to change programmatically the content of a cell of a bound DataGridView.
I implemented a minimal piece of code to show the problem.
Do the following steps to replicate the problem:
Launch example
Write the title content to create a new row
CTRL+C on inserted title
Move to grid's empty row to force the creation of a new row
CTRL+V on title cell
Click on previous row (new row creation is cancelled)
Click again to the empty row to force the creation of a new row
Exception: Operation is not valid due to the current state of the object.
Here it is the code:
Public Class Form1
Private _dgv As New DataGridView
Private _Movies As New System.ComponentModel.BindingList(Of Movie)
Public Sub New()
InitializeComponent()
Me.Controls.Add(_dgv)
_dgv.Dock = DockStyle.Fill
_dgv.DataSource = _Movies
AddHandler _dgv.KeyDown, AddressOf DataGridView_KeyDown
End Sub
Private Sub DataGridView_KeyDown(sender As Object, e As KeyEventArgs)
If e.Control AndAlso e.KeyCode = Keys.V Then
_dgv.CurrentCell.Value = Clipboard.GetText
End If
End Sub
Public Class Movie
Public Property Title As String
End Class
End Class
For sure there is something wrong in my implementation but I spent many hours searching a workaround without success. Thank you in advance for any help you can give me.
When setting up the form in the constructor, make sure the EditMode of the DataGridView is DataGridViewEditMode.EditOnEnter. This makes the cell we are pasting to enter edit mode as soon as it receives focus and makes the new row stick instead of being cancelled if we move away from it.
Public Sub New()
InitializeComponent()
Me.Controls.Add(_dgv)
_dgv.Dock = DockStyle.Fill
_dgv.DataSource = _Movies
AddHandler _dgv.KeyDown, AddressOf DataGridView_KeyDown
_dgv.EditMode = DataGridViewEditMode.EditOnEnter
End Sub
Then, instead of setting the cell value, set the Title property of the underlying Movie:
Private Sub DataGridView_KeyDown(ByVal sender As Object, ByVal e As KeyEventArgs)
If e.Control AndAlso e.KeyCode = Keys.V Then
Dim Mov As Movie = _Movies.Last
Mov.Title = Clipboard.GetText
_dgv.Refresh()
End If
End Sub
When you add a new row in the DataGridView, the BindingList is automagically adding a new Movie object to the list. _Movies.Last should gets you the newly added Movie.
Why not use the DataGridView event UserAddedRow?
system.windows.forms.datagridview.useraddedrow