AvalonDock:绑定到 LayoutAnchorablePane 位置?

AvalonDock: Binding to LayoutAnchorablePane position?

我正在为使用 AvalonDock 停靠框架的 WPF 应用程序创建自定义主题。
我已经为我在 AvalonDock 存储库上的问题打开了一个 GitHub issue,但我希望我能在这里更快地得到答案(并准备尽快对此悬赏)。


在我的自定义主题中,我已将 LayoutAnchorablePane 的选项卡项目移动到左侧垂直堆叠,并且窗格使用列大小为 Auto, *, Auto 的网格。
LayoutAnchorablePane 附加到根布局面板的右侧时,我想为将选项卡从左列移动到右列的样式编写触发器。 (这样标签总是在外面)

这是我的主题 XAML 的相关部分,我正试图启动该部分。这与 AvalonDock 中 generic.xaml 样式的 LayoutAnchorablePaneControl 模板几乎相同:

<Grid
    ClipToBounds="true"
    KeyboardNavigation.TabNavigation="Local"
    SnapsToDevicePixels="true">
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="Auto" />
    <ColumnDefinition Width="*" />
    <ColumnDefinition Width="Auto" />
    </Grid.ColumnDefinitions>
    <!--  Following border is required to catch mouse events  -->
    <Border Grid.ColumnSpan="3" Background="Transparent" />
    <StackPanel
    x:Name="HeaderPanel"
    Width="40"
    Grid.Column="0"
    Panel.ZIndex="1"
    IsItemsHost="true"
    KeyboardNavigation.TabIndex="1" />
    <Border
    x:Name="ContentPanel"
    Grid.Column="1"
    Background="Transparent"
    BorderThickness="2"
    BorderBrush="{StaticResource PrimaryBrush}"
    KeyboardNavigation.DirectionalNavigation="Contained"
    KeyboardNavigation.TabIndex="2"
    KeyboardNavigation.TabNavigation="Cycle">
    <ContentPresenter
            x:Name="PART_SelectedContentHost"
            ContentSource="SelectedContent"
            SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
    </Border>
</Grid>
<ControlTemplate.Triggers>
    <DataTrigger Binding="{Binding ??? }">
        <Setter TargetName="HeaderPanel" Property="Grid.Column" Value="2"/>
    </DataTrigger>
</ControlTemplate.Triggers>

据我所知,LayoutAnchorablePane 上没有 属性 或其任何界面显示窗格位于布局的哪一侧。所以我迷失了我可以在我的 DataTrigger 中放入 {Binding ??? } 的内容。

看来我需要自己实现 属性 并使用我自己构建的 AvalonDock。如果可能的话,我想避免这种情况;那么也许我可以在自己的代码中实现一些聪明的 MarkupExtension 或 Converter 想法?也许我认为这可以用 DataTrigger 完成的假设也可以受到挑战。我很乐意为此使用完全代码隐藏的解决方案。

AvalonDock 的实现非常奇怪。我记得当我不得不在项目中使用这个控件时也遇到了一些麻烦。我想我很了解这个控件。在我看来,它的实施非常糟糕。因为出于某种原因,他们决定使用 MVVM 本身实现此控件,而不是简单地使其为 MVVM 做好准备。这使得在高级场景下使用起来非常不方便。
语义也很混乱。例如,容器不是呈现数据的控件。容器被分配给控件的 Model 属性。数据未分配给 DataContext。不过看起来不错

此外,制表符 header 放置行为已损坏(仅允许制表符 header 位于底部)。您可能会对我的修复感兴趣,尤其是在动态选项卡 header 放置的上下文中。请参阅下面的 Style 以获得预期的选项卡 header 放置行为。它只是将 LayoutAnchorablePaneControl 的内容包装成 DockingPanel 并旋转 header 主机,这样你就可以得到 Visual Studio 中的制表符 header 对齐方式(堆叠按宽度)。就这样。如果您希望按高度堆叠 header(无旋转),只需将 AnchorablePaneTabPanel 替换为 StackPanel 并移除旋转触发器。
提供的示例基于下面的 Style。否则您将无法将制表符 header 位置传播到视图。

另一个很大的痛苦是缺少 DockingManager class 和 AvalonDock 一般公开的事件。这意味着没有机会观察拖放操作。事实上,DockingManager只暴露了三个非常无趣的事件。与 LayoutAnchorablePaneControl.
等内容主机相同 由于 AvalonDock 不使用 WPF 框架的拖放 API,处理这些事件不是解决方案。

要克服缺点,您必须处理少数几个模型事件之一,在本例中为 LayoutRoot.Updated 事件。

该解决方案仅针对 LayoutAnchorablePaneLayoutAnchorableGroupPane。要解决高级分组或 LayoutDocumentPane 您可以按照模式简单地扩展示例。
由于您只有 require/requested 两列布局,算法将完成这项工作。支持其他更高级的布局安排,但行为并不完美,因为目前并未跟踪所有条件。重点是两列布局。这是一个快速(但不是那么脏)且非常简单的解决方案。
您应该考虑明确禁止除两列布局之外的任何布局安排。

此外,AvalonDock 不提供事件来指示视觉布局过程何时完成。当布局模型是 added/removed to/from 布局模型树时,您只会通过 LayoutRoot.Updated 事件收到通知。但是您永远不知道视觉树更新的确切时间。我们需要访问可视容器,以便根据此控件的新位置设置 LayoutPanelControl.TabStripPlacement 属性。
为了克服这个问题,我使用 Dispatcher 来延迟对然后初始化和呈现的 LayoutAnchorablePaneControl 的访问。否则,选项卡 header 排列会为时过早,因为控件的布局索引尚未更改。 AvalonDock 仅允许跟踪极少的布局模型修改,但根本无法观察实际对接操作。

所以算法基本上是

  1. 处理 LayoutRoot.Updated 事件并启动使用 Dispatcher 延迟的实际定位算法
  2. 遍历所有窗格控件以更新选项卡 header 位置。如果允许嵌套,您将拥有一个必须递归遍历的布局树,就像本例中针对组窗格所做的那样。
  3. 根据索引确定窗格在布局中的位置。
  4. 根据索引设置LayoutPanelControl.TabStripPlacement 属性:索引为0表示左,索引等于项目数表示右。其他所有指数都介于两者之间。选项卡 header 根据窗格在布局中的位置放置。
  5. DockingPanel 将相应地布置选项卡项。触发器用于旋转选项卡 headers,如果它们位于左侧或右侧。

布局中可以有多个 LayoutPanelControl 元素(除非您禁止“非法”布局安排来强制执行两列布局)。

MainWindow.xaml.cs

public partial class MainWindow : Window
{
  private const Dock DefaultDockPosition = Dock.Bottom;

  private void InitializeOnDockingManager_Loaded(object sender, RoutedEventArgs e)
  {
    var dockingManager = sender as DockingManager;
    this.Dispatcher.InvokeAsync(() =>
    {
      ArrangePanel(dockingManager.LayoutRootPanel);
    },
    DispatcherPriority.Background);

    dockingManager.Layout.Updated += OnLayoutUpdated;
  }

  private void OnLayoutUpdated(object sender, EventArgs e)
  {
    var layoutRoot = sender as LayoutRoot;
    var dockingManager = layoutRoot.Manager;
    this.Dispatcher.InvokeAsync(() =>
    {
      ArrangePanel(dockingManager.LayoutRootPanel);
    },
    DispatcherPriority.ContextIdle);
  }

  private void ArrangePanel(LayoutPanelControl layoutPanelControl)
  {
    IEnumerable<ILayoutControl> layoutControls = layoutPanelControl.Children
      .OfType<ILayoutControl>()
      .Where(control =>
        control is LayoutAnchorablePaneControl paneControl
          && (paneControl.Model as ILayoutContainer).Children.Any()
        || control is LayoutAnchorablePaneGroupControl or LayoutPanelControl);

    int paneControlCount = layoutControls.Count(control => control is not LayoutPanelControl);
    int paneControlLayoutPosition = 0;

    foreach (ILayoutControl layoutControl in layoutControls)
    {
      if (layoutControl is LayoutPanelControl layoutPanel)
      {
        ArrangePanel(layoutPanel);
        continue;
      }

      paneControlLayoutPosition++;
      bool isFirst = paneControlLayoutPosition == 1;
      bool isLast = paneControlCount == paneControlLayoutPosition;

      if (layoutControl is LayoutAnchorablePaneGroupControl paneGroupControl)
      {
        PositiontabHeadersInPaneGroup((isFirst, isLast), paneGroupControl);
      }
      else if (layoutControl is LayoutAnchorablePaneControl paneControl)
      {
        if (paneControlCount == 1)
        {
          paneControl.TabStripPlacement = DefaultDockPosition;
        }
        else
        {
          PositionTabHeadersInPane(paneControl, isFirst, isLast);
        }
      }
    }
  }

  private static void PositionTabHeadersInPane(LayoutAnchorablePaneControl paneControl, bool isFirst, bool isLast)
    => paneControl.TabStripPlacement =
      (isFirst, isLast) switch
      {
        (true, _) => Dock.Left,
        (_, true) => Dock.Right,
        _ => DefaultDockPosition
      };

  private void PositiontabHeadersInPaneGroup((bool IsGroupFirst, bool IsGroupLast) parentPaneGroupPosition, LayoutAnchorablePaneGroupControl paneGroupControl)
  {
    IEnumerable<ILayoutControl> groupMembers = paneGroupControl.Children
      .OfType<ILayoutControl>();
    int groupMemberCount = groupMembers.Count();
    int layoutPosition = 0;

    foreach (ILayoutControl groupMember in groupMembers)
    {
      layoutPosition++;
      bool isFirst = layoutPosition == 1;
      bool isLast = layoutPosition == groupMemberCount;

      if (groupMember is LayoutAnchorablePaneGroupControl childGroupControl)
      {
        PositiontabHeadersInPaneGroup((isFirst, isLast), childGroupControl);
      }
      else if (groupMember is LayoutAnchorablePaneControl paneControl)
      {
        (bool IsPaneFirstInGroup, bool IsPaneLastInGroup) panePositionInGroup = (isFirst, isLast);

        paneControl.TabStripPlacement =
          !parentPaneGroupPosition.IsGroupFirst && !parentPaneGroupPosition.IsGroupLast
          || groupMemberCount == 1
            ? DefaultDockPosition
            : (parentPaneGroupPosition, panePositionInGroup, paneGroupControl.Orientation) switch
            {
              ({ IsGroupFirst: true }, { IsPaneFirstInGroup: true }, Orientation.Horizontal) => Dock.Left,
              ({ IsGroupLast: true }, { IsPaneLastInGroup: true }, Orientation.Horizontal) => Dock.Right,
              ({ IsGroupFirst: true }, _, Orientation.Vertical) => Dock.Left,
              ({ IsGroupLast: true }, _, Orientation.Vertical) => Dock.Right,
              _ => DefaultDockPosition
            };
      }
    }
  }
}

MainWindow.xaml
所需的 AnchorablePaneControlStyle 定义如下。

<xcad:DockingManager Loaded="InitializeOnDockingManager_Loaded"
                     AnchorablePaneControlStyle="{StaticResource AnchorablePaneControlStyle}"
                     Height="500"
                     Width="500"
                     HorizontalAlignment="Left">
  <xcad:LayoutRoot>
    <xcad:LayoutPanel Orientation="Horizontal">
      <xcad:LayoutAnchorablePane>
        <xcad:LayoutAnchorable ContentId="properties"
                               Title="Properties">
          <TextBlock Text="123abc" />
        </xcad:LayoutAnchorable>
        <xcad:LayoutAnchorable Title="AgendaLeft"
                               ContentId="agendaLeft">
          <TextBlock Text="Agenda Content" />
        </xcad:LayoutAnchorable>
        <xcad:LayoutAnchorable Title="ContactsLeft"
                               ContentId="contactsLeft">
          <TextBlock Text="Contacts Content" />
        </xcad:LayoutAnchorable>
      </xcad:LayoutAnchorablePane>
    </xcad:LayoutPanel>
  </xcad:LayoutRoot>
</xcad:DockingManager>

AnchorablePaneControlStyle

<Style x:Key="AnchorablePaneControlStyle"
       TargetType="{x:Type xcad:LayoutAnchorablePaneControl}">
  <Setter Property="Foreground"
          Value="{Binding Model.Root.Manager.Foreground, RelativeSource={RelativeSource Self}}" />
  <Setter Property="Background"
          Value="{Binding Model.Root.Manager.Background, RelativeSource={RelativeSource Self}}" />
  <Setter Property="TabStripPlacement"
          Value="Bottom" />
  <Setter Property="Template">
    <Setter.Value>
      <ControlTemplate TargetType="{x:Type xcad:LayoutAnchorablePaneControl}">
        <Grid ClipToBounds="true"
              SnapsToDevicePixels="true"
              KeyboardNavigation.TabNavigation="Local">
          
          <!--Following border is required to catch mouse events-->
          <Border Background="Transparent"
                  Grid.RowSpan="2" />
          
          <DockPanel>
            <xcad:AnchorablePaneTabPanel x:Name="HeaderPanel"
                                         DockPanel.Dock="{TemplateBinding TabStripPlacement}"
                                         Margin="2,0,2,2"
                                         IsItemsHost="true"
                                         KeyboardNavigation.TabIndex="1"
                                         KeyboardNavigation.DirectionalNavigation="Cycle">
              <xcad:AnchorablePaneTabPanel.LayoutTransform>
                <RotateTransform x:Name="TabPanelRotateTransform" />
              </xcad:AnchorablePaneTabPanel.LayoutTransform>
            </xcad:AnchorablePaneTabPanel>
            
          <Border x:Name="ContentPanel" DockPanel.Dock="{TemplateBinding TabStripPlacement}"
                    BorderBrush="{TemplateBinding BorderBrush}"
                    BorderThickness="{TemplateBinding BorderThickness}"
                    KeyboardNavigation.DirectionalNavigation="Contained"
                    KeyboardNavigation.TabIndex="2"
                    KeyboardNavigation.TabNavigation="Cycle">
              <ContentPresenter x:Name="PART_SelectedContentHost"                                    
                                ContentSource="SelectedContent"
                                Margin="{TemplateBinding Padding}"
                                SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
            </Border>
          </DockPanel>
        </Grid>
        
        <ControlTemplate.Triggers>
          <Trigger Property="TabStripPlacement"
                   Value="Top">
            <Trigger.EnterActions>
              <StopStoryboard BeginStoryboardName="LeftTabStripPlacementAnimation" />
              <StopStoryboard BeginStoryboardName="RightTabStripPlacementAnimation" />
              <StopStoryboard BeginStoryboardName="BottomTabStripPlacementAnimation" />
              <BeginStoryboard x:Name="TopTabStripPlacementAnimation">
                <Storyboard>
                  <DoubleAnimation Storyboard.TargetName="TabPanelRotateTransform"
                                   Storyboard.TargetProperty="Angle"
                                   To="90"
                                   Duration="0" />
                </Storyboard>
              </BeginStoryboard>
            </Trigger.EnterActions>
          </Trigger>
          <Trigger Property="TabStripPlacement"
                   Value="Bottom">
            <Trigger.EnterActions>
              <StopStoryboard BeginStoryboardName="LeftTabStripPlacementAnimation" />
              <StopStoryboard BeginStoryboardName="TopTabStripPlacementAnimation" />
              <StopStoryboard BeginStoryboardName="RightTabStripPlacementAnimation" />
              <BeginStoryboard x:Name="BottomTabStripPlacementAnimation">
                <Storyboard>
                  <DoubleAnimation Storyboard.TargetName="TabPanelRotateTransform"
                                   Storyboard.TargetProperty="Angle"
                                   To="0"
                                   Duration="0" />
                </Storyboard>
              </BeginStoryboard>
            </Trigger.EnterActions>
          </Trigger>
          <Trigger Property="TabStripPlacement"
                   Value="Left">
            <Trigger.EnterActions>
              <StopStoryboard BeginStoryboardName="TopTabStripPlacementAnimation" />
              <StopStoryboard BeginStoryboardName="RightTabStripPlacementAnimation" />
              <StopStoryboard BeginStoryboardName="BottomTabStripPlacementAnimation" />
              <BeginStoryboard x:Name="LeftTabStripPlacementAnimation">
                <Storyboard>
                  <DoubleAnimation Storyboard.TargetName="TabPanelRotateTransform"
                                   Storyboard.TargetProperty="Angle"
                                   To="90" 
                                   Duration="0" />
                </Storyboard>
              </BeginStoryboard>
            </Trigger.EnterActions>
          </Trigger>
          <Trigger Property="TabStripPlacement"
                   Value="Right">
            <Trigger.EnterActions>
              <StopStoryboard BeginStoryboardName="LeftTabStripPlacementAnimation" />
              <StopStoryboard BeginStoryboardName="TopTabStripPlacementAnimation" />
              <StopStoryboard BeginStoryboardName="BottomTabStripPlacementAnimation" />
              <BeginStoryboard x:Name="RightTabStripPlacementAnimation">
                <Storyboard>
                  <DoubleAnimation Storyboard.TargetName="TabPanelRotateTransform"
                                   Storyboard.TargetProperty="Angle"
                                   To="90"
                                   Duration="0" />
                </Storyboard>
              </BeginStoryboard>
            </Trigger.EnterActions>
          </Trigger>
        </ControlTemplate.Triggers>
      </ControlTemplate>
    </Setter.Value>
  </Setter>

  <Setter Property="ItemContainerStyle">
    <Setter.Value>
      <Style TargetType="{x:Type TabItem}">
        <Setter Property="IsSelected"
                Value="{Binding IsSelected, Mode=TwoWay}" />
        <Setter Property="IsEnabled"
                Value="{Binding IsEnabled}" />
        <Setter Property="ToolTip"
                Value="{Binding ToolTip}" />
        <Setter Property="Template">
          <Setter.Value>
            <ControlTemplate TargetType="{x:Type TabItem}">
              <Grid SnapsToDevicePixels="true">
                <Border x:Name="Bd"
                        BorderBrush="{TemplateBinding BorderBrush}"
                        BorderThickness="1,0,1,1"
                        Background="{TemplateBinding Background}">
                  <ContentPresenter x:Name="Content"
                                    ContentSource="Header"
                                    HorizontalAlignment="{Binding HorizontalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"
                                    VerticalAlignment="{Binding VerticalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"
                                    RecognizesAccessKey="True"
                                    SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
                </Border>
              </Grid>
              <ControlTemplate.Triggers>
                <Trigger Property="Selector.IsSelected"
                         Value="true">
                  <Setter Property="Background"
                          Value="White" />
                  <Setter Property="Panel.ZIndex"
                          Value="1" />
                  <Setter Property="Margin"
                          Value="0,-1,-1,-2" />
                </Trigger>
                <MultiTrigger>
                  <MultiTrigger.Conditions>
                    <Condition Property="IsMouseOver"
                               Value="true" />
                    <Condition Property="Selector.IsSelected"
                               Value="false" />
                  </MultiTrigger.Conditions>
                  <Setter Property="Background"
                          Value="{DynamicResource {x:Static SystemColors.GradientInactiveCaptionBrushKey}}" />
                  <Setter Property="BorderBrush"
                          Value="{DynamicResource {x:Static SystemColors.HighlightBrushKey}}" />
                  <Setter Property="Panel.ZIndex"
                          Value="0" />
                </MultiTrigger>
                <Trigger Property="IsEnabled"
                         Value="false">
                  <Setter Property="Foreground"
                          Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}" />
                </Trigger>
              </ControlTemplate.Triggers>
            </ControlTemplate>
          </Setter.Value>
        </Setter>
        <Style.Triggers>
          <DataTrigger Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type TabControl}}, Path=Items.Count, FallbackValue=1}"
                       Value="1">
            <Setter Property="Visibility"
                    Value="Collapsed" />
          </DataTrigger>
        </Style.Triggers>
      </Style>
    </Setter.Value>
  </Setter>

  <Setter Property="ItemTemplate">
    <Setter.Value>
      <DataTemplate>
        <xcad:LayoutAnchorableTabItem Model="{Binding}" />
      </DataTemplate>
    </Setter.Value>
  </Setter>

  <Setter Property="ContentTemplate"
          Value="{StaticResource AnchorablePaneControlContentTemplate}" />

</Style>

感谢 BionicCode 提供了非常详细的答案,我最终只需要从那个答案中得到一些提示就可以用我自己的方式解决问题。所以我认为也值得分享我的代码。


LayoutAnchorablePaneControl 继承自 TabControl,因此它已经具有样式可以绑定到其模板化父级的 TabStripPlacement 属性。

因此新样式将 Grid 替换为 DockPanel,如下所示:

<DockPanel
    ClipToBounds="true"
    KeyboardNavigation.TabNavigation="Local"
    SnapsToDevicePixels="true">
    <!--  Following border is required to catch mouse events  -->
    <Border Background="Transparent" />
    <StackPanel
    x:Name="HeaderPanel"
    Width="40"
    DockPanel.Dock="{TemplateBinding TapStripPlacement}"
    Panel.ZIndex="1"
    IsItemsHost="true"
    KeyboardNavigation.TabIndex="1" />
    <Border
    x:Name="ContentPanel"
    Background="Transparent"
    BorderThickness="2"
    BorderBrush="{StaticResource PrimaryBrush}"
    KeyboardNavigation.DirectionalNavigation="Contained"
    KeyboardNavigation.TabIndex="2"
    KeyboardNavigation.TabNavigation="Cycle">
    <ContentPresenter
            x:Name="PART_SelectedContentHost"
            ContentSource="SelectedContent"
            SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
    </Border>
</DockPanel>

现在,此样式会根据 LayoutAnchorablePaneControl.TabStripPlacement 属性.

将选项卡移动到任意一侧 (left/right/top/bottom)

在隐藏代码中(对于具有 DockingManager 的 Window)我将一个事件处理程序附加到 DockingManager.Layout.Updated 以运行以下方法:

private void UpdateTabSides()
{
    foreach (LayoutAnchorablePaneControl apc in DockManager.LayoutRootPanel.FindLogicalChildren<LayoutAnchorablePaneControl>())
    {
        var side = apc.Model.GetSide();
        if (side == AnchorSide.Right)
        {
            apc.TabStripPlacement = Dock.Right;
        }
        else
        {
            apc.TabStripPlacement = Dock.Left;
        }
    }
}

我发现这种方法比 BionicCode 的答案简单得多,但他们值得赏金,因为他们将我推向了正确的方向。