Reflection - Iterating Class with properties that are classes - vb.net

I need to iterate through the entire structure of a class. I'm interrogating the objects sent into my WCF methods and I presently have an overload of ToString() in each of my classes that list all properties and their values. This works; but is hardcoded and requires updates each time we add properties to the class.
Current solution is VB but next version will be C# - hence both tags.
The class may be comprised of primitive types only or the class may be comprised of other objects. Iterating through a simple class is not a problem.
I am having trouble identifying when a property is actually another class. So, given the example below, I can iterate through Appointment and Patient and dump the values of each of their properties.
I am stuck trying to iterate through PatientAppointment. I've scoured MSDN and SO, tried countless properties within the type, etc. to no avail.
Public Class PatientAppointment
Public Property Pat As Patient
Public Property Appt As Appointment
End Class
Public Class Appointment
Public Property ApptProp1 As String
Public Property ApptProp2 As Integer
End Class
Public Class Patient
Public Property PatProp1 As String
Public Property PatProp2 As Integer
End Class

It sounds like maybe you are looking for a way to identify your Types with some level of certainty. One way might be to use a custom attribute which will be easy to find and filter using Reflection.
Public Class RealMcCoy
Inherits Attribute
Public Property IsReal As Boolean
Public Sub New(b as Boolean)
IsReal = b
End Sub
End Class
<RealMcCoy(True)>
Public Class Patient
...
End Class
<RealMcCoy(True)>
Public Class Appointment
...
End Class
Now when iterating properties, check if it is the RealMcCoy to see if it is one you want/need to drill into. Since the Attribute will be on the Type, there is an extra step to get each property's Type and poll that
Dim props As PropertyInfo() = myFoo.GetType.GetProperties
Dim pt As Type
For Each pi As PropertyInfo In props
pt = pi.PropertyType ' get the property type
Dim attr() As RealMcCoy =
DirectCast(pt.GetCustomAttributes(GetType(RealMcCoy), True), RealMcCoy())
If attr.Length > 0 Then
' bingo, baby...optional extra test:
If attr(0).IsReal Then
Console.Beep()
End If
Else
' skip this prop - its not a real mccoy
End If
Next
End Sub
It breaks if you add a Type and dont add the Attribute, but it is less breakable than having to update each Types for their constituent properties. A fake interface would be easier to query, but has the same drawback.
I am not sure I understand the issue of "gaming" things - are you afraid that some other Types will get picked up? Attributes would be hard to "game" since they are compiled into the assembly, still the above could be used to return a GUID (maybe one attached to the assembly?) rather than bool to provide some reassurance.
Absolute certainty will be hard to come by.
The RealMcCoy attribute would probably not be applied to your top level types (PatientAppointment) just the types (classes) which will be used as properties in other types. The object being a way to identify these easily.
Depending on how it is used, a dictionary or hashtable of TypeName-PropertyName pairs already identified as RealMcCoys might be useful so the whole Reflection process can be short circuited. Rather than adding on the fly, you might be able to build the list in its entirety up front as shown in this answer - see the RangeManager.BuildPropMap procedure.
I am not so sure about the inheritance approach since you might want to actually use inheritance somewhere. An interface might work better: initially, its mere existence could be the trigger at the start, but could also be used to provide a service.
Simple test case:
' B and C classes are not tagged
Friend Class FooBar
Public Property Prop1 As PropA
Public Property Prop2 As PropB
Public Property Prop3 As PropC
Public Property Prop4 As PropD
Public Property Prop5 As PropE
End Class
Add a line to the for loop:
Dim f As New FooBar
' use instance:
Dim props As PropertyInfo() = f.GetType.GetProperties
Dim pt As Type
For Each pi As PropertyInfo In props
pt = pi.PropertyType
Dim attr() As RealMcCoy =
DirectCast(pt.GetCustomAttributes(GetType(RealMcCoy), True), RealMcCoy())
Console.WriteLine("Prop Name: {0}, prop type: {1}, IsRealMcCoy? {2}",
pi.Name, pt.Name, If(attr.Length > 0, "YES!", "no"))
Next
Output:
Prop Name: Prop1 prop type: PropA IsRealMcCoy? YES!
Prop Name: Prop2 prop type: PropB IsRealMcCoy? no
Prop Name: Prop3 prop type: PropC IsRealMcCoy? no
Prop Name: Prop4 prop type: PropD IsRealMcCoy? YES!
Prop Name: Prop5 prop type: PropE IsRealMcCoy? YES!

Many thanks to Plutonix! Here is my final function. If my answer supercedes Plutonix's answer then I'll need to adjust this as he fully deserves credit for getting me to this point.
I've tested it to 8 levels of nested types. Rudimentary exception handling due to this being a prototype only.
Function IterateClass(ByVal o As Object, ByVal topLevelFlag As Boolean, ByVal indentLevel As Integer) As String
Dim retVal As New StringBuilder
Try
'' Iterate top level of supplied type ''
'' Query each property for custom attribute ADMICompositeClassAttribute ''
'' If exists and set to true then we're dealing with a base class that we need to break down recursively ''
'' If not then immediately dump this type's properties ''
'' Build header of output ''
If topLevelFlag Then
'' <<EXCERPTED>> ''
End If
'' We start walking through the hierarchy here, no matter how much we recurse we still need to do this each time ''
Dim properties_info As PropertyInfo()
properties_info = o.GetType().GetProperties()
For Each p As PropertyInfo In properties_info
Dim propName As String = p.Name
Dim propTypeList As List(Of String) = p.PropertyType.ToString().Split(".").ToList()
Dim propType As String = propTypeList(propTypeList.Count - 1).Replace("[", "").Replace("]", "")
Dim pValue As Object = Nothing
'' We check this type for custom attribute ADMICompositeClassAttribute ''
'' Is this type a composite? ''
Dim oAttr() As ADMICompositeClassAttribute = p.PropertyType.GetCustomAttributes(GetType(ADMICompositeClassAttribute), False)
Dim indentString As String = String.Concat(Enumerable.Repeat(" ", indentLevel))
If oAttr.Length > 0 AndAlso oAttr(0).IsComposite Then
'' This is a nested type, recurse it ''
Dim dynType As Type = p.PropertyType()
Dim dynObj As Object = Activator.CreateInstance(dynType)
'' <<EXCERPTED ''
Else
pValue = p.GetValue(o, Nothing)
End If
Next
Catch ex As Exception
retVal.AppendLine(ex.ToString())
End Try
Return retVal.ToString()
End Function

Related

VBA List of Custom Datastructures

One of the main problems in VBA are custom data structures and lists.
I have a loop which generates with each iteration multiple values.
So as an example:
Each loop iteration generates a string "name" an integer "price" and an integer "value".
In C# for example I'd create a class which can hold these three values and with each loop iteration I add the class object to a list.
How can I do the same thing in VBA if I want to store multiple sets of data when not knowing how many iterations the loop will have (I cant create an array with a fixed size)
Any ideas?
The approach I use very frequently is to use a class and a collection. I also tend to use an interface model to make things more flexible. An example would look something like this:
Class Module IFoo
Option Explicit
Public Sub Create(ByVal Name as String, ByVal ID as String)
End Property
Public Property Get Name() as String
End Property
Public Property Get ID() as String
End Property
This enforces the pattern I want for my Foo class.
Class Module Foo
Option Explicit
Private Type TFoo
Name as String
ID as String
End Type
Private this as TFoo
Implements IFoo
Private Sub IFoo_Create(ByVal Name as String, ByVal ID as String)
this.Name = Name
this.ID = Name
End Sub
Private Property Get IFoo_Name() as String
IFoo_Name = this.Name
End Property
Private Property Get IFoo_ID() as String
IFoo_ID = this.ID
End Property
We get intellisense from the Private Type TFoo : Private this as TFoo where the former defines the properties of our container, the latter exposes them privately. The Implements IFoo allows us to selectively expose properties. This also allows you to iterate a Collection using an IFoo instead of a Foo. Sounds pointless until you have an Employee and a Manager where IFoo_BaseRate changes depending on employee type.
Then in practice, we have something like this:
Code Module Bar
Public Sub CollectFoo()
Dim AllTheFoos as Collection
Set AllTheFoos = New Collection
While SomeCondition
Dim Foo as IFoo
Set Foo = New Foo
Foo.Create(Name, ID)
AllTheFoos.Add Foo
Loop
For each Foo in AllTheFoos
Debug.Print Foo.Name, Foo.ID
Next
End Sub
While the pattern is super simple once you learn it, you'll find that it is incredibly powerful and scalable if implemented properly. It also can dramatically reduce the amount of copypasta that exists within your code (and thus reduce debug time).
You can use classes in VBA as well as in C#: Class Module Step by Step or A Quick Guide to the VBA Class Module
And to to the problem with the array: you can create an array with dynamic size like this
'Method 1 : Using Dim
Dim arr1() 'Without Size
'somewhere later -> increase a size to 1
redim arr1(UBound(arr1) + 1)
You could create a class - but if all you want to do is hold three bits of data together, I would define a Type structure. It needs to be defines at the top of an ordinary module, after option explicit and before any subs
Type MyType
Name As String
Price As Integer
Value As Integer
End Type
And then to use it
Sub test()
Dim t As MyType
t.Name = "fred"
t.Price = 12
t.Value = 3
End Sub

.Where method not defined on generic typed list?

When I try to use the .Where() method on a list, this does method does not seem to be defined if the list is of a generic type:
In my program, I have a class called Warning, and in another class, a list of warnings, defined as:
Dim warningList As List(Of Warning)
When I try to manipulate this list as:
Dim item = warningList.Where(Function(x) x.GetName() = "Foo").FirstOrDefault()
This works completely fine, but when I try it like this:
Dim itemList
if(type = "Warning") Then 'Please note that this condition is true...
itemList = warningList
End If
Dim item = itemList.Where(Function(x) x.GetName() = "Foo").FirstOrDefault()
I get an exception, stating that method .Where() is not defined for class Warning
Can anybody tell me why this is?
Thank you!
Now that you've edited your question it's clear.
You declare itemList without a type, so it's Object implicitly(in VB.NET with option strict set to Off which i strongly recommend against).
Now that you have declared a variable of type Object you can asssign any type to it. But you would have to cast it back to its real type List(Of Warning) to be able to use list or LINQ methods(which extend IEnumerable(Of T).
But instead declare it with the correct type:
Dim itemList As List(Of Warning)
if(type = "Warning") Then
itemList = warningList
End If
Dim item = itemList.Where(Function(x) x.GetName() = "Foo").FirstOrDefault()
Including to comment to explain why Warning is not related to this problem:
That's not the real code. If warningList is really a List(Of Warning)
you should be able to use Enumerable.Where(if LINQ is
imported). The fact that you assign this instance to another variable
(on declaration) doesn't change anything because that variable's type
is also a List(Of Warning). So itemList.Where should work too. Warning
has nothing to do with it because the type which is extended by Where
is IEnumerable(Of T), T can be any type(even Object). Since List(Of T)
implements IEnumerable(Of T) you can use Enumerable.Where on any list
(or array).
If you actually have multiple types and Warning is just one of it, you should implement a common interface. Here's an example:
Public Enum NotificationType
Warning
Info
[Error]
End Enum
Public Interface INamedNotification
ReadOnly Property Type As NotificationType
Property Name As string
End Interface
Public Class Warning
Implements INamedNotification
Public Sub New( name As String )
Me.Name = name
End Sub
Public Property Name As String Implements INamedNotification.Name
Public ReadOnly Property Type As NotificationType Implements INamedNotification.Type
Get
Return NotificationType.Warning
End Get
End Property
End Class
Now you can declare a List(Of INamedNotification) and fill it with whatever implements this interface, like the Warning class:
Dim notificationList As List(Of INamedNotification)
if type = "Warning" Then
itemList = warningList
Else If type = "Info"
itemList = infoList
End If
Dim item = notificationList.Where(Function(x) x.Name = "Foo").FirstOrDefault()

How to instantiate Class object with varying number of property values

Been working a lot with custom classes lately and I love the power you can have with them but I have come across something that I'm not able to solve and/or find anything helpful online.
I have a list of a class with properties I'm looking to only store information pulled from a database into.
Public Class CustomClass
Public _Values As String
Public _Variables As String
Public ReadOnly Property Values() As String
Get
Return _Values
End Get
End Property
Public ReadOnly Property Variables() As String
Get
Return _Variables
End Get
End Property
Sub New(ByVal values As String, ByVal variables As String)
_Values = values
_Variables = variables
End Sub
End Class
I will be iterating through some database entries, and I'm looking to store them into the appropriate property when I hit them (since I won't have them all available immediately, which is part of my problem). I want to just be able to add either the value or the variable at a time and not both of them, but since I have the sub procedure 'New' passing two arguments, it will always require passing them both. I've found the only way around this is by making them optional fields which I don't feel is the right way to solve this. Is what I'm looking to do possible with a class or would it be simpler by using a structure?
You can overload the constructor:
Friend Class Foo
' using auto-implement props:
Public Property Name As String ' creates a _Name backing field
Public Property Value as Integer
Public Sub New(newN as String, newV as Integer)
' access "hidden" backing fields if you want:
_Name = newN
_Value = newV
End Sub
Public Sub New() ' simple ctor
End Sub
Public Sub New(justName As String)
' via the prop
Name = justName
End Sub
End Class
You now have 3 ways to create the object: with full initialization, partial (name only) or as a blank object. You will often need a "simple constructor" - one with no params - for other purposes: serializers, Collection editors and the like will have no idea how to use the parameterized constructors and will require a simple one.
If rules in the App were that there was no reason for a MyFoo to ever exist unless both Name and Value being defined, implementing only the New(String, Integer) ctor enforces that rule. That is, it is first about the app rules, then about coding convenience.
Dim myFoo As New Foo ' empty one
myFoo.Name = "ziggy" ' we only know part of it
Since the default of string is nothing, you could pass nothing for the value you don't have. IE
Collection.Add(New CustomClass("My Value",Nothing))
Every type has a default, so this works with more than just strings.

Vb.Net setting properties of user control

I have a custom user control & I am looking to set some of its properties from the designer. The properties will be coming from a structure. Here is the current code
Private fooList As Foo_structure
Public Structure Foo_structure
Public Property a As Integer
Public Property b As Integer
Public Property c As Extras
End Structure
Public Structure Extras
Public Property precision As Integer
Public Property light As String
End Structure
Public Property foo As Foo_structure
Get
Return fooList
End Get
Set(ByVal value As Foo_structure)
fooList = value
End Set
End Property
I need to be able to set the properties of the Foo_structure from the designer properties panel like the eg shown in the image below.
You are going to need a TypeConverter to collapse foo into a string; and convert back from it. The nested Type means you need to write another one for Extras. You will probably need to use some attributes to handle designer persistence.
To start, I think you need to change at least Foo_structure to a Class, otherwise there is no way to add code to instance Extras (also no way to create a Foo instance). This should get you started (changed some names):
' Foo converted to Class:
<TypeConverter("FooItemConverter")>
Public Class FooBar
<DefaultValue(0)>
<DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)>
Public Property Foo As Integer
<DefaultValue(0)>
<DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)>
Public Property Bar As Integer
<EditorBrowsable(EditorBrowsableState.Always)>
<NotifyParentProperty(True)>
<DesignerSerializationVisibility(DesignerSerializationVisibility.Content)>
Public Property Ex As Extras
Public Sub New(a1 As Integer, b2 As Integer)
Foo = a1
Bar = b2
Ex = New Extras ' do not want NOTHING flying about
End Sub
End Class
DefaultValue does not do what you may think it does. It tells the IDE to serialize the value for a property when the current value does not equal the Default. DesignerSerializationVisibility tells VS to save the value for a property. Foo and Bar both need these.
Ex/Extra is different. NotifyParentProperty allows FooBar to be notified when a Extra property value has changed so the IDE window is updated, internal "DirtyFlag" set etc; DesignerSerializationVisibility.Content tells VS that we know we cant save Ex as a value, so save the contents.
Then comes the FooItemConverter. This will be the thing that displays the string you want in the Props window AND creates a Foo item from that string:
Friend Class FooItemConverter
Inherits ExpandableObjectConverter
' tells the IDE what conversions it can handle:
Public Overrides Function CanConvertTo(context As ITypeDescriptorContext,
destType As Type) As Boolean
If destType = GetType(String) Then
' Yes I Can
Return True
End If
' Probably have to also say YES to an InstanceDescriptor
Return MyBase.CanConvertTo(context, destType)
End Function
After that a ConvertTo function is used to convert foo to a string. Something like this:
Public Overrides Function ConvertTo(context As ITypeDescriptorContext,
culture As Globalization.CultureInfo,
value As Object, destType As Type) As Object
If destType = GetType(String) Then
Dim f As FooBar = CType(value, FooBar)
Return String.Format("{0}, {1}, {2}",
f.foo.ToString,
f.bar.ToString,
f.Ex.ToString)
' outputs: X, Y, <ex>
' where Ex is what we use in the ExtraItemConverter
End If
Return MyBase.ConvertTo(context, destType)
End Function
If ExtraItemConverter.ConvertTo use a format of "({0} / {1})" then the control contents will show as: F, B, (P / L) where F=Foo, B=Bar etc.
To make it work, you need 4 procedures: CanConvertTo, ConvertTo, CanConvertFrom, ConvertFrom all responding to string. You probably will be able to just use the <DefaultValue> attribute for persistence.
FooItemConverter.ConvertFrom will have to know how to create an object from that string. Normally, that is done like this:
' parse the string you made and create a Foo
Dim els As String() = str.Split(","c)
Return New myFoo(Convert.ToInt32(els(0)), Convert.ToInt32(els(1)))
Note that the 3rd element is ignored as that is actually for the ExtraItemConverter to handle. That converter would be very similar.
So, you will first have to decide whether to cling to the structure or use a class (another pro for a Class is that 99.999% of the examples you find will be Class based). These guys know a lot about TypeConverters.

How can I get a property name for a type without the need to instantiate an object of that type?

I have a requirement where I need to have a "type safe" way of accessing property names, without actually instantiating an object to get to the property. To give an example, consider a method that takes as arguments a list of IMyObject and a string that represents a property name (a property that exists in IMyObject).
The methods implementation will take the list and access all the objects in the list using the property name passed... for some reason or another, we won't dwell on that!!
Now, I know that you can do this using an instantiated object, something like ...
Dim x as MyObject = nothing
Dim prop As PropertyInfo = PropHelper.GetProperty(Of MyObject)(Function() x.MyProperty)
Where my helper method uses reflection to get the name of the property as a string - there are numerous examples of this flying around on the web!
But I don't want to have to create this pointless object, I just want to do something like MyObject.MyProperty! Reflection allows you to iterate through a types properties and methods without declaring an object of that type... but I want to access a specific property and retrieve the string version of its name without iteration and without declaring an object of that type!
The main point here is that although I am trying to get the property name as a string... this is done at run time... at compile time, I want this to be type safe so if someone changes the property name, the compilation will break.
Can anyone help in this quest!?!
So here is a quick code-listing to demonstrate the answer that I was looking for:
Imports System.Linq.Expressions
Public Class A
Public Prop1 As String
Public Prop2 As Integer
End Class
Public Class Form1
Public Function GetPropertyNameB(Of TModel, TProperty)(ByVal [property] As Expression(Of Func(Of TModel, TProperty))) As String
Dim memberExpression As MemberExpression = DirectCast([property].Body, MemberExpression)
Return memberExpression.Member.Name
End Function
Public Sub New()
InitializeComponent()
Dim propertyName As String = GetPropertyNameB(Function(myObj As A) myObj.Prop1)
Dim propertyName2 As String = GetPropertyNameB(Function(myObj As A) myObj.Prop2)
MsgBox(propertyName & " | " & propertyName2)
End
End Sub
End Class
You may be able to pass the property in as a simple lamdba expression, and take it in the method as an expression tree. You should be able to analyze the expression tree to get the string name of the property, but it the lambda expression will fail to compile if the property name changes. Check out this page for more details:
http://msdn.microsoft.com/en-us/library/bb397951.aspx
You can make use of the NameOf function:
Dim fieldName = nameOf(MyClass.MyField)