Playing with Property Grids in C# – Part 1

The PropertyGrid Control is a really handy control, well suited for any UI where the user can configure options, since it can display them in a categorized grid, with a description at the bottom for its selected property. The only important feature missing from it, in my view, is a context menu that allows properties to be reset to their default values, just like the one in the Visual Studio IDE. So I added it. Actually it turned out to be very easy to do.

This article explains the minimum you need to do to get this working. The code here is taken directly from my RomyView application, where I used a dialog with a property grid for the user settings. All of the properties displayed in this grid are quite straightforward. Part 2 will explain how to handle expandable properties (where the properties themselves are types that contain primitive types that should also be displayed on the grid).

There are 3 code files relevant to this article:

  • A dialog that contains the grid, as well as an OK and Cancel button.
  • The type that will be displayed in the grid, decorated with the relevant properties that are key to make this UI functional.
  • My custom PropertyGridObjectMapper type, which adds a context menu that resets properties to their default values. It also sets the SelectedObject property of the grid itself. (I didn’t need to do this from my custom type, but it was a convenient place to do so.)

This is what the end result looks like in the running application, just after I right-clicked the Max Cache Size property:

PreferencesDialog

Notice that the property about to be reset is shown in bold. When the relevant attributes are implemented by the type displayed on the grid, properties with non-default values (or that do not implement default values) are shown in bold. This is built in to the framework. I just added the menu to reset them. Of course, depending on the property under the mouse, the menu item will be enabled or disabled, depending on whether the property can be reset.

Since the code to reset properties and create the context menu and so on is all inside my PropertyGridObjectMapper type, there was nothing left to do in the code of the dialog itself. It just handles the button clicks, so I will not include it here. (The shadows added to the buttons and grid are inherited from my base Form type, and are not relevant to this article.)

Requirements for the type shown in the PropertyGrid

The type used is just a normal C# class. I won’t include all of its code here. When it is set as the SelectedObject value of the PropertyGrid, any public properties it implements will automatically be displayed in the grid, in the Misc category. That is as far as the default behaviour goes.

Thus, to make it look a bit better, you need to add certain attributes to the properties. They are all to be found in the System.ComponentModel namespace:

  • CategoryAttribute: This defines what category the property will be displayed in.
  • DisplayNameAttribute: If you want the property name to contain spaces, this needs to be used. Otherwise, the actual name of the property, as in your code, is shown.
  • DescriptionAttribute: This populates the description at the bottom of the grid.
  • DefaultValueAttribute: This defines what the default values for properties are. That is, this is what causes non-default values to be shown in bold. (And that is all it does. You still need to initialize the default values yourself, however you choose to do so.)

For example:

  1. [Category(“Image Viewer”),
  2. DisplayName(“Replacement BackColor”),
  3. DefaultValue(typeof(Color), “Black”),
  4. Description(“The colour to use for the image viewer background, rather than the theme colour.”)]
  5. public Color ImageBackColor
  6. {
  7.     get { return Settings.Default.ImageViewer_ImageBackColor; }
  8.     set
  9.     {
  10.         Settings.Default.ImageViewer_ImageBackColor = value;
  11.         ImageViewer.Instance.ImageBackColor = value;
  12.     }
  13. }
  14.  
  15. [Category(“Appearance”),
  16. DisplayName(“Theme”), 
  17. DefaultValue(Theme.Light),
  18. Description(“The user interface theme, which consists of colours used in different parts of the application.”)]
  19. public Theme Theme
  20. {
  21.     get { return Romy.Controls.Properties.Settings.Default.Theme; }
  22.     set { Romy.Controls.Properties.Settings.Default.Theme = value; }
  23. }

 

Notice that in the first example property above, I used the alternate syntax to define the default value of a colour. I’m not going to admit how long it took to find out that such an alternate syntax exists, but I will say it was very annoying trying to figure out how to get colours to display default values correctly.

Using my PropertyGridObjectMapper type

Actually there is one line of code from the dialog that is needed… Here is the dialog’s constructor, where preferences is a class-level field of a custom type that wraps some user settings (and decorates their properties with attributes as shown above):

  1. public OptionsDialog()
  2. {
  3.     InitializeComponent();
  4.  
  5.     propertyGridObjectMapper = new PropertyGridObjectMapper(
  6.         this,
  7.         propertyGrid,
  8.         preferences);
  9. }

 

The constructor for this type takes 3 parameters:

  1. The Form/dialog to which the ContextMenuStrip will be added.
  2. The PropertyGrid instance.
  3. The object to be displayed in the grid.

You also need to make sure you Dispose the PropertyGridObjectMapper when you are done with it. (In a Form, that means you must add a line to the generated Dispose method.) And that’s all there is to using it! For interest sake, this is its code:

PropertyGridObjectMapper Source Code
  1. /// <summary>Use this together with an object instance on a PropertyGrid. It
  2. /// adds a ContextMenuStrip, allowing properties to be reset to their default
  3. /// values, like the Visual Studio IDE.</summary>
  4. /// <remarks><para>
  5. /// The type shown must set the <b>DefaultValueAttribute</b> for propperties
  6. /// that can be reset. Other useful attributes to use are <b>CategoryAttribute</b>,
  7. /// <b>DisplayNameAttribute</b>, and <b>DescriptionAttribute</b>.</para>
  8. /// <para>
  9. /// This type implements <b>IDisposable</b> because it creates some controls that
  10. /// must be disposed later. But it is not a Control itself. It does not have an
  11. /// automatically managed IContainer to which the controls are added. Thus when you
  12. /// use this type from a Form, you will need to manually add it to the generated
  13. /// <b>Dispose</b> method. (More simply put: Always make sure you Dispose this when
  14. /// you are done with it.)</para></remarks>
  15. public class PropertyGridObjectMapper : IDisposable
  16. {
  17.     #region Fields
  18.  
  19.     private bool disposed;
  20.  
  21.     private PropertyDescriptor[] properties;
  22.  
  23.     private ContextMenuStrip contextMenu;
  24.  
  25.     private ToolStripMenuItem descriptionMenu;
  26.  
  27.     private ToolStripMenuItem resetMenu;
  28.  
  29.     private ToolStripSeparator resetSeparator;
  30.  
  31.     #endregion Fields
  32.  
  33.     #region Constructors
  34.  
  35.     public PropertyGridObjectMapper(Form dialog, PropertyGrid grid, object selected)
  36.     {
  37.         InitializeContextMenuStrip();
  38.         Dialog = dialog;
  39.         Grid = grid;
  40.         Grid.ContextMenuStrip = contextMenu;
  41.         SelectedObject = grid.SelectedObject = selected;
  42.         properties = TypeDescriptor.GetProperties(selected).ToArray<PropertyDescriptor>();
  43.     }
  44.  
  45.     ~PropertyGridObjectMapper()
  46.     {
  47.         Dispose(false);
  48.     }
  49.  
  50.     #endregion Constructors
  51.  
  52.     #region Properties
  53.  
  54.     public Form Dialog { get; set; }
  55.  
  56.     public object SelectedObject { get; set; }
  57.  
  58.     public PropertyDescriptor Descriptor { get; set; }
  59.  
  60.     public IEnumerable<PropertyDescriptor> Properties
  61.     {
  62.         get { return properties; }
  63.     }
  64.  
  65.     public PropertyGrid Grid { get; set; }
  66.  
  67.     #endregion Properties
  68.  
  69.     #region Methods
  70.  
  71.     #region Public Methods
  72.  
  73.     public void Dispose()
  74.     {
  75.         Dispose(true);
  76.         GC.SuppressFinalize(this);
  77.     }
  78.  
  79.     #endregion Public Methods
  80.  
  81.     #region Protected Methods
  82.  
  83.     protected virtual void Dispose(bool disposing)
  84.     {
  85.         if (!disposed)
  86.         {
  87.             if (disposing)
  88.             {
  89.                 contextMenu.Dispose();
  90.                 descriptionMenu.Dispose();
  91.                 resetMenu.Dispose();
  92.                 resetSeparator.Dispose();
  93.             }
  94.             disposed = true;
  95.         }
  96.     }
  97.  
  98.     #endregion Protected Methods
  99.  
  100.     #region Private Methods
  101.  
  102.     private void ContextMenu_Opening(object sender, CancelEventArgs e)
  103.     {
  104.         resetMenu.Enabled = false;
  105.  
  106.         Descriptor = properties.FirstOrDefault(d => d.DisplayName == Grid.SelectedGridItem.Label);
  107.  
  108.         if (Descriptor != null)
  109.             resetMenu.Enabled = Descriptor.CanResetValue(SelectedObject);
  110.     }
  111.  
  112.     private void DescriptionMenu_Click(object sender, EventArgs e)
  113.     {
  114.         Grid.HelpVisible = descriptionMenu.Checked;
  115.     }
  116.  
  117.     private void InitializeContextMenuStrip()
  118.     {
  119.         this.resetMenu = new ToolStripMenuItem();
  120.         this.resetSeparator = new ToolStripSeparator();
  121.         this.descriptionMenu = new ToolStripMenuItem();
  122.         this.contextMenu = new ContextMenuStrip();
  123.         this.contextMenu.SuspendLayout();
  124.  
  125.         try
  126.         {
  127.             this.contextMenu.Items.AddRange(new ToolStripItem[] {
  128.                 this.resetMenu,
  129.                 this.resetSeparator,
  130.                 this.descriptionMenu});
  131.             this.contextMenu.Name = “contextMenu”;
  132.             this.contextMenu.Size = new System.Drawing.Size(153, 76);
  133.             this.contextMenu.Opening += new System.ComponentModel.CancelEventHandler(this.ContextMenu_Opening);
  134.  
  135.             this.resetMenu.Name = “resetMenu”;
  136.             this.resetMenu.Size = new System.Drawing.Size(152, 22);
  137.             this.resetMenu.Text = “Reset”;
  138.             this.resetMenu.Click += new System.EventHandler(this.ResetMenu_Click);
  139.  
  140.             this.resetSeparator.Name = “resetSeparator”;
  141.             this.resetSeparator.Size = new System.Drawing.Size(149, 6);
  142.  
  143.             this.descriptionMenu.Checked = true;
  144.             this.descriptionMenu.CheckOnClick = true;
  145.             this.descriptionMenu.CheckState = CheckState.Checked;
  146.             this.descriptionMenu.Name = “descriptionMenu”;
  147.             this.descriptionMenu.Size = new System.Drawing.Size(152, 22);
  148.             this.descriptionMenu.Text = “Description”;
  149.             this.descriptionMenu.Click += new EventHandler(this.DescriptionMenu_Click);
  150.         }
  151.         finally { this.contextMenu.ResumeLayout(false); }
  152.     }
  153.  
  154.     private void ResetMenu_Click(object sender, EventArgs e)
  155.     {
  156.         if (Descriptor != null)
  157.         {
  158.             Descriptor.ResetValue(SelectedObject);
  159.             Grid.Refresh();
  160.         }
  161.     }
  162.  
  163.     #endregion Private Methods
  164.  
  165.     #endregion Methods
  166. }

 

And just to be thorough… you might have noticed that the last line of its constructor calls a ToArray method on a collection that isn’t generic. This is a simple extension method I wrote that copies such a collection to an array, and returns the array. (The reason for writing this is that arrays can be passed around to Linq extension methods wherever an IEnumerable<T> is called for.)

ICollection.ToArray<T> extension methods
  1. #region ToArray
  2.  
  3. /// <summary>Converts a non-generic collection to an array.</summary>
  4. public static T[] ToArray<T>(this ICollection collection)
  5. {
  6.     var items = new T[collection.Count];
  7.     collection.CopyTo(items, 0);
  8.  
  9.     return items;
  10. }
  11.  
  12. /// <summary>Converts a non-generic collection to an array, and
  13. /// returns a filtered sequence of values based on a predicate.</summary>
  14. public static T[] ToArrayWhere<T>(this ICollection collection, Func<T, bool> predicate)
  15. {
  16.     var items = new T[collection.Count];
  17.     collection.CopyTo(items, 0);
  18.  
  19.     return items.Where(t => predicate(t)).ToArray();
  20. }
  21.  
  22. #endregion
Advertisements

About Jerome

I am a senior C# developer in Johannesburg, South Africa. I am also a recovering addict, who spent nearly eight years using methamphetamine. I write on my recovery blog about my lessons learned and sometimes give advice to others who have made similar mistakes, often from my viewpoint as an atheist, and I also write some C# programming articles on my programming blog.
This entry was posted in Programming and tagged , , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s