Why can't I bind a list of custom objects to datagridview? - vb.net

I have looked through your questions as well as elsewhere on the internet for the past two hours and cannot find a solution for my problem anywhere, or at least I didn't understand it if I did. I apologize in advance if this appears redundant or inane. Let me be clear: the issue is that I am somehow NOT implementing the approach correctly, but I understand (or think I do) how it is supposed to be done.
I have a gridview on a form in which I want to display custom objects representing appointments. I want to bind to my appointment objects not a datatable (which was successful). However, the below approach will not display my appointment objects in the grid although it appears correct. Furthermore, adding objects directly to the bindingsource's internal list also fails to show them in the grid, as does setting the datasource of the gridview to the bindinglist directly. I have no idea what I am doing wrong! Please help, this seems to make no sense at all and is driving me crazy.
Public Sub DisplayItems()
Dim bindingsource As BindingSource
Dim appointment As ClsAppointment
Dim appointments As System.ComponentModel.BindingList(Of ClsAppointment)
Dim iterator As IEnumerator
appointments = New System.ComponentModel.BindingList(Of ClsAppointment)
bindingsource = New BindingSource
iterator = Items
While iterator.MoveNext '
appointment = iterator.Current
appointments.Add(appointment)
End While
bindingsource.DataSource = appointments
gridview.DataSource = bindingsource
Debug.Print("")
Debug.Print("DisplayItems()...")
Debug.Print("GridView has " & gridview.Rows.Count & " rows")
End Sub
Public Class ClsAppointment
Public FirstName As String
Public LastName As String
Public Day As String
Public [Date] As Date
Public Time As Date
Public Address As String
Public City As String
Public State As String
Public Zip As String
Public Description As String
End Class
========================================================================================
Note: DisplayItems() is a method of an adapter (ItemEditor) which I chose not to show for simplicity's sake. Another method (Items) returns the adapter's collection of items (appointments) via an enumerator. I have tested this and know that the enumerator is returning the items so the problem is not this.

You can not bind to public fields of an object. As Microsoft states "You can bind to public properties, sub-properties, as well as indexers, of any common language runtime (CLR) object." Msdn- Binding Sources Overview.
Change your ClsAppointment class to this :
Public Class ClsAppointment
Property FirstName As String
Property LastName As String
Property Day As String
Property [Date] As Date
Property Time As Date
Property Address As String
Property City As String
Property State As String
Property Zip As String
Property Description As String
End Class

Allow me to simplify your code:
Public Sub DisplayItems()
gridview.DataSource = Me.Items()
Debug.Print("")
Debug.Print("DisplayItems()...")
Debug.Print("GridView has " & gridview.Rows.Count & " rows")
End Sub
Try this, and let us know what errors you get. I know you might eventually need the BindingSource, but for the moment let's cut that out of the picture and see how things work.

Related

VBA Class with Collection of itself

I'm trying to create a class with a Collection in it that will hold other CASN (kind of like a linked list), I'm not sure if my instantiation of the class is correct. But every time I try to run my code below, I get the error
Object variable or With block not set
CODE BEING RUN:
If (Numbers.count > 0) Then
Dim num As CASN
For Each num In Numbers
If (num.DuplicateOf.count > 0) Then 'ERROR HERE
Debug.Print "Added " & num.REF_PO & " to list"
ListBox1.AddItem num.REF_PO
End If
Next num
End If
CLASS - CASN:
Private pWeek As String
Private pVendorName As String
Private pVendorID As String
Private pError_NUM As String
Private pREF_PO As Variant
Private pASN_INV_NUM As Variant
Private pDOC_TYPE As String
Private pERROR_TEXT As String
Private pAddressxl As Range
Private pDuplicateOf As Collection
'''''''''''''''' Instantiation of String, Long, Range etc.
'''''''''''''''' Which I know is working fine
''''''''''''''''''''''
' DuplicateOf Property
''''''''''''''''''''''
Public Property Get DuplicateOf() As Collection
Set DuplicateOf = pDuplicateOf
End Property
Public Property Let DuplicateOf(value As Collection)
Set pDuplicateOf = value
End Property
''''' What I believe may be the cause
Basically what I've done is created two Collections of class CASN and I'm trying to compare the two and see if there are any matching values related to the variable .REF_PO and if there is a match I want to add it to the cthisWeek's collection of class CASN in the DuplicateOf collection of that class.
Hopefully this make sense... I know all my code is working great up to this point of comparing the two CASN Collection's. I've thoroughly tested everything and tried a few different approaches and can't seem to find the solution
EDIT:
I found the error to my first issue but now a new issue has appeared...
This would be a relatively simple fix to your Get method:
Public Property Get DuplicateOf() As Collection
If pDuplicateOf Is Nothing Then Set pDuplicateOf = New Collection
Set DuplicateOf = pDuplicateOf
End Property
EDIT: To address your question - "So when creating a class, do I want to initialize all values to either Nothing or Null? Should I have a Class_Terminate as well?"
The answer would be "it depends" - typically there's no need to set all your class properties to some specific value: most of the non-object ones will already have the default value for their specific variable type. You just have to be aware of the impact of having unset variables - mostly when these are object-types.
Whether you need a Class_Terminate would depend on whether your class instances need to perform any "cleanup" (eg. close any open file handles or DB connections) before they get destroyed.

Linking Object to UI

What is the best method to use to link a class to a user form?
Sorry to be hypothetical, but using actual code will lead to a question that is pages long.
Let's say I have a class that holds data about a person.
<<class Person>>
Public FirstName as String
Public LastName as String
Public PhoneNumber as String
<<end class>>
I put that data into a VBA UserForm listview.
Now, let's say I want to change the phone number to 555-555-1234 if the user clicks on that record in the listview.
I can read the interaction with the listview with the item click event.
Private Sub lvExample_ItemClick(ByVal Item As MSComctlLib.ListItem)
' Change the phone number
End Sub
What is the best way to get from Item in the above code to my actual object? Should I add an GUID to each object and put that in the tag of the listitem, then look it up? Should I add the listitem from the listview into the class so I can loop through all my people and then see if the Item from _ItemClick equals the Item from the object?
The easiest way is to use either the Index property, if you don't have any unique identifier, or the Key property if you do, of the ListItem.
If you choose to use the Index property, then you can't (or at least it will greatly complicate it) add any functionality to rearrange the order of list items.
You would have populated the ListView based on the objects in a collection/recordset via the ListView.ListItems.Add method. You can use the Index property to get back to that original object based on the order of items in ListItems corresponding to the order of items in your original collection of objects.
Alternatively, if you prefer the greater flexibility of using a unique key but don't wish to modify the underlying object, then you can trivially construct a unique key (the simplest being CStr(some incrementing number)) as you add each object to ListItems, storing the keys alongside the objects.
You can then use the .Key property of the ListItem. The benefit here is the user can be allowed to modify what items are in, delete stuff etc without you having to invalidate your control and re-add all objects in order to keep the linkage between Index in source and index in the list.
E.g.:
Private persons As Collection
Private Sub lvExample_ItemClick(ByVal Item As MSComctlLib.ListItem)
' Change the phone number:
'Method 1, using the index of listitem within listitems
'to tie back to index in persons collection
'Don't allow rearranging/sorting of items with this method for simplicity
With persons.Item(Item.Index)
.PhoneNumber = "555-555-1234"
'...some stuff
End With
'Method 2, using a unique key
'that you generate, as the underlying person object doesn't have a necessarily unique one
'Items would have been added in a method similar to AddItemsExample1() below
With persons.Item(Item.Key)
.PhoneNumber = "555-555-1234"
'...some stuff
End With
End Sub
Sub AddItemsExample1()
'Storage requirements vs. your existing recordset or whatever
'are minimal as all it is storing is the reference to the underlying
'person object
'Adapt per how you have your existing objects
'But basically get them into a keyed collection/dictionary
Dim i As Long
Set persons = New Collection
For Each p In MyPersonsSource
persons.Add p, CStr(i)
i = i + 1
Next p
'By keying them up, you can allow sorting / rearranging
'of the list items as you won't be working off of Index position
End Sub
Finally, another way if you have them in a recordset returned by a DB is to add a new field (I imagine you have an existing object field) and do run an UPDATE query on your records populating it with an incrementing number (this should only effect the local recordset (check the recordset settings first of course!)). Use this as the key.
You mention in a comment to your question that you get the data from SQL. For all normal purposes with a list box it is still probably easiest to just run them through a Collection object as detailed above, but if you have e.g. 4 fields from SQL for a record in a recordset then you don't hav an 'object' in the sense of being able to call properties on it. Please specify in your question or in a comment to this if you do so, as there may be a better treatment to answer your question or the actual update operation will likely require different syntax or semantics (particularly if you need to propagate any update back to the source) :)
I would use a event driven approach.
Person will have properties instead of variables and assigning those properties will raise a event.
User form will have WithEvents Person, so that changes in related person will trigger user form code.
Person class:
Public Event Changed()
Private pFirstName As String
Private pLastName As String
Private pPhoneNumber As String
Public Property Get FirstName() As String
FirstName = pFirstName
End Property
Public Property Let FirstName(ByVal v As String)
pFirstName = v
RaiseEvent Changed
End Property
Public Property Get LastName() As String
LastName = pLastName
End Property
Public Property Let LastName(ByVal v As String)
pLastName = v
RaiseEvent Changed
End Property
Public Property Get PhoneNumber() As String
PhoneNumber = pPhoneNumber
End Property
Public Property Let PhoneNumber(ByVal v As String)
pPhoneNumber = v
RaiseEvent Changed
End Property
Event catching in user form:
Public WithEvents ThisPerson As Person
Private Sub ThisPerson_Changed()
'Update user form
End Sub
So whenever you assign YourForm.RelatedObject = SomePerson, any changes done to SomePerson will trigger code in YourForm.
Since you can't change returned ListItem, I have another solution:
Add a related ListItem to your Person class:
Public FirstName as String
Public LastName as String
Public PhoneNumber as String
Public RelatedObject As Object
When populating TreeView, assign it:
Set SomeListItem = SomeTreeView.ListItems.Add(...)
Set SomePerson.RelatedObject = SomeListItems
So whenever you have a change in a ListItem:
Private Sub SomeListView_ItemClick(ByVal Item As MSComctlLib.ListItem)
Dim p As Person
For Each p In (...)
If p.RelatedObject Is Item Then
'Found, p is your person!
p.PhoneNumber = ...
End If
Next
End Sub
Alternatively, if you don't want to change your Person class, you can encapsulate it in another class, eg, PersonToObject:
Public Person As Person
Public RelatedObject As Object
And use PersonToObject as link objects.

Trying to trouble Object required error in VBA

I got this problem. I have a form that retrieves a table data using the forms' record source property. When the form's opened, I set its record source property to a module's public method RetrieveMembers. Here's the code below.
Private Sub Form_Open(Cancel As Integer)
'set Form's record source property to retrieve a Members table
Me.RecordSource = mod_JoinMember.RetrieveMembers
End Sub
'mod_JoinMember Class
Public Function RetrieveMembers() As String
Dim strSQL As String
Set strSQL = "SELECT tbl_Member.Title, tbl_Member.Gender, tbl_Member.LastName,
tbl_Member.DateofBirth, tbl_Member.Occupation, tbl_Member.PhoneNoWork,
tbl_Member.PhoneNoHome, tbl_Member.MobileNo, tbl_Member.Email,
tbl_Member.Address, tbl_Member.State, tbl_Member.Postcode FROM tbl_Member;"
RetrieveMembers = strSQL
End Function
Object required error is thrown.
I couldn't comprehend this compile error. I see no wrong with my code since recordsource is a String type property. And my module's function Retrievemembers is returning a String value.
Why is it that it's not satisfied with this?
Thanks for your help.
I fixed it. The reason was because String is not really an Object to begin with. So the 'Set' keyword is not needed - since you don't need to explicitly declare String-type objects anyway!
All good now!
As you are working with a class module I think you will need to use:
Public Property Get RetrieveMembers() As String
Rather than:
Public Function RetrieveMembers() As String

Invalid cast exception

I have a simple application to store address details and edit them. I have been away from VB for a few years now and need to refreash my knowledge while working to a tight deadline. I have a general Sub responsible for displaying a form where user can add contact details (by pressing button add) and edit them (by pressing button edit). This sub is stored in a class Contact. The way it is supposed to work is that there is a list with all the contacts and when new contact is added a new entry is displayed. If user wants to edit given entry he or she selects it and presses edit button
Public Sub Display()
Dim C As New Contact
C.Cont = InputBox("Enter a title for this contact.")
C.Fname = frmAddCont.txtFName.Text
C.Surname = frmAddCont.txtSName.Text
C.Address = frmAddCont.txtAddress.Text
frmStart.lstContact.Items.Add(C.Cont.ToString)
End Sub
I call it from the form responsible for adding new contacts by
Dim C As New Contact
C.Display()
and it works just fine. However when I try to do something similar using the edit button I get errors - "Unable to cast object of type 'System.String' to type 'AddressBook.Contact'."
Dim C As Contact
If lstContact.SelectedItem IsNot Nothing Then
C = lstContact.SelectedItem()
C.Display()
End If
I think it may be something simple however I wasn't able to fix it and given short time I have I decided to ask for help here.
I have updated my class with advice from other members and here is the final version (there are some problems however). When I click on the edit button it displays only the input box for the title of the contact and actually adds another entry in the list with previous data for first name, second name etc.
Public Class Contact
Public Contact As String
Public Fname As String
Public Surname As String
Public Address As String
Private myCont As String
Public Property Cont()
Get
Return myCont
End Get
Set(ByVal value)
myCont = Value
End Set
End Property
Public Overrides Function ToString() As String
Return Me.Cont
End Function
Sub NewContact()
FName = frmAddCont.txtFName.ToString
frmStart.lstContact.Items.Add(FName)
frmAddCont.Hide()
End Sub
Public Sub Display()
Dim C As New Contact
C.Cont = InputBox("Enter a title for this contact.")
C.Fname = frmAddCont.txtFName.Text
C.Surname = frmAddCont.txtSName.Text
C.Address = frmAddCont.txtAddress.Text
'frmStart.lstContact.Items.Add(C.Cont.ToString)
frmStart.lstContact.Items.Add(C)
End Sub
End Class
You're doing
frmStart.lstContact.Items.Add(C.Cont.ToString)
That is, you are adding strings to lstContact. Of course lstContact.SelectedItem() is a string!
Since you didn't post all of your code, I'll give you my best guess which is:
lstContact seems like it is bound to an IEnumerable of strings instead of IEnumerable of Contacts.
Therefore, when you get lstContact.SelectedItem() it is returning a string instead of a Contact (and throwing an error when trying to cast string as Contact).
It'd be more helpful if you could post the code bit that binds lstContact to a data source.

How to set the text of list items in Windows Forms

I am trying to figure out how to set the value of the text that is displayed for each item of a list control, such as a checkbox list, but really this applies to most list controls, not just the checklistbox control.
I have a checklistbox control,
Friend WithEvents clstTasks As System.Windows.Forms.CheckedListBox
that I usually want to populate with a list of task names. I call the add method to add a Task object to the list. I know that if I override the ToString method, whatever value is returned by that function will be displayed as the text for the list item.
However, in rare situations, I want to display something else other than just the name. For example, perhaps I want to display the name and the value of another property, such as the value of the Boolean property "Optional" shown in parenthesis following the name.
What is the best way to do this?
The best that I can think of is to define a property which is set in the GUI layer and then used by the ToString function to determine how it should behave when called. If the controlling property is set to one value, the ToString will return the Name, else it will return the Name followed by the value of the Optional flag. This seems a little disjoint a kludged to me.
The other alternative which seems a little overkill is to define a new class which inherits from Task, for example TaskOptional, which overrides the Tostring method on the base class. In this subclass, the ToString function would return the Name/Optional flag value. However, this, too, seems a little nuts to have to come up with a new class just to modify how the text is being displayed in the presentation layer. I feel that I should be able to control this in the presentation layer without changing the business object or creating a new derived object.
What is the best way to accomplish this?
For Each CurrentTask As Task In _MasterTaskList
clstTasks.Items.Add(CurrentTask, True)
Next
Public Class Task
Private _Name As String
Private _Optional As Boolean
Public Sub New (name As String, optional As Boolean)
_Name = name
End Sub
Public Overrides Function ToString() As String
Return _Name
End If
End Function
End Class
You can set the DisplayMember property of your CheckedListBox to the name of one of your custom class' property.
Let's say you create a property like the following:
Public ReadOnly Property NameOptional() As String
Return _Name & " (" & _Optional & ")"
End Property
then you can set the display member like this:
clstTasks.DisplayMember = "NameOptional"
When you set a display member, this property will be displayed instead of the ToString value.
You could do the following
Public Overrides Function ToString() As String
Return string.Format("{0}{1}", _Name, IIF(_Optional, " (Optional)", ""))
End Function
EDIT: You will have to set the value of _optional in the constructor, which is missing in the code you have provided.