.NET Framework PropertyGrid Control and supporting API
Most of the “magic” that is enabling this example has been in the .NET framework for a number of years now. Check out this article for a broad overview of what is required to display properties in the .NET PropertyGrid. Thisold article and this one are also good for background information.
The three main .NET types that I made use of to create this example are:
- TypeConverter – I derived a type off this class in order to proffer up the actual property that was used to expose my new type instance. This derived type is also responsible for converting the custom type to other types as needed.
- PropertyDescriptor – Derived off this type in order to handle the actual setting and getting of the binary property information.
- UITypeEditor – Derived off this type in order to display the custom editor seen in the above image
With that, we’re ready to get going!
Step-by-Step Overview
Here are the steps required to add a custom stereotype property instance to an existing profile. ( In this example, I add a new property to the “C# class” stereotype, found in the “C# Profile” that ships in the box. ):
- Create a new VSIX extension to ensure assembly load and to ease testing
- Add new property to profile
- Create a new C# class that holds the data of our new property
- Derive a new type from TypeConverter
- Derive a new type from PropertyDescriptor
- Derive a new type from UITypeEditor
- Create a new Dialog to edit the values of your custom property
1) Create a new VSIX extension
I talk about creating VSIX extensions a little here, and Peter goes into detail here. The code I’m going to show you below is actually code running on the very-soon-to-be-made-public Release Candidate. The RC is so close to shipping, that I figured I’d show code targeting that build rather than Beta2, as it will also be correct for the RTM bits. But if you just can’t wait, you should be able to figure out how to make this code work on Beta2 bits without much hassle, especially after reading the two posts I just mentioned.
All that said, create a new VSIX project, add a new class, and throw in this code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel.Composition;
using Microsoft.VisualStudio.Modeling.ExtensionEnablement;
using Microsoft.VisualStudio.ArchitectureTools.Extensibility.Uml;
namespace CustomProfilePropertyTest
{
[Export(typeof( ICommandExtension))]
[ClassDesignerExtension]
class CommandExtension : ICommandExtension
{
public void Execute(IMenuCommand command)
{
}
public void QueryStatus(IMenuCommand command)
{
}
public string Text
{
get { return typeof(CustomProfileProperty).AssemblyQualifiedName; }
}
}
}
This extension is really just designed to bootstrap your new property type assembly into Visual Studio, as well as give you a quick way of seeing the fully qualified Assembly name by putting it into a menu item on the class diagram. So on my box, my menu item, given the above code, looks like this:
You’ll notice that this project has been signed. This is very important. Your custom TypeConverter ( more later ) will not be loaded by Visual Studio if it is not contained in a signed assembly. Don’t forget, or you won’t see the right results!
Easiest way to sign your assembly is to bring up the property editor of your project, click on the “Signing” tab, and click the appropriate settings:
2) Add new property to profile
Now open up the CSharp.profile file found here: %Program Files%\Microsoft Visual Studio 10.0\Common7\IDE\Extensions\Microsoft\Architecture Tools\UmlProfiles\CSharp.Profile. We’re going to add a new property to the “C# class” stereotype. You need to modify this file in two places.
The first place is in the “properties” section of the “C# class” stereotype element. Here’s a shot of my property, right after the “Is Partial” property:
Take note of the property name ( “CustomProperty” ), the display name ( “Custom Property Example”, which is what the user will see in the property editor ), and the name of the externalTypeMoniker element. This is the most important part of this example. This is where the fully qualified assembly name comes in.
The second place is is the propertyTypes section. You need to add a new “externalType” entry, like so:
Again, make sure you get those two entries right, or things won’t work right.
3) Create a new C# class that holds the data of our new property
Now create the type that will represent your new stereotype property. Here’s what I used:
[TypeConverter( typeof( CustomProfilePropertyTypeConverter ))]
[Serializable]
public class CustomProfileProperty
{
public string Name { get; set; }
public int Age { get; set; }
public DateTime Birthday { get; set; }
public Color HairColor { get; set; }
}
public class CustomProfilePropertyTypeConverter : TypeConverter
{
public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value, System.Type destinationType)
{
// We're only looking for conversion to the String type, and then only if the value coming in
// is of type CustomProfileProperty. This will be an indicator to the user that the "complex"property
// has actually been set.
if (destinationType.Equals(typeof(String)) && value is CustomProfileProperty)
return "Property Set";
return base.ConvertTo(context, culture, value, destinationType);
}
public override bool GetPropertiesSupported(ITypeDescriptorContext context)
{
// Need to return true here in order for the GetProperties method to be called
// by the framework
return true;
}
public override bool CanConvertTo(ITypeDescriptorContext context, System.Type destinationType)
{
// Need to respond true for conversions to String in order for the ConvertTo method to be called. See above
if (destinationType.Equals(typeof(string)))
return true;
return base.CanConvertTo(context, destinationType);
}
public override PropertyDescriptorCollection GetProperties(ITypeDescriptorContext context,object value, Attribute[] attributes)
{
if (context != null && context.Instance is IEnumerable)
{
IEnumerableprops = (IEnumerable )context.Instance;
var propInstance = (from i in props where i.Name == "CustomProperty" select i).First();
PropertyDescriptorCollection collection = new PropertyDescriptorCollection(
new PropertyDescriptor[] {
new CustomProfilePropertyDescriptor(propInstance, "Custom Property Example",new Attribute[] { new BrowsableAttribute(true) })
});
return collection;
}
return null;
}
}
So the first line of substance in this method is looking for the the IStereotypePropertyInstance named after our custom type we added in step 2. Once we find it, then we create a new instance of our CustomProfilePropertyDescriptor passing in the IStereotypePropertyInstance object that ultimately owns this custom property. By creating this custom property descriptor, we are essentially adding an entire new hierarchy of values that are ultimately stored on the root IStereotypePropertyInstance ( in this case called “CustomProperty” ).
In the example above, I’m only creating one property, but you could create as many as you need.
5) Derive a new type from PropertyDescriptor
Here’s the code that shows my derived type off of PropertyDescriptor:
public class CustomProfilePropertyDescriptor : PropertyDescriptor
{
public CustomProfileProperty CustomProfileType{ get; set; }
private IStereotypePropertyInstance Instance {get; set; }
public CustomProfilePropertyDescriptor( IStereotypePropertyInstance instance, string name, Attribute[] attrs)
: base(name, attrs)
{
Instance = instance;
}
public override bool CanResetValue(object component)
{
return true;
}
public override Type ComponentType
{
get { return typeof( CustomProfileProperty); }
}
public override object GetValue(object component)
{
return CustomProfileType;
}
public override bool IsReadOnly
{
get { return false; }
}
public override Type PropertyType
{
get { return typeof(CustomProfileProperty); }
}
public override void ResetValue(object component)
{
CustomProfileType = null;
}
public override void SetValue(object component, object value)
{
if (value is CustomProfileProperty)
{
CustomProfileType = value as CustomProfileProperty;
using (MemoryStream stream = new MemoryStream())
{
BinaryFormatter formatter = new BinaryFormatter();
formatter.Serialize(stream, value);
byte[] contextBytes = stream.ToArray();
Instance.Value = System.Convert.ToBase64String(contextBytes);
}
}
}
public override bool ShouldSerializeValue(object component)
{
return false;
}
public override object GetEditor(Type editorBaseType)
{
return new CustomProfilePropertyUITypeEditor();
}
}
public class CustomProfilePropertyUITypeEditor : UITypeEditor
{
public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context)
{
return UITypeEditorEditStyle.Modal;
}
public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider,object value)
{
CustomProfilePropertyEditor editor = new CustomProfilePropertyEditor( context.Instance asCustomProfileProperty);
IUIService uiService = provider.GetService(typeof(IUIService)) as IUIService;
if (uiService != null)
{
if (uiService.ShowDialog(editor) == System.Windows.Forms.DialogResult.OK)
{
return editor.CustomProfileType;
}
}
return null;
}
}
7) Create a new Dialog to edit the values of your custom property
The dialog used to modify the custom properties can literally be anything. I created that simple WinForm dialog in no time by leveraging the Data Sources window to add a Object data source type to the form and binding it to the CustomProfileProperty type we created in step 3. But just for completeness sake, here’s the code:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
namespace CustomProfilePropertyTest
{
public partial class CustomProfilePropertyEditor : Form
{
public CustomProfileProperty CustomProfileType { get; set; }
public CustomProfilePropertyEditor( CustomProfileProperty customType )
{
InitializeComponent();
CustomProfileType = customType ?? new CustomProfileProperty();
customerProfileBindingSource.Add(CustomProfileType);
}
private void OKButtonClicked(object sender, EventArgs e)
{
CustomProfileType.Name = nameTextBox.Text;
CustomProfileType.Birthday = birthdayDateTimePicker.Value;
Close();
}
}
}
Summary
So that’s it! I hope you can make use of this technique, as it is an easy way to add more complicated types and data to your modeling data that doesn’t require any new serialization strategies, and participates nicely with the Visual Studio undo / redo mechanism without you having to be aware of the details. Cheers!
No comments:
Post a Comment
Your Comments/Posts are invited...