WPF MVVM 富文本演示(使用 YDock[Panel])
使用停靠框架、RichText、MVVM 工具包以及更多功能
引言
本文及演示的重点是入门使用 YDock
结合 CommunityToolkit.Mvvm
以及一些自定义的 MessageBox
和对话框的接口/服务。
背景
CodeProject 上有很多关于其他停靠框架的文章,但没有关于 YDock
的。所以我开始尝试结合 YDock
和 CommunityToolkit.Mvvm
。
模型、视图和视图模型(MVVM 模式)是组织或构建代码的良好方式,有助于简化、开发和测试(例如,单元测试)您的应用程序。
模型包含数据,与应用程序逻辑无关。
视图模型充当模型和视图之间的连接。
视图是用户界面。
我不会详细描述和解释整个演示项目的每一个细节。重点在于如何使用停靠框架、MVVM 工具包以及其中一些功能。
Using the Code
MVVM 结构 / 功能
MVVM 工具包来自微软,其他一些使用的功能也不是我原创的:来源如 鸣谢 / 参考部分 所列。
内容快速概览
YDock
CommunityToolkit.Mvvm
和 .NET 4.8RelayCommand
OnPropertyChanged
ObservableRecipient
(Messenger
和ViewModelBase
)DependencyInjection
(将MsgBox
和Dialog
作为服务运行)ObservableCollection
(用于鸣谢的Listbox
)
- Ribbon 菜单
- 服务 / 对话框
- 使用
ICommand
将按钮绑定到ViewModel
安装 MVVM Toolkit
安装 MVVM Toolkit 的 NuGet 包时,它还会安装 6 或 7 个其他包。
对于 DependencyInjection
,我们需要安装另一个 NuGet 包
Microsoft.Extensions.DependencyInjection
我还为 MsgBox
和一些对话框创建了接口/服务。
DependencyInjection
能够**独立于** viewmodel
s 启动以下对话框。
FontDlgVM
MsgBoxService
DialogVM
OpenFileDlgVM
RibbonStatusService
SaveAsFileDlgVM
SearchDlgVM
这允许使用自定义 Messagebox
/对话框以及进行单元测试。
停靠框架的使用
在 GitHub 上找到了优秀的 YDock
和 YDockTest
项目。
为了实现可重用的示例,我们需要用 usercontrol
s 替换一些占位符 document
s 和 toolwindow
s。
我的示例项目名为 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
显示了 usercontrol
s:RibbonView
、CreditsView
、DocView
和 LogView
。
其他 usercontrol
s 仍然是占位符。
服务/视图模型的注册在 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 按钮,这些按钮与 ActiveRichTextBox
或 ActiveTextBox
一起工作。
这就是我们在模型类 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。您还可以更改 Ribbon
的 backcolor
。
DependencyInjection 或 ServiceInjection
如前所述,App 的代码隐藏中有一些相关的代码。
带有 ISaveAsFileDlgVM 的保存文件对话框示例
它使用了 interface ISaveAsFileDlgVM
和 service
/ 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
来处理 RibbonView
、LogView
和 CreditsView
。
但是,即使我们对不同的视图使用相同的 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
时,我们必须取消注册消息。
消息会显示在 StatusBar
和 Ribbon
上。
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
中的一些文本,并使用 RibbonButton
s 来格式化它。其中许多是 EditingCommand
s,仅出现在 .xaml 文件中。
更新至 1.4 版本
重新设计了所有 ViewModel 类
在许多情况下,使用 RelayCommand
而非 ICommand
或 ParamCommand
。
清理了代码并删除了大多数被注释掉的代码段。
结论
这只是一个演示应用程序 – 它尚未达到生产就绪状态。
但我认为 YDock
框架和 MVVM 工具包可以实现多种扩展。
最后说明:我对任何形式的反馈都非常感兴趣 - 问题、建议等。
致谢/参考
- [1] GitHub - yzylovepmn/YDock: 一个完全基于 WPF 的停靠框架,风格完全与 Visual Studio 一致
- [2] 使用 ICommand 和 MVVM 模式 - CodeProject
- [3] MVVM 工具包简介 - Windows Community Toolkit | Microsoft Docs
- [4] C# WPF WYSIWYG HTML 编辑器 - CodeProject
- [5] 大型 MVVM 模板 - CodeProject
- [6] https://social.msdn.microsoft.com/forums/vstudio/en-US/fc46affc-9dc9-4a8f-b845-89a024b263bc/how-to-find-and-replace-words-in-wpf-richtextbox
- [7] https://www.itcodar.com/csharp/changing-font-for-richtextbox-without-losing-formatting.html
历史
- 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.Mvvm
、YDock
和 YDockTest
项目均采用 MIT 许可证。