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

WPF MVVM 富文本演示(使用 YDock[Panel])

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2023 年 3 月 1 日

CPOL

5分钟阅读

viewsIcon

21488

downloadIcon

256

使用停靠框架、RichText、MVVM 工具包以及更多功能

 

引言

本文及演示的重点是入门使用 YDock 结合 CommunityToolkit.Mvvm 以及一些自定义的 MessageBox 和对话框的接口/服务。

背景

CodeProject 上有很多关于其他停靠框架的文章,但没有关于 YDock 的。所以我开始尝试结合 YDockCommunityToolkit.Mvvm

模型、视图视图模型MVVM 模式)是组织或构建代码的良好方式,有助于简化、开发和测试(例如,单元测试)您的应用程序。

模型包含数据,与应用程序逻辑无关。

视图模型充当模型和视图之间的连接。

视图是用户界面。

我不会详细描述和解释整个演示项目的每一个细节。重点在于如何使用停靠框架、MVVM 工具包以及其中一些功能。

Using the Code

MVVM 结构 / 功能

MVVM 工具包来自微软,其他一些使用的功能也不是我原创的:来源如 鸣谢 / 参考部分 所列。

内容快速概览

  • YDock
  • CommunityToolkit.Mvvm 和 .NET 4.8
    • RelayCommand
    • OnPropertyChanged
    • ObservableRecipientMessengerViewModelBase
    • DependencyInjection(将 MsgBoxDialog 作为服务运行)
    • ObservableCollection(用于鸣谢的 Listbox
  • Ribbon 菜单
  • 服务 / 对话框
  • 使用 ICommand 将按钮绑定到 ViewModel

安装 MVVM Toolkit

安装 MVVM Toolkit 的 NuGet 包时,它还会安装 6 或 7 个其他包。

对于 DependencyInjection,我们需要安装另一个 NuGet 包

Microsoft.Extensions.DependencyInjection

我还为 MsgBox 和一些对话框创建了接口/服务。

DependencyInjection 能够**独立于** viewmodels 启动以下对话框。

  • FontDlgVM
  • MsgBoxService
  • DialogVM
  • OpenFileDlgVM
  • RibbonStatusService
  • SaveAsFileDlgVM
  • SearchDlgVM

这允许使用自定义 Messagebox/对话框以及进行单元测试。

停靠框架的使用

在 GitHub 上找到了优秀的 YDockYDockTest 项目。

为了实现可重用的示例,我们需要用 usercontrols 替换一些占位符 documents 和 toolwindows。

我的示例项目名为 MyWorksForYDock

MainWindow 类背后的代码是

namespace MyWorksForYDock
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            Loaded += MainWindow_Loaded;
            Closing += MainWindow_Closing;

            _Init();
        }

        static string SettingFileName { get { return string.Format(@"{0}\{1}", 
                      Environment.CurrentDirectory, "Layout.xml"); } }

        public DocView doc_new;
        public DocView doc_0;
        private Doc left;
        private Doc right;
        private RibbonView top;
        public LogView bottom;
        private Doc left_1;
        private Doc right_1;
        private CreditsView bottom_1;
        private MainWindow wnd;

        private void _Init()
        {
            doc_new = new DocView("New_doc");
            doc_0 = new DocView("doc_0");
            left = new Doc("left");
            right = new Doc("right");
            top = new RibbonView("Ribbon");
            bottom = new LogView("Log");
            left_1 = new Doc("left_1");
            right_1 = new Doc("right_1");
            bottom_1 = new CreditsView("Credits");

            DockManager.RegisterDocument(doc_0);
            DockManager.RegisterDock(top, DockSide.Top);
            DockManager.RegisterDock(bottom, DockSide.Bottom);
            DockManager.RegisterDock(left, DockSide.Bottom);
            DockManager.RegisterDock(right, DockSide.Right);
            DockManager.RegisterDock(left_1, DockSide.Left);
            DockManager.RegisterDock(right_1, DockSide.Right);
            DockManager.RegisterDock(bottom_1, DockSide.Bottom);
        }

        private void MainWindow_Loaded(object sender, RoutedEventArgs e)
        {
            wnd = this;
            if (File.Exists(SettingFileName))
            {
                var layout = XDocument.Parse(File.ReadAllText(SettingFileName));
                foreach (var item in layout.Root.Elements())
                {
                    var name = item.Attribute("Name").Value;
                    if (DockManager.Layouts.ContainsKey(name))
                        DockManager.Layouts[name].Load(item);
                    else DockManager.Layouts[name] = 
                         new YDock.LayoutSetting.LayoutSetting(name, item);
                }

                DockManager.ApplyLayout("MainWindow");
            }
            else
            {
                doc_0.DockControl.Show();
                top.DockControl.Show();
                bottom.DockControl.Show();
                left.DockControl.Show();
                right.DockControl.Show();
                left_1.DockControl.Show();
                right_1.DockControl.Show();
                bottom_1.DockControl.Show();
            }
        }

        private void MainWindow_Closing(object sender, CancelEventArgs e)
        {
            DockManager.SaveCurrentLayout("MainWindow");

            var doc = new XDocument();
            var rootNode = new XElement("Layouts");
            foreach (var layout in DockManager.Layouts.Values)
                layout.Save(rootNode);
            doc.Add(rootNode);

            doc.Save(SettingFileName);

            DockManager.Dispose();
        }

        private void OnClick(object sender, RoutedEventArgs e)
        {
            var item = sender as MenuItem;
            if (item.Header.ToString() == "left")
                left.DockControl.Show();
            if (item.Header.ToString() == "left_1")
                left_1.DockControl.Show();
            if (item.Header.ToString() == "Ribbon (top)")
                top.DockControl.Show();
            if (item.Header.ToString() == "right")
                right.DockControl.Show();
            if (item.Header.ToString() == "right_1")
                right_1.DockControl.Show();
            if (item.Header.ToString() == "Log (bottom)")
                bottom.DockControl.Show();
            if (item.Header.ToString() == "Credits")
                bottom_1.DockControl.Show();
            if (item.Header.ToString() == "doc_0")
                doc_0.DockControl.Show();
            if (item.Header.ToString() == "New_doc" && (doc_new.DockControl is object))
                 if (doc_new.DockControl == null)
                    // TODO
                    System.Windows.MessageBox.Show
                    ("Unexpected error: New_doc n.a. " + Environment.NewLine );
                else if (doc_new.DockControl is object)
                                    doc_new.DockControl.Show();
        }
    }

    public class Doc : TextBlock, IDockSource
    {
        public Doc(string header)
        {
            _header = header;
        }

        private IDockControl _dockControl;
        public IDockControl DockControl
        {
            get
            {
                return _dockControl;
            }

            set
            {
                _dockControl = value;
            }
        }

        private string _header;
        public string Header
        {
            get
            {
                return _header;
            }
        }

        public ImageSource Icon
        {
            get
            {
                return null;
            }
        }
    }
}

视图概念和代码

MainWindow 显示了 usercontrols:RibbonViewCreditsViewDocViewLogView

其他 usercontrols 仍然是占位符。

服务/视图模型的注册在 App 的代码隐藏中进行。

namespace MyWorksForYDock
{
    /// <summary>
    /// Interaction logic for App.xaml
    /// </summary>
    public partial class App : Application
    {
        private bool blnReady;

        public App()
        {
            InitializeComponent();
            Exit += (_, __) => OnClosing();
            Startup += Application_Startup;

            try
            {
                Mod_Public.sAppPath = Directory.GetCurrentDirectory();
                Ioc.Default.ConfigureServices(
                    new ServiceCollection()
                    .AddSingleton<IMsgBoxService, MsgBoxService>()
                    .AddSingleton((IDialog)new DialogVM())
                    .AddSingleton((IOpenFileDlgVM)new OpenFileDlgVM())
                    .AddSingleton((ISaveAsFileDlgVM)new SaveAsFileDlgVM())
                    .AddSingleton((IRichTextDlgVM)new RichTextDlgVM())
                    .BuildServiceProvider());
            }
            catch (Exception ex)
            {
                File.AppendAllText(Mod_Public.sAppPath + @"\Log.txt", 
                     string.Format("{0}{1}", Environment.NewLine, 
                     DateAndTime.Now.ToString() + "; " + ex.ToString()));
                var msgBoxService = Ioc.Default.GetService<IMsgBoxService>();
                msgBoxService.Show("Unexpected error:" + Constants.vbNewLine + 
                     Constants.vbNewLine + ex.ToString(), img: MessageBoxImage.Error);
            }
        }

        private void OnClosing()
        {
        }

        private void Application_Startup(object sender, EventArgs e)
        {
            blnReady = true;
        }
    }
}

Mod_Public

Mod_Public 包含

   public static void ErrHandler(string sErr)

   public static string ReadTextLines(string FileName)

MVVM 模式 - 详细信息

从 WPF Ribbon 的角度来看,数据结构/模型很简单

Ribbon 包含菜单项/Ribbon 按钮,这些按钮与 ActiveRichTextBoxActiveTextBox 一起工作。

这就是我们在模型TextData 中看到的内容。

名为 TextData 的模型类
using CommunityToolkit.Mvvm.ComponentModel;
using MyWorksForYDock.Views;
using System.ComponentModel;
using System.Windows.Controls;
using System.Windows.Controls.Ribbon;

namespace MyWorksForYDock
{

    public class TextData : ObservableRecipient, INotifyPropertyChanged
    {
        private string _text;
        private string _richText;
        private RibbonTextBox _NotifyTest;
        private string _readText;
        private TextBox _ActiveTextBox;

        private RichTextBox _ActiveRichTextBox;
        private Ribbon _MyRibbonWPF;
        private RibbonView _MyMainWindow;

        public TextData()
        {
            // 
        }

        public RibbonView MyMainWindow
        {
            get { return _MyMainWindow; }
            set { _MyMainWindow = value; }
        }

        public Ribbon MyRibbonWPF
        {
            get { return _MyRibbonWPF; }
            set { _MyRibbonWPF = value; }
        }

        public RibbonTextBox NotifyTestbox
        {
            get { return _NotifyTest; }
            set { _NotifyTest = value; }
        }

        public string RichText
        {
            get { return _richText; }
            set 
            {
                _richText = value;
                OnPropertyChanged("RichText");
            }
        }

        public string GetText
        {
            get { return _text; }
            set 
            { _text = value;
                OnPropertyChanged("GetText");
            }
        }

        public string ReadText
        {
            get { return _readText; }
            set
            {
                _readText = value;
                GetText = _readText;
                OnPropertyChanged("ReadText");
            }
        }

        public TextBox ActiveTextBox
        {
            get { return _ActiveTextBox; }
            set
            {
                _ActiveTextBox = value;
                OnPropertyChanged("ActiveTextBox");
            }
        }

        public RichTextBox ActiveRichTextBox
        {
            get { return _ActiveRichTextBox; }
            set {_ActiveRichTextBox = value; }
        }
    }
}
名为 TestingViewModel 的视图模型类

名为 TestingViewModel 的类包含用于测试某些 MVVM 功能的属性、ICommands 和方法。它还包含用于 ObservableCollection(Of Credits)的代码,该代码用于 Listview(包含本文的参考/鸣谢)。

名为 DocViewModel 的视图模型类

此类用于 DocView

整合 - WPF 概念和代码

QAT (QuickAccessToolbar)

您可以从 QAT 中移除按钮(右键单击时会出现上下文菜单)。您可以将 QAT 显示在 Ribbon **下方**。您还可以从“设置”选项卡恢复 QAT。您还可以更改 Ribbonbackcolor

DependencyInjection 或 ServiceInjection

如前所述,App 的代码隐藏中有一些相关的代码。

带有 ISaveAsFileDlgVM 的保存文件对话框示例

它使用了 interface ISaveAsFileDlgVMservice / viewmodel SaveAsFileDlgVM

... 
public ICommand SaveAsFileDlgCommand { get; set; } 
... 
    RelayCommand cmdSAFD = new RelayCommand(SaveAsFileDialog); 
    SaveAsFileDlgCommand = cmdSAFD; 
... 
    private void SaveAsFileDialog() 
    {
        var dialog = Ioc.Default.GetService<ISaveAsFileDlgVM>(); 
        if (ActiveRichTextBox is object) 
        { 
            dialog.SaveAsFileDlg(_textData.RichText, ActiveRichTextBox); 
        } 
        if  (ActiveTextBox is object) 
        { 
            dialog.SaveAsFileDlg(_textData.GetText, ActiveTextBox); 
        } 
    } 
...

并且,非常重要的是,在 RibbonView 的 XAML 文件中使用了 Command="{Binding SaveAsFileDlgCommand}"/>

<RibbonButton x:Name="SaveAs" Content="RibbonButton" HorizontalAlignment="Left" 
         Height="Auto" 
         Margin="94,24,-162,-70" VerticalAlignment="Top" Width="80" 
         Label=" Save As"  KeyTip="S" 
         AutomationProperties.AccessKey="S" AutomationProperties.AcceleratorKey="S" 
             SmallImageSource="/Images/save16.png" 
             CanAddToQuickAccessToolBarDirectly="False" 
             ToolTipTitle="Save As"
         Command="{Binding SaveAsFileDlgCommand, Mode=OneWay, 
             UpdateSourceTrigger=PropertyChanged}"/>

Ribbon,您可以启动和测试其他对话框或 messagebox,方法是:

  • 打开对话框
  • 搜索(来源如鸣谢/参考部分所列)
  • OpenFileDialog
  • 选项卡“帮助” >“关于”
  • 字体对话框

Messenger

添加 Inherits ObservableRecipient 很重要,其他详细信息在 ObservableObject - Windows Community Toolkit | Microsoft Docs 中有描述。

"视图特定的消息应在视图的 Loaded 事件中注册,并在 Unloaded 事件中取消注册,以防止内存泄漏和多次回调注册的问题。"

我们使用 Class TestingViewModel 来处理 RibbonViewLogViewCreditsView

但是,即使我们对不同的视图使用相同的 VM,我们也有不同的 VM 实例在运行,并且必须从 Class TestingViewModel 发送 Msg 以进行数据交换。

using CommunityToolkit.Mvvm.Messaging;

namespace MyWorksForYDock
{ public class OpenFileDlgVM : ObservableRecipient, IOpenFileDlgVM
    {
        private string msg;
        private RichTextBox myRTB;
        private TextBox myTB;

        public OpenFileDlgVM()
        {
            // 
        }

        public string OpenFileDlg(object ActiveTBox)
        {
            FileDialog dialog = new OpenFileDialog();
            try
            {
                dialog.Filter = "All Files(*.*)|*.*|RTF Files (*.rtf)|*.rtf";
                dialog.FilterIndex = 1;
                dialog.Title = "RTE - Open File";
                dialog.DefaultExt = "rtf";
                // dialog.Filter = "Rich Text Files|*.rtf|" &
                // "Text Files|*.txt|HTML Files|" &
                // "*.htm|All Files|*.*"
                dialog.ShowDialog();
                if (string.IsNullOrEmpty(dialog.FileName))
                    return default;
                string strExt;
                strExt = System.IO.Path.GetExtension(dialog.FileName);
                strExt = strExt.ToUpper();

                string FileName = Path.GetFileName(dialog.FileName);
                SetStatus("TestingViewModel", FileName);

                if (ActiveTBox.GetType().ToString() == 
                   "System.Windows.Controls.RichTextBox") // IsNot Nothing Then
                {
                    myRTB = (RichTextBox)ActiveTBox;

                    switch (strExt ?? "")
                    {
                        case ".RTF":
                            {
                                var t = new TextRange
                                        ((TextPointer)myRTB.Document.ContentStart, 
                                        (TextPointer)myRTB.Document.ContentEnd);
                                var file = new FileStream
                                           (dialog.FileName, FileMode.Open);
                                t.Load(file, DataFormats.Rtf);
                                file.Close();
                                break;
                            }

                        default:
                            {
                                var t = new TextRange((TextPointer)
                                        myRTB.Document.ContentStart, 
                                        (TextPointer)myRTB.Document.ContentEnd);
                                var file = new FileStream
                                           (dialog.FileName, FileMode.Open);
                                t.Load(file, DataFormats.Text);
                                file.Close();
                                break;
                            }
                    }
                    string currentFile = dialog.FileName;
                    dialog.Title = "Editor: " + currentFile.ToString();
                }
                else if (ActiveTBox.GetType().ToString() == 
                                    "System.Windows.Controls.TextBox")
                {
                    myTB = (TextBox)ActiveTBox;
                    string currentFile = dialog.FileName;
                    myTB.Text = Mod_Public.ReadTextLines(dialog.FileName);
                }
            }
            catch (Exception ex)
            {
                File.AppendAllText(Mod_Public.sAppPath + @"\Log.txt", 
                    string.Format("{0}{1}", Environment.NewLine, 
                    DateAndTime.Now.ToString() + "; " + ex.ToString()));
                var MsgCmd = new RelayCommand<string>
                             (m => MessageBox.Show("Unexpected error:" + 
                              Constants.vbNewLine + 
                              Constants.vbNewLine + ex.ToString()));
                MsgCmd.Execute("");
            }

            return default;
        }

        public void SetStatus(string r, string m)
        {
            try
            {
                var s = Messenger.Send(new StatusMessage(m));
            }
            catch (Exception ex)
            {
                SetStatus("OpenFileDlgVM", ex.ToString());
                Mod_Public.ErrHandler(ex.ToString());
            }
        }

只有在消息已注册的情况下才能发送 Msg

using Microsoft.Toolkit.Mvvm.Messaging;
...
     Messenger.Register<DialogMessage>(this, (r, m) => DialogMessage = m.NewStatus);
     Messenger.Register<StatusMessage>(this, (r, m) => StatusBarMessage = m.NewStatus);
     Messenger.Register<PassActiveRTBoxMsg>(this, 
               (r, m) => PassActiveRTBoxMsg = m.ActiveRTBox);
     Messenger.Register<GetCreditsMsg>(this, (r, m) => GetCreditsMsg = m.AddCredit);
...
    ~TestingViewModel()
        {
        Messenger.Unregister<StatusMessage>(this);
        Messenger.Unregister<DialogMessage>(this);
        Messenger.Unregister<PassActiveRTBoxMsg>(this);
        Messenger.Unregister<GetCreditsMsg>(this);
        }

关闭 viewmodel 时,我们必须取消注册消息。

消息会显示在 StatusBarRibbon 上。

EventTrigger

要求: Microsoft.Xaml.Behaviors.Wpf (NuGet 包)

<UserControl
   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" 
   xmlns:local="clr-namespace:MyWorksForYDock.Views" 
   xmlns:b="http://schemas.microsoft.com/xaml/behaviors"
   xmlns:MyWorksForYDock="clr-namespace:MyWorksForYDock" 
   x:Class="MyWorksForYDock.Views.DocView"
   mc:Ignorable="d" 
   d:DesignHeight="450" d:DesignWidth="800">
   
<Grid>
    <RichTextBox x:Name="myRichTextBox" HorizontalScrollBarVisibility="Auto" 
                 VerticalScrollBarVisibility="Auto" 
                 Margin="2,2,2,0" IsDocumentEnabled="True" AcceptsTab="True">
        <RichTextBox.DataContext>
            <MyWorksForYDock:DocViewModel/>
        </RichTextBox.DataContext>
        <FlowDocument>
             <Paragraph>
                <Run Text="RichTextBox"/>
             </Paragraph>
        </FlowDocument>
        <b:Interaction.Triggers>
            <b:EventTrigger EventName= "MouseWheel">
                <b:InvokeCommandAction Command=
                                "{Binding ParameterisedCommand, Mode=OneWay}" 
                                  CommandParameter="{Binding ElementName=myRichTextBox, 
                                  Mode=OneWay}"/>
            </b:EventTrigger>
            <b:EventTrigger EventName= "MouseDoubleClick">
                <b:InvokeCommandAction Command=
                                "{Binding ParameterisedCommand, Mode=OneWay}" 
                                  CommandParameter="{Binding ElementName=myRichTextBox,
                                  Mode=OneWay}"/>
            </b:EventTrigger>
            <b:EventTrigger EventName= "TextChanged">
                <b:InvokeCommandAction Command=
                                "{Binding ParameterisedCommand, Mode=OneWay}" 
                                  CommandParameter="{Binding ElementName=myRichTextBox,
                                  Mode=OneWay}"/>
            </b:EventTrigger>
                <b:EventTrigger EventName= "MouseEnter">
                    <b:InvokeCommandAction Command=
                                "{Binding ParameterisedCommand, Mode=OneWay}" 
                                  CommandParameter="{Binding ElementName=myRichTextBox,
                                  Mode=OneWay}"/>
            </b:EventTrigger>
        </b:Interaction.Triggers>
    </RichTextBox>

</Grid>
</UserControl>

这用于获取 ActiveRichTextBox

ObservableCollection

它是 viewmodel TestingViewModel 的一部分,并用于显示包含鸣谢/参考的 listbox

public class TestingViewModel : ObservableRecipient, INotifyPropertyChanged

#Region " fields"
...
private ObservableCollection<Credits> _credit = new ObservableCollection<Credits>();
...
#End Region
...
    _credit = new ObservableCollection<Credits>()
    {
       new Credits()
           {
              Item = "MVVM Community Toolkit",
              Note = "Microsoft",
              Link = "https://docs.microsoft.com/en-us/windows/
                      communitytoolkit/mvvm/introduction"
           },
       new Credits()
           {
              Item = "MVVMLight",
              Note = "GalaSoft",
              Link = "https://codeproject.org.cn/Articles/768427/The-big-MVVM-Template"
           },
       new Credits()
           {
              Item = "YDock",
              Note = "GitHub",
              Link = "https://github.com/yzylovepmn/YDock"
           },
       new Credits()
           {
              Item = "ICommand with MVVM pattern",
              Note = "CPOL",
              Link = "https://codeproject.org.cn/Articles/863671/
                      Using-ICommand-with-MVVM-pattern"
           },
       new Credits()
           {
              Item = "C# WPF WYSIWYG HTML Editor - CodeProject",
              Note = "CPOL",
              Link = "https://codeproject.org.cn/Tips/870549/
                      Csharp-WPF-WYSIWYG-HTML-Editor"
           },
       new Credits()
           {
              Item = "SearchDialog",
              Note = "Forum Msg",
              Link = "https://social.msdn.microsoft.com/forums/vstudio/en-US/
                      fc46affc-9dc9-4a8f-b845-89a024b263bc/
                      how-to-find-and-replace-words-in-wpf-richtextbox"
           },
       new Credits()
           {
              Item = "Find/ReplaceDialog",
              Note = "Forum Msg",
              Link = "https://www.itcodar.com/csharp/
                      changing-font-for-richtextbox-without-losing-formatting.html"
           }
       };
...
public class Credits
    {
        public string Item { get; set; }
        public string Note { get; set; }
        public string Link { get; set; }
    }

ObservableCollection 的优点在于我们不需要为 Listbox 设置 UpdateTrigger

RichText

在“RichText”选项卡中,您可以选择 RichTextBox 中的一些文本,并使用 RibbonButtons 来格式化它。其中许多是 EditingCommands,仅出现在 .xaml 文件中。

 

更新至 1.4 版本

重新设计了所有 ViewModel

在许多情况下,使用 RelayCommand 而非 ICommandParamCommand

清理了代码并删除了大多数被注释掉的代码段。

 

结论

这只是一个演示应用程序 – 它尚未达到生产就绪状态。

但我认为 YDock 框架和 MVVM 工具包可以实现多种扩展。

最后说明:我对任何形式的反馈都非常感兴趣 - 问题、建议等。

致谢/参考

历史

  • 2023 年 3 月 1 日 - 版本 1.0 
  • 2023 年 3 月 3 日 - 版本 1.1
  • 2023 年 3 月 5 日 - 版本 1.2 修复了 OpenFileDialog 错误
  • 2024 年 5 月 5 日 - 版本 1.3 重新设计了名为 TextData 的类
  • 2024 年 5 月 9 日 - 版本 1.4 重新设计了所有 ViewModel 类
  • 2024 年 5 月 23 日 - 文章新标题

关注点 / 许可证

CommunityToolkit.MvvmYDockYDockTest 项目均采用 MIT 许可证

© . All rights reserved.