Reference to object properties - vb.net

I have a list of objects of type say Person, and I want to export Person records to an excel-sheet (I am using a proprietary excel component for VB.NET). Using a form with checkboxes the user can specify which Person properties should be exported.
Instead of having an enormous if-then-else tree where I check to see if each checkbox (corresponding to a property) has been checked, I have a data structure where for each property in Person I keep a boolean (checked/unchecked) and the name of the property as a string. I then use two for-loops like this:
For Each p As Person In Persons
...
For Each item As ExportColumnData In ExportColumnTable
...
If item.Checked Then
...
Dim o As Object = CallByName(p, item.PropertyName, CallType.Get, Nothing)
SaveValueToExcelSheet(o)
...
End If
...
Next
...
Next
However, this is not type-safe since I am using CallByName supplying PropertyName as a string. Is there a more elegant and type-safe way I can achieve the same thing? I need some way (other than a string) to reference the properties of these Person objects.

The CallByName function uses reflection to find and execute the property getter by string name, so you are right that it is unsafe in the sense that there will be no compile-time checking done to ensure that the properties by those names actually do exist in the Person type.
Unfortunately, short of a big If/Else block, or something similar, there is no "safe" way to do this in a way which will allow for compile-time type checking. If you want it to check that at compile-time, you need to call the property by name directly in code, and if you are doing that, it will have to be in a big conditional block of some sort.
There are things you could do to minimize or shift the location of the ugliness. For instance, you could create an enumeration of all the Person properties and add a method to the Person class which returns the property value given the enumeration item using a big Select Case block. That would make the logic reusable but not really any less ugly. Not only that, but doing it that way kind of puts the type-checking responsibility on your code, not the compiler.
Alternatively, you could, for instance, set the tag of each CheckBox control to a delegate which takes a Person object and returns the correct property value for that option from the given Person object. Then, in the loop, you could just call the delegate in the tag to retrieve the value. For instance, if you had a delegate like this:
Private Delegate Function GetPersonProperty(x As Person) As Object
Then you could set the Tag of the CheckBox controls like this:
chkFullName.Tag = New GetPersonProperty(Function(x As Person) x.FullName)
chkAge.Tag = New GetPersonProperty(Function(x As Person) x.Age)
Then, in your loop, you could invoke the delegate in the Tag to get the value, like this:
Dim myDelegate As GetPersonProperty = CType(item.Tag, GetPersonProperty)
Dim value As Object = myDelegate.Invoke(p)
But that's rather overly-complicated for such a simple task.
In the end, if the compile-time type checking is really important, I'd just bite the bullet and make the big conditional block. If it's not really that important, I'd just stick with the reflection and put some decent exception handling in the code.

You say you already have a class where you store the information about the properties of your Person class. You can use this to store the PropertyInfos as well.
Here's an example:
Class Person
Public Property Name As String
Public Property Age As Integer
End Class
Class ExportProperty
Public Property [Property] As PropertyInfo
Public Property Export As Boolean
End Class
Sub Main()
'' Create a List(Of ExportProperty) from all public properties of Person
Dim properties = GetType(Person).GetProperties() _
.Select(Function(p) New ExportProperty With { .[Property] = p}) _
.ToList()
'' Say we want to export only the Age field
properties.Single(Function(p) p. [Property].Name = "Age").Export = True
'' Create a person instance to export
Dim pers = New Person With { .Name = "FooBar", .Age = 67 }
'' Only export the properties with Export = True
For Each prop in properties.Where(Function(p) p.Export)
'' Use the PropertyInfo.GetValue-method to get the value of the property
''
Console.WriteLine(prop.[Property].GetValue(pers, Nothing))
Next
End Sub

Your solution is perfectly fine, as long as the contents in ExportColumnData are correct. If these are computed dynamically at runtime, you're fine.
Otherwise, or alternatively, you can do the following: use Type.GetProperties to get a list of PropertyInfo objects. You can then use these instead of a mere String to extract property values in your loop:
Dim o As Object = item.PropertyInfo.GetValue(p, Nothing)

Related

vb.net modifying values of custom attributes in a class

I had an idea to use custom attributes on the properties in a class for databinding purposes in a winforms interface. For example, setting and changing the backcolor, forecolor, and tooltip on a textbox with invalid data. I find that I can bind up the control properties of txtTest for backcolor, etc., to a custom attribute such as BackColorAttr decorating a property in the class such as Name, with no problem. The property value itself is bound to the Text of the textbox, two-way binding of that works just fine, and the initial backcolor, forecolor, etc., are set from the initial values of the custom attributes just the way I had hoped. I'm doing all this through a BindingHelper class that reduces all the coding to a couple of generic methods.
Where I'm stumped is manipulating the values of the custom attributes at a later time. Changing the backcolor to red, for example. Nothing I've tried seems to work. Has anybody tried something like this, or have some guidance as to how I might proceed?
I dont quite follow the first part or what binding has to do with colors or Attributes, but thats not how Attributes work. They are not Property wrappers and Properties, Methods and Types have no idea of the Attributes associated with them (and vice-versa). They are meta data compiled into the assembly. As such, you cant change the value in any meaningful way.
Test class and test Attribute:
Public Class BarState
Inherits Attribute
Public Property State As String
Public Sub New(t As String)
State = t
End Sub
End Class
Public Class Foo
<BarState("red")>
Public Property Name As String
End Class
Since State is a property, test if we can set it:
Dim f As New Foo
' get props for the Type
Dim pi As PropertyInfo = f.GetType.GetProperty("Name")
Dim attr = pi.GetCustomAttributes(GetType(BarState), False)
If attr.Length > 0 Then
' get prop info for the State property on the Attr Type
Dim pa As PropertyInfo = attr(0).GetType.GetProperty("State")
' change it
CType(attr(0), BarState).State = "GREEN"
' or
'pa.SetValue(attr(0), "GREEN", Nothing)
' print it (prints "GREEN" but it does not persist)
Console.WriteLine(CType(attr(0), BarState).State)
End If
'get the attr again as you might do next time thru
attr = pi.GetCustomAttributes(GetType(BarState), False)
' print the value (Print "red")
Console.WriteLine(CType(attr(0), BarState).State)
The first print will be "GREEN" but that is only for this instance - it does not persist. The next time you get it, it reverts to "red". Since an Attribute is a Type, we can try to Reflection to change the value using pa.SetValue(attr(0), "GREEN", Nothing) which is commented out. It still wont persist because "red" is compiled into the assembly which is what your starting point will always be.
It might seem like you could keep a Dictionary or collection of the attribute instances for all the properties on all the types. That could work except, they all look alike, so you would have to create a hash to track which Attribute instance goes with what Property on what Type.
And you'd have to keep that collection in sync with the underlying instance objects. The Attribute instance wont know the instance it came from is gone and so the state setting should revert, so your Attribute manager would need to handle that.
You might look into "weavers" which use attributes to tag things (like a value range) then rewrite the assembly to weave in range checks for the tagged properties. Sort of sounds like what you are after I dont know what else they might do along the lines of what you describe.

Using CallByName to set Item(x) property

As a bit of background, I have a .net <-> COM object bridge that uses VB.net as a middleman, with a lot of reflection to get the job done.
I've run into a hurdle where I'm needing to use CallByName() to set a pretty standard property which is defined as
Public Default Property Item (
index As Integer
) As String
Get
Set
which would normally be called as .Object(1) = "new value", however the bridge code at the moment tries to get .Object(1) as an object then call Set on it using CallByName() (which obviously doesn't work).
With other collections I am happily able to use CallByName() to make method calls .Clear() and .Add("new value") but this property doesn't have these methods and besides, I'd like to solve it for a more generic approach so that code from the other side of the bridge can call the .Object directly.
Is someone able to suggest a way to Set an array-type property directly using CallByName(), or perhaps suggest an alternative reflection function that can be called to achieve this?
The default property can be used as a normal property, using its name. So, given a class:
Class Foo
Default Public Property Item(index As Integer) As String
Get
'...
End Get
Set(value As String)
'...
End Set
End Property
End Class
These three property assignments all have the same effect:
Dim Bar As New Foo
Bar(1) = "x"
Bar.Item(1) = "x"
CallByName(Bar, "Item", CallType.Set, 1, "x")
For array-type properties, the parameter(s) are passed to CallByName before the value when setting.
You did not show how you were using CallByName on that property, which leaves us to guess what is wrong. The syntax of .Object(1) = "new value" is also a little confusing: does the leading dot means that Object itself is some sort of collection on some other Type?
The basic answer lies in looking at the declaration, not how it is used normally. The fact that you can omit "Item" normally because it is the Default, does not apply here:
'foo(1) ==> foo.Item(1) = "Ziggy" ==>
CallByName(foo, "Item", CallType.Set, 1, "Ziggy")
The procName argument would be the property name, Item in this case. CallType.Set means you want the prop setter (Let or Set seem to both work). The first argument would be the index of the item to set/get, the last would be the data to pass.
If .Object is supposed to mean you are trying to reference a collection property, then the answer is about the same:
'foo.bars(1) ==> foo.Bars.Item(1) = "Zoey" ==>
CallByName(foo.Bars, "Item", CallType.Set, 1, "Zoey")

Pass an argument to a generic type New constructor in VB.Net Generics

I'm trying to be able to pass a Type parameter to a function called ConvertList, and have that function create some instances of the specified type. So, if I passed in type Foo, the function would create new objects of type Foo and put the created objects into a custom List object (SLMR_OBjList).
The function is in a generic class that is defined:
Public Class BOIS_Collection_Base(Of T)
The function would accept types other than what is passed in the class definition. So, if we create an instance of BOIS_Collection_Base(Of MyTypeA) we may call the function ConvertList(Of MyTypeB).
I want the private variable _convertedList to be of a different type than the class. Is this possible? I can only seem to define it with (Of T).
Here is what I have so far:
Public Class BOIS_Collection_Base(Of T)
Private _convertedList As SLMR_ObjList(Of T) ' I can only seem to define this as (Of T), but want to make sure I can pass in a Type other than the Base(Of T)
Public Function ConvertedObjList(Of myT)() As SLMR_ObjList(Of T) ' Should this be (Of T) or (Of myT) since I want it to use whatever Type is passed in
For Each tempVar In Me.ObjList
Dim newitem As myT = Activator.CreateInstance(GetType(myT), tempVar)
' Next line won't compile, says on newitem 'Value of type 'myT' cannot be converted to 'T'
_convertedList.Add(newitem)
Next
_convertedList.Sort_Direction = Me.Sort_Direction
_convertedList.Sort_Expression_List = Me.Sort_Expression_List
Return _convertedList
End Function
Here is what I would like to be able to do:
Dim mainCollInstance As New BOIS_Collection_Base(Of MyTypeA)
....
'Code that populates the BOIS_Collection_Base.ObjList property with an SLMR_ObjList(Of MyTypeA)
....
' Now I want to take that ObjList, and cast all the items in it to MyTypeB
Dim newListObj As SLMR_ObjList(Of MyTypeB) = mainCollInstance.ConvertList(Of MyTypeB)
Is this possible? Am I going about it wrong?
In response to Plutonix:
If I define _convertedList inside the method, like this:
Public Function ConvertedObjList(Of myT)() As SLMR_ObjList(Of myT)
Dim _convertedList = New SLMR_ObjList(Of myT)
my errors go away, and the method does what I want, but _convertedList is no longer persistant in the object.
If you want to persist the list, then you can't really allow the consuming code to pass a different type for the list each time. That doesn't really make much sense, unless each time it's called, you only want the function to return the portion of the persisted list which contains objects of the given type. If that's the case, then you just need to declare _convertedList As SLMR_ObjList(Of Object) and then filter it and convert it to the correct type as necessary.
If, however, as I suspect is the case, the consumer will always be requesting that it be converted to the same type each time the function is called, then that output type is not really a property of the function call. Rather, it's a property of the whole class. In that case, you should make your class take two generic type arguments, like this:
Public Class BOIS_Collection_Base(Of T, TOut)
Private _convertedList As SLMR_ObjList(Of TOut)
Public Function ConvertedObjList() As SLMR_ObjList(Of TOut)
For Each tempVar As T In Me.ObjList
Dim newitem As TOut = DirectCast(Activator.CreateInstance(GetType(TOut), tempVar), TOut)
' Next line won't compile, says on newitem 'Value of type 'myT' cannot be converted to 'T'
_convertedList.Add(newitem)
Next
_convertedList.Sort_Direction = Me.Sort_Direction
_convertedList.Sort_Expression_List = Me.Sort_Expression_List
Return _convertedList
End Function
End Class
Based on the previous related question and an assumption that MyTypeA and MyTypeB inherit from the same class (never got an answer), you may not need Generics for this. At any rate, this should help with the ctor part of the question. I do not as yet see where Generics fit in since inheritance may do what you want already:
Class MustInherit BiosItem
Public Property Name As String
Public Property TypeCode As String
...
MustOverride Function Foo(args...) As Type
Overridable Property FooBar As String
' etc - the more stuff in the base class the better
End Class
Class TypeA
Inherits ABClass
Public Sub New
MyBase.New ' stuff common to all child types
TypeCode = "A" ' EZ type ID rather than GetType
...
End Sub
End Class
Class TypeB would be the same, but initialize TypeCode to "B". The same for C-Z. These allow you to poll the object rather than needing GetType: If thisObj.TypeCode = "A" Then.... Now, the collection class:
Public Class BIOSItems
Inherits Collection(Of BiosItem)
' inheriting from Collection<T> provides Add, Count, IndexOf for us
' most important the Items collection
'
End Class
Typing the collection as BiosItem will allow TypeA or TypeJ or TypeQ in it. As is, your collection will hold one Type only as it should be. This works because an item which is GetType(TypeA) is also GetType(BiosItem). See also note at the end.
Converting one item to another would seem to be something that would largely be handled by the NEW item being created or converted to. Since they are likely to be very similar then it can be handled by a constructor overload (if they are not similar, well we are well down the wrong road):
' a ctor overload to create the new thing based on the old things props
Public Sub New(oldThing As BiosItem)
MyClass.New ' start with basics like TypeCode, MyBase.New
With BiosItem ' coversion
myFoo = .Foo
myBar = .Bar ' copy common prop vals to self
...
Select Case .TypeCode
Case "B"
myProp1 = .Prop33 ' conversions
myProp3 = .Prop16 + 3.14
...
End Select
' then initialize stuff unique to this type maybe
' based on other props
If .PropX = "FooBar" Then myPropZ = "Ziggy"
End With
End Sub
Code to create, convert, store:
Dim varOldBItem As TypeB = myBiosCol(ndx) ' get old item
Dim varAItem As New TypeA(varOldBItem) ' call the ctor above
myBiosCol.Add(varAItem) ' add new item
myBiosCol.Remove(varoldBItem) ' delete the old if need be
If BOIS_Collection_Base is always supposed to contain MyTypeA, then type it that way (inheriting from Collection<T> still seems in order). If also MyTypeB objects are never added to the collection directly, but converted to MyTypeA first (Edit makes that less clear), then most of the above still applies, except for the inheritance. A ctor overload on MyTypeA could still take an old B object and create itself based on it. I'd be less inclined to do it via the ctor if they do not inherit from the same base class, but it could be done.

Copy VB.NET list object to another list object

I am having a problem when setting VB.NET list object to another. In the example below
I create an instance on ReadLocations and than create an object of ReadLocation where then I loop through ReadLocations and set ReadLocation1 equal to rl.
What happens is that if I then go and change ReadLocation1 to something else (assdfhsd) it will also change the ReadLocations index. I am really confused why it would be doing that unless it is "=" sign means referencing instead of actually setting the value. Please help as I am a C# developer but the program I am modifying is in VB.NET.
Dim ReadLocations As New List(Of Model.ReadLocation)
Dim rl1 As New Model.ReadLocation
rl1.LL = "a"
Dim rl2 As New Model.ReadLocation
rl2.LL = "b"
ReadLocations.Add(rl1)
ReadLocations.Add(rl2)
Dim ReadLocation11 As New Model.ReadLocation
For Each rl As Model.ReadLocation In ReadLocations
ReadLocation11 = rl
Next
If ReadLocation is a reference type (a Class), then all variables set to instances of objects of that class will always be references. The = operator only ever sets a new reference to an object when it is operating on reference types. It will never make a clone of the object (unless it is a value type). The same is true in C#. The only way to do what you want to do, would be to clone the objects when you add them to the second list. Unfortunately, .NET doesn't provide a simple automatic method for cloning any object. The standard way to do this would be to implement the ICloneable interface in your ReadLocation class. Then you could clone it by calling the clone method:
ReadLocation1 = CType(rl.Clone(), ReadLocation)
However, inside that clone method, you will need to create a new instance of ReadLocation and manually set all of its properties and then return it. For example:
Public Class ReadLocation
Implements ICloneable
Public Function Clone() As Object Implements ICloneable.Clone
Dim clone As New ReadLocation()
clone.Property1 = Me.Property1
clone.Property2 = Me.Property2
Return clone
End Function

ToolStripComboBox + objects default string value

I am using a ToolStripComboBox to list some data. I am populating this combo with some custom objects:
For Each obj As myObject In myObjectList
myToolStripComboBox.Items.Add(obj)
Next
This works great except the text displayed in the combo is the name of the object class. I can understand why and realise that I need to do something to make the combo use a property from my class instead.
The help files state:
To add objects to the list at run
time, assign an array of object
references with the AddRange method.
The list then displays the default
string value for each object. You can
add individual objects with the Add
method.
The bit in bold suggests I need to setup the default string value in my class. How would I go about doing this?
You need to add an Overrides ToString to your myObject class :
Public Overrides Function ToString() As String
--return whatever you want to display
End Function