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

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

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

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


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

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

person O. R. Mapper    schedule 07.06.2015    source источник
comment
Может кто-нибудь объяснить отрицательный голос, пожалуйста?   -  person O. R. Mapper    schedule 07.06.2015
comment
Чем макет вашей пользовательской панели отличается от макета DockPanel? Возможно, вы можете получить от DockPanel и реализовать различия в MeasureOverride и ArrangeOverride после вызова методов базового класса.   -  person Clemens    schedule 07.06.2015
comment
@Clemens: я хотел бы управлять макетом с другими присоединенными свойствами, чем у DockPanel. Внутри, при настройке одного из моих собственных прикрепленных свойств, должны быть установлены свойства DockPanel. Однако я намерен сделать это деталью реализации; вполне возможно, что, например, внутренний DockPanel будет заменен на Grid в будущих версиях (и если это произойдет, я все еще не хочу, чтобы ColumnDefinitions и RowDefinitions были видны снаружи).   -  person O. R. Mapper    schedule 07.06.2015
comment
@Clemens: В любом случае спасибо за замечание, я добавил предложение к вопросу.   -  person O. R. Mapper    schedule 11.06.2015
comment
Просто скопируйте необходимые части кода макета DockPanel в свой класс Panel?   -  person Clemens    schedule 30.06.2015
comment
@Clemens: Не будет ли это юридически ограничивать то, что я могу делать с полученным кодом, если его части скопированы из несвободных источников?   -  person O. R. Mapper    schedule 30.06.2015
comment
Часть .NET уже имеет открытый исходный код. Вам придется ознакомиться с условиями лицензии, но я не думаю, что будут какие-либо ограничения на то, как можно использовать ваш элемент управления.   -  person Clemens    schedule 30.06.2015
comment
@Clemens: Вы имеете в виду Справочный источник? Его лицензия гласит: специально исключает право распространять программное обеспечение за пределами вашей компании. лицензия MIT .NET Core не включает WPF (пока?).   -  person O. R. Mapper    schedule 01.07.2015
comment
Тем не менее, я думаю... В любом случае, из справочного источника вы можете получить представление о том, как это работает, и реализовать его аналогичным образом. Не должно быть слишком много кода для повторной реализации.   -  person Clemens    schedule 01.07.2015
comment
Я не могу придумать лучшего нехакерского варианта. Панели в конце концов являются панелями, у них нет внутренних панелей или шаблонов, они просто определяют логику компоновки. Если часть этой логики похожа на логику DockPanel, вполне логично (без каламбура) воспроизвести часть ее кода.   -  person almulo    schedule 01.07.2015
comment
@almulo: почему-то копирование и вставка больших объемов кода кажется мне неоптимальным; Кроме того, DockPanel по-прежнему довольно прост, но воспроизведение логики макета Grid кажется серьезной задачей.   -  person O. R. Mapper    schedule 02.07.2015
comment
Ну, что я могу сказать... Создание пользовательских элементов макета никогда не бывает легкой задачей в WPF. 100% ПРАВИЛЬНЫМ способом будет создание панели с нуля. Копирование кода из эталонного источника — это просто наш способ сэкономить вам много работы. Если вы предпочитаете отказаться от панели (понятно), то это либо пользовательский ItemsControl (с Items вместо Children), либо какой-то хакерский гибридный класс, такой как подход 2.   -  person almulo    schedule 02.07.2015
comment
Мне очень трудно понять, ПОЧЕМУ вы хотите это сделать, и ПОЧЕМУ вам нужны эти жесткие требования и опасения по поводу злоупотребления общедоступной собственностью. Если вы укажете причины для попытки этого, вы, вероятно, сможете лучше помочь решить эту проблему.   -  person Zache    schedule 06.07.2015
comment
@Zache: я несколько удивлен, что для общих передовых методов требуется дополнительное обоснование, такое как повторное использование существующего кода, а не его дублирование, или правильная инкапсуляция функциональности путем запрета доступа внешнего кода к любым членам, которые могут мешать предполагаемому поведению, но я попытаюсь чтобы уточнить это.   -  person O. R. Mapper    schedule 06.07.2015
comment
@ O.R.Mapper Мне просто кажется, что вы пытаетесь сделать что-то сложное и сложное, когда могли бы сделать что-то гораздо более простое. Если вы создаете библиотеку управления для продажи, я понимаю ваши потребности, но если вы просто не доверяете своим коллегам, я не понимаю;)   -  person Zache    schedule 06.07.2015
comment
@Zache: Возможно, мы следуем разным философиям разработки, но, как правило, я склонен применять точно такие же стандарты инкапсуляции, сокрытия информации и удобства обслуживания к API и коду, которые я просто использую сам, как к API и коду, который Я продаю клиентам. Хотя, по сути, эта панель должна быть частью библиотеки управления, которой будут пользоваться посторонние.   -  person O. R. Mapper    schedule 06.07.2015


Ответы (2)


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

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

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

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

реализация метода FindChild: https://stackoverflow.com/a/1759923/891715


ИЗМЕНИТЬ:

Если вам нужен «обычный» привязка по 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 внутренней панели.

person Arie    schedule 01.07.2015
comment
Но тогда пользователи элемента управления должны были бы знать, что по какой-то причине обычный способ привязки к дочернему элементу управления (от {Binding ElementName=innerPanelButtonName, Path=Content}) не работает, и его необходимо обойти с помощью обозначения индекса, предложенного в этот ответ, который выглядит как ошибка. Ведь ни одна другая панель WPF не требует такого синтаксиса, все они работают напрямую с ElementName. - person O. R. Mapper; 01.07.2015
comment
в этом сценарии можно включить обычную привязку ElementName. Вы должны зарегистрировать имена элементов управления в вашей внутренней панели в NameScope вашего корневого элемента пользовательского интерфейса. Теперь проблема будет заключаться в том, как найти его надежно (например, Application.Current.MainWindow вернет null во время разработки). Я обновляю свой ответ... - person Arie; 01.07.2015
comment
Редакция выглядит многообещающе. Я полагаю, это вызывает вопрос, почему дочерние элементы внутренней панели не находятся в той же области имен, что и у внешней панели, или можем ли мы и как мы можем обеспечить одну и ту же область имен для двух (в противном случае, я предполагаю, что все станет уродливым когда имена изменяются во время выполнения, о чем, как я полагаю, позаботится встроенная по умолчанию обработка областей имен). Сами панели (даже внешние), похоже, не имеют установленной области имен; однако Snoop сообщает мне, что не только хост Window, но и хост Border, непосредственно вложенный в это окно, имеет непустую область имени. - person O. R. Mapper; 01.07.2015
comment
Я расширил описание моего подхода 2), чтобы включить ценную информацию о NameScope, полученную из вашего ответа. - person O. R. Mapper; 01.07.2015
comment
Кроме того, я запустил отдельный вопрос, который может дать некоторые подсказки относительно того, как для получения и обновления соответствующих NameScope. - person O. R. Mapper; 02.07.2015
comment
NameScope был хорошим советом — я смог назначить пользовательский INameScope во внутреннюю панель, которая перенаправляет любые вызовы области имен, найденной путем сканирования логического дерева от внешней панели вверх. Я сделаю еще несколько тестов, чтобы проверить, действительно ли это работает. - person O. R. Mapper; 06.07.2015
comment
@O.R.Mapper, пожалуйста, поделитесь решением проблемы с NameScope, даже если оно в конечном итоге не решит вашу проблему. Это очень интересно и кажется полезным, но я не нашел надежного способа программно использовать NameScope в сети. - person Arie; 07.07.2015
comment
Я взял на себя смелость отредактировать ваш ответ, чтобы добавить краткое описание того, что я наконец сделал. - person O. R. Mapper; 07.07.2015

Я не уверен, что вы можете полностью скрыть внутреннюю структуру вашей панели - насколько мне известно, 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 }.

person Grx70    schedule 06.07.2015