.Net Lets Me Lie
One of the things that's fascinated me with .Net is the way objects are queried, and the way in which a description of an object is obtained. For example, when you want the list of properties that an object has, you ask the the TypeDescriptor for a list of PropertyDescriptors, each of which describes one property.
Given that just about every class that exists can be inherited, you can quite easily lie to your callers about your details. Of course, most of the time you don't want to lie, but when you need to (a classic example is for use in the property grid) then it's actually pretty easy to do.
Let's have a look at how it works by creating a class that has no properties, but does render some in the propertygrid.
First, we need a class that implements ICustomTypeDescriptor. If you have a class that implements this interface, then calls to the TypeDescriptor forward those calls onto your class instead. This interface requires the implementation of 12 different methods. The good news is that most of these don't need to be implemented too well for our simple purposes, however it's fairly easy to override all of them, depending on your needs. In the case where we don't want to worry about it, we can simply forward on the call to the standard TypeDescriptor (letting it know not to ask the CustomTypeConverter anymore, otherwise it will call back to us, which calls it, which calls back to us, which calls it....etc :)
Imports System.ComponentModel
Public Class LyingBastard
Implements ICustomTypeDescriptor
Public Sub New()
'TODO: Fill this in if necessary.
End Sub
'This is the interesting one. We can override the list of properties returned here.
Public Overloads Function GetProperties(ByVal attributes() As System.Attribute) _
As System.ComponentModel.PropertyDescriptorCollection _
Implements System.ComponentModel.ICustomTypeDescriptor.GetProperties
'TODO: Fill this in.
End Function
'Forwarc the call to the above implementation of the same method
Public Overloads Function GetProperties() _
As System.ComponentModel.PropertyDescriptorCollection _
Implements System.ComponentModel.ICustomTypeDescriptor.GetProperties
Return Me.GetProperties(Nothing)
End Function
'Me! Me! Me! I own the properties!
Public Function GetPropertyOwner(ByVal pd As System.ComponentModel.PropertyDescriptor) _
As Object Implements System.ComponentModel.ICustomTypeDescriptor.GetPropertyOwner
Return Me
End Function
'All the following methods are simply forwarded on the generic type descriptor.
'Note the 'True' parameter, which tells it not to invoke the customtypedescriptor (that's us!)
Public Function GetAttributes() As System.ComponentModel.AttributeCollection _
Implements System.ComponentModel.ICustomTypeDescriptor.GetAttributes
Return TypeDescriptor.GetAttributes(Me, True)
End Function
Public Function GetClassName() As String _
Implements System.ComponentModel.ICustomTypeDescriptor.GetClassName
Return TypeDescriptor.GetClassName(Me, True)
End Function
Public Function GetComponentName() As String _
Implements System.ComponentModel.ICustomTypeDescriptor.GetComponentName
Return TypeDescriptor.GetComponentName(Me, True)
End Function
Public Function GetConverter() As System.ComponentModel.TypeConverter _
Implements System.ComponentModel.ICustomTypeDescriptor.GetConverter
Return TypeDescriptor.GetConverter(Me, True)
End Function
Public Function GetDefaultEvent() As System.ComponentModel.EventDescriptor _
Implements System.ComponentModel.ICustomTypeDescriptor.GetDefaultEvent
Return TypeDescriptor.GetDefaultEvent(Me, True)
End Function
Public Function GetDefaultProperty() As System.ComponentModel.PropertyDescriptor _
Implements System.ComponentModel.ICustomTypeDescriptor.GetDefaultProperty
Return TypeDescriptor.GetDefaultProperty(Me, True)
End Function
Public Function GetEditor(ByVal editorBaseType As System.Type) As Object _
Implements System.ComponentModel.ICustomTypeDescriptor.GetEditor
Return TypeDescriptor.GetEditor(Me, editorBaseType, True)
End Function
Public Overloads Function GetEvents() As System.ComponentModel.EventDescriptorCollection _
Implements System.ComponentModel.ICustomTypeDescriptor.GetEvents
Return TypeDescriptor.GetEvents(Me, True)
End Function
Public Overloads Function GetEvents(ByVal attributes() As System.Attribute) _
As System.ComponentModel.EventDescriptorCollection _
Implements System.ComponentModel.ICustomTypeDescriptor.GetEvents
Return TypeDescriptor.GetEvents(Me, attributes, True)
End Function
End Class
So as you can see, the only method in this case that we're interested is the GetProperties function. To write it however, we need to enable more of the lie! The GetProperties method is expected to return a PropertyDescriptorCollection, which is a collection (duh!) of PropertyDescriptor objects. So, we need to write a new class to inherit from that, where we can continue the deceit.
Imports System.ComponentModel
Public Class LyingProperty
Inherits PropertyDescriptor
#Region " Private Members "
Private miIdentifier As Int32
#End Region
#Region " Constructors "
'let the base construct itself.
Public Sub New(ByVal psName As String)
MyBase.New(psName, Nothing)
miIdentifier = -1
End Sub
#End Region
#Region " Public Properties "
'accessor property to get and set the identifier
Public Property Identifier() As Int32
Get
Return miIdentifier
End Get
Set(ByVal Value As Int32)
miIdentifier = Value
End Set
End Property
#End Region
#Region " Custom Events "
'every request to the descriptor forwards it on to the owner via an event.
Public Event onGetValue(ByVal sender As LyingProperty, ByRef value As Object)
Public Event onSetValue(ByVal sender As LyingProperty, ByVal value As Object)
Public Event onResetValue(ByVal sender As LyingProperty)
Public Event onGetDisplayName(ByVal sender As LyingProperty, ByRef value As String)
Public Event onGetPropertyType(ByVal sender As LyingProperty, ByRef value As System.Type)
Public Event onGetReadOnly(ByVal sender As LyingProperty, ByRef value As Boolean)
Public Event onGetCanResetValue(ByVal sender As LyingProperty, ByRef value As Boolean)
Public Event onGetShouldSerializeValue(ByVal sender As LyingProperty, ByRef value As Boolean)
Public Event onCreateAttributesCollection(ByVal sender As LyingProperty, ByRef value As System.ComponentModel.AttributeCollection)
Public Event onGetDescription(ByVal sender As LyingProperty, ByRef value As String)
#End Region
#Region " Required PropertyDescriptor Overrides "
Public Overrides Function CanResetValue(ByVal component As Object) As Boolean
Dim bRet As Boolean
RaiseEvent onGetCanResetValue(Me, bRet)
Return bRet
End Function
Public Overrides ReadOnly Property ComponentType() As System.Type
Get
Return Me.GetType
End Get
End Property
Public Overrides Function GetValue(ByVal component As Object) As Object
Dim oRet As Object
RaiseEvent onGetValue(Me, oRet)
Return oRet
End Function
Public Overrides ReadOnly Property IsReadOnly() As Boolean
Get
Dim bRet As Boolean
RaiseEvent onGetReadOnly(Me, bRet)
Return bRet
End Get
End Property
Public Overrides ReadOnly Property PropertyType() As System.Type
Get
Dim oRet As System.Type
RaiseEvent onGetPropertyType(Me, oRet)
Return oRet
End Get
End Property
Public Overrides Sub ResetValue(ByVal component As Object)
RaiseEvent onResetValue(Me)
End Sub
Public Overrides Sub SetValue(ByVal component As Object, ByVal value As Object)
RaiseEvent onSetValue(Me, value)
End Sub
Public Overrides Function ShouldSerializeValue(ByVal component As Object) As Boolean
Dim bRet As Boolean
RaiseEvent onGetShouldSerializeValue(Me, bRet)
Return bRet
End Function
#End Region
#Region " Optional PropertyDescriptor Overrides "
Public Overrides ReadOnly Property DisplayName() As String
Get
Dim sRet As String
RaiseEvent onGetDisplayName(Me, sRet)
Return sRet
End Get
End Property
Protected Overrides Function CreateAttributeCollection() As System.ComponentModel.AttributeCollection
Dim oColl As System.ComponentModel.AttributeCollection
RaiseEvent onCreateAttributesCollection(Me, oColl)
Return oColl
End Function
Public Overrides ReadOnly Property Description() As String
Get
Dim sRet As String
RaiseEvent onGetDescription(Me, sRet)
Return sRet
End Get
End Property
#End Region
End Class
So, here's our little liar. Whenever information about an object is requested, it ends up here, where we can say whatever the hell we like! However, we keep to keep things nice and flexible for the future, so let's not do anything. All it does is raise an event back to whoever wants them to provide the answer.
So now that we've got this, we can go back and fill in our GetProperties function on the LyingBastard.
Private miNumFakeProperties As Int32
Public Sub New(ByVal piNumFakeProperties As Int32)
miNumFakeProperties = piNumFakeProperties
End Sub
Public Overloads Function GetProperties(ByVal attributes() As System.Attribute) _
As System.ComponentModel.PropertyDescriptorCollection _
Implements System.ComponentModel.ICustomTypeDescriptor.GetProperties
'we need somewhere to store the properties
Dim oPropertyDescriptorArray(miNumFakeProperties - 1) As PropertyDescriptor
Dim oLiar As LyingProperty
For i As Int32 = 1 To miNumFakeProperties
oLiar = New LyingProperty("FakeProperty" & i.ToString)
oLiar.Identifier = i
AddHandler oLiar.onGetCanResetValue, AddressOf onGetLiarCanResetValue
AddHandler oLiar.onGetDisplayName, AddressOf onGetLiarDisplayName
AddHandler oLiar.onGetPropertyType, AddressOf onGetLiarPropertyType
AddHandler oLiar.onGetReadOnly, AddressOf onGetLiarReadOnly
AddHandler oLiar.onGetShouldSerializeValue, AddressOf onGetLiarShouldSerializeValue
AddHandler oLiar.onGetValue, AddressOf onGetLiarValue
AddHandler oLiar.onResetValue, AddressOf onResetLiarValue
AddHandler oLiar.onSetValue, AddressOf onSetLiarValue
AddHandler oLiar.onCreateAttributesCollection, AddressOf onCreateLiarAttributesCollection
AddHandler oLiar.onGetDescription, AddressOf onGetLiarDescription
'add the property to the array
oPropertyDescriptorArray(i - 1) = oLiar
Next
'now return the array as a collection
Return New PropertyDescriptorCollection(oPropertyDescriptorArray)
End Function
What I'm doing here is just creating a set of fake property descriptors, and returning them in a PrropertyDescriptorCollection. The amount to make is defined as a parameter of the constructor for the class.
You'll recall that the LyingProperty class threw all manner of events when it was queried. Well here, we've got to catch them so that we can set some 'real' information.
So how are the event handlers implemented? Easy.
#Region " LyingProperty Events "
'return a nice generic description here to prove the point
Public Sub onGetLiarDescription(ByVal sender As LyingProperty, ByRef value As String)
value = "This is the description text for LyingProperty number " & sender.Identifier.ToString
End Sub
'we don't want to add any attributes
Public Sub onCreateLiarAttributesCollection(ByVal sender As LyingProperty, ByRef value As System.ComponentModel.AttributeCollection)
value = New System.ComponentModel.AttributeCollection(Nothing)
End Sub
'what value shall we say is in this property? Let's multiply the identifier.
Public Sub onGetLiarValue(ByVal sender As LyingProperty, ByRef value As Object)
value = (sender.Identifier * 10)
End Sub
'This is a simple example. We won't store the new value
Public Sub onSetLiarValue(ByVal sender As LyingProperty, ByVal value As Object)
Return
End Sub
'since we're not setting, we can't exactly reset either.
Public Sub onResetLiarValue(ByVal sender As LyingProperty)
Return
End Sub
'how do we want the property name to appear?
Public Sub onGetLiarDisplayName(ByVal sender As LyingProperty, ByRef value As String)
value = sender.Name
End Sub
'for this example, the property is an int.
Public Sub onGetLiarPropertyType(ByVal sender As LyingProperty, ByRef value As System.Type)
value = GetType(Int32)
End Sub
'is this a readonly property?
Public Sub onGetLiarReadOnly(ByVal sender As LyingProperty, ByRef value As Boolean)
value = False
End Sub
'can the value be reset? nope!
Public Sub onGetLiarCanResetValue(ByVal sender As LyingProperty, ByRef value As Boolean)
value = False
End Sub
'if the lying bastard should ever be serialized, should this property be serialized? nope!
Public Sub onGetLiarShouldSerializeValue(ByVal sender As LyingProperty, ByRef value As Boolean)
value = False
End Sub
#End Region
So now the LyingProperty can return some information back to the caller. In this case, I've said that the LyingProperty is an integer, and it's value is 10 times the identifier set on it. Just as easily we could take the identifier and go and look some information up from a database, or anywhere you like. You'll note that the displayname sends back the real name of the property? I often take this opportunity to return a much nicer display name - prepend all capital letters found in the property name with a space - then you just have to name your properties well!
Just to prove that it works, this is what you end up with (if you load it into a property grid):

If you read through the code, you'll see it's pretty easy to take it further and make them complete dynamic properties, with database lookups or whatever. While this is essentially all my own work (I sat and wrote by hand every peice of code here :) I learned how to do all this myself from someone else, including the idea of throwing events up the chain to the caller. So I really do owe credit to this article for teaching me what I needed to know :)
Listening to: over now - alice in chains - (7:03)