Pattern to implement a toolbox in .Net/Wpf (not the visual part)?
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.
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.
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 isIEnumerable<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 orfalse
if not, something like thisbool 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.
精彩评论