Before getting into examining property attributes in code, you need to be aware that you cannot set Option Strict to On.
Caution | To elaborate, you cannot set Option Strict to On in the code module where you process the property attributes. This is because the type of reflection you are performing requires late binding. In general, this is not a good practice, but it is acceptable in this situation. |
Now, having said that, let's start coding. Go into the form code module and import the System.Reflection namespace. Next, add a module-level variable for the ComputerListMgr as follows:
Private mobjCLMgr As ComputerListMgr
Add the code in Listing 10-4 to the frmList class.
Listing 10-4: The btnComputers_Click Method
Private Sub btnComputers_Click(ByVal sender As Object, _ ByVal e As System.EventArgs) Handles btnComputers.Click Dim t As Type = GetType(ComputerList) mobjCLMgr = New ComputerListMgr() mobjCLMgr.Add(New ComputerList("Lightning", "P4", 1.4, 699.0, "Dell")) mobjCLMgr.Add(New ComputerList("Thunder", "P4", 1.7, 799.0, "Dell")) LoadList(t, CType(mobjCLMgr, CollectionBase)) End Sub
The first line of this code gets the type of the ComputerList class and stores it in a type variable. Next you instantiate the computer list manager and add two items to the collection. Finally you call the LoadList method (which you will code next) and pass in your object type and your collection. Notice, however, that you are passing your manager in as a CollectionBase object. In a real application, you would probably want to overload this method to accept virtually any type of collection. The reason why you are passing in generic values is so that the LoadList method remains as flexible as possible. Listing 10-5 contains the code for the LoadList method. Do not panic! You will get an explanation of everything line by line after the listing.
Listing 10-5: The LoadList Method
Private Sub LoadList(ByVal t As Type, ByRef col As CollectionBase) Dim p As PropertyInfo() Dim i, j As Integer Dim SortedL As New Collections.SortedList() lvwList.Clear p = t.GetProperties(BindingFlags.Public Or BindingFlags.Instance) For i = 0 To p.Length - 1 Dim a As Object() a = p(i).GetCustomAttributes(False) If a.Length > 0 Then For j = 0 To a.Length - 1 If a(j).GetType Is GetType(ListAttribute) Then Dim la As ListAttribute = CType(a(j), ListAttribute) SortedL.Add(la.Column, p(i)) End If Next End If Next For i = 0 To SortedL.Count - 1 Dim pi As PropertyInfo = CType(SortedL.Item(i), PropertyInfo) Dim a As Object = pi.GetCustomAttributes(False) For j = 0 To a.Length - 1 If a(j).GetType Is GetType(ListAttribute) Then Dim la As ListAttribute = CType(a(j), ListAttribute) lvwList.Columns.Add(la.Heading, _ lvwList.Width / SortedL.Count - 2, _ HorizontalAlignment.Left) Exit For End If Next Next Dim obj As Object Dim myObject() As Object For Each obj In col Dim cl As Object = Convert.ChangeType(obj, t) Dim k As Integer Dim lst As New ListViewItem() For i = 0 To SortedL.Count - 1 Dim pr As PropertyInfo = CType(SortedL.Item(i), PropertyInfo) Dim strValue As String = "" strValue = Convert.ToString(t.InvokeMember(pr.Name, _ BindingFlags.GetProperty, Nothing, cl, myObject)) If i = 0 Then lst.SubItems(i).Text = strValue Else lst.SubItems.Add(strValue) End If Next lvwList.Items.Add(lst) Next End Sub
So now that you think you may be lost, let's try to straighten everything out and explain what is going on here. The first line declares an array of PropertyInfo variables. The PropertyInfo type holds information about—you guessed it—properties. The i and j variables are just counter variables. The SortedL variable stores the properties in the order you have specified they be displayed in (by way of your attribute settings in the ComputerList class). You will see this in action in a minute. Then you clear the listview of all of its current contents—headers and all:
Dim p As PropertyInfo() Dim i, j As Integer Dim SortedL As New Collections.SortedList() lvwList.Clear
This next line calls the GetProperties method on your type variable. So this line reads, "Get all of the properties of the type (in this case, the ComputerList class) that are public or instance properties." This method returns an array of PropertyInfo types:
p = t.GetProperties(BindingFlags.Public Or BindingFlags.Instance)
The next block of code continues your process of discovering information about the properties. First, you start by looping through the array of PropertyInfo values:
For i = 0 To p.Length - 1
The variable a is an object array to hold all of the custom attributes on the specific property:
Dim a As Object()
The GetCustomAttributes returns an object array because there may be several types of attributes associated with the property you are examining. The False parameter indicates that you do not want to look at any other property values in the inheritance chain for this class:
a = p(i).GetCustomAttributes(False)
Now you check the length of the array to see if there were any custom attributes associated with the property. Remember, for your class there are only three: the Cname, Proc, and Speed properties:
If a.Length > 0 Then
If it does find at least one custom attribute, you loop through the array of custom attributes:
For j = 0 To a.Length - 1
Here you check the type of custom attribute. This is the only known type in the entire method:
If a(j).GetType Is GetType(ListAttribute) Then
If the custom attribute is of type ListAttribute, then you convert that custom attribute into a value that you can manipulate easily by performing a ctype on it:
Dim la As ListAttribute = CType(a(j), ListAttribute)
Finally, you add the column number as the key in the sorted list, and you add the PropertyInfo variable as the object in the sorted list so you can reference it later:
SortedL.Add(la.Column, p(i))
The next block of code adds the column headers to the listview. You start by looping through the sorted list collection and retrieving the PropertyInfo objects. Then you get the custom attributes of the property. Next, you again loop through the custom attributes looking for the ListAttribute. When you find it, you convert it into a ListAttribute variable and extract the heading name. When this block of code finishes executing, the column headers will have been added to the listview:
For i = 0 To SortedL.Count - 1 Dim pi As PropertyInfo = CType(SortedL.Item(i), PropertyInfo) Dim a As Object = pi.GetCustomAttributes(False) For j = 0 To a.Length - 1 If a(j).GetType Is GetType(ListAttribute) Then Dim la As ListAttribute = CType(a(j), ListAttribute) lvwList.Columns.Add(la.Heading, _ lvwList.Width / SortedL.Count - 2, _ HorizontalAlignment.Left) Exit For End If Next Next
The obj variable helps you iterate through the ComputerListMgr collection. Because you only know that this is a collection, you cannot use a For Next loop to iterate through the collection. You can only use the For Each enumeration. And because you do not know what type of object is returned to you by the collection (remember, this is a wholly generic routine, so you cannot declare a variable of type ComputerList anywhere), you need to use an object variable. The myObject object array is used as a parameter to the InvokeMethod call. It is a throwaway variable:
Dim obj As Object Dim myObject() As Object
Finally, you get to the block of code that adds the values from the collection into the listview. Before you start examining this block of code, think about what it is doing. You are taking a collection that you know nothing about, that stores objects you know nothing about, and that has properties you know nothing about and extracting that data and placing it in a listview! This block of code, in a nut-shell, shows exactly how powerful the .NET Framework can be when used to its fullest potential.
Let's now look at what is happening here. The For Each statement, as mentioned earlier, is the only way to iterate through your collection object:
For Each obj In col
The cl variable is an object that you are converting to the type you have passed in to the method—in this case, the ComputerList type. You do this using the ChangeType method of the Convert class. This is an example of late binding and the chief reason you cannot use Option Strict On in this code module:
Dim cl As Object = Convert.ChangeType(obj, t)
The k variable is just a counter variable, and lst is the listviewitem you will be adding to the listview:
Dim k As Integer Dim lst As New ListViewItem()
Next you loop through the sorted list collection (yet again) to get the properties for which you need to retrieve the values:
For i = 0 To SortedL.Count - 1
This line retrieves the PropertyInfo from the sorted list collection:
Dim pr As PropertyInfo = CType(SortedL.Item(i), PropertyInfo)
StrValue holds the value you retrieve from whatever property you are calling. It is initialized to an empty string because you may have a property that was not set and this would leave strValue with a value of nothing, which you absolutely do not want:
Dim strValue As String = ""
This next line is the workhorse of this method. This line says the following: "Call the method whose name is returned by PropertyInfo variable (pr). Look for this method in the class's collection of properties using the default binder (do not worry about what this is right now, for more information check the MSDN documentation). Call this method on the given object (cl) with the parameters given in myObject and store the return value in the string variable strValue." That was a handful to say the least. The myObject array would, if you were calling a method that required parameters to be passed to it, contain a list of values to pass in to the method:
strValue = Convert.ToString(t.InvokeMember(pr.Name, _ BindingFlags.GetProperty, Nothing, cl, myObject))
If this is the first time through the loop, assign the value to item 0 of the subitems collection (because you already instantiated the lst variable previously); otherwise, add a new subitem to the listviewitem. Finally, add the listviewitem to the listview:
If i = 0 Then lst.SubItems(i).Text = strValue Else lst.SubItems.Add(strValue) End If Next lvwList.Items.Add(lst)
Now, if you have not done so yet, run the application and click the Computer List button. The result should look something like Figure 10-2.
Figure 10-2: List of computers displayed by the LoadList method
Now, as a test, edit the ComputerList class and change the order you would like things to display on the screen (by changing the numeric value in the List Attribute tag) and run the application again. Pretty neat, huh?
Listing 10-6 contains the code for the BookList class and the BookListMgr class. They are the same as what you have just done, but they have different properties.
Listing 10-6: The BookList and BookListMgr Classes
Public Class BookList Private mstrTitle As String Private mstrAuthor As String Private mdblPrice As Double Private mstrPublisher As String Public ReadOnly Property Price() As Double Get Return mdblPrice End Get End Property Public ReadOnly Property Publisher() As String Get Return mstrPublisher End Get End Property <List("Author", 1)> Public ReadOnly Property Author() As String Get Return mstrAuthor End Get End Property <List("Book Title", 0)> Public ReadOnly Property Title() As String Get Return mstrTitle End Get End Property Public Sub New(ByVal sTitle As String, ByVal sAuthor As String, _ ByVal dPrice As Double, ByVal sPub As String) mstrTitle = sTitle mstrAuthor = sAuthor mdblPrice = dPrice mstrPublisher = sPub End Sub End Class Public Class BookListMgr Inherits System.Collections.CollectionBase Public Sub Add(ByVal obj As BookList) list.Add(obj) End Sub Public Sub Remove(ByVal Index As Integer) list.RemoveAt(Index) End Sub Public Function Item(ByVal Index As Integer) As BookList Return CType(list.Item(Index), BookList) End Function End Class
Next, add the following module-level declaration in frmList:
Private mobjBKMgr As BookListMgr
Finally, Listing 10-7 shows the code for the btnBooks_Click method.
Listing 10-7: The btnBooks_Click Method
Private Sub btnBooks_Click(ByVal sender As Object, _ ByVal e As System.EventArgs) Handles btnBooks.Click Dim t As Type = GetType(BookList) mobjBKMgr = New BookListMgr() mobjBKMgr.Add(New BookList("Life With .NET", "Anonymous", 49.95, _ "Apress")) mobjBKMgr.Add(New BookList("Life With Java", "Unknown", 19.95, _ "ABC Publishing")) LoadList(t, CType(mobjBKMgr, CollectionBase)) End Sub
Now try running the application and clicking either button. Try changing the methods with which the custom attributes are associated. No matter what you do, this code will work.