2009-09-15

Creating design-time compatible properties and events, part 2

Hi all. In part 1 of this article, we learned how to make our simple properties show up in the properties window at design-time, so that they looked just like the built in ones that .NET controls have. Here, in part 2, we’ll discuss how to make your events show up, as well as some more advanced stuff, like adding custom lists.

First, events, opposite to Properties, don’t show by default in the properties Windows (that is, they’re not Browsable by default), so in order to show them all we need to do is to set that property to true:

  1: [Browsable(true)]
  2: public event EventHandler MyEvent;

It’s all it takes.

Now, the fun part. Let’s add some custom lists. Suppose we want to add a property to our NiceTextBox called SomeList, which can hold any of these four values: One, Two, Three and Four. There are several ways to achieve this, but I’m gonna take the simple way and use a string. So, first, we’ll have to create a “converter” class. Converter classes are used to convert specific types to and from other different types. Since we’re using a string, we’ll need to create a class that inherits System.ComponentModel.StringConverter. We’'ll use this to convert my list to and from a string.

  1: public class SomeListConverter : StringConverter
  2: {
  3:     //Values list. You could bring this from a database or so
  4:     private readonly string[] values = new[] { "One", "Two", "Three", "Four" };
  5: 
  6:     public override bool GetStandardValuesSupported(ITypeDescriptorContext context)
  7:     {
  8:         //When true, only a value from the list can be selected
  9:         return true;
 10:     }
 11: 
 12:     public override StandardValuesCollection GetStandardValues(ITypeDescriptorContext context)
 13:     {
 14:         //Return the list as a collection of standard values.
 15:         //This is the method the designer will use to pupulate the list
 16:         return new StandardValuesCollection(values);
 17:     }
 18: 
 19:     public override bool GetStandardValuesExclusive(ITypeDescriptorContext context)
 20:     {
 21:         //When false, the list will be editable, meaning that the user could type 
 22:         //a value as well. When true, the list will be read-only
 23:         return true;
 24:     }
 25: }

Now, let’s create the property. We’ll have to use the TypeConverter attribute to tell the property what class it should use to convert our List (of type StandardValuesCollection) to and from a string:

  1: [TypeConverter(typeof(SomeListConverter))]
  2: [Category("Custom properties"), DefaultValue(""), Description("Some custom list used")]
  3: public string SomeList { get; set; }

The result will be pretty neat:

OK, so that’s it for simple lists. But what if one of our custom properties needs to be set through a more complex class? Let’s take a look at the following example, we’ll create a property called Version, which will have all four parts of a regular assembly version: Major, Minor, Build and Private. We’ll do it so that the user can set the version value directly (something like 2.1.0.17), or he can set the value for each part separately. That kind of behavior, like the one we see in common properties as Location and Size, is achieved through something called expandable objects, specifically by using the ExpandableObjectConverter class.

First, we’ll create an ApplicationVersion class:

  1: public class ApplicationVersion
  2: {
  3:     public short Major { get; set; }
  4: 
  5:     public short Minor { get; set; }
  6: 
  7:     public short Build { get; set; }
  8: 
  9:     public short Private { get; set; }
 10: }

This class we’ll represent the type of our Version property (which we’ll construct later). Now, we’ll have to create the converter. Like said before, because this property will be an expandable object, we’ll have to inherit the ExpandableObjectConverter class:

  1: internal class ApplicationVersionConverter : ExpandableObjectConverter
  2: {
  3:     public override bool CanConvertTo(ITypeDescriptorContext context, 
  4:         Type destinationType)
  5:     {
  6:         //This will indicate if the Converter can convert the object TO the 
  7:         //specified type. In this case, ApplicationVersion to string
  8:         return destinationType == typeof(ApplicationVersion) || 
  9:             base.CanConvertTo(context, destinationType);
 10:     }
 11: 
 12:     public override bool CanConvertFrom(ITypeDescriptorContext context, 
 13:         Type sourceType)
 14:     {
 15:         //This will indicate if the Converter can convert the object FROM the 
 16:         //specified type. In this case, string to ApplicationVersion
 17:         return sourceType == typeof(String) || 
 18:             base.CanConvertFrom(context, sourceType);
 19:     }
 20: 
 21:     public override object ConvertFrom(ITypeDescriptorContext context, 
 22:         System.Globalization.CultureInfo culture, object value)
 23:     {
 24:         //This will implement the logic to convert from string to object. 
 25:         /It will be used when the user directly enters the version value 
 26:         //(2.0.4.17) instead of setting each part individually
 27:         if (value is string)
 28:             try
 29:             {
 30:                 string s = value.ToString();
 31:                 string[] versionParts = s.Split('.');
 32:                 if (versionParts.Length > 0)
 33:                 {
 34:                     var version = new ApplicationVersion();
 35:                     if (versionParts[0] != null)
 36:                         version.Major = Convert.ToInt16(versionParts[0]);
 37:                     if (versionParts[1] != null)
 38:                         version.Minor = Convert.ToInt16(versionParts[1]);
 39:                     if (versionParts[2] != null)
 40:                         version.Build = Convert.ToInt16(versionParts[2]);
 41:                     if (versionParts[3] != null)
 42:                         version.Private = Convert.ToInt16(versionParts[3]);
 43:                     return version;
 44:                 }
 45:             }
 46:             catch (Exception)
 47:             {
 48:                 throw new ArgumentException(
 49:                     string.Format(
 50:                         "The value {0} isn't a valid Version number", value));
 51:             }
 52:         return base.ConvertFrom(context, culture, value);
 53:     }
 54: 
 55:     public override object ConvertTo(ITypeDescriptorContext context, 
 56:         System.Globalization.CultureInfo culture, 
 57:         object value, Type destinationType)
 58:     {
 59:         //This will implement the logic to convert from object to string. 
 60:         //It serves the exact opposite purpose of the above method
 61:         if (destinationType == typeof(string) && value is ApplicationVersion)
 62:         {
 63:             var version = (ApplicationVersion)value;
 64:             return string.Format("{0}.{1}.{2}.{3}", version.Major, 
 65:                 version.Minor, version.Build, version.Private);
 66:         }
 67:         return base.ConvertTo(context, culture, value, destinationType);
 68:     }
 69: }

Finally, we’ll just create our Version property:

  1: [TypeConverter(typeof(ApplicationVersionConverter))]
  2: [Category("Custom properties"), DefaultValue("0.0.0.0"), Description("App version")]
  3: public ApplicationVersion Version { get; set; }

Here’s what we’ll get:

So, it’s possible to create properties and events that are fully design-time compatible. It might take a little work, but it’s definitely worth it, specially for people that create custom controls and plan to share or sell them. Hope you enjoyed the articles.

No hay comentarios:

Publicar un comentario

Your tips and thoughts are always welcome, and they provide good motivation: