Подтвердить что ты не робот

Как повторно использовать существующий код макета для нового класса Panel?

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"/>

Текстовый блок должен быть оставлен из текстового поля, и он должен показывать все, что в настоящее время написано в текстовом поле.

Я ожидал бы, что текстовое поле будет доступно для кликов, а выходное представление не будет отображать какие-либо ошибки привязки (так что привязка также должна работать).


Таким образом, мой вопрос:

  • Можно ли зафиксировать любую из моих попыток, чтобы привести к правильному решению? Или есть совершенно другой способ, который предпочитает то, что я пытался делать, что я ищу?
4b9b3361

Ответ 1

Если ваша единственная проблема с вашим вторым подходом (укажите список дочерних элементов моей панели, который является всего лишь оберткой вокруг списка дочерних элементов внутренней панели) - это отсутствие возможности привязки к элементам управления внутренней панели по имени, затем soluton будет:

    public DependencyObject this[string childName]
    {
        get
        {
            return innerPanel.FindChild<DependencyObject>(childName);
        }
    }

а затем пример привязки:

"{Binding ElementName=panelOwner, Path=[innerPanelButtonName].Content}"

реализация метода FindChild: fooobar.com/questions/30206/...


EDIT:

Если вы хотите, чтобы работать с обычным привязкой ElementName, вам нужно зарегистрировать имена элементов управления, которые являются дочерними элементами innerPanel в соответствующем NameScope:

var ns = NameScope.GetNameScope(Application.Current.MainWindow);

foreach (FrameworkElement child in innerPanel.Children)
{
    ns.RegisterName(child.Name, child);
}

Теперь привязка {Binding ElementName=innerPanelButtonName, Path=Content} будет работать во время выполнения.

Проблема с надежным поиском элемента UI корня для получения NameScope (здесь: Application.Current.MainWindow - не будет работать во время разработки)


EDIT by OP: этот ответ привел меня на правильный путь, поскольку он упомянул NameScope class.

Мое окончательное решение основано на TestPanel1 и использует пользовательскую реализацию INameScope interface. Каждый из его методов поднимает логическое дерево, начиная с внешней панели, чтобы найти ближайший родительский элемент, NameScope свойство не null:

  • RegisterName и UnregisterName пересылают свой вызов соответствующим методам найденного объекта INameScope и в противном случае генерируют исключение.
  • FindName переводит свой вызов в FindName найденного объекта INameScope и в противном случае (если такой объект не был найден) возвращает null.

Экземпляр этой реализации INameScope устанавливается как NameScope внутренней панели.

Ответ 2

Я не уверен, что вы можете полностью затенять внутреннюю структуру вашей панели - для всех, кого я знаю, никакой "бэкдор" доступ не используется WPF при построении визуально-логического дерева, поэтому, как только вы что-то скрываете от пользователя, вы также скрыть его от WPF. Я бы хотел сделать структуру "доступной только для чтения" (сохраняя доступную структуру, вам не нужно беспокоиться о механизмах привязки). Для этого я предлагаю извлечь из UIElementCollection и переопределить все методы, используемые для изменения состояния коллекции, и использовать ее в качестве коллекции ваших панелей. Что касается "обманывания" XAML для добавления детей непосредственно во внутреннюю панель, вы можете просто использовать ContentPropertyAttribute вместе со свойством, раскрывающим коллекцию дочерних элементов внутренней панели. Здесь приведен пример работы (по крайней мере, для вашего тестового примера) такой панели:

[ContentProperty("Items")]
public class CustomPanel : Panel
{
    public CustomPanel()
    {
        //the Children property seems to be lazy-loaded so we need to
        //call the getter to invoke CreateUIElementCollection
        Children.ToString();
    }

    private readonly Panel InnerPanel = new DockPanel();

    public UIElementCollection Items { get { return InnerPanel.Children; } }

    protected override Size ArrangeOverride(Size finalSize)
    {
        InnerPanel.Arrange(new Rect(new Point(0, 0), finalSize));
        return finalSize;
    }

    protected override UIElementCollection CreateUIElementCollection(FrameworkElement logicalParent)
    {
        return new ChildCollection(this);
    }

    protected override Size MeasureOverride(Size availableSize)
    {
        InnerPanel.Measure(availableSize);
        return InnerPanel.DesiredSize;
    }

    private sealed class ChildCollection : UIElementCollection
    {
        public ChildCollection(CustomPanel owner)
            : base(owner, owner)
        {
            //call the base method (not the override) to add the inner panel
            base.Add(owner.InnerPanel);
        }

        public override int Add(UIElement element) { throw new NotSupportedException(); }

        public override void Clear() { throw new NotSupportedException(); }

        public override void Insert(int index, UIElement element) { throw new NotSupportedException(); }

        public override void Remove(UIElement element) { throw new NotSupportedException(); }

        public override void RemoveAt(int index) { throw new NotSupportedException(); }

        public override void RemoveRange(int index, int count) { throw new NotSupportedException(); }

        public override UIElement this[int index]
        {
            get { return base[index]; }
            set { throw new NotSupportedException(); }
        }
    }
}

В качестве альтернативы вы можете пропустить ContentPropertyAttribute и открыть детскую коллекцию внутренней панели с помощью public new UIElementCollection Children { get { return InnerPanel.Children; } } - это также будет работать, поскольку ContentPropertyAttribute("Children") наследуется от Panel.

З

Чтобы предотвратить вмешательство внутренней панели с использованием неявных стилей, вы можете инициализировать внутреннюю панель с помощью new DockPanel { Style = null }.