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
Yes, I've switched to Live Writer full time now. The VSPaste plug-in is excellent.
Editing Properties
The theory is baked and the anticipation is high, it’s time to edit Properties inside the designer. I add the DesignablePropertyDescriptor type to the solution and add the GetDesignProperties method to the IDesignableControl interface. With this added I need to go about providing implementations. This project is going to start and end with the purple Couch.
public Color FillColor { get; set; }
public List<DesignablePropertyDescriptor> GetDesignProperties()
{
return new List<DesignablePropertyDescriptor>();
}
I need to implement this for the FillColor property.
public List<DesignablePropertyDescriptor> GetDesignProperties()
{
List<DesignablePropertyDescriptor> props = new List<DesignablePropertyDescriptor>();
DesignablePropertyDescriptor fillColor = new DesignablePropertyDescriptor();
fillColor.PropertyInfo = GetType().GetProperty("FillColor");
fillColor.DisplayName = "Color";
fillColor.DisplayType = typeof(TextBlock);
fillColor.Editable = true;
fillColor.EditorType = typeof(ComboBox);
fillColor.SupportsStandardValues = true;
fillColor.StandardValues = new List<object>( new object[] {Colors.Purple, Colors.Red, Colors.Gray, Colors.Brown, Colors.Gray});
return props;
}
I’m already questioning the decision not to use attributes, that’s a lot of code for one property. Still, I’ll follow this through for now. Later I’ll dig up some WinForms designer code for comparison. Now I need to provide my default implementation for IDesignableControlInspector.
public interface IDesignableControlInspector : IService
{
List<DesignablePropertyDescriptor> Inspect(IDesignableControl idt);
} public class DefaultInspector : IDesignableControlInspector
{
public System.Collections.Generic.List<DesignablePropertyDescriptor> Inspect(IDesignableControl idt)
{
if (null != idt)
{
return idt.GetDesignProperties();
}
return null;
}
}
Refactor: At this point, I can see that my IService interface is broken. I’m forcing myself to implement Startup() and Shutdown() methods that I have yet to use in any meaningful way. Removing Startup from the IService definition gives a single compiler error from ServiceManager. The methods are gone until I need them. Of course, this leaves IService as a simple “marker” interface with no methods. Not terribly evil, but there are better ways to supply what is essentially metadata in .NET.
Now I create the IDesignEditorService interface:
public interface IDesignEditorService:IService
{
Control Visual { get; }
void Edit(IList<IDesignableControl> targets);
}
For now, I will leave IDesignablePropertyEditor alone. I need to create a PropertyGrid control and wire up the code needed to use it. The specifics of making the templatable PropertyGrid will be in the next article. The PropertyGrid must implement IDesignEditorService.
public Control Visual
{
get
{
return this;
}
}
public void Edit(IList<IDesignableControl> targets)
{
//TODO: something
}
Now, in order to begin doing our work of displaying and editing properties, we need to get whatever IDesignableControlInspector has been registered.
public PropertyGrid()
{
DefaultStyleKey = typeof(PropertyGrid);
Inspector = ServiceManager.Resolve<IDesignableControlInspector>();
}
IDesignableControlInspector Inspector { get; set; }
And now we can begin doing some work inside Edit.
public virtual void Edit(IList<IDesignableControl> targets)
{
if (null != targets && targets.Count > 0 && null != targets[0])
{
if (targets.Count > 1)
{
EditMultiple(targets);
}
else
{
EditSingle(targets[0]);
}
}
else
{
ClearProperties();
}
}
I've decided it really would be nice to handle editing multiple items with the same Properties, so we'll do that too. In order to keep methods small and readable and provide some extra support for alternative implementations of IDesignEditorService I have created EditServiceHelper to assist with the various reflection and control instantiation operations. In order to more easily see how well this is working, I start by wiring up Selection events with the PropertyGrid. This means that my DesignSurface control now needs to get an IDesignEditorService injected and we need to handle selection events.
private ISelectionService _selectionSvc;
public ISelectionService SelectionSvc
{
get
{
return _selectionSvc;
}
set
{
if (null != _selectionSvc)
{
_selectionSvc.SelectionChanged -= new EventHandler(SelectionSvc_SelectionChanged);
}
_selectionSvc = value;
_selectionSvc.SelectionChanged += new EventHandler(SelectionSvc_SelectionChanged);
}
}void SelectionSvc_SelectionChanged(object sender, EventArgs e)
{
List<IDesignableControl> idtList = new List<IDesignableControl>();
foreach (var idt in SelectionSvc.GetSelection())
{
if (idt is DesignSite)
{
idtList.Add(((DesignSite)idt).HostedContent);
}
else
{
idtList.Add(idt);
}
}
EditSvc.Edit(idtList);
}
Ok, nothing crazy so far, then things got interesting. How to generically have some kind of binding between "some visual representation of a property" and "some property on an IDesignableControl" ? I went back and added another property to my Couch, namely the "DesignTimeName", in order to illustrate this better. This is where things stand now inside Couch.GetDesignProperties()
public List<DesignablePropertyDescriptor> GetDesignProperties()
{
List<DesignablePropertyDescriptor> props = new List<DesignablePropertyDescriptor>();
DesignablePropertyDescriptor name = new DesignablePropertyDescriptor();
name.PropertyInfo = GetType().GetProperty("DesignTimeName");
name.DisplayName = "Name";
name.Editable = true;
DesignablePropertyDescriptor fillColor = new DesignablePropertyDescriptor();
fillColor.PropertyInfo = GetType().GetProperty("FillColor");
fillColor.DisplayName = "Color";
fillColor.DisplayType = typeof(TextBlock);
fillColor.Editable = true;
fillColor.EditorType = typeof(ComboBox);
fillColor.SupportsStandardValues = true;
fillColor.StandardValues = new List<object>( new object[] {Colors.Purple, Colors.Red, Colors.Gray, Colors.Brown, Colors.Gray});
props.Add(name);
props.Add(fillColor);
return props;
}
Inside PropertyGrid.EditSingle, we build up our Grid rows & columns using the EditServiceHelper I mentioned earlier.
protected virtual void EditSingle(IDesignableControl idt)
{
List<DesignablePropertyDescriptor> props = Inspector.Inspect(idt);
var propCount = 0;
foreach (var descriptor in props)
{
_propertyPart.RowDefinitions.Add(new RowDefinition());
TextBlock tb = new TextBlock();
tb.Text = descriptor.DisplayName;
tb.SetValue(Grid.RowProperty, propCount);
tb.SetValue(Grid.ColumnProperty, 0);
//
FrameworkElement displayElement = EditServiceHelper.GetBoundDisplayElement(descriptor, idt);
displayElement.SetValue(Grid.RowProperty, propCount);
displayElement.SetValue(Grid.ColumnProperty, 1);
displayElement.GotFocus += new RoutedEventHandler(displayElement_GotFocus);
//
_propertyPart.Children.Add(tb);
_propertyPart.Children.Add(displayElement);
++propCount;
}
}
That's not too bad so far. Here's a screenshot with my so far, so lame PropertyGrid template.
I'm going to skip over the implementations inside EditServiceHelper for now, it's in the code if you're curious. Suffice to say it contains some Property to Control mappings for some basic supported types. For example, a Property of Type string will be both displayed and edited using a TextBox element and a Two Way Binding. When we get into more complex Visualization and Edit scenarios in a later article.
The next thing we need to do is handle a couple of simple edit scenarios when the display element gets focus, as shown above in EditSingle.
void displayElement_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
VerifyEditVisualization(sender);
}
void displayElement_GotFocus(object sender, RoutedEventArgs e)
{
VerifyEditVisualization(sender);
}
private void VerifyEditVisualization(object sender)
{
if (null != _targets && _targets.Count > 0)
{
FrameworkElement element = (FrameworkElement)sender;
Type t = sender.GetType();
DesignablePropertyDescriptor descriptor = _descriptorMap[element];
Type editType = EditServiceHelper.GetEditElementType(descriptor);
if (!t.Equals(editType))
{
FrameworkElement editElement = EditServiceHelper.GetBoundEditElement(descriptor, editType, _targets[0]);
editElement.SetValue(Grid.RowProperty, element.GetValue<int>(Grid.RowProperty) );
editElement.SetValue(Grid.ColumnProperty, 1);
_propertyPart.Children.Add(editElement);
_editElement = editElement;
}
//else do nothing, the Editor is the same as the Displayor
}
}
One thing you can see immediately is that my colors are listed using their Hex #s, so I need to introduce the value converters into the mix. I also need to fill in a better look and feel for the PropertyGrid, handle Multiple IDC Edit, and invent a scenario where I can test out the IDesignablePropertyEditor idioms. This article is already getting a bit long, so I will do some more cleanup and stop here for now.
Making Property Registration Easier
I'm still thinking that writing code for this property edit scheme this will be easier than the current WinForms equivalent, but I realized immediately that it could be better. I have created a couple of new methods in EditServiceHelper:
/// <summary>
/// Returns common props from IDesignableControl : DesignTimeName,
/// </summary>
/// <returns></returns>
public static List<DesignablePropertyDescriptor> GetDefaultDescriptors()
/// <summary>
/// Return default <code>DesignablePropertyDescriptor</code> objects for each Property name
/// </summary>
/// <param name="names"></param>
/// <returns></returns>
public static List<DesignablePropertyDescriptor> GetDescriptors(IEnumerable<string> names, IDesignableControl idc)
The default can be merged with any custom list a control author wishes to return.
Updating the Existing Controls
Refactor: The Couch, Chair, Center channel speaker, and Tower speaker controls were all refactored to use the new convienience methods to return their own properties. The descriptions no longer say "red chair" or "purple" couch, since we can actually edit the properties of items on the surface now!
Here's what the application looks like now:
Yes, we left Property Editing half done in order to keep each article shorter.
Conclusion
If you're following the source code, there were several tiny refactorings made that weren't mentioned here. Speaking of code, as I previously blogged, the project is in CodePlex now at http://www.codeplex.com/agt. The current source code for this article, then, is the 0.7.16.1 Release. Tune in next time to see Property Editing reach a more finished state.
The live demo has been updated at http://www.damonpayne.com/agt