Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Binding an AvalonDock LayoutAnchorable IsVisible property

Tags:

c#

binding

wpf

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;
    }
}
like image 939
wotnot Avatar asked Oct 11 '25 11:10

wotnot


2 Answers

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;
}
like image 87
BionicCode Avatar answered Oct 14 '25 10:10

BionicCode


There are two major issues with your bindings.

  1. The IsVisible property is not a DependencyProperty, but just a CLR property, so you cannot bind it
  2. A LayoutAnochorable 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 window

I 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">
like image 35
thatguy Avatar answered Oct 14 '25 12:10

thatguy



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!