The AvalonDock implementation is pretty weird. I remember having some troubles too when I had to use this control in a project. I think I know this control quite well. It is very badly implemented, in my opinion. Because they, for some reason, decided to implement this control itself using MVVM instead of making it simply MVVM ready. This makes it very inconvenient to use in advanced scenarios.
Semantics are also quite confusing. For example, containers are not the controls that render data. Containers are assigned to the Model
property of the controls. Data is not assigned to the DataContext
. But it looks good.
Also, the tab header placement behavior is broken (only allows tab headers at the bottom). My fix could be of interest for you, especially in context of dynamic tab header placement. See the Style
below to get the expected tab header placement behavior. It simply wraps the content of the LayoutAnchorablePaneControl
into a DockingPanel
and rotates the header host, so that you get tab header alignment you have in Visual Studio (stacked by width). That's all. If you wish to stack the headers by their height (no rotation) simply replace the AnchorablePaneTabPanel
with a e.g., StackPanel
and remove the rotation triggers.
The provided example is based on that Style
below. Otherwise you won't be able to propagate tab header position to the view.
Another big pain is the lack of events that are exposed by the DockingManager
class and AvalonDock in general. This means there is no chance to observe the drag and drop actions. As a matter of fact, DockingManager
only exposes three quite uninteresting events. Same to the content hosts like the LayoutAnchorablePaneControl
.
Since AvalonDock does not use the WPF framework's Drag & Drop API, handling those events is not a solution.
To overcome the short comings, you must handle one of the few model events, the LayoutRoot.Updated
event in this case.
The solution only targets the LayoutAnchorablePane
and LayoutAnchorableGroupPane
. To address advanced grouping or the LayoutDocumentPane
you can simple extend the example by following the pattern.
Since you only require/requested a two column layout, the algorithm will do the job. Other more advanced layout arrangements are supported, but the behavior is not perfect as not all conditions are currently tracked. The focus is on a two column layout. It's a quick (but not so dirty) and very simple solution.
You should consider to disallow any layout arrangement other than the two column layout explicitly.
Additionally, AvalonDock does not provide an event to indicate when the visual layout process is completed. You only get a notification via the LayoutRoot.Updated
event when the layout model is added/removed to/from the layout model tree. But you never know when exactly the visual tree is updated. We need to have access to the visual containers in order to set the LayoutPanelControl.TabStripPlacement
property based on the new position of this control.
To overcome this, I used the Dispatcher
to defer the access to the then initialized and rendered LayoutAnchorablePaneControl
. Otherwise the tab header arrangement would be premature, because the control's layout index is yet to change. AvalonDock only allows to track very few layout model modifications but no observation of actual docking operations at all.
So the algorithm is basically
- Handle the
LayoutRoot.Updated
event and start the actual positioning algorithm deferred using the Dispatcher
- Iterate over all pane controls to update the tab header placement. In case nesting is allowed, you will have a layout tree that you have to traverse recursively, like it is done in this example for group panes.
- Identify the position of the pane in the layout based on their index.
- Set the
LayoutPanelControl.TabStripPlacement
property according to the index: an index of 0 means left and an index that equals the item count means right. Every other index is in between. The tab headers are placed based on the pane's position in the layout.
- The DockingPanel will layout the tab items accordingly. Triggers are used to rotate the tab headers if they are positioned left or right.
There can be multiple LayoutPanelControl
elements in the layout (except you disallow "illegal" layout arrangements to enforce the two column layout).
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
The required AnchorablePaneControlStyle
is defined below.
<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>