Welcome to CrankyGoblin.Com Sign in | Join | Help

Public Class GeoffAppleby

Inherits Microsoft.VisualBasic.MVP : Implements IBrainFart
.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)
Posted: Tuesday, 1 March 2005 2:10 PM by Geoff Appleby
Filed under:

Comments

Corneliu I. Tusnea said:

You can go further and "let it know" your properties have child properties.
Have a look here at a practical way to use this "lie" (Check the "Methods Tab"):
<a href="http://www.codeproject.com/useritems/RuntimeObjectEditor.asp">http://www.codeproject.com/useritems/RuntimeObjectEditor.asp</a>
One of the nice parts of this "lie" is that you can also "inject" attributes to your (so called) "properties".
I realy like the "events up the chain" idea.
# March 2, 2005 5:44 AM

Public Class GeoffAppleby said:

Download the code for this&amp;nbsp;post here (117k).
The PropertyGrid is a pretty cool control to have...
# June 16, 2005 7:32 AM

Public Class GeoffAppleby said:

People who read my blog know that I'm happiest coding in the back end, or in places where things like...
# March 5, 2006 10:24 PM

Public Class GeoffAppleby said:

Author: &lt;a href=&quot;blogs/geoff.appleby&quot;&gt;Geoff Appleby&lt;/a&gt;&lt;br /&gt;I thought I'd go through an example of how to get user based settings from My.Settings displayed nicely in the propertygrid - and by nicely I mean with all the bells and whistles :) Sorry C# guys, I'm dealing with the My namespace from VB. To be fair though. My.Settings is only a small part of it. To do this, I'm going to need to pull together a few different things that I've blogged about before.
# March 6, 2006 8:55 AM
Leave a Comment

(required) 

(required) 

(optional)

(required) 

To submit your comment, click on these pictures:
  • Geoff's big sister's tongue
  • Super Geoff!
  • Sleepy Geoff
Gaptcha Image - No Peeking! Gaptcha Image - No Peeking! Gaptcha Image - No Peeking!
Gaptcha Image - No Peeking! Gaptcha Image - No Peeking! Gaptcha Image - No Peeking!
Gaptcha Image - No Peeking! Gaptcha Image - No Peeking! Gaptcha Image - No Peeking!
Can't recognise the people in these pictures? Look here for a quick introduction.
There's a time limit for you to get your comment submitted before this set of pictures expires. If you think it's been longer than 10 minutes, get some new pictures first (you won't lose what you've typed so far).
Get some new pictures 

Comment Notification

If you would like to receive an email when updates are made to this post, please register here

Subscribe to this post's comments using RSS