tl;dr: я хочу повторно использовать существующую логику макета предварительно определенного панель WPF для пользовательского класса панели WPF. Этот вопрос содержит четыре разных попытки решить эту проблему, каждая из которых имеет разные недостатки и, следовательно, разные точки отказа. Кроме того, ниже можно найти небольшой тестовый пример.
Вопрос в следующем: как правильно достичь этой цели
- определение моей пользовательской панели во время
- внутреннее повторное использование логики компоновки другой панели, без
- столкнулись с проблемами, описанными в моих попытках решить эту проблему?
Я пытаюсь написать собственный WPF панель. Для этого класса панели я хотел бы придерживаться рекомендуемых методов разработки и поддерживать чистый API и внутреннюю реализацию. Конкретно это означает:
- Я хотел бы избежать копирования и вставки кода; если несколько частей кода имеют одну и ту же функцию, код должен существовать только один раз и использоваться повторно.
- Я хотел бы применить правильную инкапсуляцию и позволить внешним пользователям получать доступ только к тем членам, которые можно безопасно использовать (без нарушения какой-либо внутренней логики или без раскрытия какой-либо внутренней информации, специфичной для реализации).
На данный момент я собираюсь придерживаться существующего макета, я хотел бы повторно использовать код макета другой панели (вместо того, чтобы снова писать код макета, как предлагается, например, здесь). Для примера я объясню на основе DockPanel
, хотя я хотел бы знать, как это сделать вообще, на основе любого вида Panel
.
Чтобы повторно использовать логику компоновки, я собираюсь добавить DockPanel
в качестве визуального дочернего элемента в свою панель, который затем будет содержать и размещать логические дочерние элементы моей панели.
Я попробовал три разных идеи о том, как решить эту проблему, и еще одна была предложена в комментарии, но каждая из них пока терпит неудачу в разных точках:
1) Добавьте внутреннюю панель макета в шаблон управления для пользовательской панели
Это кажется самым элегантным решением — таким образом, панель управления для пользовательской панели может иметь ItemsControl
, чей ItemsPanel
использует DockPanel
, и чье ItemsSource
свойство привязано к Children
свойство пользовательской панели.
К сожалению, Panel
< /a> не наследуется от Control
и, следовательно, не имеет Template
свойство, ни функция поддержки шаблонов элементов управления.
С другой стороны, Children
представлено Panel
и, следовательно, не представлено в Control
, и я считаю, что можно было бы считать хакерством вырваться из предполагаемой иерархии наследования и создать панель, которая на самом деле является Control
, но не Panel
.
2) Предоставьте список дочерних элементов моей панели, который является просто оболочкой для списка дочерних элементов внутренней панели
Такой класс выглядит так, как показано ниже. Я создал подкласс UIElementCollection
в своем классе панелей и вернул его из переопределенной версии CreateUIElementCollection
метод. (Я скопировал только те методы, которые на самом деле вызываются здесь; я реализовал остальные так, чтобы генерировалось исключение NotImplementedException
, поэтому я уверен, что никакие другие переопределяемые члены не вызывались.)
using System;
using System.Windows;
using System.Windows.Controls;
namespace WrappedPanelTest
{
public class TestPanel1 : Panel
{
private sealed class ChildCollection : UIElementCollection
{
public ChildCollection(TestPanel1 owner) : base(owner, owner)
{
if (owner == null) {
throw new ArgumentNullException("owner");
}
this.owner = owner;
}
private readonly TestPanel1 owner;
public override int Add(System.Windows.UIElement element)
{
return this.owner.innerPanel.Children.Add(element);
}
public override int Count {
get {
return owner.innerPanel.Children.Count;
}
}
public override System.Windows.UIElement this[int index] {
get {
return owner.innerPanel.Children[index];
}
set {
throw new NotImplementedException();
}
}
}
public TestPanel1()
{
this.AddVisualChild(innerPanel);
}
private readonly DockPanel innerPanel = new DockPanel();
protected override UIElementCollection CreateUIElementCollection(System.Windows.FrameworkElement logicalParent)
{
return new ChildCollection(this);
}
protected override int VisualChildrenCount {
get {
return 1;
}
}
protected override System.Windows.Media.Visual GetVisualChild(int index)
{
if (index == 0) {
return innerPanel;
} else {
throw new ArgumentOutOfRangeException();
}
}
protected override System.Windows.Size MeasureOverride(System.Windows.Size availableSize)
{
innerPanel.Measure(availableSize);
return innerPanel.DesiredSize;
}
protected override System.Windows.Size ArrangeOverride(System.Windows.Size finalSize)
{
innerPanel.Arrange(new Rect(new Point(0, 0), finalSize));
return finalSize;
}
}
}
Это работает почти правильно; макет DockPanel
повторно используется, как и ожидалось. Единственная проблема заключается в том, что привязки не находят элементы управления на панели по имени (с ElementName
свойство).
Я попытался вернуть внутренних детей из LogicalChildren
свойство, но это ничего не изменило:
protected override System.Collections.IEnumerator LogicalChildren {
get {
return innerPanel.Children.GetEnumerator();
}
}
В ответе пользователя Арье, Было указано, что класс NameScope
играет решающую роль в этом: имена дочерних элементов управления по какой-то причине не регистрируются в соответствующем классе NameScope
. Это может быть частично исправлено путем вызова RegisterName
для каждого дочернего элемента, но потребуется получить правильный экземпляр NameScope
. Кроме того, я не уверен, будет ли поведение, когда, например, изменяется имя дочернего элемента, таким же, как и в других панелях.
Вместо этого установка NameScope
на внутренней панели кажется правильным путем. Я попробовал это с помощью прямой привязки (в конструкторе TestPanel1
):
BindingOperations.SetBinding(innerPanel,
NameScope.NameScopeProperty,
new Binding("(NameScope.NameScope)") {
Source = this
});
К сожалению, это просто устанавливает NameScope
внутренней панели на null
. Насколько мне удалось выяснить с помощью Snoop, реальный экземпляр NameScope
хранится только в NameScope
прикрепленное свойство любого из родительских окон или корень окружающего визуального дерева, определяемый шаблоном элемента управления (или, возможно, каким-либо другим ключевым узлом?), независимо от типа. Конечно, экземпляр элемента управления может добавляться и удаляться в разных позициях в дереве элементов управления в течение его жизни, поэтому соответствующий NameScope
может время от времени меняться. Это, опять же, требует привязки.
Здесь я снова застрял, потому что, к сожалению, нельзя определить RelativeSource
для привязки на основе произвольного условия, такого как *первый обнаруженный узел, которому присвоено значение, отличное от null
, для NameScope
прикрепленное свойство.
Если другой вопрос о том, как реагировать на обновления в окружающем визуальном дереве, не дает полезный ответ, есть ли лучший способ получить и/или привязать к NameScope
, актуальным в данный момент для любого данного элемента фреймворка?
3) Используйте внутреннюю панель, список дочерних элементов которой просто такой же, как у внешней панели
Вместо того, чтобы хранить дочерний список во внутренней панели и перенаправлять вызовы в дочерний список внешней панели, это работает как бы наоборот. Здесь используется только дочерний список внешней панели, в то время как внутренняя панель никогда не создает свой собственный, а просто использует тот же экземпляр:
using System;
using System.Windows;
using System.Windows.Controls;
namespace WrappedPanelTest
{
public class TestPanel2 : Panel
{
private sealed class InnerPanel : DockPanel
{
public InnerPanel(TestPanel2 owner)
{
if (owner == null) {
throw new ArgumentNullException("owner");
}
this.owner = owner;
}
private readonly TestPanel2 owner;
protected override UIElementCollection CreateUIElementCollection(FrameworkElement logicalParent)
{
return owner.Children;
}
}
public TestPanel2()
{
this.innerPanel = new InnerPanel(this);
this.AddVisualChild(innerPanel);
}
private readonly InnerPanel innerPanel;
protected override int VisualChildrenCount {
get {
return 1;
}
}
protected override System.Windows.Media.Visual GetVisualChild(int index)
{
if (index == 0) {
return innerPanel;
} else {
throw new ArgumentOutOfRangeException();
}
}
protected override System.Windows.Size MeasureOverride(System.Windows.Size availableSize)
{
innerPanel.Measure(availableSize);
return innerPanel.DesiredSize;
}
protected override System.Windows.Size ArrangeOverride(System.Windows.Size finalSize)
{
innerPanel.Arrange(new Rect(new Point(0, 0), finalSize));
return finalSize;
}
}
}
Здесь работает компоновка и привязка к элементам управления по имени. Однако элементы управления не кликабельны.
Я подозреваю, что мне нужно как-то перенаправлять звонки на HitTestCore(GeometryHitTestParameters)
и HitTestCore(PointHitTestParameters)
на внутреннюю панель. Однако на внутренней панели я могу получить доступ только к InputHitTest
, поэтому я не уверен, как безопасно обрабатывать необработанные HitTestResult
без потери или игнорирования какой-либо информации, которая учитывалась бы в исходной реализации, а также как обрабатывать GeometryHitTestParameters
, как InputHitTest
принимает только простой Point
< /а>.
Кроме того, элементы управления также не могут быть сфокусированы, например. нажав Tab. Я не знаю, как это исправить.
Кроме того, я немного опасаюсь идти по этому пути, так как я не уверен, какие внутренние связи между внутренней панелью и исходным списком дочерних элементов я разрываю, заменяя этот список дочерних элементов пользовательским объектом.
4) Наследовать непосредственно от класса панели
Пользователь Clemens предлагает напрямую наследовать мой класс от DockPanel
. Однако есть две причины, почему это не очень хорошая идея:
- Текущая версия моей панели будет опираться на логику компоновки
DockPanel
. Однако возможно, что в какой-то момент в будущем этого будет недостаточно, и кому-то действительно придется писать собственную логику компоновки в моей панели. В этом случае замена внутреннегоDockPanel
кодом пользовательского макета тривиальна, но удалениеDockPanel
из иерархии наследования моей панели будет означать критическое изменение. - Если моя панель наследуется от
DockPanel
пользователи панели могут саботировать код макета, возясь со свойствами, предоставляемымиDockPanel
, в частностиLastChildFill
. И хотя это именно то свойство, я хотел бы использовать подход, который работает со всемиPanel
. Например, пользовательская панель, полученная изGrid
откроетColumnDefinitions
иRowDefinitions
, с помощью которых любой автоматически сгенерированный макет можно было полностью уничтожить через общедоступный интерфейс пользовательской панели.
В качестве тестового примера для наблюдения за описанными проблемами добавьте экземпляр тестируемой пользовательской панели в XAML и внутри этого элемента добавьте следующее:
<TextBox Name="tb1" DockPanel.Dock="Right"/>
<TextBlock Text="{Binding Text, ElementName=tb1}" DockPanel.Dock="Left"/>
Текстовый блок должен быть слева от текстового поля, и он должен отображать все, что в данный момент написано в текстовом поле.
Я ожидаю, что текстовое поле будет интерактивным, а выходное представление не будет отображать никаких ошибок привязки (поэтому привязка также должна работать).
Таким образом, мой вопрос:
- Можно ли исправить любую из моих попыток, чтобы получить полностью правильное решение? Или есть совершенно другой способ, который предпочтительнее того, что я пытался сделать, то, что я ищу?
DockPanel
. Внутри, при настройке одного из моих собственных прикрепленных свойств, должны быть установлены свойстваDockPanel
. Однако я намерен сделать это деталью реализации; вполне возможно, что, например, внутреннийDockPanel
будет заменен наGrid
в будущих версиях (и если это произойдет, я все еще не хочу, чтобыColumnDefinitions
иRowDefinitions
были видны снаружи). - person O. R. Mapper   schedule 07.06.2015DockPanel
по-прежнему довольно прост, но воспроизведение логики макетаGrid
кажется серьезной задачей. - person O. R. Mapper   schedule 02.07.2015Items
вместоChildren
), либо какой-то хакерский гибридный класс, такой как подход 2. - person almulo   schedule 02.07.2015