绑定 AvalonDock 的 LayoutAnchorable 布局可见性属性

3

我试图将AvalonDock的LayoutAnchorables绑定到WPF中对应的菜单项。如果在菜单中选中,则锚定窗格应该可见。如果未在菜单中选中,则应隐藏锚定窗格。

IsCheckedIsVisible都是布尔类型,因此我不希望需要使用转换器。 我可以将LayoutAnchorableIsVisible属性设置为TrueFalse,并且在设计视图中行为符合预期。

但是,如果尝试实现如下绑定,则会出现错误:

无法在类型为“LayoutAnchorable”的“IsVisible”属性上设置“Binding”。只能在DependencyObject的DependencyProperty上设置“Binding”。

问题在于这里:

<dock:LayoutAnchorable ContentId="content1" IsVisible="{Binding IsChecked, ElementName=mnuPane1}" x:Name="anchorable1" IsSelected="True">

我该如何做到这一点?

<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>

更新:

我实现了BionicCode的答案。我剩下的问题是,如果我关闭一个窗格,菜单项仍然保持选中状态。

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>

代码后置

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;
    }
}
2个回答

2
AvalonDock XAML布局元素既不是控件,也不是派生自UIElement。它们作为简单的模型存在(尽管它们扩展了DependencyObject)。 LayoutAnchorable的属性没有实现为DependencyProperty,而是实现为INotifyPropertyChanged(如前所述,布局元素作为控件的视图模型)。因此,它们不支持数据绑定(作为绑定目标)。
每个XAML布局元素都有一个对应的控件,实际上将使用布局元素作为DataContext进行呈现。名称与布局元素的名称相同,并附加Control后缀。如果您想将这些控件或项目容器(例如LayoutAnchorableItem)连接到您的视图模型,则必须创建一个针对该容器的Style。此容器的DataContext不是控件打算显示的数据模型,而是控件的内部模型。要访问您的视图模型,您需要访问例如LayoutAnchorableControl.LayoutItem.Model(因为LayoutAnchorableControl.DataContextLayoutAnchorable)。
作者显然在过于热衷于使用MVVM(如其文档所述)实现控件本身时迷失了方向,忘记了针对MVVM客户端应用程序。他们打破了常见的WPF模式。外表看起来不错,但内部却不太好。
要解决您的问题,您必须在视图中引入一个中间依赖属性。注册的属性更改回调将委托可见性以切换锚定项的可见性。
还要注意的是,AvalonDock的作者没有使用UIElement.Visibility来处理可见性。他们引入了一种独立于框架属性的自定义可见性逻辑。
如前所述,总是有纯模型驱动方法,您可以通过提供ILayoutUpdateStrategy实现来布局初始视图。然后,您定义样式来连接视图和视图模型。在XAML布局元素中硬编码视图会在更高级别的场景中导致某些不便。 LayoutAnchorable公开了Show()Close()方法或IsVisible属性来处理可见性。您还可以绑定到命令,当访问LayoutAnchorableControl.LayoutItem(例如从ControlTemplate内部)时,它返回一个LayoutAnchorableItem。这个LayoutAnchorableItem公开了一个HideCommandMainWindow.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;
}

我所指的错误可以通过使用我在问题中展示的代码进行复现,至少对我来说是这样。只需从第35行中删除“IsVisible”属性并执行即可。 - wotnot
我刚刚执行了你的代码。原始代码抛出了无效绑定异常(当然)。然后,按照指示,我从LayoutAnchorable中删除了IsVisible属性,一切都编译良好,并显示实际视图。在这种情况下,我还测试了我的解决方案,也运行良好。我刚刚从构造函数将IsAnchorable1Visible的初始值设置为true(我已更新示例)。你描述的问题必须是由你在帖子中未显示的某些代码引入的。你是否在其他地方样式化/操作AnchorablePaneTitle - BionicCode
创建一个空项目,移除问题中的 IsVisible 属性后运行/构建代码 - 您会发现它可以工作。使用文本搜索查找任何 "AnchorablePaneTitle"。还有,这个异常是在哪里抛出的?是抛出在 Xceed 库代码中吗?当应用程序由于异常而暂停时,请检查堆栈跟踪。向后步进直到达到您自己的代码行。这一行触发了异常。不要忘记搜索针对 AnchorablePaneTitle 的样式的 XAML 文件。 - BionicCode
可能是因为之前从未在这个项目中使用过,所以出现了问题。很好,你能够解决它。你应该尝试通过Visual Studio NuGet包管理器获取最新的Xceed版本。这样你也会收到更新通知。右键单击你的项目,然后选择“管理NuGet包...”。我有“Extended.Wpf.Toolkit 4.0.1”。 - BionicCode
谢谢。那是我复制粘贴的错误。这应该是构造函数。构造函数没有返回类型。只需从该方法中删除void即可。我已经更新了我的答案。 - BionicCode
显示剩余15条评论

2
您的绑定存在两个主要问题。
  1. IsVisible属性不是DependencyProperty,而只是一个CLR属性,因此您无法进行绑定。
  2. LayoutAnochorable不是可视树的一部分,因此ElementName和RelativeSource绑定不起作用,您将在输出窗口中看到相应的绑定错误。
我不确定是否有特定的设计选择或限制使IsVisible属性不成为依赖属性,但您可以通过创建一个附加属性来解决这个问题。当更改时,该属性可以被绑定并设置LayoutAnchorable上的CLR属性IsVisible。
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;
   }
}

您可以在XAML中绑定此属性,但是由于LayoutAnchorable不在可视树中,因此这将不起作用。对于DataGrid列,同样存在此问题。在此相关帖子中,您可以找到使用BindingProxy类的解决方法。请将此类复制到您的项目中。
在您的DockingManager.Resources中创建一个绑定代理的实例,它用于访问菜单项。
<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>

移除旧的IsVisible绑定。使用mnuPane1Proxy添加到附加属性的绑定。

<xcad:LayoutAnchorable ContentId="content1"
                       x:Name="anchorable1"
                       IsSelected="True"
                       local:LayoutAnchorableProperties.IsVisible="{Binding Data.IsChecked, Source={StaticResource mnuPane1Proxy}}">

最后,在您的菜单项中将默认的IsChecked状态设置为true,因为这是IsVisible的默认状态,而且由于在附加属性中设置了默认值,绑定在初始化时不会更新,这是必要的,以防止抛出InvalidOperationException,因为控件尚未完全初始化。

<MenuItem Header="_Foo1" Name="mnuPane1" IsCheckable="True" IsChecked="True">

感谢您详细的解释,对于菜单而言这很有帮助。然而,它不是双向的。如果我关闭窗格(通过点击“X”),菜单项仍会保持选中状态。如果我没记错的话,我曾读过关闭窗格会让其折叠而不是将IsVisible设置为false,这可能是原因吗? - wotnot

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接