views:

305

answers:

2

I'm building a Wpf app that currently has a toolbox which looks and functions similarly to the Visual Studio toolbox. I have a range of classes that are represented in the toolbox and different types of objects that can each contain some, but not all, of the toolbox items.

As a stopgap measure to get my app up and going, I hardcoded the toolbox functionality into the UserControls. Now I must progress to a better design that isn't hardcoded.

I've got a simple ToolBoxItem class that has properties for Type, Label and Icon. Each class that needs to go into the toolbox is added to a collection of ToolBoxItems and is displaying correctly.

But I am struggling to come up with an elegant solution for two problems:

  • How to create an instance of the class represented by the ToolBoxItem Type in the object being edited.
  • How to hide Toolbox items that are not compatible with the item being edited. I manually do this now with hardcoded properties that each edited class must implement via an interface (ie CanSupportClassX, CanSupportBaseClassY).

My feeling is that delegates and perhaps generics could be a solution but I've only consumed those features in the past and am unsure how to proceed.

+1  A: 

I think the best way is the one taken by Visual Studio itself: Each object describes what it can contain by the types of its properties.

So for Visual Studio I can create a new object that can hold Widgets like this:

[ContentAttribute("Children")]
public class MyObject
{
  ...
  public ICollection<Widget> Children { get ... set ... }
}

the Visual Studio type system does the rest.

Of course there are limitations here that might necessitate some extensions or even another technique. For example, if your object can accept two different types of content that don't share a common base class except for object, you will have to declare your property as ICollection<object> in which case your toolbox won't know what it can really take unless you have an additional mechanism.

Many add-on mechanisms could be used for these special cases, for example:

  • Allow multiple "content" attributes, for example ICollection<Widget> WidgetChildren { get ... set ...}; ICollection<Doodad> DoodadChildren { get ... set ... }
  • Create an attribute you can apply to the class to give it an object type it can contain, for example [ContentTypeAllowed(typeof(Widget))] [ContentTypeAllowed(typeof(Doodad))] where your actual content property is IEnumerable<object>
  • Create an attribute that just has a list of class names, for example [ContentTypesAllowed("Widget,Doodad")]
  • Create a method that, if defined in the target object, evaluates a potential content class and returns true if it can be a child or false if not, something like this bool CanAcceptChildType(Type childType) { return type.IsSubclassOf(Widget) && !type==typeof(BadWidget); }
  • Create an attribute listing illegal children, for example [ContentTypesDisallowed("BadWidget")]
  • Add attributes or methods to your item class to represent this data, for example [TargetMustRespondTo(EditCommands.Cut)] public class CutTool { ... }
  • Add data to your item class to represent this data, for example new ToolBoxItem { Name="Widget", AllowedContainers=new[] { typeof(MyObject), typeof(OtherObject) }
  • Combinations of the above

All of these are viable techniques. Consider your own situation to see which one(s) make the most sense to implement.

To actually implement hiding your tool box item, just use a IMultiValueConverter bound to a Visibilty property in your tool box item's DataTemplate. Pass two bindings to the converter: your tool box item and your target. Then implement the logic you have decided on in your IMultiValueConverter.

For example, in the simplest case where you only care about the collection type of the ContentAttribute, this code would work:

public object Convert(object[] values, ...)
{
  var toolItem = values[0] as ToolItem;
  var container = values[1];

  var contentPropertyAttribute =
    container.GetType().GetCustomAttributes()
    .OfType<ContentPropertyAttribute>()
    .FirstOrDefault();
  if(contentPropertyAttribute!=null)
  {
    var contentProperty = container.GetType().GetProperty(contentPropertyAttribute.Name);
    if(contentProperty!=null &&
       contentProperty.Type.IsGeneric &&
       contentProperty.Type.GetGenericArguments()[0].IsAssignableFrom(toolItem.Type))
      return Visibility.Visible;
  }
  return Visibility.Collapsed;
}

In real situations things may be a little more complex, for example not all Content properties are ICollection, so you'll have to do additional checking and maybe implement more algorithms. It would also be a good idea to add some caching so you aren't using reflection as frequently.

Ray Burns
Those are some good suggestions on how to customize the toolbox - I'm working through them now to find the direction that I want to go.Can you offer help with the 1st bullet point, which is how the toolboxitem can create an instance of the class it represents in the the edited container?
awx
Sorry, I missed that part of the question. The answer is extremely simple: object item = Activator.CreateInstance(toolBoxItem.Type). You can set toolBoxItem.Type using typeof(), or if you're reading from a database you can use assembly.GetType(name)
Ray Burns
Do you think a delegate to a static CreateInstance method would be better than Activator.CreateInstance?
awx
It depends on where the toolbox item definitions are coming from. If the tool items are constructed in C# it is easy to create delegates. On the other hand if the tool items are constructed via VB.NET, XAML, XML or database queries, creating such delegates is a pain (long VB.NET or Reflection.Emit). Activator.CreateInstance is almost always easier. It will take a few few nanoseconds versus a fraction of a nanosecond for the delegate. But if you only use it on mouse clicks and not in a tight loop, the user will never know the difference so Activator.CreateInstance is better IMHO.
Ray Burns
A: 

A good example of what you're trying to achieve can be found in AvalonDock, which is completely open source. If you want to see how to use AvalonDock with MVVM (i.e. putting tools in windows and so on), check out the SoapBox Core Framework.

Scott Whitlock