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

tl;dr: я хочу повторно использовать существующую логику компоновки предварительно определенной панели WPF для пользовательского класса панели WPF. Этот вопрос содержит четыре различные попытки решить эту проблему, каждая из которых имеет свои недостатки и, следовательно, разные точки отказа. Кроме того, небольшой тестовый пример можно найти ниже.

Вопрос в том, как мне правильно достичь этой цели.

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

Я пытаюсь написать пользовательскую панель WPF. Для этого класса панели я бы хотел придерживаться рекомендуемых методов разработки и поддерживать чистый API и внутреннюю реализацию. Конкретно это означает:

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

В настоящее время я собираюсь пристально придерживаться существующего макета, я хотел бы повторно использовать код макета другой панели (вместо того, чтобы снова писать код макета, как предлагается, например, здесь). Для примера я объясню на основе DockPanel хотя я хотел бы знать, как это сделать вообще, основываясь на любом виде Panel,

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

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


1) Введите внутреннюю панель макета в шаблоне управления для пользовательской панели

Это кажется самым элегантным решением - таким образом, панель управления для пользовательской панели может иметь ItemsControl чья ItemsPanel свойство использует DockPanel и чей ItemsSource собственность связана с Children свойство пользовательской панели.

К несчастью, 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 вложенное свойство родительского окна или корня вложенного визуального дерева, определяемого шаблоном элемента управления (или, возможно, каким-либо другим ключевым узлом?), независимо от его типа. Конечно, экземпляр элемента управления может быть добавлен и удален в разных позициях в дереве элемента управления в течение срока его службы, поэтому соответствующие 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"/>

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

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


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

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

2 ответа

Решение

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

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

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

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

реализация метода FindChild: /questions/2343240/kak-ya-mogu-najti-elementyi-upravleniya-wpf-po-imeni-ili-tipu/2343250#2343250


РЕДАКТИРОВАТЬ:

Если вы хотите, чтобы "обычная" привязка 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} будет работать во время выполнения.

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


РЕДАКТИРОВАТЬ OP: Этот ответ привел меня на правильный путь, так как он упомянул NameScope класс

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

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

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

Я не уверен, что вы можете полностью скрыть внутреннюю структуру вашей панели - ведь я знаю, что WPF не использует доступ "backdoor" при построении визуального / логического дерева, поэтому, как только вы что-то скрываете от пользователя, вы также скрываете его из 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 },

Другие вопросы по тегам