Tuesday, March 23, 2010

Custom Profile Properties

image

.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:

  1. 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.
  2. PropertyDescriptor – Derived off this type in order to handle the actual setting and getting of the binary property information.
  3. 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. ):

  1. Create a new VSIX extension to ensure assembly load and to ease testing
  2. Add new property to profile
  3. Create a new C# class that holds the data of our new property
  4. Derive a new type from TypeConverter
  5. Derive a new type from PropertyDescriptor
  6. Derive a new type from UITypeEditor
  7. 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:

image

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:

image

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:

image

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:

image

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; }
}
You’ll notice that I have applied two attributes to the new type, both quite important. The TypeConverter attribute associates the “CustomProfilePropertyTypeConverter” with the “CustomProfileProperty” type. The framework now knows what type of TypeConverter to use when dealing with a CustomProfileProperty. ( We’ll be creating the TypeConverter in our next step, so hang tight! ).
The “Serializable” attribute is also important, as I make use of the BinaryFormatter object later on. This makes serializing the instance data of this type real simply, allowing us to store that data in a string variable associated with the stereotype property instance ( again, more later ).
4) Derive a new type from TypeConverter
Now let’s create that new custom TypeConverter. Here’s the code:
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)
{
IEnumerable props = (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;
}
}
I put comments explaining most of the simple methods, but let me describe what’s going on in the GetProperties method. When examining an element out of the UML store, you can find out what Stereotypes are applied to a particular element which returns you a collection of IStereotypeInstances. Each IStereotypeInstance can have many IStereotypePropertyIntance objects. Hopefully the following image will make this more clear, and show how these types equate to what you see in the CSharp.profile file:
image

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();
}
}
Most of the methods are self explanatory, but I’ll call out a couple. In the SetValue method is where the data that the user has provided for your the new property gets set back onto the IStereotypePropertyInstance object associated with the Class element. Ultimately, that data is serialized back into an XML file, so you can’t just set the Value property on Instance to some XML markup, as Value is stored as an XML attribute. So we’ll just serialize the object to a MemoryStream, then convert into a string value. This works great in this instance, as Visual Studio will later use BinaryFormatter to rehydrate the serialized object when reading the information back from disk at a later time.
You’ll also notice the GetEditor method, which is where you can return you’re derived type off of UITypeEditor, which I’ll show you now.
6) Derive a new type from UITypeEditor
Here’s the code:
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;
}
}
The key in this type is to return UITypeEditorEditStyle.Modal from the GetEditStyle method in order to get the browse button ( the button with the “…” as the caption ) on the property grid for the property. Then when the user clicks on that button, the EditValue method is called. In our case, we new up our new dialog, use the IUIService provided by Visual Studio to show that dialog, and if everything went ok, return the newly modified custom property.

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...