12

tl;dr:カスタム WPF パネル クラスの定義済みWPF パネルの既存のレイアウト ロジックを再利用したいと考えています。この質問には、これを解決するための 4 つの異なる試みが含まれており、それぞれに異なる欠点があり、異なる障害点があります。また、小さなテスト ケースがさらに下にあります。

問題は、この目標を適切に達成するにはどうすればよいかです。

  • カスタムパネルを定義しながら
  • 別のパネルのレイアウト ロジックを内部的に再利用します。
  • これを解決するための私の試みで説明されている問題に遭遇しましたか?

カスタム WPFパネルを作成しようとしています。このパネル クラスでは、推奨される開発手法に固執し、きれいな API と内部実装を維持したいと思います。具体的には、次のことを意味します。

  • コードのコピーと貼り付けは避けたいです。コードのいくつかの部分が同じ機能を持っている場合、コードは一度だけ存在し、再利用する必要があります。
  • 適切なカプセル化を適用し、外部ユーザーが安全に使用できるメンバーのみにアクセスできるようにしたいと考えています (内部ロジックを壊したり、内部の実装固有の情報を提供したりすることはありません)。

当面は、既存のレイアウトに固執するつもりです。別のパネルのレイアウト コードを再利用したいと思います (ここで提案されているように、レイアウト コードを再度記述するのではなく)。例として に基づいて説明DockPanelしますが、どのような種類の にも基づいて、一般的にこれを行う方法を知りたいですPanel

レイアウト ロジックを再利用するために、パネルにビジュアルの子として を追加するつもりですDockPanel。これにより、パネルの論理的な子が保持され、レイアウトされます。

これを解決する方法について 3 つの異なるアイデアを試しましたが、別のアイデアがコメントで提案されましたが、これまでのところ、それぞれが異なる時点で失敗しています。


1) カスタム パネルのコントロール テンプレートに内部レイアウト パネルを導入する

これは最も洗練されたソリューションのように思えます。このように、カスタム パネルのコントロール パネルは、ItemsControlプロパティItemsPanelを使用しDockPanelプロパティがカスタム パネルのプロパティItemsSourceバインドされているを特徴とすることができます。Children

残念ながら、Panelは から継承してControlいないため、Templateプロパティも、コントロール テンプレートの機能サポートもありません。

一方、Childrenプロパティは によって導入されるPanelため、 には存在しません。また、意図した継承階層から抜け出して、実際にはであるが ではないControlパネルを作成するのはハッキーと見なされる可能性があると思います。ControlPanel


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による回答では、クラスがこれに重要な役割を果たしていることが指摘されました。何らかの理由で、子コントロールの名前が関連するものに登録されません。これは、子ごとに呼び出すことで部分的に修正される可能性がありますが、正しいインスタンスを取得する必要があります。また、例えば子供の名前が変わったときの振る舞いが他のパネルと同じになるかどうかはわかりません。NameScopeNameScopeRegisterNameNameScope

代わりにNameScope、インナーパネルの設定を行う方法のようです。TestPanel1(コンストラクターで)簡単なバインディングでこれを試しました:

        BindingOperations.SetBinding(innerPanel,
                                     NameScope.NameScopeProperty,
                                     new Binding("(NameScope.NameScope)") {
                                        Source = this
                                     });

残念ながら、これNameScopeは内部パネルの を に設定するだけnullです。Snoopを使用して確認できる限り、実際のNameScopeインスタンスは、親ウィンドウのNameScope添付プロパティ、またはコントロール テンプレート (またはおそらく他のキー ノードによって定義されたビジュアル ツリーのルート) にのみ格納されます。 )、タイプは問いません。もちろん、コントロール インスタンスは、その有効期間中にコントロール ツリーのさまざまな位置で追加および削除される可能性があるため、関連するNameScopeものは時々変更される可能性があります。これも、バインディングが必要です。

残念ながら、 *the first found node that has a non- value assigned to the attached propertyRelativeSourceなどの任意の条件に基づいてバインディングを定義することはできないため、ここで再び立ち往生します。nullNameScope

周囲のビジュアル ツリーの更新に対応する方法に関するこの他の質問が有用な回答をもたらさない限り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生のインスタンスを安全に処理する方法も、を処理する方法もわかりません。シンプル。HitTestResultGeometryHitTestParametersInputHitTestPoint

さらに、コントロールは、たとえば を押してフォーカスすることもできませんTab。これを修正する方法がわかりません。

さらに、内側のパネルと子の元のリストの間のどの内部リンクが、その子のリストをカスタムオブジェクトに置き換えることによって壊れているかわからないため、この方法には少し注意が必要です。


4) パネルクラスから直接継承

ユーザーClemensは、クラスに から直接継承させることを提案していDockPanelます。ただし、これが適切ではない理由が 2 つあります。

  • 私のパネルの現在のバージョンは、 のレイアウト ロジックに依存しDockPanelます。ただし、将来のある時点で、それでは十分ではなくなり、誰かが実際にパネルにカスタム レイアウト ロジックを記述しなければならなくなる可能性があります。その場合、インナーDockPanelをカスタム レイアウト コードに置き換えるのは簡単ですがDockPanel、パネルの継承階層から削除すると、重大な変更が発生します。
  • 私のパネルが から継承している場合、パネルのユーザーは、特にDockPanelによって公開されているプロパティをいじって、レイアウト コードを妨害できる可能性があります。それだけのプロパティですが、すべてのサブタイプで機能するアプローチを使用したいと思います。たとえば、 から派生したカスタム パネルはおよびプロパティを公開します。これにより、自動生成されたレイアウトは、カスタム パネルのパブリック インターフェイスを介して完全に破棄される可能性があります。DockPanelLastChildFillPanelGridColumnDefinitionsRowDefinitions

説明されている問題を観察するためのテスト ケースとして、テスト対象のカスタム パネルのインスタンスを XAML で追加し、その要素内に以下を追加します。

<TextBox Name="tb1" DockPanel.Dock="Right"/>
<TextBlock Text="{Binding Text, ElementName=tb1}" DockPanel.Dock="Left"/>

テキスト ブロックはテキスト ボックスの左側に配置し、現在テキスト ボックスに書かれている内容を表示する必要があります。

テキスト ボックスはクリック可能であり、出力ビューにはバインディング エラーが表示されないことが期待されます (したがって、バインディングも機能するはずです)。


したがって、私の質問は次のとおりです。

  • 私の試みのいずれかを修正して、完全に正しい解決策に導くことはできますか? または、私が探していることをしようとしたことよりも好ましい完全に他の方法はありますか?
4

2 に答える 2

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 }

于 2015-07-06T16:56:36.063 に答える
2

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 による「通常の」バインディングを機能させたい場合は、適切なNameScopeで innerPanel の子であるコントロールの名前を登録する必要があります。

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- 設計時に機能しません) 。


OPによる編集:この回答は、NameScopeclassについて言及しているため、私を正しい軌道に乗せました。

私の最終的なソリューションは、 interfaceTestPanel1のカスタム実装に基づいており、それを使用しています。その各メソッドは、外側のパネルから開始して論理ツリーをたどり、プロパティが ではない最も近い親要素を見つけます。INameScopeNameScopenull

  • RegisterNameUnregisterNameその呼び出しをその見つかったINameScopeオブジェクトのそれぞれのメソッドに転送し、そうでなければ例外をスローします。
  • FindNameFindName見つかったオブジェクトの呼び出しを転送しINameScope、それ以外の場合 (そのようなオブジェクトが見つからない場合) を返しますnull

INameScopeその実装のインスタンスはNameScope、内部パネルの として設定されます。

于 2015-07-01T07:47:37.137 に答える