Custom Conversion, Editing, and Formatting Simplified

Jun 7, 2011 at 6:51 PM

I really like the wpg but it was way too difficult to get custom formatting to function at a granular level.  I have a lot of experience working with the type and property descriptors from a previous job so i took a look at the code to see what i could do to simplify things.  I didn't want to make too many changes, just the bare minimum to extend the abilities of the wpg to include simplified custom conversion (i.e. formatting).  I know there are probably a lot of people who want to convert their old winforms pg to wpg and have probably stumbled in this area (because there is no simple conversion, until now), so I included two methods of doing the custom conversions.  One is the old way, using TypeConverter attributes on the properties, and the second way is the newer approach of using data templates. Hopefully this satisfies all parties.

I'll start first with the modifications i made (and i'll try and explain them as i go on).  The way the WPG currently selects the proper data template to display your property value (and editor) is through the use of a resource ID (specifically, a ComponentResourceKey).  This resource key describes the resource to look for, and then a property selector just searches up the visual wpf tree until it finds the right data template, and if it can't find anything it falls back on the default template (this can all be seen in the WPGTemplates.xaml file).  The resource key contains a resource ID property which is what is actually unique to the data template resource item.  WPG uses data types as the resource id, which is pretty standard practice.  So an In32 data type is linked to a specific template and all int's will then use this template (look for ResourceId={x:Type clr:Int32}).  The resource id doesn't need to be a type though (as can be seen with the default template), the property itself is an object, so you can use any unique identifiable object.  This is where i wanted to extend the template code, i didn't want to have to just use the same template for all properties of the same type.  The resource ID is dynamic enough that we can override it's value and customize it at compile time.  The end result is that you could specify an explicit resource ID for a property and then create a data template resource with that ID, thus allowing you to have custom templates at a property by property level.

To do this is fairly simple, we just need to provide a method of overriding the resource id on a property and then try and load the resource for that ID.  First I created a new Attribute for overriding the resource ID:

// ResourceIDAttribute.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace WPG
{
    [AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
    public sealed class ResourceIDAttribute : Attribute
    {
        public ResourceIDAttribute(object resourceID)
        {
            ResourceID = resourceID;
        }

        public object ResourceID { get; set; }
    }
}

Then in the PropertyTemplateSelector.cs file, i just try to load by an overrided resource first.  But to make things more elegant, i first made a quick mod to the Property class.

// first I added a new field
protected object _resouceID;

// then in the constructor added some code to check for the attribute's presence
// note that you'll need to include System.Linq for this stuff
var rid = property.Attributes.OfType<Attribute>().SingleOrDefault(x => x is ResourceIDAttribute) as ResourceIDAttribute;
if (rid != null)
{
    _resouceID = rid.ResourceID;
}

// finally i added a property for our new resource ID override
public object ResourceID
{
    get { return _resouceID; }
}

With those changes, i just need to modify the FindDataTemplate function in the PropertyTemplateSelector.cs file a bit.  I'll just post the whole function so it's easier to see.

private DataTemplate FindDataTemplate(Property property, FrameworkElement element)
{
	Type propertyType = property.PropertyType;
    DataTemplate template = null;

    // this is where i overrided the resource id.
    if (property.ResourceID != null)
    {
        template = TryFindDataTemplate(element, property.ResourceID);
    }

    // the rest is almost exactly the same, with some checks to make sure we don't override our override.
    if (template == null && !(property.PropertyType is String) && property.PropertyType is IEnumerable)
    {
        propertyType = typeof(List<object>);
    }

    if (template == null)
    {
        template = TryFindDataTemplate(element, propertyType);
    }

    while (template == null && propertyType.BaseType != null)
	{
		propertyType = propertyType.BaseType;
		template = TryFindDataTemplate(element, propertyType);
	}
	if (template == null)
	{
		template = TryFindDataTemplate(element, "default");
	}
	return template;
}

Now that we have these mods in place, the rest of the process becomes quite simple.  in our instance object, we just decorate a property with our resource id attribute:

public class TestObject
{
    [ResourceID("TestObject.Age")]
    public TimeSpan Age { get; set; }
}

and add our data template into the visual tree of the wpg:

<Grid>
    <Grid.Resources>
        <DataTemplate x:Key="{ComponentResourceKey TypeInTargetAssembly={x:Type wpg:PropertyGrid}, ResourceId=TestObject.Age}">
            <TextBox IsReadOnly="{Binding Path=IsReadOnly}" Style="{DynamicResource {ComponentResourceKey TypeInTargetAssembly={x:Type wpg:PropertyGrid}, ResourceId=TextBoxStyle}}">
                <TextBox.Text>
                    <Binding Mode="OneWay" Path="Value.TotalSeconds" StringFormat="{}{0:0.00} s">
                        <Binding.ValidationRules>
                            <ExceptionValidationRule />
                        </Binding.ValidationRules>
                    </Binding>
                </TextBox.Text>
            </TextBox>
        </DataTemplate>
    </Grid.Resources>
    <wpg:PropertyGrid DataContext="{Binding}" Instance="{Binding Path=MyTestObject, Mode=OneWay}"/>
</Grid>

Our resource ID override will intercept the WPG from grabbing the regular data template and use the above one instead, but only for the Age property, not for all TimeSpan properties.  If you read carefully, you'll notice that I actually just copy the default template and changed the binding to OneWay and added a StringFormat.  In reality, you can do anything you want with your custom template at this point, which is really REALLY powerful.

I won't go too in depth on the winforms method, but just give a brief overview.  The reason that TypeConverter decorators don't work right now is that the property descriptor is not used to grab the converter.  If you head back into the Property.cs file and look at the Value property, you'll see that in the setter the TypeConverter is acquired via the property's type.  If you want to inject your own custom type converter's as property decorators you simply need to make use of the _property.Converter (as opposed to TypeDescriptor.GetConverter(_property.PropertyType)).  However, if your property is of a custom class type and you put your type converter on the class level, then you will get access to the converter (for set only though).  As i said i don't really want to get into the type converter stuff too deeply, mostly because it is old tech and should probably be avoided in favor of data templates.  But here is a quick example on how to get type converters to work for display formatting:

get
{
    object value = _property.GetValue(_instance);
    if (_property.Converter.CanConvertTo(typeof(object)))
    {
        value = _property.Converter.ConvertTo(value, typeof(object));
    }
    return value;
}

If it isn't clear, that is the new version of the getter for the Value property.  You would have to do something similar for the setter, but i haven't tested that out so i won't post any code.  This simply uses the decorated converter (which defaults to the property type's converter if no decoration exists) to try and convert to object.  Converting to object doesn't make much sense, so default (built-in) converters will ignore it by returning false in the CanConvertTo function.  But in your custom converter you can just return true and provide your own custom display formatting.  The setter may be a bit more difficult to achieve, but should work by doing more or less the same thing.

 


Well that's it.  Hopefully this helps out anyone trying to do what i wanted to do (from the example you can see that it was custom display formatting on a per property basis).  for the dev team of WPG, feel free to add this stuff into the code base if you think it's worth while.  And as a stupid disclaimer, i literally spent like an hour looking at the code and whipping this mod up (and about the same time writing this up).  I did test it briefly, but by no means heavily.  There is every possibility that something does not work properly, or that i did something stupid, or that i overlooked an existing method to do this (PLEASE let me know if this is the case).  At the very least, by reading this post you will have gained some knowledge on how the WPG works.