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

AvalonDock [2.0] 教程第三部分 - AvalonDock 中的 AvalonEdit

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.97/5 (11投票s)

2013 年 4 月 1 日

CPOL

10分钟阅读

viewsIcon

74081

downloadIcon

2582

如何使用 MVVM 将 AvalonEdit 集成到 AvalonDock [2.0] 中

简介  

我花了很多时间来学习 AvalonDock [2.0] 和 WPF [2] 中的 MVVM 模式。我希望通过这一系列文章,能够将我的一些经验传达给其他编码人员,同时我也自己建立了一个仓库(如果您想贡献的话) https://github.com/Dirkster99/AvalonDock

我关于这个主题的前两篇文章 [1] 侧重于使用自定义工具窗口(最近使用的文件 TW,带可固定列表)和自定义文档控件(开始页)。我将继续这个系列,展示如何在不弯曲甚至不破坏 MVVM 模式的情况下,将 SharpDevelop 中知名的 AvalonEdit 编辑器集成到 AvalonDock [2.0] 中。本系列文章随后将展示 AvalonDock [2.0]/AvalonEdit 的特定功能,以及如何以 MVVM 为中心来实现它们。

准备工作

如果您想理解我在这里展示的所有细节,可以重新创建我所经历的所有步骤。本节将描述遵循我的讲解所必需的准备步骤。

  • 您可以重现 第一个教程步骤 中概述的应用程序,或者从那里下载 Version_01_Edi.zip 文件。无论哪种方式,这都应该为您提供一个 AvalonDock 应用程序的起点,我们将在其中进行扩展。
     
  • AvalonEdit 的准备工作包括从 SharpDevelop 下载源代码,提取 AvalonEdit,并将其集成到 Edi 示例应用程序中。以下是准备好将 AvalonEdit 集成到 AvalonDock [2.0] 所需的内容:

    历史

    • https://github.com/icsharpcode/SharpDevelop 下载 SharpDevelop-master.zip
      • 在以下位置找到 AvalonEdit 子项目:

        SharpDevelop-master\src\Libraries\AvalonEdit
        并将其复制到 Edi 解决方案中。
      • Copy SharpDevelop-master\src\Main\GlobalAssemblyInfo.cs
        Edi\AvalonEdit
        目录。
         
      • 在 Visual Studio 中打开 AvalonEdit 项目,并删除项目解决方案资源管理器条目“属性”部分中对 GlobalAssemblyInfo.cs 文件的损坏引用。
         
      • 点击项目,选择“添加”>“现有项”,添加您刚刚复制到已提取的 AvalonEdit 项目中的 GlobalAssemblyInfo.cs 文件。
         
      • 查看项目属性,并将二进制输出文件夹从
        ..\..\..\..\bin\
        重置为更合适的位置,例如
        bin\
      • 如果想使用不同的 .Net 框架进行开发,请考虑删除
        DOTNET4
        项目属性中的符号。
      • 关闭项目。
      • 打开 Edi 解决方案。
      • 通过解决方案上的右键菜单“添加”>“现有项目...”添加 AvalonEdit 项目,并添加 ICSharpCode.AvalonEdit.csproj
      • 从主应用程序项目 Edi 添加对 ICSharpCode.AvalonEdit.csproj 项目的引用。



      •  
      • 生成解决方案(应成功生成)。 

      成功完成此步骤意味着我们重新配置了 AvalonEdit,使其可以在 AvalonDock 项目中编译。接下来,我们将编辑示例项目,使其使用 AvalonEdit 作为文本编辑组件。

      将 AvalonEdit 集成到基于 AvalonDock [2.0] 的应用程序中

      基于 MVVM 的 WPF 应用程序通常至少有三个层(视图、视图模型和模型),其中最后一层有时是视图模型类的一部分。AvalonDock 示例应用程序已经包含了一个用于文本编辑的视图和视图模型类,因此我们只需将其替换为比 Microsoft 标准文本框更高级的东西。接下来的小节列出了调整视图和视图模型所需的更改。 

      View

      打开 Edi 解决方案,导航到 View/MainWindow.xaml。这是您启动应用程序时看到的 M_ain Window。将 AvalonEdit 添加到此视图需要命名空间引用,当然还需要 XAML 标签本身。因此,我们在大约第 10 行添加一个命名空间引用。

      xmlns:avalonEdit="http://icsharpcode.net/sharpdevelop/avalonedit"

      接下来,我们添加 XAML 标签本身。找到引用当前配置为与 FileViewModel 类使用的视图的 TextBox 条目。

      <pane:PanesTemplateSelector.FileViewTemplate>
        <DataTemplate>
          <TextBox Text="{Binding TextContent, UpdateSourceTrigger=PropertyChanged}"/>
        </DataTemplate>
      </pane:PanesTemplateSelector.FileViewTemplate> 

      将上面的 XAML 标签替换为下面的 AvalonEdit 引用。

      <pane:PanesTemplateSelector.FileViewTemplate>
        <DataTemplate>
          <avalonEdit:TextEditor
            Document="{Binding Document, UpdateSourceTrigger=PropertyChanged}"
          />
        </DataTemplate>
      </pane:PanesTemplateSelector.FileViewTemplate> 

      ViewModel

      打开 Edi 解决方案,导航到 ViewModel/FileViewModel.cs 类,查看 **TextContent** 属性,并将其替换为 AvalonDock 特定的 Document 属性。

      #region TextContent
      private TextDocument _document = null;
      public TextDocument Document
      {
        get { return this._document; }
        set
        {
          if (this._document != value)
          {
            this._document = value;
            RaisePropertyChanged("Document");
            IsDirty = true;
          }
        }
      }
      #endregion

      .Net 框架中的依赖属性系统非常高效,但(在您输入时)通过依赖属性发送和接收文本更改会减慢编辑器的速度。因此,TextDocument 类围绕编辑文本进行包装(而不是直接通过绑定公开字符串)。实现 Document 绑定的 TextDocument 类通过简单的 CLR 属性公开文本。

      视图和视图模型之间通信文本的思路是,应用程序可以在加载或保存文本数据时使用 TextDocument 类中的 CLR Text 属性来读写文本字符串;-而将编辑工作留给编辑器。我们将在本教程后面看到,这种方法在保持 MVVM(选择文本时)方面需要额外的工作。但是,它是实现处理大量数据的 ef_ficient WPF 应用程序的一个好设计措施。因此,这一点值得特别注意。

      接下来,我们需要编辑示例应用程序中已实现的文本加载和保存方法。加载方法实际上位于 FileViewModel 类中的 Filepath 属性中。编辑 Filepath 属性,使其使用新的 Document 属性: 

      #region FilePath
      private string _filePath = null;
      public string FilePath
      {
        get { return _filePath; }
        set
        {
          if (_filePath != value)
          {
            _filePath = value;
            RaisePropertyChanged("FilePath");
            RaisePropertyChanged("FileName");
            RaisePropertyChanged("Title");
      
            if (File.Exists(_filePath))
            {
              this._document = new TextDocument();
      
              using (FileStream fs = new FileStream(this._filePath,
                                         FileMode.Open, FileAccess.Read, FileShare.Read))
              {
                using (StreamReader reader = FileReader.OpenStream(fs, Encoding.UTF8))
                {
                  this._document = new TextDocument(reader.ReadToEnd());
                }
              }
      
              ContentId = _filePath;
            }
          }
        }
      }
      #endregion

      文本保存方法在 Workspace.cs 文件中。找到 Save 方法并用下面的代码替换它: 

      internal void Save(FileViewModel fileToSave, bool saveAsFlag = false)
      {
        if (fileToSave.FilePath == null || saveAsFlag)
        {
          var dlg = new SaveFileDialog();
          if (dlg.ShowDialog().GetValueOrDefault())
            fileToSave.FilePath = dlg.SafeFileName;
        }
      
        File.WriteAllText(fileToSave.FilePath, fileToSave.Document.Text);
        ActiveDocument.IsDirty = false;
      }

      AvalonEdit 还支持标准的 WPF 应用程序级命令,例如:

      • 复制、剪切、粘贴、删除、撤销、重做和全选

      Microsoft 的 .Net Framework 设计人员 定义了这些 ApplicationCommands,目的是让控件和应用程序开发人员尽可能重用它们。因此,在我们的应用程序中使用这些命令只需要将图标文件夹中的图像以及 MainWindow.xaml 视图工具栏部分中相应的按钮定义添加到其中。

      <ToolBarTray Grid.Row="1" SnapsToDevicePixels="True" >
        <ToolBar  VerticalAlignment="Stretch" ToolBarTray.IsLocked="True"
                  SnapsToDevicePixels="True">
      
          <Button Command="Copy" SnapsToDevicePixels="True"
                  ToolTip="{Binding RelativeSource={RelativeSource Self}, Path=Command.Text}">
            <Image Source="/Edi;component/Images/App/Edit_Copy32.png" Height="32" SnapsToDevicePixels="True" />
          </Button>
          <Button Command="Cut" SnapsToDevicePixels="True"
                  ToolTip="{Binding RelativeSource={RelativeSource Self}, Path=Command.Text}">
            <Image Source="/Edi;component/Images/App/Edit_Cut32.png" Height="32" SnapsToDevicePixels="True" />
          </Button>
          <Button Command="Paste" SnapsToDevicePixels="True"
                  ToolTip="{Binding RelativeSource={RelativeSource Self}, Path=Command.Text}">
            <Image Source="/Edi;component/Images/App/Edit_Paste32.png" Height="32" SnapsToDevicePixels="True" />
          </Button>
          <Button Command="Delete" SnapsToDevicePixels="True"
                  ToolTip="{Binding RelativeSource={RelativeSource Self}, Path=Command.Text}">
            <Image Source="/Edi;component/Images/App/Edit_Delete32.png" Height="32"/>
          </Button>
          <Separator Margin="3"/>
      
          <Button Command="Undo" SnapsToDevicePixels="True"
                ToolTip="{Binding RelativeSource={RelativeSource Self}, Path=Command.Text}">
            <Image Source="/Edi;component/Images/App/Edit_Undo32.png" Height="32"/>
          </Button>
          <Button Command="Redo" SnapsToDevicePixels="True"
                ToolTip="{Binding RelativeSource={RelativeSource Self}, Path=Command.Text}">
            <Image Source="/Edi;component/Images/App/Edit_Redo32.png" Height="32"/>
          </Button>
        </ToolBar>
      </ToolBarTray>

      这个原则的应用如此简单,以至于我们还可以通过在控件定义标签中添加 XAML 来实现上下文菜单。

      <avalonEdit:TextEditor Document="{Binding Document, UpdateSourceTrigger=PropertyChanged}">
      ...
      <avalonEdit:TextEditor.ContextMenu>
        <ContextMenu>
          <MenuItem Command="Cut" Header="Cut">
            <MenuItem.Icon>
              <Image Source="/Edi;component/Images/App/Edit_Cut32.png" Height="16"/>
            </MenuItem.Icon>
          </MenuItem>
          <MenuItem Command="Copy" Header="Copy">
            <MenuItem.Icon>
              <Image Source="/Edi;component/Images/App/Edit_Copy32.png" Height="16"/>
            </MenuItem.Icon>
          </MenuItem>
          <MenuItem Command="Paste" Header="Paste">
            <MenuItem.Icon>
              <Image Source="/Edi;component/Images/App/Edit_Paste32.png" Height="16"/>
            </MenuItem.Icon>
          </MenuItem>
          <MenuItem Command="Delete" Header="Delete">
            <MenuItem.Icon>
              <Image Source="/Edi;component/Images/App/Edit_Delete32.png" Height="16"/>
            </MenuItem.Icon>
          </MenuItem>
         <MenuItem Command="SelectAll" Header="Select All"/>
      <Separator />
      
          <MenuItem Command="Undo" Header="Undo">
            <MenuItem.Icon>
              <Image Source="/Edi;component/Images/App/Edit_Undo32.png" Height="16"/>
            </MenuItem.Icon>
          </MenuItem>
          <MenuItem Command="Redo" Header="Redo">
            <MenuItem.Icon>
              <Image Source="/Edi;component/Images/App/Edit_Redo32.png" Height="16"/>
            </MenuItem.Icon>
          </MenuItem>
        </ContextMenu>
      </avalonEdit:TextEditor.ContextMenu>
      </avalonEdit:TextEditor>

      就这样。这就是用 AvalonDock [2.0] 中的 AvalonEdit 控件替换标准 WPF 文本框的全部内容。

      添加语法高亮

      通常根据正则表达式或其他搜索规则对文本进行着色,这被称为语法高亮。具有 ef_ficient 编辑功能的开源 WPF 编辑器中,选择语法高亮器的选项非常少。唯一似乎符合要求的编辑器是 ScintillaNET(在 CodePlex 上)的实现或 SharpDevelop 的 AvalonEdit。我更喜欢 AvalonEdit,因为它完全用 C# 和 WPF 从头开始编写。

      本节列出了实现 AvalonEdit 语法高亮所需的项目。我实现了一个仅使用一种高亮模式的简单解决方案。我假设在本示例中,我们只想编辑和查看 XML。如果您正在寻找具有广泛高亮模式的更高级解决方案,请参阅 Edi [2]。

      ViewModel

      AvalonEdit 编辑器通过 IHighlightingDefinition 接口管理高亮。因此,需要该类型的属性才能使视图能够绑定到它。在 FileViewModel.cs 类中添加一个语法高亮属性: 

      #region HighlightingDefinition
      private IHighlightingDefinition _highlightdef = null;
      public IHighlightingDefinition HighlightDef
      {
        get { return this._highlightdef; }
        set
        {
          if (this._highlightdef != value)
          {
            this._highlightdef = value;
            RaisePropertyChanged("HighlightDef");
            IsDirty = true;
          }
        }
      }
      #endregion

      并添加...

      this.HighlightDef = HighlightingManager.Instance.GetDefinition("XML");
      在 FilePath 属性代码中,在下面的加载文本文件的代码中。
      this._document = new TextDocument();
      语句。

      View

      在 View/MainWindow.xaml 文件中添加 SyntaxHighlighting 绑定。

      <avalonEdit:TextEditor
                Document="{Binding Document, UpdateSourceTrigger=PropertyChanged}"
                SyntaxHighlighting="{Binding HighlightDef}"
                >
                ...
      <avalonEdit:TextEditor/>

      现在我们可以启动应用程序并打开任何 XML 文件,以验证 XML 可以被查看和编辑,并具有蓝色和红色高亮显示。 

      添加更多 AvalonEdit 功能

      前面的部分已经展示了如何使用 AvalonEdit 在 AvalonDock [2.0] 中编辑带有语法高亮的文本。我曾提到,用于围绕 Text CLR 属性进行包装的基于 TextDocument 类的依赖属性用于在视图和视图模型之间发送文本。乍一看,这种设计似乎有些奇怪,但 IsDirty 属性(显示文本是否被编辑)的实现表明,我们仍然可以在适当的情况下使用绑定。

      下面描述的 IsDirty 属性稍后将与 IsReadOnly 属性的描述进行对比,该属性展示了我们如何将 AvalonEdit 用作具有高亮功能的查看器,并演示了如何自定义 AvalonDock [2.0] 中的文档选项卡。

      视图中的脏标记支持

      我如今使用的所有编辑器,当用户编辑了某些内容(文本或图形)时,都会在其窗口或文档标题中显示一个星号“*”。这个常见的功能称为脏标记,可以按此处讨论的方式实现。 

      在 View/MainWindow.xaml 文件中添加 IsModified 绑定。

      <avalonEdit:TextEditor
        Document="{Binding Document, UpdateSourceTrigger=PropertyChanged}"
        SyntaxHighlighting="{Binding HighlightDef}"
        IsModified="{Binding Path=IsDirty, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
                >
                ...
      <avalonEdit:TextEditor/>

      视图模型中的脏标记支持

      添加初始化 IsDirty 语句。

      this._isDirty = false;

      在 FileViewModel 类中,在 FilePath 属性的文本加载代码中。

      接下来,我们需要重写 FileViewModel 类中的 Title 属性,因为它否则将从 PaneViewModel 类继承。之所以需要继承,是为了简化 IsDirty 属性代码中的 RaisePropertyChanged("Title"); 调用,这可以确保在文本被编辑时,文档标题显示星号。

      因此,我们需要为 FileViewModel 添加一个重写的 Title,并在 IsDirty 属性中添加 RaisePropertyChanged("Title"); 调用,如下所示:

      #region Title
      public string Title
      {
        get
        {
          return System.IO.Path.GetFileName(this.FilePath) + (this.IsDirty == true ? "*" :
                                                                                      string.Empty);
        }
      
        set
        {
          base.Title = value;
        }
      }
      #endregion
      
      #region IsDirty
      
      private bool _isDirty = false;
      public bool IsDirty
      {
        get { return _isDirty; }
        set
        {
          if (_isDirty != value)
          {
            _isDirty = value;
            RaisePropertyChanged("IsDirty");
            RaisePropertyChanged("FileName");
            RaisePropertyChanged("Title");
          }
        }
      }
      
      #endregion

      现在打开文件并进行编辑,应该会在文档选项卡标题中看到一个星号。

      只读查看器支持

      我经常遇到这种情况:我想编辑一个文件,但因为文件是只读的、在另一个应用程序中打开或由于其他原因无法访问,所以无法编辑。一旦理解了这种不可访问性,它本身通常不是问题。我的问题通常是可用性问题,源于各种可能的实现。有些编辑器会让你编辑文本,然后告诉你无法保存,而你的电脑已经关机,或者你真的急着要离开。其他的编辑器则什么也不告诉你,只是简单地阻止 UI 并将其显示为只读(让用户猜测发生了什么)。

      本节中的解决方案避免了上述情况,它实现了一个只读文本编辑控件(这可以确保用户在开始键入时至少可以读取和写入文件)。通过在文件无法以写模式打开时,在文档选项卡中显示一个带工具提示的锁定符号,可以进一步改进这种行为。

      视图模型中的 IsReadOnly 支持

      本节讨论的 IsReadOnly 功能只需在 FileViewModel 类中添加一个布尔属性和一个字符串属性。前者属性告诉 GUI 不要编辑此文本文档,因为它无法保存;后者字符串属性可以用于向用户传达此行为的原因(应该存在多种可能的原因)。 

      #region IsReadOnly
      private bool mIsReadOnly = false;
      public bool IsReadOnly
      {
        get
        {
          return this.mIsReadOnly;
        }
      
        protected set
        {
          if (this.mIsReadOnly != value)
          {
            this.mIsReadOnly = value;
            this.RaisePropertyChanged("IsReadOnly");
          }
        }
      }
      
      private string mIsReadOnlyReason = string.Empty;
      public string IsReadOnlyReason
      {
        get
        {
          return this.mIsReadOnlyReason;
        }
      
        protected set
        {
          if (this.mIsReadOnlyReason != value)
          {
            this.mIsReadOnlyReason = value;
            this.RaisePropertyChanged("IsReadOnlyReason");
          }
        }
      }
      #endregion IsReadOnly

      视图中的 IsReadOnly 支持

      一旦知道如何操作,在 AvalonDock 中自定义文档标签显示就非常简单了。只需在 View/MainWindow.xaml 文件中的 DockingManager 部分添加一个 DocumentHeaderTemplate 标签。

      <avalonDock:DockingManager.DocumentHeaderTemplate>
      <DataTemplate>
        <StackPanel Orientation="Horizontal">
        <TextBlock Text="{Binding Title}" TextTrimming="CharacterEllipsis"
                   VerticalAlignment="Center" />
        <!-- Show IsReadOnly Icon in document tab if that properties' present and true -->
        <Image Source="/Edi;component/Images/App/DocTab/Lock-icon.png"
               Margin="3,0,0,0"
               VerticalAlignment="Center">
        <Image.Visibility>
           <PriorityBinding FallbackValue="Collapsed" >
              <Binding Path="Content.IsReadOnly" Mode="OneWay"
                       Converter="{StaticResource BoolToVisibilityConverter}" />
           </PriorityBinding>
        </Image.Visibility>
          <Image.ToolTip>
            <PriorityBinding FallbackValue="">
               <Binding Path="Content.IsReadOnlyReason" Mode="OneWay" />
            </PriorityBinding>
          </Image.ToolTip>
        </Image>
        </StackPanel>
      </DataTemplate>
      </avalonDock:DockingManager.DocumentHeaderTemplate>

      我在这里使用优先级绑定作为备用,因为我想避免对不支持此属性的文档(如 StartPage)出现绑定错误。我也不希望 StartPage 可编辑或显示锁定符号。因此,我将其显示设为可选,因为如果未显示,它实际上并不重要。

      现在,让我们为 AvalonEdit 标签添加一个 IsReadOnly 绑定来完成这项工作。

      <avalonEdit:TextEditor
                Document="{Binding Document, UpdateSourceTrigger=PropertyChanged}"
                SyntaxHighlighting="{Binding HighlightDef}"
                IsModified="{Binding Path=IsDirty, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
                IsReadOnly="{Binding IsReadOnly}"
                >
                ...
      <avalonEdit:TextEditor/>

      结论

      本文再次展示了 WPF 如何能够基于简单的原则实现复杂的应用程序。我们特别关注了在 AvalonDock [2.0] 中使用知名的 AvalonEdit 文本编辑器 [3]。最后一个带有文档选项卡中锁定符号的部分为我们设计有趣的应用程序提供了另一个线索。我们需要解放思想,摆脱过去框架的限制,学习新的方法,并以焕然一新的创造力来应用它们。

      参考文献

    • [1]
      AvalonDock [2.0] 教程第一部分 - 添加工具窗口
      AvalonDock [2.0] 教程第二部分 - 添加开始页
      AvalonDock [2.0] 教程第四部分 - 集成 AvalonEdit 选项
      AvalonDock [2.0] 教程第五部分 - 加载/保存布局,带解引用 DockingManager
       
    • [2] EDI 来自 http://edi.codeplex.com/
    • [3] 使用 AvalonEdit (WPF 文本编辑器)
    • [4] AvalonDock
      • 2013 年 3 月 31 日 发布了本文的第一个版本
© . All rights reserved.