65.9K
CodeProject 正在变化。 阅读更多。
Home

AvalonDock [2.0] 教程第二部分 - 添加开始页

starIconstarIconstarIconstarIconstarIcon

5.00/5 (17投票s)

2012年10月25日

CPOL

8分钟阅读

viewsIcon

91540

downloadIcon

4091

如何基于 AvalonDock [2.0] 创建一个启动页。

下载

引言   

如果您是本系列文章的新手,请阅读本教程的第一部分 [1]。

我并非 AvalonDock 的开发者,但觉得是时候记录一些基本内容,并在此期间启动了我自己的仓库(如果您愿意贡献) https://github.com/Dirkster99/AvalonDock

 

本教程的这一部分将解释如何在基于 AvalonDock [2.0] 构建的应用程序中添加启动页,我还会记录一些我在文档之间进行 CTRL+TAB 时遇到的焦点问题以及文档选项卡的某些自定义选项。之后可能还会有关于其他 AvalonDock 主题(主题化、本地化、AvalonDock 2.0 中的 AvalonEdit)的部分。那些等不及后续文章的人可以看看 Edi(http://edi.codeplex.com),在我在这里记录之前了解一些事情。本文实际上是用 Edi 撰写的。

本文基于一个解决方案,该解决方案仅需要 AvalonDock 项目(您可以从 CodePlex 下载)、一个 SimpleControls 库(我在 CodeProject 上记录过)和一个名为 EDI 的应用程序项目。那么,让我们来看看本文附带的解决方案。 

ViewModel 

首先要注意的是 `Edi.ViewModel.StartPageViewModel` 和 `Edi.View.StartPage` 类,它们都用于实现启动页项目的 ViewModel 和 View。我们还注意到引入了一个名为 `FileBaseViewModel` 的新基 ViewModel 类,用于为文档 ViewModel `FileViewModel` 和 `StartPageViewModel` 提供一个公共根。`FileBaseViewModel` 是一个抽象类,具有以下属性: 

  • IsFilePathReal - 获取当前路径是否在磁盘上存在。这例如在执行了 File>New 但尚未执行 File>Save 时很有用。在这种情况下,文档可能有一个名称(Untitled.txt),但在这种情况下 `System.IO.File.Exists` 可能没有用处。'

    启动页当然永远不会有有效路径。 
  • FilePath - 这是确定文档保存位置的字符串。
  • IsDirty - 此属性决定是否需要保存文档。
     
  • CloseCommand - 当用户单击文档选项卡关闭按钮时调用此命令。
     
  • SaveCommand - 当文档保存时调用此命令。返回 null 将禁用绑定到此命令的控件,这正是我们需要的启动页。

此公共根是必需的,因为文档集合存储在 `Workspace` ViewModel 中: 

ObservableCollection<filebaseviewmodel> _files = null;
ReadOnlyObservableCollection<filebaseviewmodel> _readonyFiles = null;
public ReadOnlyObservableCollection<filebaseviewmodel> Files
{
  get
  {
    if (_readonyFiles == null)
      _readonyFiles = new ReadOnlyObservableCollection<filebaseviewmodel>(_files);

    return _readonyFiles;
  }
}

当然,我也可以使用更抽象的东西,如 `ObservableCollection<object>`,但我认为继承一个公共根在这里会更合适,尤其是在未来出现更多文档类型时。所以,这就是 AvalonDock [2.0] 中所有文档的存储位置,而启动时是否显示启动页取决于 `Workspace` ViewModel 构造函数中的以下语句:

protected Workspace()
{
  _files = new ObservableCollection<filebaseviewmodel>();
  _files.Add(new StartPageViewModel());
}

但我们也可以通过相应的 `CloseCommand` 关闭启动页,并通过 `AppCommand` 中的 `ShowStartPage` 命令重新打开它,以及以下 `WorkSpace` 语句: 

win.CommandBindings.Add(new CommandBinding(AppCommand.ShowStartPage,
(s, e) =>
{
  StartPageViewModel spage = this.GetStartPage(true);

  if (spage != null)
  {
    this.ActiveDocument = spage;
  }
}));

这是 `GetStartPage` 部分。

internal StartPageViewModel GetStartPage(bool CreateNewViewModelIfNecessary)
{
  List<StartPageViewModel> l = this._files.OfType<StartPageViewModel>().ToList();

  if (l.Count == 0)
  {
    if (CreateNewViewModelIfNecessary == false)
      return null;
    else
    {
      StartPageViewModel s = new StartPageViewModel();
      this._files.Add(s);

      return s;
    }
  }

  return l[0];
}

一旦这些事情确定下来,继续进行就几乎微不足道了——我曾遇到过理解 `OfType` 强制转换和其他一些 Linq 问题,这些问题在需要处理特定类型 ViewModel 时会让生活更轻松。我在我的 Edi 应用程序中甚至构建了专用的属性,例如:

private List<EdiViewModel> Documents
{
  get
  {
    return this.mFiles.OfType<EdiViewModel>().ToList();
  }
}

通过这些属性进行工作和绑定甚至隐藏了强制转换,使事情更容易处理。完整的展示可能需要特殊的转换器和一些高级绑定技巧,例如,在绑定仅在存在适当类型的 ViewModel 时才起作用的地方使用 FallBack 值。

一个需要调整的转换器是 `ActiveDocumentConverter` 类。当文档的 ViewModel 分配给 `Workspace` 类中的 `ActiveDocument` 属性时,会调用此转换器。

class ActiveDocumentConverter : IValueConverter
{
  public object Convert(object value, Type targetType, object parameter,
                        System.Globalization.CultureInfo culture)
  {
    if (value is EdiViews.ViewModel.Base.FileBaseViewModel)
      return value;

    return Binding.DoNothing;
  }

  public object ConvertBack(object value, Type targetType, object parameter,
                            System.Globalization.CultureInfo culture)
  {
    if (value is EdiViews.ViewModel.Base.FileBaseViewModel)
      return value;

    return Binding.DoNothing;
  }
}

这里重要的部分是检查 `FileBaseViewModel` 类。为了好玩,您可以将其编辑到 FileViewModel 中,这样您就会看到,如果您关闭启动页,打开一个文件,然后选择 Tools>Start Page,启动页就不会置于最前面(最后一个文本文件将保持在最前面)。

因此,这就是我们在 AvalonDock 中组织新文档的方式。但它是如何显示的,AvalonDock [2.0] 如何将正确的 ViewModel 与视图关联起来?接下来我们将讨论这个问题。

View

扩展新文档类型的视图部分几乎与扩展上一篇文章中讨论的工具窗口视图部分相同。我最终扩展了 `View.Pane.PanesStyleSelector`,并添加了一个 `Style` 属性。

public Style StartPageStyle
{
  get;
  set;
}

...以及 `SelectStyle` 方法中的以下语句。

if (item is StartPageViewModel)
  return StartPageStyle;

...这与 `MainWindow.xaml` 文件中的相应扩展一起工作。

<pane:panestemplateselector.startpageviewtemplate>
  <datatemplate>
    <view:startpage>
  </view:startpage>
</pane:panestemplateselector.startpageviewtemplate>

和在第一部分一样,我们来看一个非常相似的样式设置。

View.Pane.PanesStyleSelector 属性

public Style StartPageStyle
{
  get;
  set;
}

View.Pane.PanesStyleSelector 方法在 `SelectStyle` 中的扩展

if (item is StartPageViewModel)
  return StartPageStyle;

MainWindow.xaml 文件

<pane:panesstyleselector.startpagestyle>
<style targettype="{x:Type avalonDock:LayoutItem}">
  <Setter Property="Title" Value="{Binding Model.Title}"/>
  <Setter Property="ToolTip" Value="{Binding Model.StartPageTip}"/>
  <Setter Property="CloseCommand" Value="{Binding Model.CloseCommand}"/>
  <Setter Property="IconSource" Value="{Binding Model.IconSource}"/>
  <Setter Property="ContentId" Value="{Binding Model.ContentId}"/>
</style>
</pane:panesstyleselector.startpagestyle>

文档焦点问题

AvalonDock [2.0] 要求文档视图能够获得键盘焦点。这是支持通过 CTRL+TAB 在文档之间切换所必需的。我是一名键盘用户,因此一旦实现了启动页,发现它不起作用就非常令人讨厌。**我最终花了整整两天时间来调试 AvalonDock [2.0] 并确定我的焦点问题的性质。** 

事实证明,如果您为每个文档视图使用专用的用户控件,可以增强测试应用程序。因此,我用以下用户控件替换了 `DataTemplate` - `TextBox` 示例,该用户控件在 AvalonDock 的 DataTemplate 管理器中引用。

<UserControl x:Class="Edi.View.DocumentView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300"
             FocusManager.FocusedElement="{Binding ElementName=DemoTextBox}"
             >
  <Grid>
    <TextBox Name="DemoTextBox" 
      Text="{Binding TextContent, UpdateSourceTrigger=PropertyChanged}"/>
  </Grid>
</UserControl> 

这里重要的部分是 `FocusManager.FocusedElement="{Binding ElementName=DemoTextBox}"` 绑定——移除它,您会发现导航器窗口在使用 CTRL+TAB 时会不稳定一半。

现在,如果您的文档视图有一个可以获得键盘焦点的控件,这个解决方案就能起作用,或者如果您使用一个默认获得焦点的控件(例如 AvalonEdit),您甚至不会注意到这个问题。但是,如果我们有一个既没有 TextBox 也没有 AvalonEdit 的启动页,我们该怎么办?在这种情况下(我吃力地发现),我们可以使用一个附加行为来将焦点设置到 UserControl 本身。

<UserControl ...
Focusable="True"
KeyboardNavigation.IsTabStop="True"
Name="StartPageView"
behav:KeyboardFocus.On="{Binding ElementName=StartPageView}"
>

这就是全部内容了。除此之外,我还需要做一些调整来支持启动页功能。例如,我添加了一个 `ApplicationCommands.New` 和 `ApplicationCommands.Open` 命令绑定,以支持启动页中的这些功能。我还添加了一个 `AppCommand.BrowseURL` 命令,允许用户在单击右上角的 logo(狗)时浏览到应用程序主页。 

我还调整了一些语句,将 `StartPageViewModel` 操作的执行与 `FileViewModel` 操作的执行分开。`ViewModel.Workspace.Open()` 方法的开头部分提供了一个例子:

List<FileViewModel> filesFileViewModel = this._files.OfType<FileViewModel>().ToList();

// Verify whether file is already open in editor, and if so, show it
var fileViewModel = filesFileViewModel.FirstOrDefault(fm => fm.FilePath == filepath);

我在这里按 FileViewModel 对象进行筛选,自动删除任何其他类型的对象。

自定义文档面板

我经常问自己(也看到别人问了完全相同的问题),是否可以配置 AvalonDock 中文档的显示方式。

更具体地说,我们可以更改文档标题的显示方式吗?我们可以自定义 AvalonDock 文档标题上的弹出菜单吗?

答案是“是的,我们可以”。我在 `avalonDock:DockingManager` XAML 标签(在 `MainWindow.xaml` 中)中使用以下 `DocumentHeaderTemplate`。

<avalonDock:DockingManager.DocumentHeaderTemplate>
    <DataTemplate>
      <StackPanel Orientation="Horizontal">
        <TextBlock Text="{Binding Title}" TextTrimming="CharacterEllipsis" VerticalAlignment="Center" />
        <!-- Show IsReadOnly Icon if that properties' true -->
        <Image Source="{StaticResource Image_DocTabReadOnly}"
               Margin="3,0,0,0"
               Visibility="{Binding Content.IsReadOnly, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}"
               ToolTip="{Binding Content.IsReadOnlyReason}"
               VerticalAlignment="Center"
               />
      </StackPanel>
    </DataTemplate>
</avalonDock:DockingManager.DocumentHeaderTemplate>

编辑器 能够打开文件但无法写入访问时,添加一个额外的图标(带工具提示)。

上下文菜单也可以以类似的方式自定义。在 `MainWindow.xaml` 中的 `avalonDock:DockingManager` XAML 标签的定义中添加如下所示的 `DocumentContextMenu` 标签。

<avalonDock:DockingManager.DocumentContextMenu>
    <ContextMenu>
      <MenuItem Header="Open Containing Folder..." 
                Command="{Binding Model.OpenContainingFolderCommand}"
                />
      <MenuItem Header="Copy URL to Clipboard" 
                Command="{Binding Model.CopyFullPathtoClipboard}"
                />
      <Separator/>

      <MenuItem Header="{x:Static avalonDockProperties:Resources.Document_Close}" 
                Command="{Binding Path=CloseCommand}"
                Visibility="{Binding Path=IsEnabled, RelativeSource={RelativeSource Self}, 
                  Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}"
                />
      <MenuItem Header="{x:Static avalonDockProperties:Resources.Document_CloseAllButThis}" 
                Command="{Binding Path=CloseAllButThisCommand}"
                />
      <MenuItem Header="{x:Static avalonDockProperties:Resources.Document_Float}" 
                Command="{Binding Path=FloatCommand}"
                />
      <MenuItem Header="{x:Static avalonDockProperties:Resources.Document_DockAsDocument}" 
                Command="{Binding Path=DockAsDocumentCommand}"
                />
      <MenuItem Header="{x:Static avalonDockProperties:Resources.Document_NewHorizontalTabGroup}" 
                Command="{Binding Path=NewHorizontalTabGroupCommand}"
                Visibility="{Binding Path=IsEnabled, RelativeSource={RelativeSource Self}, 
                  Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}"
                >
        <MenuItem.Icon>
          <Image Source="/AvalonDock.Themes.Aero;component/Images/HTabGroup.png" Stretch="Uniform" Width="14"/>
        </MenuItem.Icon>
      </MenuItem>
    </ContextMenu>
</avalonDock:DockingManager.DocumentContextMenu>

在我的情况下,这产生了以下上下文菜单。

显而易见,这种扩展只有在绑定正常工作时才能奏效。我已经绑定到了 FileViewModel 中的命令(或上面示例中的 `StartPageViewModel`)。我将不再详细解释这一点,因为这似乎相当容易,但您可以下载我的开源编辑器 Edi 来自行查看这些细节。

摘要

再次,我可以这样说,这就是全部内容。我认为本教程的这一部分展示了 WPF 的一些真正优势。它能够在给定的应用程序的许多不同方式和位置显示相同的数据。它通过列表中的 Pine 等项目丰富地表达功能。它有... 

参考文献

历史  

  • 2012年10月24日:初次创建。
  • 2012年11月9日:包含文档标题和上下文菜单的示例自定义。
  • 2013年2月28日:我发现了一些重要问题,例如焦点部分未得到处理,因此我更新了文章和源代码,增加了更高级的功能以及更最新的 AvalonDock 版本。
© . All rights reserved.