AvalonDock LayoutAnchorableIsVisibleプロパティのバインド

Aug 29 2020

AvalonDockLayoutAnchorablesをWPFのそれぞれのメニュー項目にバインドしようとしています。メニューでチェックすると、アンカー可能オブジェクトが表示されます。メニューでチェックされていない場合、アンカー可能オブジェクトは非表示になっている必要があります。

IsCheckedIsVisibleは両方ともブール値なので、コンバーターが必要になるとは思わないでしょう。LayoutAnchorable IsVisibleプロパティをTrueまたはに設定できFalse、動作はデザインビューで期待どおりです。

ただし、以下のようにバインディングを実装しようとすると、エラーが発生します

タイプ「LayoutAnchorable」の「IsVisible」プロパティに「Binding」を設定することはできません。「バインディング」は、DependencyObjectのDependencyPropertyにのみ設定できます。

問題はここにあります:

<dock:LayoutAnchorable ContentId="content1" IsVisible="{Binding IsChecked, ElementName=mnuPane1}" x:Name="anchorable1" IsSelected="True">

これどうやってするの?

<Window x:Class="TestAvalonBinding.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:dock="http://schemas.xceed.com/wpf/xaml/avalondock"
    mc:Ignorable="d"
    Title="MainWindow"
    Height="450"
    Width="800">
<Grid>

    <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />
        <RowDefinition Height="*" />
    </Grid.RowDefinitions>

    <!-- Menu -->
    <Menu Height="18" HorizontalAlignment="Stretch" Name="menu1" VerticalAlignment="Top" Grid.Row="0">
        <MenuItem Header="File">
            <MenuItem Header="_Foo1" Name="mnuPane1" IsCheckable="True">
            </MenuItem>
            <MenuItem Header="Foo2" Name="mnuPane2" IsCheckable="True">
            </MenuItem>
        </MenuItem>
    </Menu>

    <!-- AvalonDock -->
    <dock:DockingManager x:Name="Dockman" DockPanel.Dock="Left" Grid.Row="1" >

        <dock:LayoutRoot x:Name="_layoutRoot">
            <dock:LayoutPanel Orientation="Horizontal">
                <dock:LayoutAnchorablePaneGroup Orientation="Vertical">
                    <dock:LayoutAnchorablePane FloatingWidth="150" FloatingHeight="150" FloatingLeft="100" FloatingTop="300">
                        <dock:LayoutAnchorable ContentId="content1" IsVisible="{Binding IsChecked, ElementName=mnuPane1}" x:Name="anchorable1" IsSelected="True">
                            <GroupBox Header="Foo1"/>
                        </dock:LayoutAnchorable>
                    </dock:LayoutAnchorablePane>
                    <dock:LayoutAnchorablePane FloatingWidth="150" FloatingHeight="150" FloatingLeft="100" FloatingTop="300">
                        <dock:LayoutAnchorable ContentId="content2" x:Name="anchorable2" IsSelected="True">
                            <GroupBox Header="Foo2"/>
                        </dock:LayoutAnchorable>
                    </dock:LayoutAnchorablePane>
                </dock:LayoutAnchorablePaneGroup>
            </dock:LayoutPanel>
        </dock:LayoutRoot>
    </dock:DockingManager>
    
</Grid>
</Window>

更新:

BionicCodeの答えの私の実装。私の残りの問題は、ペインを閉じてもメニュー項目がチェックされたままになることです。

XAML

<Grid>

    <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />
        <RowDefinition Height="*" />
    </Grid.RowDefinitions>

    <!-- Menu -->
    <Menu Height="18" HorizontalAlignment="Stretch" Name="menu1" VerticalAlignment="Top" Grid.Row="0">
        <MenuItem Header="File">
            <MenuItem Header="_Foo1" Name="mnuPane1" IsCheckable="True" IsChecked="{Binding RelativeSource={RelativeSource AncestorType=local:MainWindow}, Path=IsAnchorable1Visible}"/>
            <MenuItem Header="Foo2" Name="mnuPane2" IsCheckable="True" IsChecked="{Binding RelativeSource={RelativeSource AncestorType=local:MainWindow}, Path=IsAnchorable2Visible}"/>
        </MenuItem>
    </Menu>

    <!-- AvalonDock -->
    <dock:DockingManager x:Name="Dockman" DockPanel.Dock="Left" Grid.Row="1" >
        <dock:LayoutRoot x:Name="_layoutRoot">
            <dock:LayoutPanel Orientation="Horizontal">
                <dock:LayoutAnchorablePaneGroup Orientation="Vertical">
                    <dock:LayoutAnchorablePane FloatingWidth="150" FloatingHeight="150" FloatingLeft="100" FloatingTop="300">
                        <dock:LayoutAnchorable ContentId="content1" x:Name="anchorable1" IsSelected="True" >
                            <GroupBox Header="Foo1"/>
                        </dock:LayoutAnchorable>
                    </dock:LayoutAnchorablePane>
                    <dock:LayoutAnchorablePane FloatingWidth="150" FloatingHeight="150" FloatingLeft="100" FloatingTop="300">
                        <dock:LayoutAnchorable ContentId="content2" x:Name="anchorable2" IsSelected="True" >
                            <GroupBox Header="Foo2"/>
                        </dock:LayoutAnchorable>
                    </dock:LayoutAnchorablePane>
                </dock:LayoutAnchorablePaneGroup>
            </dock:LayoutPanel>
        </dock:LayoutRoot>
    </dock:DockingManager>

</Grid>

背後にあるコード

partial class MainWindow : Window
{
    public static readonly DependencyProperty IsAnchorable1VisibleProperty = DependencyProperty.Register(
      "IsAnchorable1Visible",
      typeof(bool),
      typeof(MainWindow),
      new PropertyMetadata(default(bool), MainWindow.OnIsAnchorable1VisibleChanged));

    public static readonly DependencyProperty IsAnchorable2VisibleProperty = DependencyProperty.Register(
      "IsAnchorable2Visible",
      typeof(bool),
      typeof(MainWindow),
      new PropertyMetadata(default(bool), MainWindow.OnIsAnchorable2VisibleChanged));

    public bool IsAnchorable1Visible
    {
        get => (bool)GetValue(MainWindow.IsAnchorable1VisibleProperty);
        set => SetValue(MainWindow.IsAnchorable1VisibleProperty, value);
    }
    public bool IsAnchorable2Visible
    {
        get => (bool)GetValue(MainWindow.IsAnchorable2VisibleProperty);
        set => SetValue(MainWindow.IsAnchorable2VisibleProperty, value);
    }

    public MainWindow()
    {
        InitializeComponent();
        this.IsAnchorable1Visible = true;
        this.IsAnchorable2Visible = true;
    }

    private static void OnIsAnchorable1VisibleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        (d as MainWindow).anchorable1.IsVisible = (bool)e.NewValue;
    }
    private static void OnIsAnchorable2VisibleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        (d as MainWindow).anchorable2.IsVisible = (bool)e.NewValue;
    }
}

回答

1 BionicCode Aug 29 2020 at 11:48

AvalonDock XAMLレイアウト要素は、コントロールでも、から派生したものでもありませんUIElement。それらはプレーンモデルとして機能します(拡張されますがDependencyObject)。
のプロパティはLayoutAnchorableとして実装されませんDependencyPropertyが、代わりに実装されますINotifyPropertyChanged(前述のように、レイアウト要素はコントロールのビューモデルとして機能します)。したがって、(拘束力のあるターゲットとして)データ入札をサポートしていません。

これらの各XAMLレイアウト要素には、対応するコントロールがあり、実際にはレイアウト要素をとしてレンダリングされDataContextます。名前は、Controlサフィックスが付加されたレイアウト要素の名前と同じです。これらのコントロールまたはアイテムコンテナをLayoutAnchorableItemビューモデルなどに接続する場合は、Styleこのコンテナを対象とするを作成する必要があります。次の欠陥はDataContext、このコンテナのが、コントロールが表示することを意図したデータモデルではなく、コントロールの内部モデルであるということです。ビューモデルにアクセスLayoutAnchorableControl.LayoutItem.ModelするにLayoutAnchorableControl.DataContextは、たとえばにアクセスする必要があります(がであるためLayoutAnchorable)。

著者は明らかに、MVVMを使用してコントロール自体を実装することに熱心すぎて(ドキュメントに記載されているように)迷子になり、MVVMクライアントアプリケーションをターゲットにするのを忘れています。彼らは一般的なWPFパターンを破りました。外見は良く見えますが、中はあまり良くありません。

問題を解決するには、ビューに中間の依存関係プロパティを導入する必要があります。登録されたプロパティ変更コールバックは、可視性を委任して、アンカー可能なものの可視性を切り替えます。
AvalonDockの作成者がUIElement.Visibility可視性の処理にを使用しなかったことに注意することも重要です。彼らは、フレームワークプロパティから独立したカスタム可視性ロジックを導入しました。

前に述べたように、ILayoutUpdateStrategy実装を提供することによって初期ビューをレイアウトする、純粋なモデル駆動型アプローチが常にあります。次に、スタイルを定義して、ビューとビューモデルを接続します。XAMLレイアウト要素を使用してビューをハードコーディングすると、より高度なシナリオで特定の不便が発生します。

LayoutAnchorableShow()andClose()メソッドまたはIsVisibleプロパティを公開して可視性を処理します。アクセス時にLayoutAnchorableControl.LayoutItem(たとえば、内からControlTemplate)コマンドにバインドすることもできますLayoutAnchorableItem。これにより、が返されます。これLayoutAnchorableItemにより、が公開されますHideCommand

MainWindow.xaml

<Window>
  <Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />
        <RowDefinition Height="*" />
    </Grid.RowDefinitions>

    <!-- Menu -->
    <Menu Grid.Row="0">
      <MenuItem Header="File">
        <MenuItem Header="_Foo1" 
                  IsCheckable="True"
                  IsChecked="{Binding RelativeSource={RelativeSource AncestorType=MainWindow}, Path=IsAnchorable1Visible}" />
      </MenuItem>
    </Menu>

    <!-- AvalonDock -->
    <dock:DockingManager Grid.Row="1" >
      <dock:LayoutRoot>
        <dock:LayoutPanel>
          <dock:LayoutAnchorablePaneGroup>
            <dock:LayoutAnchorablePane>
              <dock:LayoutAnchorable x:Name="Anchorable1"
                                     Hidden="Anchorable1_OnHidden">
                <GroupBox Header="Foo1" />
              </dock:LayoutAnchorable>
            </dock:LayoutAnchorablePane>
          </dock:LayoutAnchorablePaneGroup>
        </dock:LayoutPanel>
      </dock:LayoutRoot>
    </dock:DockingManager>    
  </Grid>
</Window>

MainWindow.xaml.cs

partial class MainWindow : Window
{
  public static readonly DependencyProperty IsAnchorable1VisibleProperty = DependencyProperty.Register(
    "IsAnchorable1Visible",
    typeof(bool),
    typeof(MainWindow),
    new PropertyMetadata(default(bool), MainWindow.OnIsAnchorable1VisibleChanged));

  public bool IsAnchorable1Visible
  {
    get => (bool) GetValue(MainWindow.IsAnchorable1VisibleProperty);
    set => SetValue(MainWindow.IsAnchorable1VisibleProperty, value);
  }

  public MainWindow()
  {
    InitializeComponent();
    this.IsAnchorable1Visible = true;
  }

  private static void OnIsAnchorable1VisibleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
  { 
    (d as MainWindow).Anchorable1.IsVisible = (bool) e.NewValue;
  }

  private void Anchorable1_OnHidden(object sender, EventArgs e) => this.IsAnchorable1Visible = false;
}
1 thatguy Aug 29 2020 at 11:53

バインディングには2つの大きな問題があります。

  1. IsVisibleプロパティではありませんDependencyProperty、あなたがそれをバインドすることはできませんので、ちょうどCLRプロパティ
  2. ALayoutAnochorableはビジュアルツリーの一部ではないためElementNameRelativeSourceバインディングが機能しないため、出力ウィンドウに対応するバインディングエラーが表示されます。

プロパティを依存関係プロパティにしないための特定の設計上の選択または制限があるかどうかはわかりませんIsVisibleが、添付プロパティを作成することでこれを回避できます。このプロパティは、バインドされ、CLRプロパティを設定することが可能IsVisibleLayoutAnchorable、それが変更されたとき。

public class LayoutAnchorableProperties
{
   public static readonly DependencyProperty IsVisibleProperty = DependencyProperty.RegisterAttached(
      "IsVisible", typeof(bool), typeof(LayoutAnchorableProperties), new PropertyMetadata(true, OnIsVisibleChanged));

   public static bool GetIsVisible(DependencyObject dependencyObject)
   {
      return (bool)dependencyObject.GetValue(IsVisibleProperty);
   }

   public static void SetIsVisible(DependencyObject dependencyObject, bool value)
   {
      dependencyObject.SetValue(IsVisibleProperty, value);
   }

   private static void OnIsVisibleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
   {
      if (d is LayoutAnchorable layoutAnchorable)
         layoutAnchorable.IsVisible = (bool)e.NewValue;
   }
}

このプロパティはXAMLでバインドできますが、言われてLayoutAnchorableいるように、ビジュアルツリーにないため、これは機能しません。同じ問題がDataGrid列でも発生します。で、この関連の記事あなたが持つ回避策を見つけるBindingProxy私たちが使用するクラスを。このクラスをプロジェクトにコピーしてください。

にバインディングプロキシのインスタンスを作成しますDockingManager.Resources。メニュー項目にアクセスするのに役立ちます。

<dock:DockingManager x:Name="Dockman" DockPanel.Dock="Left" Grid.Row="1">
   <dock:DockingManager.Resources>
      <local:BindingProxy x:Key="mnuPane1Proxy" Data="{Binding ElementName=mnuPane1}"/>
   </dock:DockingManager.Resources>
   <!-- ...other XAML code. -->
</dock:DockingManager>

古いIsVisibleバインディングを削除します。を使用して、添付プロパティにバインディングを追加しmnuPane1Proxyます。

<xcad:LayoutAnchorable ContentId="content1"
                       x:Name="anchorable1"
                       IsSelected="True"
                       local:LayoutAnchorableProperties.IsVisible="{Binding Data.IsChecked, Source={StaticResource mnuPane1Proxy}}">

最後に、IsCheckedメニュー項目のデフォルト状態をに設定します。trueこれはのデフォルト状態でIsVisibleあり、添付プロパティにデフォルト値を設定しているため、初期化時にバインディングが更新されません。これはInvalidOperationException、コントロールがスローされるのを防ぐために必要です。完全に初期化されていません。

<MenuItem Header="_Foo1" Name="mnuPane1" IsCheckable="True" IsChecked="True">