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 .
Related
I'm building a form that has many buttons, all buttons do the same thing: add 1 every time they are clicked. Every pressed button is sent to a datagridview along with the time they are pressed. Datagrid values look like this:
a_1_serv (button name), 18:05:00(time).
Sometimes I want to delete the last row. Everything works fine so far.
When I delete the last row, I want to change the text of the button (a_1_serv).
I can parse the dgv value (a_1_serv) to a variable but I can't bind it to the appropriate button name so I can control it.
Is there a way to do it?
Don't store your program state in your UI
Create a data structure to hold the information, and let the DataGridView be a "view", not treating it as a variable. You will save yourself headaches vs using the UI as a variable.
That said, create a class to represent your information
Public Class Data
Public Sub New(button As Button, time As DateTime)
Me.Button = button
Me.Time = time
End Sub
<System.ComponentModel.Browsable(False)>
Public Property Button As Button
Public ReadOnly Property Text As String
Get
Return Button.Name
End Get
End Property
Public Property Time As DateTime
End Class
And your code can manipulate the data in a variable off the UI. Bind the data to the DataGridView for display.
Private datas As New List(Of Data)()
Private Sub Button_Click(sender As Object, e As EventArgs) Handles Button1.Click, Button2.Click, Button3.Click, Button4.Click
addButton(DirectCast(sender, Button))
End Sub
Private Sub RemoveLastButton_Click(sender As Object, e As EventArgs) Handles RemoveLastButton.Click
removeLast()
End Sub
Private Sub addButton(b As Button)
datas.Add(New Data(b, DateTime.Now))
bindData()
End Sub
Private Sub removeLast()
Dim b = datas.Last.Button
b.Text = "new text" ' change to whatever
datas.RemoveAt(datas.Count - 1)
bindData()
End Sub
Private Sub bindData()
DataGridView1.DataSource = Nothing
DataGridView1.DataSource = datas
End Sub
This does exactly what you stated but there may be inconsistency in these two bits of information you provided: a_1_serv (button name) and I want to change the text of the button .... This changes the button text but not the name. The name is displayed in the grid. You can change the data class to display the text or whatever. But the point is this approach will keep your data off the UI and you won't need to look up the control by name anymore.
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
Hello, can someone help me with my project that I'm working on? Anything is appreciated!
So, what do I need? Its simple, I need link and name bars to actually create a hyperlink into the LSB listbox. Can someone please help me??
PS: Could you please keep it simple for me, I just started with this language.
OK This isn't as simple as I would like it to be for you but here goes. I'll try to explain as I go along
OK all the bits code below goes in your Form1 class as one lump - without my explanations that is. What I've done here is create a new class called Hyperlink
It has a couple of properties called Name and Link and a constructor called New
Class HyperLink
Private friendlyName As String
Private link As String
Public Property Name As String
Get
Return friendlyName
End Get
Set(ByVal value As String)
friendlyName = value
End Set
End Property
Public Property URL As String
Get
Return link
End Get
Set(value As String)
link = URL
End Set
End Property
Public Sub New(nm As String, ln As String)
friendlyName = nm
link = ln
End Sub
End Class
Here I'm creating list called linkList - this will hold the list of hyperlinks - I'm doing this so that later on the listbox will be set to use this as it's source of list items
Dim linklist As New List(Of HyperLink)
This sub called addLinkToListbox actually does several things. The important bits are that it tells you program to temporarily stop responding when the index of the listbox changes(which happens even when you're changing the contents off the listbox)
Then it adds a new **hyperlink* to the list of hyperlinks taking data from the link textbox and the name textbox.
To actually refresh the data shown in the textbox, I have to change the listbox datasource to nothing and then back to to the linkList
The next two lines tell the listbox to display the Name property inn the list box and when the item is actually clicked, to return the URL property.
Finally, I tell the program to start responding again when the index of the listbox index changes.
Private Sub addLinkToListbox(linkName As String, linkURL As String)
RemoveHandler ListBox1.SelectedIndexChanged, AddressOf ListBox1_SelectedIndexChanged
linklist.Add(New HyperLink(linkName, linkURL))
ListBox1.DataSource = Nothing
ListBox1.DataSource = linklist
ListBox1.DisplayMember = "Name"
ListBox1.ValueMember = "URL"
AddHandler ListBox1.SelectedIndexChanged, AddressOf ListBox1_SelectedIndexChanged
End Sub
These last two bits of code are hopefully self-explanatory
Private Sub btnAddLink_Click(sender As Object, e As EventArgs) Handles btnAddLink.Click
addLinkToListbox(txtName.Text, txtLink.Text)
End Sub
Private Sub ListBox1_SelectedIndexChanged(sender As Object, e As EventArgs) Handles ListBox1.SelectedIndexChanged
MessageBox.Show(ListBox1.SelectedValue.ToString)
End Sub
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.
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