The AGT (Argentum Tela) series of articles is an effort to do two things. Usually an idea is presented only in its finished form. The first goal is to do some Reality Blogging, to show an idea evolve over time without pulling any punches. The second goal, and the example vehicle for the evolution aspect, is an extensible Design Surface for Silverlight similar to what we have in Visual Studio 2008. This type of application has all sorts of interesting uses. My example is a Home Theater layout tool. Read the entire saga: http://www.damonpayne.com/2008/09/14/RunTimeIsDesignTimeForAGT0.aspx
Finishing Property Editing
In the last article, I mentioned the need for some more complex test cases to use to help determine when Property Editing was done. I could see that too much logic was living in EditServiceHelper and that the editing of properties for multiple items at once was going to become very difficult. It has been far longer than I intended since the last article (user group presentations, etc.) and there's a lot of cool stuff we can get to after we get Property Editing working the rest of the way so let's get to it.
A Better Example
A much better example was required right off the bat. This required some more up front thought and setup. Each Property potentially has a Display Visual and an Edit Visual, and in some cases these may be the same instance. I need to test some reasonable scenarios:
- If multiple items are selected that happen to have a Property with the same name and of the same type and with the same Editors we can edit this Property for all selected items with a single Edit Visual even if the items are not of the same Type. Only the lowest common denominator of Properties should display.
- Basic edit scenarios: an Enum Property is editable in a ComboBox, a string Property is Editable in a TextBox, and so forth.
- In some cases the Display Visual will be a custom IDesignablePropertyVisualizer Type which should support some kind of Binding so it auto-updates when edited.
- In some cases the Edit Visual will be a custom IDesignablePropertyEditor Type which should support binding as well as setting the value to the actual object on loss of focus or Enter key.
- Refactor: It seems clear at this point that since I'm heavily using Bindings, IValueConverter will come in handy, so this has been added to DesignablePropertyDescriptor.
In order to test all of this I'll make the following changes:
- Couch and Chair will get a Softness property of type Double which I'll edit with a custom slider. This should work for single or multi selection. (Tests #1, #4)
- Softness will be displayed with a custom "volume style" Visualizer (Tests #3)
- The Color property of Chair & Couch will get a Converter that displays a Color name instead of the hex value as shown in the previous article. (Tests #1, #2, #5)
Beyond this, we'll simply need to think about the various "basic" edit scenarios to support, like editing boolean values with a radio button or checkbox. I've added "Editors" and "Visualizers" namespaces to the DamonPayne.HTLayout project and created the requisite implementations.
PropertyGridModel
The various private fields I had been adding to PropertyGrid for keeping track of various states and such clearly needed to be moved to a single class which I now call PropertyGridModel. All of the methods are virtual to allow enterprising developers to easily tweak behavior. The public interface for PropertyGridModel :
/// <summary>
/// What are we editing?
/// </summary>
public virtual List<IDesignableControl> Selection { get; set; }
/// <summary>
/// Properties common across the entire Selection
/// </summary>
/// <param name="props"></param>
public virtual void SetProperties(List<DesignablePropertyDescriptor> props)
/// <summary>
/// Only 1 property is editing at a time
/// </summary>
public FrameworkElement CurrentEditElement { get; set; }
public virtual void SetDisplayElement(DesignablePropertyDescriptor d, FrameworkElement fe)...
public virtual FrameworkElement GetDisplayElement(DesignablePropertyDescriptor d)...
public virtual List<FrameworkElement> GetAllDisplayElements()...
public virtual void SetEditElement(DesignablePropertyDescriptor d, FrameworkElement fe) ...
public virtual FrameworkElement GetEditElement(DesignablePropertyDescriptor d)...
public virtual DesignablePropertyDescriptor GetDescriptorForEditElement(FrameworkElement fe)...
public virtual DesignablePropertyDescriptor GetDescriptorForDisplayElement(FrameworkElement fe)...
/// <summary>
/// Remove everything from the Model
/// </summary>
public virtual void Reset()...
EditServiceHelper
I continued by overhauling the EditServiceHelper class. I realized my notion of the TypeDescriptor-like functionality of this class was not fully baked, but sitting down to look at it the second time a much more cohesive purpose coalesced. I also made sure to pull in the concepts of IDesignablePropertyEditor and IDesignablePropertyVisualizer right away. I'm only going to show the Display path and no the Edit path here for brevity, they are very similar.
In a static constructor we set up some defaults, this will be expanding as I go.
AddDefaultDisplayor(typeof(string), typeof(TextBlock));
AddDefaultDisplayor(typeof(Color), typeof(TextBlock));
AddDefaultEditor(typeof(string), typeof(TextBox));
AddDefaultEditor(typeof(Color), typeof(ComboBox));
AddBasicBindableDisplayType(typeof(TextBlock), TextBlock.TextProperty);
AddBasicBindableEditType(typeof(TextBox), TextBox.TextProperty);
AddBasicBindableEditType(typeof(ComboBox), ComboBox.SelectedItemProperty);
These are all public static methods in case someone needs to expand this faster than I get to it.
Client code (PropertyGrid) will call GetDisplayInstance to get some type of Visual that can be used to represent the value of a single property
public static FrameworkElement GetDisplayInstance(IDesignableControl instance, DesignablePropertyDescriptor desc)
{
//if null we try to find a default, otherwise see if we can do a binding anyway using the override
if (null == desc.DisplayType || _basicBindableDisplayTypes.ContainsKey(desc.DisplayType))
{
Type displayType = _displayors[desc.PropertyInfo.PropertyType];
FrameworkElement displayInstance = null;
if (null != displayType)
{
displayInstance = (FrameworkElement)Activator.CreateInstance(displayType);
SetupDisplayInstanceBinding(instance, desc, displayInstance);
}
return displayInstance;
}
else if (desc.DisplayType.ImplementsInterface(typeof(IDesignablePropertyVisualizer)))
{
var visualizer = (IDesignablePropertyVisualizer)Activator.CreateInstance(desc.DisplayType);
visualizer.Initialize(instance, desc);
return visualizer.Visual;
}
return null;
}
First check defaults, then check special overrides, then ultimately give up. ImplementsInterface is an extension method in the DamonPayne.AGT.IoC project. SetupDisplayInstanceBinding handles the rest of the work which is fairly simple with the help of Silverlight Binding.
protected static void SetupDisplayInstanceBinding(IDesignableControl instance,
DesignablePropertyDescriptor desc, FrameworkElement display)
{
if (_basicBindableDisplayTypes.ContainsKey(display.GetType()))
{
var dProp = _basicBindableDisplayTypes[display.GetType()];
Binding b = new Binding(desc.PropertyInfo.Name);
b.Converter = desc.Converter;
b.Mode = BindingMode.TwoWay;
b.Source = instance;
display.SetBinding(dProp, b);
}
}
How might a custom Property Visualizer work? Take a look at part of the code behind for displaying the "softness" property of Couches & Chairs:
public double Softness
{
get { return (double)GetValue(SoftnessProperty); }
set { SetValue(SoftnessProperty, value); }
}
// Using a DependencyProperty as the backing store for Softness. This enables animation, styling, binding, etc...
public static readonly DependencyProperty SoftnessProperty =
DependencyProperty.Register("Softness", typeof(double), typeof(SoftnessVisualizer), new PropertyMetadata(SoftnessChanged));
public static void SoftnessChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
if(obj is SoftnessVisualizer)
{
((SoftnessVisualizer)obj).ScaleSoftness();
}
}
protected void ScaleSoftness()
{
if (Softness > 0.0)
{
VisualScale.ScaleX = Softness;
VisualTranslate.X = 10 * Softness;
}
else
{
VisualScale.ScaleX = 1.0;
VisualTranslate.X = 0.0;
}
}
/// <summary>
/// We know we're just for softness!
/// </summary>
/// <param name="instance"></param>
/// <param name="desc"></param>
public void Initialize(DamonPayne.AGT.Design.IDesignableControl instance, DamonPayne.AGT.Design.Types.DesignablePropertyDescriptor desc)
{
if (null != instance)
{
Binding b = new Binding("Softness");
b.Converter = desc.Converter;
b.Mode = BindingMode.TwoWay;
b.Source = instance;
SetBinding(SoftnessVisualizer.SoftnessProperty, b);
}
}
The visual representation of softness here is a triangle that I am stretching based on Softness but that's not important. In Initialize we set up a binding to the particular Property we care about which handles keeping in sync with the underlying object. Using a changed callback for the Softness DependencyProperty allows us to tweak the UI if an editor changes this value in some fashion.
You can see here that I am making a heavy dependence on System.Windows.Data.Binding concepts which in turn means DependencyProperties. This is OK as far as I'm concerned. The Binding in Silverlight/WPF is the Data Binding you've always wished you had. It is awesome. It is likely going to be used frequently and you should learn it, love it, learn where it breaks down, and return to loving it.
The PropertyGrid logic is now fairly simple and just swaps an Edit Visual out for a Display Visual when the Display Visual is clicked on.
The ColorConverter I wrote may be helpful as well...
public class ColorConverter : IValueConverter
{
static ColorConverter()
{
_colorNameToColor = new Dictionary<string, Color>();
_colorToName = new Dictionary<Color, string>();
Type cType = typeof(System.Windows.Media.Colors);
PropertyInfo[] colors = cType.GetProperties(BindingFlags.Public | BindingFlags.Static);
foreach (var propInfo in colors)
{
var c = (Color)propInfo.GetValue(null,null);
_colorNameToColor.Add(propInfo.Name, c);
_colorToName.Add(c, propInfo.Name);
}
}
private static Dictionary<string, Color> _colorNameToColor;
private static Dictionary<Color, string> _colorToName;
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (value is Color)
{
Color src = (Color)value;
if (targetType == typeof(string))
{
if (_colorToName.ContainsKey(src))
{
return _colorToName[src];
}
else
{
return src.ToString();
}
}
}
return null;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (value is string)
{
string src = value.ToString();
if (targetType == typeof(Color))
{
if (_colorNameToColor.ContainsKey(src))
{
return _colorNameToColor[src];
}
}
}
return null;
}
}
How does it look?
I needed to create some better visuals than the placeholders I had at the end of the previous article. The default ControlTemplate for PropertyGrid now looks like this with a Chair selected. Click on any image for the larger version.
Note my special visualizer (I am no artist) for Softness. When I select Softness, I get a custom Editor as well:
With a chair, a couch, and a Dummy Button selected, the only Property they have in common is Name, so only name shows up in the grid:
Couches and Chairs have lots in common, so I could edit the Color of them all at once:
Unit Tests
Jeff Wilcox released a full Silverlight 2 version of his Silverlight Unit Test Framework, so the AGT Solution has been updated to use this. I have yet to find new Templates, so the Templates still say Beta and the default test project has to be tweaked slightly. This is extremely handy for things like IValueConverters, and I will be greatly increasing test coverage in the future.
Conclusion
So, we've got Property Editing working now and this is starting to look like a real Design Time Environment.
The source code is checked into Codeplex as downloadable Change Set 8353 here. I have also created a 0.7.17.2 "Planning" source drop on Codeplex.
The Live Demo has been updated at http://www.damonpayne.com/agt/
In the next article, we will be going through some refactoring items and some questions that I've been meaning to get to.