3

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;
    }
}
wotnot
  • 261
  • 1
  • 12

2 Answers2

2

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;
}
BionicCode
  • 1
  • 4
  • 28
  • 44
  • Thanks for the help. I am trying to test / implement either of the answers given here but cannot execute the code due to a XAML parse exception. I am getting a `NotImplemented` exception for `'Initialization of 'Xceed.Wpf.AvalonDock.Controls.AnchorablePaneTitle'`. I have tried both adding lots of code from my real application (where I do not get this error) and cutting it right down to the minimum, but without success so far. Do we know what causes this error? – wotnot Aug 31 '20 at 21:32
  • (The error I am referring to can be reproduced - for me, at least - using the code shown in my question. Just remove the `IsVisible` property from line 35 and execute it.) – wotnot Aug 31 '20 at 21:43
  • I have just executed your code. The original code throws the invalid binding exception (of course). Then I removed, as instructed, the `IsVisible` attribute from the `LayoutAnchorable` and everything compiles well and executes showing the actual view. In this context I've also tested my solution, which also works well. I have just set the initial value of `IsAnchorable1Visible` to `true` from the constructor (I have updated the example). The problem you are describing must be introduced by some code not shown in your post. Do you style/manipulate the `AnchorablePaneTitle` somewhere? – BionicCode Sep 01 '20 at 00:18
  • Create an empty project and run/build the code from your question after removing the `IsVisible` attribute - you will see that it works. Use text search to find any "AnchorablePaneTitle". Also where is this exception thrown, which line of code? It looks like it may thrown in Xceed library code. When the application holds due to the exception, check the stack trace. Step back until you reach the line of code that is yours. This line is triggering the exception. Don't forget to search your XAML files for any styles that target the `AnchorablePaneTitle`. – BionicCode Sep 01 '20 at 00:31
  • Thanks again. :-) I recreated the project and still got the same result. There was nothing in it other than the XAML from this question. Stack trace just contained System and Win32 methods so I was none the wiser. But resolved through trial and error. It was related to Xceed DLLs (Version, I guess?). I replaced the Xceed DLLs in my test project with ones I had used in other projects, and then it worked. I seem to only have one version of these locally (i.e. timestamps are all the same) so not sure exactly what happened. It may be that when I created the test project I fetched them from Nuget. – wotnot Sep 01 '20 at 06:31
  • Weird, maybe it never worked with this project. Good you were able to solve it. You should try to get the latest Xceed version via the Visual Studio NuGet Package Manager. This way you also get notified about updates. Right-click your project, then select _Manage NuGet Packages..._. I've got _Extended.Wpf.Toolkit 4.0.1_. – BionicCode Sep 01 '20 at 07:48
  • I have still got issues implementing your code above. For one, `public void MainWindow` causes the error _'Member names cannot be the same as their enclosing type'_. If this is simply a typo and it is intended to be a constructor as I presume, then this is easily resolved. Also in the XAML, in the binding, I am getting _MainWindow is not supported in a WPF project_. – wotnot Sep 01 '20 at 08:09
  • Thank you. That is a copy&paste error on my side. This is supposed to be the constructor. Constructor has no return type. Just remove the `void` from the method. I have updated my answer. – BionicCode Sep 01 '20 at 08:11
  • I have also updated the `OnIsAnchorable1VisibleChanged` method (although it didn't produced a compiler error). – BionicCode Sep 01 '20 at 08:12
  • Thanks. I have just figured out the other error. I needed a `local:` to point it to the namespace of `MainWindow`. – wotnot Sep 01 '20 at 08:17
  • Yes thats right. For the binding `RelativeSource.AncestorType=local:MainWIndow`. You always have to qualify a type using its namespace. In C# as well as in XAML. In XAML the namespace qualifier for framework types is not required. Since I don't know your namespaces, I always leave them in my examples. Now it should work, right? – BionicCode Sep 01 '20 at 08:23
  • I can now run this code. I have also added a second dependency property for my second AnchorablePane, and they work independently as intended. The remaining issue, as with the other answer given here, is that this is not operating in two-way mode. If I close the pane the menu item remains checked. (I suspect this may be because it is collapsed rather than hidden?) – wotnot Sep 01 '20 at 08:25
  • It works for me, definitely. But we will find out. `MenuItem.IsChecked` binds `TwoWay` by default. This means you must break it somewhere. Have you used the exact binding from the example? – BionicCode Sep 01 '20 at 08:32
  • Also if you change visibility as I sugested in the example, then the `DockingManager` will handle it (by hiding it). – BionicCode Sep 01 '20 at 08:34
  • Also when adding a new dependency property, you must register a new property changed callback. Can you post your current version please? – BionicCode Sep 01 '20 at 08:36
  • Thanks - done. I have added latest code as an update to my question. – wotnot Sep 01 '20 at 08:55
  • Thank you. All looks well. I've just ran your code and it works fine. I am not able to reproduce your issue with your posted code. Can you please clean and rebuild your solution? If the issue remains, please try to set the `Binding.Mode` explicitly to `TwoWay`. Do you have any styles defined that target `MenuItem`? – BionicCode Sep 01 '20 at 09:16
  • I have recreated the project. I just pasted in my current code as given above, referenced the Xceed `AvalondDock` and `Toolkit` DLLs, and executed it. Behaviour is the same. If I close either of the anchorables using the 'X' button and then look at the `File` menu, both remain checked. There is nothing in my project other than the code given above and the XAML `` definition. To confirm, if you run it, close `Foo2` by clicking on the 'X' and then go to the `File` menu, you see `Foo1` checked and `Foo2` not checked? – wotnot Sep 01 '20 at 10:13
  • Ahh, the 'X' button is the close button on the pane title? I was thinking you are still talking about the menu and X is the check mark. Like toggling the menu button will hide the pane but leave the button in a checked state. You changed the context or subject. I'll provide a solution. You must handle the corresponding `Hidden` event. Please wait a minute... – BionicCode Sep 01 '20 at 10:19
  • Thanks. :-) That is what I was trying to achieve. Very grateful for your help. – wotnot Sep 01 '20 at 10:40
2

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">
thatguy
  • 21,059
  • 6
  • 30
  • 40
  • Thanks for the helpful and thorough explanation. This works as far as the menu goes. It is not two-way, though. If I close the pane (by clicking on the 'X') the menu item remains checked. If I remember rightly, I have read that closing a pane collapses it rather than setting `IsVisible` to `false`. Could be the reason(?) – wotnot Sep 01 '20 at 07:43