C# MVVM 工具包演示
本文和演示是关于如何开始使用 MVVM Community Toolkit 以及一些自定义的 Message Box 和对话框接口/服务。
新内容: 下载 NET8CsMvvmToolkit.zip
引言
本文和演示是关于如何开始使用 MVVM 工具包以及一些自定义的 MessageBox
和对话框接口/服务。
背景
CodeProject 上有很多关于其他 MVVM 框架的文章,但几乎没有关于 WPF 和 MVVM Toolkit 的。所以我开始撰写本文档。
模型、视图和视图模型(MVVM 模式)是组织或构建代码的好方法,有助于简化、开发和测试(例如单元测试)您的应用程序。
模型保存数据,与应用程序逻辑无关。
视图模型充当模型和视图之间的连接。
视图是用户界面。
我不会详细描述和解释整个演示项目。重点是如何测试其中的一些功能。
Using the Code
MVVM 结构/功能
MVVM Toolkit 来自微软,并且一些其他使用的功能也不是我自己的:来源如“致谢/参考”部分所示。
内容快速概览
- MVVM Toolkit 和 .NET 4.7.2
RelayCommand
OnPropertyChanged
ObservableRecipient
(Messenger
和ViewModelBase
)DependencyInjection
(将MsgBox
和Dialog
作为服务运行)ObservableCollection
(用于“致谢”Listbox
)
- Ribbon 菜单
- 服务/对话框
- 使用
RelayCommand
和ICommand
将按钮绑定到ViewModel
安装 MVVM Toolkit
安装 MVVM Toolkit 的 NuGet 包时,它还会安装 6 或 7 个其他包。
对于 DependencyInjection
,我们需要安装另一个 NuGet 包
我为 MsgBox
和一些对话框创建了接口/服务。
DependencyInjection
能够独立于 viewmodel
启动以下对话框
FontDlgVM
MsgBoxService
DialogVM
OpenFileDlgVM
RibbonStatusService
SaveAsFileDlgVM
SearchDlgVM
这允许使用自定义 Messagebox
/对话框以及进行单元测试。
主窗口概念和代码
MainWindow
显示 Ribbon
。
其下方是 Tabcontrol
,其中包含“MVVM Toolkit Testing”和“RichText
”选项卡。
MainWindow
类后面的代码是
{
/// <summary>
/// Interaktionslogik für MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new TestingViewModel();
}
}
}
Application
服务/视图模型的注册在 App 的代码后面完成。
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 System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Controls;
using System.Windows.Controls.Ribbon;
namespace CsMvvmToolkit_CP
{
public class TextData : ObservableRecipient, INotifyPropertyChanged
{
private string _text;
private string _richText;
private RibbonTextBox _NotifyTest;
private string _readText;
private TextBox _ActiveTextBox;
private RichTextBox __ActiveRichTextBox;
private RichTextBox _ActiveRichTextBox
{
[MethodImpl(MethodImplOptions.Synchronized)]
get { return __ActiveRichTextBox; }
[MethodImpl(MethodImplOptions.Synchronized)]
set { __ActiveRichTextBox = value; }
}
private Ribbon _MyRibbonWPF;
private MainWindow _MyMainWindow;
public TextData()
{
//
}
public MainWindow MyMainWindow
{
get { return _MyMainWindow; }
set { _MyMainWindow = (MainWindow)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
的ViewModel 类
名为 TestingViewModel
的类包含用于测试某些 MVVM 功能的属性、RelayCommands
、ICommands
和方法。它还包含用于 ObservableCollection
(Credits)的代码,该集合用于显示本篇文章的“参考/致谢”的 Listview
。
整合 - WPF 概念和代码
QAT (QuickAccessToolbar)
您可以从 QAT(右键单击时会出现一个上下文菜单)中移除按钮。您可以将 QAT 显示在 Ribbon
的下方。您也可以从“设置”选项卡恢复 QAT。您还可以更改 Ribbon
的背景颜色
。
DependencyInjection 或 ServiceInjection
如前所述,App 后面的代码中有一些用于此的代码。
带有 ISaveAsFileDlgVM 的保存文件对话框示例
它使用了 interface ISaveAsFileDlgVM
和 service
/viewmodel SaveAsFileDlgVM
。
public class TestingViewModel
: ObservableRecipient
, INotifyPropertyChanged
...
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);
}
}
...
并且,非常重要,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"
="Save As" Command="{Binding SaveAsFileDlgCommand}"/>
从 Ribbon
,您可以通过以下方式启动和测试其他对话框或 messagebox
- 打开对话框
- 搜索(来源如“致谢/参考”部分所示)
OpenFileDialog
- 选项卡“帮助”>“关于”
字体对话框
Messenger 测试
重要的是要添加 Inherits ObservableRecipient
,这和其他细节已在 ObservableObject - Windows Community Toolkit | Microsoft Docs 中进行了描述。
"视图特定的消息应在视图的 Loaded 事件中注册,并在 Unloaded 事件中注销,以防止内存泄漏和多个回调注册问题。"
我们可以从 Class
TestingViewModel
发送 Msg
Imports Microsoft.Toolkit.Mvvm.Messaging
public class TestingViewModel : ObservableRecipient, INotifyPropertyChanged
private string msg;
…
_cmdMsg = new Command(SendMsgRibbonButton_Click);
…
public ICommand SendMsg
{
get { return _cmdMsg; }
}
…
private void SendMsgRibbonButton_Click()
{
try
{
// DataExchange / Messenger
string msg = "Test Msg...";
SetStatus("TestingViewModel", msg);
}
catch (Exception ex)
{
SetStatus("TestingViewModel", ex.ToString());
Mod_Public.ErrHandler(ex.ToString());
}
}
...
public void SetStatus(string r, string m)
{
try
{
Messenger.Send(new DialogMessage(m));
}
catch (Exception ex)
{
SetStatus("TestingViewModel", ex.ToString());
Mod_Public.ErrHandler(ex.ToString());
}
}
...
public class StatusMessage
{
public StatusMessage(string status)
{
NewStatus = status;
}
public string NewStatus { get; set; }
}
仅当消息已注册时,才能发送 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.Unregister<StatusMessage>(this);
Messenger.Unregister<DialogMessage>(this);
关闭 viewmodel
时,我们必须取消注册消息。
消息会显示在 StatusBar
和 Ribbon
上。
PropertyChanged 测试
<RibbonTextBox x:Name="ribbonTextBox"
Text="{Binding OnPropertyChangedTest, UpdateSourceTrigger=PropertyChanged}"
HorizontalAlignment="Right" Margin="0,0,-90,-30"
TextWrapping="Wrap" VerticalAlignment="Bottom"
Width="120" UndoLimit="10" FontSize="12"/>
<RibbonTextBox x:Name="NotifyTextBox" Text="{Binding OnPropertyChangedTest,
UpdateSourceTrigger=PropertyChanged}" HorizontalAlignment="Right" Margin="0,0,-90,-53"
TextWrapping="Wrap" VerticalAlignment="Bottom" Width="120"
UndoLimit="10" FontSize="12"/>
通常,两个 textbox
只在 activeTextbox
与“RichText
”或“PlainText
”相关时显示。但如果您手动编辑上面的 textbox
,您会立即看到下面的 textbox
内容发生了变化。
这是由 XAML 文件中的 UpdateSourceTrigger=PropertyChanged
引起的。
EventTrigger
要求: Microsoft.Xaml.Behaviors.Wpf
(NuGet 包)
xmlns:b="http://schemas.microsoft.com/xaml/behaviors"
...
<b:Interaction.Triggers>
<b:EventTrigger EventName= "MouseWheel">
<b:InvokeCommandAction Command="{Binding ParamCommandTBx}"
CommandParameter="{Binding ElementName=myTextBox, Mode=OneWay}"/>
</b:EventTrigger>
<b:EventTrigger EventName= "MouseDoubleClick">
<b:InvokeCommandAction Command="{Binding ParamCommandTBx}"
CommandParameter="{Binding ElementName=myTextBox, Mode=OneWay}"/>
</b:EventTrigger>
<b:EventTrigger EventName= "TextChanged">
<b:InvokeCommandAction Command="{Binding ParamCommandTBx}"
CommandParameter="{Binding ElementName=myTextBox, Mode=OneWay}"/>
</b:EventTrigger>
<b:EventTrigger EventName= "MouseEnter">
<b:InvokeCommandAction Command="{Binding ParamCommandTBx}"
CommandParameter="{Binding ElementName=myTextBox, Mode=OneWay}"/>
</b:EventTrigger>
</b:Interaction.Triggers>
...
当 Ribbon
通过 ContextMenu
最小化以及用于其他事物时,会使用此功能。
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 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 = "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"
}
};
...
public class Credits
{
public string Item { get; set; }
public string Note { get; set; }
public string Link { get; set; }
}
使用 ObservableCollection 进行测试
点击“清空列表框”以删除致谢信息。
将 XML 读取到 Listbox
会恢复参考文献。
ObservableCollection
的优点是,我们不需要为 Listbox
设置 UpdateTrigger
。
RichText
在“RichText
”选项卡中,您可以选择 RichTextBox
中的文本,并使用 RibbonButton
进行格式化。其中许多是 EditingCommand
s,仅出现在 UserCtlRibbonWPF.xaml 文件中。
RelayCommands 替换了 2.2 版的一些 ICommands
#region RelayCommands
NewFile = new RelayCommand(New_Click);
ExitApp = new RelayCommand(Exit_Click);
Print = new RelayCommand(Print_Click);
Info = new RelayCommand(Info_Click);
GreenBackground =
new RelayCommand(BackgroundGreenRibbonButton_Click);
WhiteBackground =
new RelayCommand(BackgroundWhiteRibbonButton_Click);
RestoreQAT = new RelayCommand(RestoreQAT_Click);
Apploaded = new RelayCommand(App_Loaded);
ClearListbox = new RelayCommand(ClearListboxButton_Click);
SaveXml = new RelayCommand(SaveXml_Click);
ReadXml = new RelayCommand(ReadXml_Click);
ReadLog = new RelayCommand(ReadLog_Click);
SendMsg = new RelayCommand(SendMsgRibbonButton_Click);
GetError = new RelayCommand(GetErrorButton_Click);
#endregion
CommunityToolkit 的 RelayCommand
比我以前使用 ICommand
的版本代码更简洁。下面是一个例子
...
public IRelayCommand NewFile { get; }
...
NewFile = new RelayCommand(New_Click);
...
private void New_Click()
{
try
{
if (ActiveRichTextBox is object)
{
ActiveRichTextBox.Document.Blocks.Clear();
}
if (ActiveTextBox is object)
{
GetText = Constants.vbNullString;
}
}
...
==========================================
升级到 NET8,使用项目 NET8CsMvvmToolkit 版本 1.1
使用 Source Generator 的 RelayCommands 的可能性
以下描述基于MS Learn 链接
MVVM source generators - Community Toolkits for .NET | Microsoft Learn:
从 8.0 版开始,MVVM Toolkit 包含全新的 Roslyn 源生成器,这些生成器将大大减少编写 MVVM 架构代码时的样板代码。它们可以简化设置可观察属性、命令等场景。如果您不熟悉源生成器,可以在此处了解更多信息。这在 .Net Framework 4.8 中是不可能的,这就是为什么我创建了这个项目的 NET8 版本。
这是一个旧版本和一个新版本的 Relay Command 的源版本
OLD:
public IRelayCommand<RichTextBox> RTBoxCommand { get; }
RTBoxCommand = new RelayCommand<RichTextBox>(DoParameterisedCommand);
private void DoParameterisedCommand(object parameter)
{
_textData.ActiveRichTextBox = (RichTextBox)parameter;
_textData.ActiveTextBox = null;
OnPropertyChangedTest = "RichText";
}
NEW:
[RelayCommand]
private void ParameterRichTBox(object parameter)
{
_textData.ActiveRichTextBox = (RichTextBox)parameter;
_textData.ActiveTextBox = null;
OnPropertyChangedTest = "RichText";
}
您可以看到,命令的两行可以被禁用并替换为 [RelayCommand].
为了更好地阅读,我已经将方法名从 DoParameterisedCommand
重命名为 ParameterRichTBox
。
命令由源生成器创建并保存在某个地方
.../users/{UserName}/AppData/Local/VSGeneratedDocuments/...
或者在项目子文件夹中
\obj\net8.0\generated\.....
在我的项目中,我们有三种类型的命令需要更新。
普通命令、传递参数的命令和启动对话框的命令。
更多信息来自 RelayCommand attribute - Community Toolkits for .NET | Microsoft Learn
为了工作,带注释的方法必须位于一个partial 类中。如果类型是嵌套的,则声明语法树中的所有类型也必须注释为 partial。否则将导致编译错误,因为生成器将无法生成该类型的另一个 partial 声明以及所需的命令。
生成的命令名称将基于方法名称创建。生成器将使用方法名称并在末尾附加“Command”,并删除“On”前缀(如果存在)。此外,对于异步方法,在附加“Command”之前也会删除“Async”后缀。
结论
这只是一个演示 - 它还没有准备好用于生产。
但我认为 MVVM Community Toolkit 将为您提供多种扩展。
最后说明:我对任何形式的反馈都非常感兴趣 - 问题、建议和其他。
致谢/参考
- [1] 使用 ICommand 和 MVVM 模式 - CodeProject
- [2] MVVM Toolkit 简介 - Windows Community Toolkit | Microsoft Docs
- [3] C# WPF WYSIWYG HTML 编辑器 - CodeProject
- [4] 大型 MVVM 模板 - CodeProject
- [5] https://social.msdn.microsoft.com/forums/vstudio/en-US/fc46affc-9dc9-4a8f-b845-89a024b263bc/how-to-find-and-replace-words-in-wpf-richtextbox
历史
- 2024 年 5 月 16 日 - 添加新章节:升级到 NET8,使用项目 NET8CsMvvmToolkit
- 2024 年 5 月 7 日 - 版本 2.2 - Relay Commands 替换了一些 ICommands
- 2024 年 5 月 6 日 - 版本 2.1 - 重构了
TextData
和TestingViewModel
类 - 2023 年 2 月 23 日 - 版本 1.1 - 由于
Microsoft.ToolKit.Mvvm
已弃用,我们现在必须使用此替代包:CommunityToolkit.Mvvm
- 2022 年 6 月 8 日 - 添加了模型、视图和视图模型(MVVM 模式)解释
- 2022 年 5 月 19 日 - 初始提交