Datagridview bound to object - Making the new rows appear blank - vb.net

I have a Datagridview that changes its content according to a selection the user makes in a listBox.
The DGV consits of 2 comboboxes (Country, Product) and 1 textbox (Quantity).
I've created a class combined of 3 integers.
This class is used as a type of list, which is the datasource for the DGV.
There is also another list containing the prior list, so I have a list of datasources.
The DGV's datasource is a BindingSource that changes whenever the SelectedIndex of the listBox is fired.
My problem occurs whenever a new row is added to the DGV:
I use the BindingSource.AddNew which calls the constructor of the class, but it must assign values to each item in the class. That way, whenever I click any cell in the DGV I don't get a blank row.
Moreover, when the BS changes and then returned, another row is added.
What I want to get is a blank row - empty comboboxes and textbox.
Thanks for your help!
The class:
Public Class PoList
Private _CountryID As Integer
Private _ProductID As Integer
Private _Quantity As Integer
Public Sub New(ByVal CountryID As Integer, ByVal ProductID As Integer, ByVal Quantity As Integer)
_CountryID = CountryID
_ProductID = ProductID
_Quantity = Quantity
End Sub
Private Sub New()
_CountryID = 1
_ProductID = 2
_Quantity = Nothing
End Sub
Public Property CountryID() As Integer
Get
Return _CountryID
End Get
Set(ByVal value As Integer)
_CountryID = value
End Set
End Property
Public Property ProductID() As Integer
Get
Return _ProductID
End Get
Set(ByVal value As Integer)
_ProductID = value
End Set
End Property
Public Property Quantity() As Integer
Get
Return _Quantity
End Get
Set(ByVal value As Integer)
_Quantity = value
End Set
End Property
Public Shared Function CreateNewPoList() As PoList
Return New PoList
End Function
End Class
Private Sub List_AddRow(ByVal sender As Object, ByVal e As AddingNewEventArgs) Handles AllListBindingSource.AddingNew
e.NewObject = PoList.CreateNewPoList
End Sub
Creating a new inner list:
AllList.Add(New List(Of PoList))
AllListBindingSource.AddNew()
AllListBindingSource.DataSource = AllList(TableCounter)
AddPoDetails.DataSource = AllListBindingSource
SelectedIndexChanged event:
AllListBindingSource.DataSource = AllList(AddPoList.SelectedIndex)
AddPoDetails.DataSource = Nothing
AddPoDetails.DataSource = AllListBindingSource

Right, lets see if I can help you.
As I interpret it you have a list filled with lists. These lists don't know their own identity and is based on the current index in the list.
First of I wouldn't use Bindingsource.AddNew I would add the new object straight to the list instead.
AllList(TableCounter).Add(New Polist())
This way you know exactly how many objects has been created, by using events you aren't quite sure are you.
To refresh the list do this:
AllListBindingSource.ResetBindings(true)
Which will update your DGV with the new line.
Now you need to restructure your class since when you create a new Polist you set a value to nothing. This will crash your table. What you need to do is this:
Private _Quantity As String
Public Property Quantity() As String
Get
Return _Quantity
End Get
Set(ByVal value As String)
_Quantity = value
End Set
End Property
Using a string is the only way for you to get a blank textbox, I would recommend you to have 0 as default if you are using Quantity as an integer (which you should). Your constructor needs to be changed to this:
Private Sub New()
_CountryID = 0
_ProductID = 0
_Quantity = ""
End Sub
In your combobox columns you have to add a blank item in the top (I'm guessing your adding them manually), should be possible by having a blank row in the top of the items.

Related

How can I edit null values in a BindingList when a DataGridView is bound to it?

I am still very new to data binding in Windows Forms with Visual Basic .NET, but trying to get familiar with it. I tried looking for information on this already, but to no avail.
I want to set up two-way binding between a DataGridView control and a list of objects (let's say they are of a made-up type called MyListElementClass), in a manner similar to what I saw in this answer to another question. Below is my implementation for MyListElementClass, in a file called MyListElementClass.vb:
Imports System.ComponentModel
Imports System.Runtime.CompilerServices
<Serializable>
Public NotInheritable Class MyListElementClass
Implements INotifyPropertyChanged
Implements IMyListElementClass
#Region "Fields"
Private _a As UShort
Private _b As Double
Private _c, _d, _e As Boolean
' End fields region.
#End Region
#Region "INotifyPropertyChanged implementation"
Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
Private Sub NotifyPropertyChanged(<CallerMemberName()> Optional ByVal propertyName As String = Nothing)
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
End Sub
' End INotifyPropertyChanged implementation region.
#End Region
#Region "IMyListElementClass implementation"
Public Property PropertyA As UShort Implements IMyListElementClass.PropertyA
Get
Return _a
End Get
Set(value As UShort)
If _a <> value Then
_a = value
NotifyPropertyChanged()
End If
End Set
End Property
Public Property PropertyB As Double Implements IMyListElementClass.PropertyB
Get
Return _b
End Get
Set(value As Double)
If _b <> value Then
_b = value
NotifyPropertyChanged()
End If
End Set
End Property
Public Property PropertyC As Boolean Implements IMyListElementClass.PropertyC
Get
Return _c
End Get
Set(value As Boolean)
If _c <> value Then
_c = value
NotifyPropertyChanged()
End If
End Set
End Property
Public Property PropertyD As Boolean Implements IMyListElementClass.PropertyD
Get
Return _d
End Get
Set(value As Boolean)
If _d <> value Then
_d = value
NotifyPropertyChanged()
End If
End Set
End Property
Public Property PropertyE As Boolean Implements IMyListElementClass.PropertyE
Get
Return _e
End Get
Set(value As Boolean)
If _e <> value Then
_e = value
NotifyPropertyChanged()
End If
End Set
End Property
' End IMyListElementClass implementation region.
#End Region
#Region "Constructors"
Public Sub New()
PropertyA = 0
PropertyB = 0
PropertyC = False
PropertyD = False
PropertyE = False
End Sub
Public Sub New(a As UShort, b As Double, c As Boolean, d As Boolean, e As Boolean)
Me.PropertyA = a
Me.PropertyB = b
Me.PropertyC = c
Me.PropertyD = d
Me.PropertyE = e
End Sub
Public Sub New(other As IMyListElementClass)
If other Is Nothing Then Throw New ArgumentNullException(NameOf(other))
CopyFrom(other)
End Sub
' End constructors region.
#End Region
#Region "Methods"
Public Sub CopyFrom(other As IMyListElementClass)
If other Is Nothing Then Throw New ArgumentNullException(NameOf(other))
With other
PropertyA = .PropertyA
PropertyB = .PropertyB
PropertyC = .PropertyC
PropertyD = .PropertyD
PropertyE = .PropertyE
End With
End Sub
' End methods region.
#End Region
End Class
The idea here is that the DataGridView control will show a list of available "slots" (rows) that instances of MyListElementClass can be entered into. However, some of these slots could be empty, and may need to be filled in or cleared later. The number of rows in the table is specified by a number entered elsewhere, so the user cannot add or remove rows on the fly; They have to work with the space that's given.
My current attempt at this is to have the DataGridView control bound to a BindingList(Of MyListElementClass), where its size is always equal to the number of available slots and empty slots are represented by null elements. However, I found that if I have these null values present in the BindingList(Of MyListElementClass), these rows cannot be edited by the user in the DataGridView control which is bound to it, and I'm not really sure how to handle this.
An example of what I'm trying to do in my user control which contains the DataGridView (named dgvDataGridView here and with columns already set up through the designer):
Public Class MyUserControl
Private _myBindingList As BindingList(Of MyListElementClass)
Public Sub New()
' This call is required by the designer.
InitializeComponent()
' Add any initialization after the InitializeComponent() call.
dgvDataGridView.AutoGenerateColumns = False ' Columns already created through the Visual Studio designer with the ordering and header text I want.
SetUpTableDataBinding()
End Sub
Private Sub SetUpTableDataBinding()
colA.DataPropertyName = NameOf(MyListElementClass.PropertyA)
colB.DataPropertyName = NameOf(MyListElementClass.PropertyB)
colC.DataPropertyName = NameOf(MyListElementClass.PropertyC)
colD.DataPropertyName = NameOf(MyListElementClass.PropertyD)
colE.DataPropertyName = NameOf(MyListElementClass.PropertyE)
Dim initialList As New List(Of MyListElementClass)(Enumerable.Repeat(Of MyListElementClass)(Nothing, 1)) ' First row will contain a null value, and hence be "empty".
_myBindingList = New BindingList(Of MyListElementClass)(initialList)
Dim source = New BindingSource(_myBindingList, Nothing)
dgvDataGridView.DataSource = source
' Some test data for data binding.
_myBindingList.AddNew() ' Adds a new MyListElementClass instance with default property values.
_myBindingList.Add(New MyListElementClass(2345, 7.4, False, True, False)) ' Just some sample values.
End Sub
End Class
After this user control loads, I can see an empty row, a row with default values for the MyListElementClass, and a row with some sample values appear, for three rows total. I can edit the second and third rows, but not the first (any values I enter immediately vanish).
Again, in completely unfamiliar territory here, so bear with me. If I cannot get this to work, then I will abandon this idea and return to manually setting and retrieving data in the DataGridView cells like I've always done up until now.
Null values cannot be edited, replace them with empty strings instead and I think you will find that it will work as intended.

DataGridView Auto-calculate cells In Vb

I want to multiply two columns of a datagridview and show the product in Column 3 of the same datagridview.
Example
Column1 - Column2 - Column3
12 2 24
15 2 30
Here is my Code
Private Sub Table1DataGridView1_CellContentClick(sender As Object, e As DataGridViewCellEventArgs) Handles Table1DataGridView1.CellValidated
Try
Dim iCell1 As Integer
Dim icell2 As Integer
Dim icellResult As Integer
iCell1 = Table1DataGridView1.CurrentRow.Cells(1).Value
icell2 = Table1DataGridView1.CurrentRow.Cells(2).Value
If String.IsNullOrEmpty(iCell1) OrElse String.IsNullOrEmpty(icell2) Then Exit Sub
If Not IsNumeric(iCell1) OrElse Not IsNumeric(icell2) Then Exit Sub
icellResult = CDbl(iCell1) * CDbl(icell2)
Table1DataGridView1.CurrentRow.Cells(3).Value = icellResult
Catch ex As Exception
MessageBox.Show(ex.ToString)
End Try
End Sub
It works but a new row is added afterwards. So please help.
An elegant way is to use Databinding (the bread and butter method with datagridviews):
We create a new class, I call MultiplyPair. This class basically has to changable properties, Value1 and Value2.
It also has a third, readonly property called Product.
Whenever Value1 or Value2 changes, we notify any bindingsource that also the product has changed (by implementing INotifyPropertyChanged).
This has several advantages:
You avoid many quirks of the DGV that may arise during manual edits
It's a great way to learn to use databinding (I'm just beginning myself to use the concept thoroughly and it's amazing)
It's easily adaptable: Want also the product, sum and quotient? Just add properties and simple notifications.
You have a clear structure, so other people can understand your code.
You can reuse the whole logic, just bind another datagridview to the same bindinglist and you can display and change the information at multiple locations, with hardly any code.
To use the class we create a Bindinglist(Of MultiplyPair) and bind this list to the datagridview. Then we just ass values to the list and the datagridview is populated automatically. You can even change the values in the Datagridview and the product is automatically updated.
Public Class Form1
Private WithEvents Values As New System.ComponentModel.BindingList(Of MultiplyPair) 'This holds one object for every line in the DGV
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
Values = New System.ComponentModel.BindingList(Of MultiplyPair) 'Initialize the list
DataGridView1.AllowUserToAddRows = False 'Disallow new rows, as per your question
DataGridView1.DataSource = Values 'Bind the list to the datagridview
'Add two example lines
Values.Add(New MultiplyPair With {.Value1 = 2, .Value2 = 12})
Values.Add(New MultiplyPair With {.Value1 = 15, .Value2 = 2})
End Sub
End Class
Public Class MultiplyPair
Implements System.ComponentModel.INotifyPropertyChanged
'The first value
Private _Value1 As Double
Public Property Value1
Get
Return _Value1
End Get
Set(value)
_Value1 = value
'Notify not only that Value1 has changed, but also that the Product has changed
Notify("Value1")
Notify("Product")
End Set
End Property
'Same as above
Private _Value2 As Double
Public Property Value2
Get
Return _Value2
End Get
Set(value)
_Value2 = value
Notify("Value2")
Notify("Product")
End Set
End Property
'This will show the product of Value1 and Value2 whenever asked
Public ReadOnly Property Product
Get
Return Value1 * Value2
End Get
End Property
'Helper sub to raise the PropertyChanged event with the given Propertyname
Private Sub Notify(name As String)
RaiseEvent PropertyChanged(Me, New System.ComponentModel.PropertyChangedEventArgs(name))
End Sub
'INotifyPropertyChanged implementation
Public Event PropertyChanged(sender As Object, e As System.ComponentModel.PropertyChangedEventArgs) Implements System.ComponentModel.INotifyPropertyChanged.PropertyChanged
End Class
Edit: To avoid additional rows, set the AllowUsersToAddRows property of the Datagridview to false.

Working with bindinglist

I have a datagridview and a bindinglist. They work together pretty ok, but I want to make the properties appear in the rows, not on the column. Is there any way to achieve that ?
My code for anyone who is interested.
Public Class Form1
Dim listaBindingSource As New BindingList(Of pessoa)
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
Dim b1 As New pessoa()
listaBindingSource.Add(b1)
dgv.DataSource = listaBindingSource
End Sub
End Class
Public Class pessoa
Dim sells_Month1 As String
Public Sub New() 'ByVal nome_fora As String)
sells_Month1 = "0"
End Sub
Property vendas1 As String
Get
Return sells_Month1
End Get
Set(value As String)
sells_Month1 = value
End Set
End Property
The other properties are vendas2, vendas3.. and are the same as this one.
Edit:
I´m kind of lost here. What I want is to make the values of the properties of my objects appear on some kind of data visualizer. When I add new objects on the list, they appear on this data visualizer and when I change the values of the cells there, the values of the properties change. Anyone has a good sugestion ? Apparentely dgv is not the way to go.
Thanks,
Ricardo S.
I want to make the properties appear in the rows´ headers, not on
the column.
I'm afraid this is not possible, there is no built-in solution for that in DataGidView. You can display the properties in columns only.
To control the text displayed in the column header, try to set the DisplayName attribut:
<System.ComponentModel.DisplayName("DisplayedText")>
Property vendas1 As String
Get
Return sells_Month1
End Get
Set(value As String)
sells_Month1 = value
End Set
End Property
Or if you import System.ComponentModel namespace.
<DisplayName("DisplayedText")>
Property vendas1 As String
Get
Return sells_Month1
End Get
Set(value As String)
sells_Month1 = value
End Set
End Property

Storing a value for each ComboBox item

In lack of a Value property I planned to use Classes for storing Text and Value properties for my ComboBox items. So far I've succeeded.
Here is my class:
Public Class clCombobox
Public cname As String
Public cvalue As Integer
Public Property Display() As String
Get
Return Me.cname
End Get
Set(ByVal value As String)
Me.cname = value
End Set
End Property
Public Property Value() As String
Get
Return Me.cvalue
End Get
Set(ByVal value As String)
Me.cvalue = value
End Set
End Property
Public Sub New(ByVal name As String, ByVal value As String)
cname = name
cvalue = value
End Sub
Public Overrides Function ToString() As String
Return cname
End Function
End Class
Data is being added to the ComboBox like this:
cmbComboxBox.Items.Add(New clCombobox("Text", 1))
It seems like this works so far. But how do I get data back. Like if I want the value of the selected CheckBox item?
I tried using:
CType(cmbCombobox.SelectedItem, clCombobox).Value()
Did not work.
As per the documentation, use the SelectedItem property to retrieve the object that you stored in it.
Code to retrieve value you want:
Dim selectedItem as clCombobox = CType(cmbComboBox.SelectedItem, clCombobox)
Dim value As Integer = selectedItem.cvalue

Keep two-way binding when whole object has changed

I have a class:
Public Class TestClass
Implements INotifyPropertyChanged
Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
Public Sub OnNotifyChanged(ByVal pName As String)
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(pName))
End Sub
Private _One As Integer
Private _Two As Integer
Public Sub New(ByVal One As Integer, ByVal Two As Integer)
_One = One
_Two = Two
End Sub
Public Property One() As Integer
Get
Return _One
End Get
Set(ByVal value As Integer)
_One = value
OnNotifyChanged("One")
End Set
End Property
Public Property Two() As Integer
Get
Return _Two
End Get
Set(ByVal value As Integer)
_Two = value
OnNotifyChanged("Two")
End Set
End Property
End Class
I can create an instance and bind two TextBoxes to the object:
Dim MyObject As New TestClass(1, 2)
TextBoxOne.DataBindings.Add("Text", MyObject, "One")
TextBoxTwo.DataBindings.Add("Text", MyObject, "Two")
Now I can change the TextBoxes or the object:
MyObject.Two = 3
..the object and TextBoxes are updated in two ways.
Now I want to update the whole object:
MyObject = New TestClass(3, 4)
...but this doesn't update the TextBoxes.
What am I doing wrong?
Your text boxes hold a reference to the first instance of the object you've created.
Now you're creating a second instance, supposedly in order to replace the existing instance, but the text boxes are unaware of the change.
You needs to either:
Pass the new instance to the text boxes (directly, as you assigned the first instance, or indirectly by having some "Model" object that both boxes are bound to).
Update the existing instance instead of replacing it with a new one (you can simply assign values to the fields, or create some "AssignFrom (other)" method, etc.)
Get yourself some other - more orderly - way of notifiying the controls that their underlying data source has changed / should be changed.