Я создаю приложение Wpf, в котором в настоящее время есть набор инструментов, который выглядит и работает так же, как набор инструментов Visual Studio. У меня есть ряд классов, представленных в панели инструментов, и различные типы объектов, каждый из которых может содержать некоторые, но не все элементы панели инструментов.

В качестве временной меры для запуска моего приложения я жестко встроил функциональность панели инструментов в UserControls. Теперь я должен перейти к лучшему дизайну, который не запрограммирован жестко.

У меня есть простой класс ToolBoxItem, у которого есть свойства Type, Label и Icon. Каждый класс, который должен войти в панель инструментов, добавляется в коллекцию ToolBoxItems и отображается правильно.

Но я изо всех сил пытаюсь найти элегантное решение двух проблем:

  • Как создать экземпляр класса, представленного типом ToolBoxItem в редактируемом объекте.
  • Как скрыть элементы Toolbox, несовместимые с редактируемым элементом. Я вручную делаю это сейчас с жестко заданными свойствами, которые каждый редактируемый класс должен реализовывать через интерфейс (например, CanSupportClassX, CanSupportBaseClassY).

Я считаю, что делегаты и, возможно, дженерики могут быть решением, но я использовал только эти функции в прошлом и не знаю, как действовать дальше.

0
awx 9 Ноя 2009 в 07:28

2 ответа

Лучший ответ

Хороший пример того, чего вы пытаетесь достичь, можно найти в AvalonDock, исходный код которого полностью открыт. . Если вы хотите узнать, как использовать AvalonDock с MVVM (например, размещение инструментов в окнах и т. Д.), Ознакомьтесь с SoapBox Core Framework.

1
Scott Whitlock 10 Ноя 2009 в 06:38

Я думаю, что лучший способ - это тот, который использует сама Visual Studio: каждый объект описывает то, что он может содержать, по типам его свойств.

Итак, для Visual Studio я могу создать новый объект, который может содержать такие виджеты:

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

Все остальное сделает система типов Visual Studio.

Конечно, здесь есть ограничения, которые могут потребовать некоторых расширений или даже другой техники. Например, если ваш объект может принимать два разных типа контента, которые не имеют общего базового класса, за исключением object, вам придется объявить свое свойство как ICollection<object>, и в этом случае ваш набор инструментов выиграет. Я не знаю, что это действительно может сделать, если у вас нет дополнительного механизма.

Для этих особых случаев можно использовать множество дополнительных механизмов, например:

  • Разрешить несколько атрибутов «содержимого», например ICollection<Widget> WidgetChildren { get ... set ...}; ICollection<Doodad> DoodadChildren { get ... set ... }
  • Создайте атрибут, который вы можете применить к классу, чтобы присвоить ему тип объекта, который он может содержать, например [ContentTypeAllowed(typeof(Widget))] [ContentTypeAllowed(typeof(Doodad))], где ваше фактическое свойство содержимого - IEnumerable<object>
  • Создайте атрибут, который содержит просто список имен классов, например [ContentTypesAllowed("Widget,Doodad")]
  • Создайте метод, который, если он определен в целевом объекте, оценивает потенциальный класс контента и возвращает true, если он может быть дочерним, или false, если нет, что-то вроде этого bool CanAcceptChildType(Type childType) { return type.IsSubclassOf(Widget) && !type==typeof(BadWidget); }
  • Создайте атрибут, в котором перечислены незаконные дочерние элементы, например [ContentTypesDisallowed("BadWidget")]
  • Добавьте атрибуты или методы в свой класс элемента для представления этих данных, например [TargetMustRespondTo(EditCommands.Cut)] public class CutTool { ... }
  • Добавьте данные в свой класс элемента для представления этих данных, например new ToolBoxItem { Name="Widget", AllowedContainers=new[] { typeof(MyObject), typeof(OtherObject) }
  • Сочетания вышеперечисленного

Все это жизнеспособные методы. Обдумайте свою собственную ситуацию, чтобы понять, какие из них наиболее целесообразно реализовать.

Чтобы фактически реализовать скрытие элемента панели инструментов, просто используйте IMultiValueConverter, привязанный к свойству Visibilty в элементе панели инструментов DataTemplate. Передайте конвертеру две привязки: элемент ящика для инструментов и цель. Затем реализуйте логику, которую вы определили, в своем IMultiValueConverter.

Например, в простейшем случае, когда вас интересует только тип коллекции ContentAttribute, этот код будет работать:

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;
}

В реальных ситуациях все может быть немного сложнее, например, не все свойства содержимого являются ICollection, поэтому вам придется выполнить дополнительную проверку и, возможно, реализовать больше алгоритмов. Также было бы неплохо добавить кеширование, чтобы вы не использовали отражение так часто.

1
Ray Burns 9 Ноя 2009 в 20:10
Это несколько хороших советов о том, как настроить набор инструментов - сейчас я работаю над ними, чтобы найти направление, в котором я хочу двигаться. Можете ли вы предложить помощь с 1-м пунктом, а именно с тем, как элемент набора инструментов может создать экземпляр класса, который он представляет, в отредактированном контейнере?
 – 
awx
10 Ноя 2009 в 00:18
Извините, я пропустил эту часть вопроса. Ответ предельно прост: объект item = Activator.CreateInstance (toolBoxItem.Type). Вы можете установить toolBoxItem.Type с помощью typeof () или, если вы читаете данные из базы данных, вы можете использовать assembly.GetType (name)
 – 
Ray Burns
10 Ноя 2009 в 02:56
Как вы думаете, лучше использовать делегат статического метода CreateInstance, чем Activator.CreateInstance?
 – 
awx
12 Ноя 2009 в 03:34
Это зависит от того, откуда берутся определения элементов панели инструментов. Если элементы инструментов созданы на C #, легко создавать делегатов. С другой стороны, если элементы инструмента создаются с помощью VB.NET, XAML, XML или запросов к базе данных, создание таких делегатов является проблемой (длинный VB.NET или Reflection.Emit). Activator.CreateInstance почти всегда проще. Это займет несколько наносекунд против долей наносекунды для делегата. Но если вы используете его только при щелчках мышью, а не в жестком цикле, пользователь никогда не узнает разницы, поэтому Activator.CreateInstance лучше IMHO.
 – 
Ray Burns
12 Ноя 2009 в 07:36