Linking Object to UI - vba

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.

Related

Is it possible to change the appearance of a custom class's object in the VBA editor's locals and watch windows? [duplicate]

Although an experienced VBA programmer it is the first time that I make my own classes (objects). I am surprised to see that all properties are 'duplicated' in the Locals Window. A small example (break at 'End Sub'):
' Class module:
Private pName As String
Public Property Let Name(inValue As String)
pName = inValue
End Property
Public Property Get Name() As String
Name = pName
End Property
' Normal module:
Sub Test()
Dim objTest As cTest
Set objTest = New cTest
objTest.Name = "John Doe"
End Sub
Why are both Name and pName shown in the Locals Window? Can I in some way get rid of pName?
As comments & answers already said, that's just the VBE being helpful.
However if you find it noisy to have the private fields and public members listed in the locals toolwindow, there's a way to nicely clean it up - here I put the Test procedure inside ThisWorkbook, and left the class named Class1:
So what's going on here? What's this?
Here's Class1:
Option Explicit
Private Type TClass1
Name As String
'...other members...
End Type
Private this As TClass1
Public Property Get Name() As String
Name = this.Name
End Property
Public Property Let Name(ByVal value As String)
this.Name = value
End Property
The class only has 1 private field, a user-defined type value named this, which holds all the encapsulated data members.
As a result, the properties' underlying fields are effectively hidden, or rather, they're all regrouped under this, so you won't see the underlying field values unless you want to see them:
And as an additional benefit, you don't need any pseudo-Hungarian prefixes anymore, the properties' implementations are crystal-clear, and best of all the properties have the exact same identifier name as their backing field.
All the Inspection windows not only show the public interface of the objects to you, but also their private members. AFAIK there is nothing you can do about it.
Consider it a nice feature to get even more insights while debugging.
In my experience this is less of an issue in real world objects as they tend to have more fields and properties. Assuming a consistent naming (as your example shows), fields and properties are nicely grouped together.
If you really dont want to see even Mathieu's This you could wrap it into a function. This is a bit more involved, and can be achieved using
a second class that stores the data in public variables. This will be marginally slower then Mattieu's implementation
a collection object that accesses the data using keys. This does not require additional clutter in the project exporer's 'class module' list but will be a little slower if you call the This repeatedly in fast sucession
An example for each is given below. If you break in the Class's Initialisation function, you can add me to the watch window and only the Name property will be listed
Using 2 Objects example
insert a classmodule and name it: InvisibleObjData
Option Explicit
Public Name As String
Public plop
Private Sub Class_Initialize()
Name = "new"
plop = 0
End Sub
insert a classmodule and name it: InvisibleObj
Option Explicit
Private Function This() As InvisibleObjData
Static p As New InvisibleObjData 'static ensures the data object persists at successive calls
Set This = p
End Function
Private Sub Class_Initialize()
This.Name = "invisible man": Debug.Print Name
Me.Name = "test": Debug.Print Name
This.plop = 111: Debug.Print This.plop
End Sub
Property Let Name(aname As String): This.Name = aname: End Property
Property Get Name() As String: Name = This.Name: End Property
'_______________________________________________________________________________________
' in the immediate window type
'
' set x=new invisibleObj
If you dont like splitting the class over two objects, a similar behaviour can be generated using a 'wrapped' collection object:
insert a classmodule and name it: InvisibleCol
Option Explicit
Private Function This() As Collection
Static p As New Collection
'static ensures the collection object persists at successive calls
'different instances will have different collections
'note a better dictionary object may help
Set This = p
End Function
Private Function add2this(s, v)
'a better dictionary object instead of the collection would help...
On Error Resume Next
This.Remove s
This.Add v, s
End Function
Private Sub Class_Initialize()
add2this "name", "invisible man": Debug.Print Name
Me.Name = "test": Debug.Print Name
add2this "plop", 111
Debug.Print This("plop") ' use the key to access your data
Debug.Print This!plop * 2 ' use of the BANG operator to reduce the number of dbl quotes
' Note: This!plop is the same as This("plop")
End Sub
Property Let Name(aname As String): add2this "name", aname: End Property
Property Get Name() As String: Name = This!Name: End Property
'_______________________________________________________________________________________
' in the immediate window type
'
' set x=new invisibleCol

ToString not updating on object when altering ListBox in VB

I have a form that alters the content of a class within a list box. The information is updated correctly, but my ToString override on my object doesn't refresh - meaning the old ToString doesn't change. How would I fix this?
Here's my object:
Public Class Destination
Public strDestinationName As String
Public strAddress As String
Public intQuality As Integer
Public intPrice As Integer
Public Overrides Function ToString() As String
Return strDestinationName
End Function
End Class
Here's the code where it should be updated
Dim selectedDestination As Destination
selectedDestination = CType(ListForm.lbNames.SelectedItem, Destination)
selectedDestination.strDestinationName = tbName.Text
selectedDestination.strAddress = tbAddress.Text
selectedDestination.intPrice = cbPrice.SelectedIndex
selectedDestination.intQuality = cbQuality.SelectedIndex
Me.Close()
Regardless of how you add items to a ListBox, it is the ListBox that actually displays the data. In your case, it appears that you are adding Destination objects to the ListBox somehow, given that the SelectedItem is a Destination object. Given that you have written that ToString method, you are presumably relying on that to produce the text that the ListBox displays for each item. You are now expecting to be able to change the value of the strDestinationName field of one of the items and have the ListBox reflect that change. How exactly do you think that is going to happen?
The ToString method has to be called in order to get the new value and who do you think is going to call it? It would be the ListBox that calls it because it is the ListBox that displays the result. When you change that field, you are expecting the ListBox to call your ToString method but why would it do that? What reason has the ListBox got to call that method? It has no knowledge of the change you made so why would it think that it has to get new data?
The solution to your problem is to change your code in some way to notify the ListBox that data has changed so that it knows that it needs to get that new data and display it. There are multiple ways that you could do that.
The simplest option would be to bind your data to the ListBox via a BindingSource and then, when you modify an item, call the ResetItem method or similar of the BindingSource. That will raise an event that is handled by the ListBox and the ListBox then knows that it needs to refresh the data for that item. That is what will prompt the ListBox to call your ToString method and get the new data to display. You would add the BindingSource to the form in the designer and then do the binding where you are currently adding the items, e.g.
Dim destinations As New List(Of Destination)
For i = 1 To 10
Dim d As New Destination
d.strDestinationName = "Destination " & i
destinations.Add(d)
Next
destinationBindingSource.DataSource = destinations
destinationListBox.DataSource = destinationBindingSource
The modification would look something like this:
Dim selectedDestination = DirectCast(destinationBindingSource.Current, Destination)
selectedDestination.strDestinationName = "New Destination"
destinationBindingSource.ResetCurrentItem()
The Current property returns the item currently selected in the bound UI and the ResetCurrentItem method notifies the bound UI to refresh the display of that item.
This is really not the best way to go about it though, given that you have control over the item type. What you ought to do is implement the type using properties rather than fields, get rid of the ToString method that only returns the value of one property and then add a change event to that property:
Public Class Destination
Private _destinationName As String
Public Property DestinationName As String
Get
Return _destinationName
End Get
Set(value As String)
If _destinationName <> value Then
_destinationName = value
OnDestinationNameChanged(EventArgs.Empty)
End If
End Set
End Property
Public Property Address As String
Public Property Quality As Integer
Public Property Price As Integer
Public Event DestinationNameChanged As EventHandler
Protected Overridable Sub OnDestinationNameChanged(e As EventArgs)
RaiseEvent DestinationNameChanged(Me, e)
End Sub
End Class
You can now bind a list of Destination objects directly and specify any of those properties as the DisplayMember to have that property value displayed:
Dim destinations As New List(Of Destination)
For i = 1 To 10
Dim d As New Destination
d.strDestinationName = "Destination " & i
destinations.Add(d)
Next
destinationListBox.DisplayMember = "DestinationName"
destinationListBox.DataSource = destinations
You don't need the ToString method because the DisplayMember specifies that the value of the property with that name should be displayed. When you modify the value of the DestinationName property of an item, it will raise the DestinationNameChanged event and that will notify the ListBox that it needs to refresh the display for that item, so you don't need any additional code to make the ListBox update.
That's fine if you only plan to modify existing items. There's still a problem if you want to add and/or remove items after binding though. The List(Of T) class that is used to bind the items to the control in this example does not have any events to notify the control of changes to the list like that. In that case, you can use a BindingSource again if you want. If you add and remove items via the BindingSource then it will raise that appropriate events and the ListBox will update. If you wanted to add and remove via the underlying list then you'd have to call an appropriate method of the BindingSource when you made a change.
An alternative would be to use a BindingList(Of Destination) instead of a List(Of Destination). As the name suggests, the BindingList(Of T) class is made for binding, so it will automatically raise the appropriate events when the list changes to enable the UI to update without extra code from you. Using the combination of property change events in your item class and a BindingList(Of T), you can add, edit and remove items in the bound list and the UI will reflect those changes automatically.

Looping through dictionary of objects in vba

I'm writing code to instantiate a form that shows one record in each instance. I have functions to open and close instances using a dictionary but now I need to check whether a record is already opened.
Dictionaries and collections only allow you to store pairs of data (key/item) so created a class with two properties: form object and the opened record id. I store key and this object in a dictionary.
Now I want to check if a record id is already opened so I have to loop trough the dictionary checking the record id (servicioid in code below) property of the item.
Class module:
Private propFormulario As Form
Private propServicioId As Long
Public Property Let FormObj(frmFormObj As Form)
Set propFormulario = frmFormObj
End Property
Public Property Get FormObj() As Form
Set FormObj = propFormulario
End Property
Public Property Let servicioid(lngServicioId As Long)
propServicioId = lngServicioId
End Property
Public Property Get servicioid() As Long
servicioid = propServicioId
End Property
Open and close instances:
Public dicFormServicios As New Dictionary
Public Sub AbrirServicio(lngServicioId As Long)
Dim ServicioAbierto As clsServiciosAbiertos
Set ServicioAbierto = New clsServiciosAbiertos
ServicioAbierto.FormObj = New Form_servicios2
ServicioAbierto.servicioid = lngServicioId
dicFormServicios.Add CStr(ServicioAbierto.FormObj.hwnd), ServicioAbierto
ServicioAbierto.FormObj.visible = True
End Sub
Public Sub CerrarServicio(InstanciaHwnd As Long)
If dicFormServicios.Exists(CStr(InstanciaHwnd)) Then
dicFormServicios.Remove CStr(InstanciaHwnd)
End If
End Sub
My question is how do I loop trough the dictionary and how do I check an ID is in the servicioid property of any item.
My VBA is a bit rusty, so you're going to want to do something along the lines of...
dicFormServicios.Add myForm.FormId, myForm
Then to recover a value try...
myReturnForm = dicFormServicios.Item("SomeFormName")
Details here...
Dictionary Object
Whether a value exists
Recover an item from the dictionary
(The Dictionary reference above is very useful, but really...) All of the objects that you need are already there. You can reference an object directly from the Forms collection using either an index number...
Set myForm = Forms![0]
...or by using the form's name...
Set myForm = Forms!["myFormName"]
(Such a long time since I've done any of this stuff!)

Passing values across viewmodels

I am new to MVVM and struggling with some concepts, in particular how to pass values between viewmodels and keep them in sync as property values change.
Take the following scenario, where there are three levels of ViewModel.
JobVM - JobItemsVM - ProductListVM
Each VM is exposed as a property of it's parent. The ProductList contains a ClientID property to filter the list of available products by client. The top level job also contains a ClientID property which identifies who the client for the job is.
Now, what I want to do is for the pick list to react to any change in the client of the top level job (i.e. ProductList.ClientID = Job.ClientID).
I am using MVVMLight. Is using messaging an appropriate solution?
Below is some cut-down code of the classes involved which hopefully helps explain how I currently have things. The question is how can I "synchronise" the JobViewModel.CoreDataEntity.ClientID property with the PickList.FilterClientID property, and trigger PickList.LoadList(true) if JobViewModel.CoreDataEntity.ClientID is changed by the user. Or should I be approaching the whole thing differently?
CODE:
This is the business object representing a job, which includes a ClientID property which is the client assigned to the job
Public Class Job
Implements BLL.Base.ICoreDataEntity
Public Property JobID Implements BLL.Base.ICoreDataEntity.RecordID
Public Property ClientID
End Class
This is the primary view model for the job form, used to display and edit a job and its related records.
Public Class JobViewModel
'THIS PROPERTY HOLDS THE JOB BUSINESS OBJECT
Public Property CoreDataEntity As BLL.Base.ICoreDataEntity
'THIS CONTAINS ADDITIONAL VIEWMODELS USED TO MANAGE SUBLISTS (E.G. JOB ITEMS, COMMUNICATIONS)
Public Property SecondaryDataEntities As List(Of SecondaryDataVMHelper)
Public Sub New(recordID As Integer, enableEdit As Boolean)
'POPULATE THE COREDATAENTITY WITH A JOB FROM THE DATABASE
CoreDataEntity = New BLL.Jobs.Job(recordID)
'initialize secondary view models
Me.SecondaryDataEntities.Add(New SecondaryDataVMHelper(EntityTypes.JobItem))
Me.SecondaryDataEntities.Add(New SecondaryDataVMHelper(EntityTypes.Communication))
'pass down selected details from parent record to secondary view models
'NOTE - I COULD PASS DOWN THE CLIENT ID HERE AS WELL BUT WHAT IF CLIENTID CHANGES AFTER INITIALIZED?
For Each x As SecondaryDataVMHelper In SecondaryDataEntities
x.ParentRecordID = CoreDataEntity.RecordID
x.ParentEntityType = EntityTypes.Job
x.ParentRecordDescription = CoreDataEntity.ToString()
Next
End Sub
End Class
This viewmodel is used to manage a "sub list" related to a parent jobs (e.g. line items on a job)
Public Class SecondaryDataVMHelper
'THIS PROPERTY TELLS US WHAT TYPE OF RECORD WE ARE DEALING WITH IN THE SUB LIST
'FOR INSTANCE, JOB ITEMS
Public Property EntityType As EntityTypes
'AN ADDITIONAL VIEWMODEL TO MANAGE A "PICK LIST" WHICH CAN BE USED TO ADD ITEMS
'FOR INSTANCE, A PRODUCT LIST
Public Property PickList As PickListViewModel
Public Sub New(entityType As EntityTypes)
'SET UP WHAT TYPE OF DATA WE ARE DEALING WITH
Me.EntityType = entityType
'INITIALIZE THE APPROPRIATE PICK LIST
PickList = New PickListViewModel()
End Sub
End Class
And finally this viewmodel is used to manage a "pick list" displayed to the user
Public Class PickListViewModel
'THE CLIENT TO FILTER THE PICK LIST BY (E.G. ONLY SHOW PRODUCTS RELATING TO THAT CLIENT)
Public Property FilterClientID As Integer
'THIS STORES THE LIST POPULATED FROM THE DATABASE WHICH IS DISPLAYED IN THE UI
Public Property Records As ObservableCollection(Of Product)
'JUST TO TRACK IF THE PICK LIST HAS BEEN LOADED
Public Property IsLoaded As Boolean
Public Sub LoadList(forceReload As Boolean)
If forceReload = False And IsLoaded Then Exit Sub
Records = BLL.Products.GetLiveList(FilterClientID) 'LOAD THE DATA, FILTERING BY CLIENT
IsLoaded = True
End Sub
End Class

Why can't I bind a list of custom objects to datagridview?

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.