目录
DependencyInjection或ServiceInjection
介绍
本文和演示是关于如何开始使用YDock和CommunityToolkit.Mvvm以及为MessageBox和一些对话框创建的一些自定义接口/服务。
背景
有很多关于其他对接框架的 CodeProject 文章,但没有一篇关于YDock。所以我开始将YDock和CommunityToolkit.Mvvm结合在一起。
Model、View 和 ViewModel(MVVM模式)是组织或构建代码的好方法,可帮助您简化、开发和测试(例如,单元测试)应用程序。
模型保存数据,与应用程序逻辑无关。
ViewModel 充当Model和View之间的连接。
视图是用户界面。
我不会描述和解释完整演示项目的每一个细节。重点是如何使用对接框架、MVVM工具包和一些功能。
使用代码
MVVM结构/特性
MVVM工具包来自Microsoft,其他一些使用的功能也不是我自己的:来源如“信用/参考资料”部分所列。
内容快速概览
- YDock
- CommunityToolkit.Mvvm和.NET 4.8
- RelayCommand
- OnPropertyChanged
- ObservableRecipient (Messenger和ViewModelBase)
- DependencyInjection(作为服务运行MsgBox和Dialog)
- ObservableCollection(对于信用Listbox)
- Ribbon菜单
- 服务/对话框
- 使用ICommand将按钮绑定到ViewModel
安装MVVM工具包
安装MVVM工具包的NuGet包时,它会安装6或7个其他包。
对于DependencyInjection,我们需要安装另一个NuGet包:
Microsoft.Extensions.DependencyInjection
我为MsgBox和一些对话框制作了接口/服务。
DependencyInjection设法独立于viewmodel启动以下对话框:
- FontDlgVM
- MsgBoxService
- DialogVM
- OpenFileDlgVM
- RibbonStatusService
- SaveAsFileDlgVM
- SearchDlgVM
这允许使用自定义Messageboxes/对话框以及单元测试。
对接框架的使用
发现优秀YDock,YDockTest项目@GitHub。
对于可重复使用的样本,我们必须用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显示usercontrol:RibbonView、CreditsView、DocView和LogView。
其他的usercontrol仍然是仿制品。
注册服务/视图模型在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具有菜单项/功能区按钮,它们与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的ViewModel类
调用TestingViewModel的类包含用于测试某些MVVM功能的属性,ICommands和方法。它还包含ObservableCollection(Of Credits)的代码,它用于Listview和本文的参考文献/信用。
名为DocViewModel的ViewModel类
此类用于DocView。
将内容放在一起——WPF概念和代码
QAT(QuickAccess工具栏)
您可以从QAT中删除按钮(右键单击时,会出现一个上下文菜单)。您可以在Ribbon上显示QAT。您也可以从“设置”选项卡恢复QAT。您可以更改Ribbon的backcolor。
DependencyInjection或ServiceInjection
如前所述,在应用程序后面的代码中有一些代码。
使用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:
- 打开对话框
- 搜索(来源,如Credits/References中所列)
- OpenFileDialog
- 选项卡帮助>信息
- FontDialog
信使
添加Inherits ObservableRecipient是很重要的,这个和其他细节在ObservableObject - Windows Community Toolkit |Microsoft 文档中有描述 。
“查看特定消息应在已加载事件中注册一个视图并取消注册在已卸载事件中注册,以防止内存泄漏和多次回调注册问题。”
我们为CreditsView使用ClassTestingViewModel,RibbonView和LogView。
但是,即使我们将相同的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上。
事件触发器
要求: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的一部分,用于带有Credits / References的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://www.codeproject.com/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://www.codeproject.com/Articles/863671/
Using-ICommand-with-MVVM-pattern"
},
new Credits()
{
Item = "C# WPF WYSIWYG HTML Editor - CodeProject",
Note = "CPOL",
Link = "https://www.codeproject.com/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来格式化它。其中许多是EditingCommand,并且仅显示在.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文档
- [4] C# WPF所见即所得HTML编辑器——CodeProject
- [5] 大型MVVM模板——CodeProject
- [6] How to find and replace words in WPF richTextBox | Microsoft Learn
- [7] https://www.itcodar.com/csharp/changing-font-for-richtextbox-without-losing-formatting.html
https://www.codeproject.com/Articles/5355820/WPF-MVVM-RichText-Demo-using-YDock-Panel