tl; dr: Я хочу повторно использовать существующую логику компоновки предопределенной панели WPF для пользовательский класс панели WPF. Этот вопрос содержит четыре различные попытки решить эту проблему, каждая из которых имеет разные недостатки и, следовательно, другую точку отказа. Кроме того, небольшой тестовый пример можно найти ниже.
Вопрос: Как правильно достичь этой цели
- определение моей пользовательской панели, когда
- внутреннее повторное использование логики компоновки другой панели без
- работает в проблемах, описанных в моих попытках решить эту проблему?
Я пытаюсь написать собственный WPF panel. Для этого класса панели я хотел бы придерживаться рекомендуемых методов разработки и поддерживать чистый API и внутреннюю реализацию. Конкретно это означает:
- Я бы хотел избежать копирования и вставки кода; если несколько частей кода имеют одну и ту же функцию, код должен существовать только один раз и повторно использоваться.
- Я хотел бы применить правильную инкапсуляцию и предоставить внешним пользователям доступ только к таким членам, которые можно безопасно использовать (без нарушения какой-либо внутренней логики или без предоставления какой-либо внутренней информации, специфичной для реализации).
В настоящее время я собираюсь придерживаться существующего макета, я хотел бы повторно использовать другой код компоновки панели (вместо того, чтобы снова писать код компоновки, как предлагается, например здесь). Для примера я объясню на основе DockPanel
, хотя я хотел бы знать, как это сделать в целом, на основе любой вид Panel
.
Чтобы повторно использовать логику компоновки, я собираюсь добавить DockPanel
в качестве визуального дочернего элемента в моей панели, который затем будет удерживать и компоновать логические дочерние элементы моей панели.
Я пробовал три разные идеи о том, как это решить, и еще один был высказан в комментарии, но каждый из них до сих пор терпит неудачу в другой точке:
1) Внесите внутреннюю панель макета в шаблон управления для настраиваемой панели
Это похоже на самое элегантное решение. Таким образом, панель управления для настраиваемой панели может содержать ItemsControl
, чья ItemsPanel
свойство использует DockPanel
, а ItemsSource
свойство привязано к Children
property пользовательской панели.
К сожалению, Panel
не наследуется от 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();
}
}
В ответе пользователем Arie NameScope
классбыло указано, что она играет ключевую роль в этом: имена дочерних элементов управления по какой-либо причине не регистрируются в соответствующем NameScope
. Это может быть частично исправлено путем вызова RegisterName
для каждого дочернего элемента, но нужно было бы получить правильный экземпляр NameScope
. Кроме того, я не уверен, будет ли поведение, когда, например, изменение имени ребенка, будет таким же, как и в других панелях.
Вместо этого установка NameScope
внутренней панели, по-видимому, является способом выхода. Я пробовал это с прямой привязкой (в конструкторе TestPanel1
):
BindingOperations.SetBinding(innerPanel,
NameScope.NameScopeProperty,
new Binding("(NameScope.NameScope)") {
Source = this
});
К сожалению, это просто устанавливает NameScope
внутренней панели на null
. Насколько я могу узнать с помощью Snoop, фактический экземпляр NameScope
сохраняется только в NameScope
прикрепленное свойство как родительского окна, так и корень вложенного визуального дерева, определенного шаблоном управления (или, возможно, каким-либо другим ключом node?), независимо от того, какой тип. Разумеется, экземпляр управления может быть добавлен и удален в разных положениях дерева управления в течение его жизненного цикла, поэтому релевантный NameScope
может время от времени меняться. Это, опять же, требует привязки.
Здесь я снова застрял, потому что, к сожалению, невозможно определить RelativeSource
для привязки на основе произвольного условия как * первый встреченный node, который имеет значение не 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
, поэтому я не уверен, как безопасно обрабатывать raw 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"/>
Текстовый блок должен быть оставлен из текстового поля, и он должен показывать все, что в настоящее время написано в текстовом поле.
Я ожидал бы, что текстовое поле будет доступно для кликов, а выходное представление не будет отображать какие-либо ошибки привязки (так что привязка также должна работать).
Таким образом, мой вопрос:
- Можно ли зафиксировать любую из моих попыток, чтобы привести к правильному решению? Или есть совершенно другой способ, который предпочитает то, что я пытался делать, что я ищу?