I am trying to bind AvalonDock LayoutAnchorables
to their respective menu items in WPF. If checked in the menu the anchorable should be visible. If not checked in the menu, the anchorable should be hidden.
Both IsChecked
and IsVisible
are boolean so I wouldn't expect a converter to be required. I can set the LayoutAnchorable
IsVisible
property to True
or False
, and behavior is as expected in the design view.
However, if trying to implement binding as below I get the error
'Binding' cannot be set on the 'IsVisible' property of type 'LayoutAnchorable'. A 'Binding' can only be set on a DependencyProperty of a DependencyObject.
The problem is here:
<dock:LayoutAnchorable ContentId="content1" IsVisible="{Binding IsChecked, ElementName=mnuPane1}" x:Name="anchorable1" IsSelected="True">
How can I do this?
<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>
Update:
My implementation of BionicCode's answer. My remaining issue is that if I close a pane, the menu item remains checked.
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>
Code behind
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;
}
}
The AvalonDock XAML layout elements are neither controls nor derived of UIElement
. They serve as plain models (although they extend DependencyObject
).
The properties of LayoutAnchorable
are not implemented as DependencyProperty
, but instead implement INotifyPropertyChanged
(as said before, the layout elements serve as the control's view model). Hence they don't support data biding (as binding target).
Each of those XAML layout elements has a corresponding control which will be actually rendered with the layout element as DataContext
. The names equal the layout element's name with the Control suffix attached. If you want to connect those controls or item containers e.g., LayoutAnchorableItem
to your view model, you'd have to create a Style
that targets this container. The next flaw is that the DataContext
of this containers is not your data model that the control is intended to display, but the control's internal model. To get to your view model you would need to access e.g. LayoutAnchorableControl.LayoutItem.Model
(because the LayoutAnchorableControl.DataContext
is the LayoutAnchorable
).
The authors obviously got lost while being too eager to implement the control itself using MVVM (as stated in their docs) and forget to target the MVVM client application. They broke the common WPF pattern. Looks good on the outside, but not so good on the inside.
To solve your problem, you have to introduce an intermediate dependency property on your view. A registered property changed callback would then delegate the visibility to toggle the visibility of the anchorable.
It's also important to note that the authors of AvalonDock didn't use the UIElement.Visibility
to handle visibility. They introduced a custom visibility logic independent of the framework property.
As mentioned before, there is always the pure model driven approach, where you layout the initial view by providing a ILayoutUpdateStrategy
implementation. You then define styles to wire up view and view models. Hardcoding the view using the XAML layout elements leads to certain inconvenience in more advanced scenarios.
LayoutAnchorable
exposes a Show()
and Close()
method or the IsVisible
property to handle visibility. You can also bind to a command when accessing LayoutAnchorableControl.LayoutItem
(e.g. from within a ControlTemplate
), which returns a LayoutAnchorableItem
. This LayoutAnchorableItem
exposes a 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;
}
There are two major issues with your bindings.
IsVisible
property is not a DependencyProperty
, but just a CLR property, so you cannot bind itLayoutAnochorable
is not part of the visual tree, so ElementName
and RelativeSource
bindings do not work, you will see the corresponding binding errors in your output windowI am not sure if there is a specific design choice or limitation to not make the IsVisible
property a dependency property, but you can work around this by creating an attached property. This property can be bound and sets the CLR property IsVisible
on the LayoutAnchorable
when it changes.
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;
}
}
You can bind this property in your XAML, but as being said, this will not work, because of the LayoutAnchorable
not being in the visual tree. The same issue occurs for DataGrid
columns. In this related post you find a workaround with a BindingProxy
class that we will use. Please copy this class into your project.
Create an instance of the binding proxy in your DockingManager.Resources
. It serves to access the menu item.
<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>
Remove your old IsVisible
binding. Add a binding to the attached property using the mnuPane1Proxy
.
<xcad:LayoutAnchorable ContentId="content1"
x:Name="anchorable1"
IsSelected="True"
local:LayoutAnchorableProperties.IsVisible="{Binding Data.IsChecked, Source={StaticResource mnuPane1Proxy}}">
Finally, set the default IsChecked
state in your menu item to true
, as that is the default state for IsVisible
and the binding is not updated on initialization due to setting the default value in the attached properties, which is needed to prevent the InvalidOperationException
that is thrown because the control is not completely initialized.
<MenuItem Header="_Foo1" Name="mnuPane1" IsCheckable="True" IsChecked="True">
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With