WinForm VB.NET 托管 WPF 功能区 - 第 3 部分 - 现已集成 MVVM 工具包及更多内容
我的想法是在 WinForm VB.NET 项目中托管一个包含功能区的 WPF 用户控件,并尝试使用 MVVM 模式。
引言
这篇文章/技巧和演示的灵感来自于 CodeProject 的文章 Using ICommand with MVVM pattern,该文章根据 MVVM 模式创建和使用 ICommands
。
我使用那里的代码作为我自己的类/接口的模板。
第 3 部分 讨论了 MVVM 工具包 的一部分用法,以及用于 MsgBox
和一些对话框的自制接口/服务。
背景
我的第一篇文章 WinForm VB.NET 托管 WPF 功能区 侧重于让演示运行的基本工作。
但我对 WinForm 和 WPF 用户控件之间的接口/通信并不满意。这一点在 WinForm VB.NET 托管 WPF 功能区 - 第 2 部分 - 使用 ICommand 和 MVVM 模式 中得到了改变。
在这里,介绍第 3 部分,我们将只讨论前两篇文章/技巧中缺失的、以及/或者新增的或已更改的内容。
新的 MVVM 结构/特性
第 2 部分文章/技巧中使用的 ICommand
对我来说是重要的一步,但并非完美解决方案。
它适用于 menuitem
和按钮的单击事件;但对于任何其他 EventTrigger
,如 .MouseDoubleClick
或 .ContextMenuClosing
,则帮助不大。
因此,我决定使用一个当前框架。
安装 MVVM Toolkit
安装 MVVM Toolkit 的 NuGet 包时,它还会安装 6 或 7 个其他包。
MVVM Toolkit 简介 - Windows Community Toolkit | Microsoft Docs
MVVM 工具包部件的使用
RelayCommand
OnPropertyChanged
ObservableRecipient
(Messenger
和ViewModelBase
)DependencyInjection
(将MsgBox
和Dialogs
作为服务运行)
对于 DependencyInjection
,我们需要安装另一个 NuGet 包
我为 MsgBox
和一些对话框制作了接口/服务。
DependencyInjection
能够独立于 viewmodels 启动以下对话框:
FontDlgVM
PasteImageDlgVM
MsgBoxService
DialogVM
OpenFileDlgVM
RibbonStatusService
SaveAsFileDlgVM
SearchDlgVM
这允许使用自定义 Messagebox
/对话框以及进行单元测试。
Winform 概念和代码
AppForm
托管带有 Ribbon
的 WPF Usercontrol
。
有了这个配置,我们就没有 Application.xaml,因此用于注册服务/viewmodels 的代码在 Winform 的代码隐藏中。
还有用于在 Winform 加载时恢复 QAT 状态和关闭应用程序时保存 QAT 状态的代码。
Imports System.ComponentModel
Imports System.Windows.Input
Imports Microsoft.Extensions.DependencyInjection
Imports Microsoft.Toolkit.Mvvm.DependencyInjection
Imports Microsoft.Toolkit.Mvvm.Input
Public Class AppForm
Private WithEvents MyRibbonUserControl As New UserControlRibbonWPF
Private _RibbonSizeIsMinimized As Boolean
Private blnIsMinimized As Boolean
Public ReadOnly Property CloseCommand() As ICommand
Get
Return New RelayCommand(AddressOf Me.CloseMe)
End Get
End Property
Private Sub CloseMe()
Application.Exit()
End Sub
Private Sub ContextMenuIsClosing()
Try
blnIsMinimized = cTextDataVM.MyRibbonWPF.IsMinimized
UserControlWPFisMinimized = blnIsMinimized
Catch ex As Exception
IO.File.AppendAllText(sAppPath & "\Log.txt", String.Format("{0}{1}", _
Environment.NewLine, Now.ToString & "; " & ex.ToString()))
End Try
End Sub
Private Sub AppForm_Load(sender As Object, e As EventArgs) Handles Me.Load
Try
Ioc.[Default].ConfigureServices(New ServiceCollection().AddSingleton _
(Of IMsgBoxService, MsgBoxService)().AddSingleton _
(Of IDialog)(New DialogVM()).AddSingleton _
(Of IOpenFileDlgVM)(New OpenFileDlgVM()).AddSingleton _
(Of IPasteImageDlgVM)(New PasteImageDlgVM()).AddSingleton _
(Of IRibbonStatusService)(New RibbonStatusService()).AddSingleton _
(Of ISaveAsFileDlgVM)(New SaveAsFileDlgVM()).AddSingleton _
(Of ISearchDlgVM)(New SearchDlgVM()).AddSingleton _
(Of IFontDlgVM)(New FontDlgVM()).BuildServiceProvider)
ElementHost1.Dock = DockStyle.Fill
MyRibbonUserControl = ElementHost1.Child
If My.Settings.AppCmdNewQAT_Visible = False Then
MyRibbonUserControl.RibbonWPF.QuickAccessToolBar.Items.Remove_
(MyRibbonUserControl.AppCmdNewQAT)
End If
If My.Settings.AppCmdOpenQAT_Visible = False Then
MyRibbonUserControl.RibbonWPF.QuickAccessToolBar.Items.Remove_
(MyRibbonUserControl.AppCmdOpenQAT)
End If
If My.Settings.AppCmdSaveAsQAT_Visible = False Then
MyRibbonUserControl.RibbonWPF.QuickAccessToolBar.Items.Remove_
(MyRibbonUserControl.AppCmdSaveAsQAT)
End If
If My.Settings.AppCmdCloseQAT_Visible = False Then
MyRibbonUserControl.RibbonWPF.QuickAccessToolBar.Items.Remove_
(MyRibbonUserControl.AppCmdCloseQAT)
End If
Catch ex As Exception
IO.File.AppendAllText(sAppPath & "\Log.txt", String.Format("{0}{1}", _
Environment.NewLine, Now.ToString & "; " & ex.ToString()))
End Try
End Sub
Private Sub AppForm_Shown(sender As Object, e As EventArgs) Handles Me.Shown
Try
If System.IO.File.Exists(sAppPath & "\Log.txt") _
Then LogRichTextBox.Text = ReadTextLines("Log.txt")
' Configuration | Settings for WPF Ribbon
With MyRibbonUserControl
blnIsMinimized = cTextDataVM.RibbonSizeIsMinimized
.TabHelp.IsEnabled = True
.RibbonWPF.Visibility = Windows.Visibility.Visible
End With
Catch ex As Exception
IO.File.AppendAllText(sAppPath & "\Log.txt", String.Format("{0}{1}", _
Environment.NewLine, Now.ToString & "; " & ex.ToString()))
End Try
End Sub
Private Sub TextBox_Enter(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles PlainTextBox.MouseClick,
PlainTextBox.Enter, PlainTextBox.MouseEnter, PlainTextBox.TextChanged
cTextDataVM.ActiveTextBox = CType(sender, TextBox)
cTextDataVM.ActiveRichTextBox = Nothing
MyRibbonUserControl.StackpanelRT1.Visibility = Windows.Visibility.Hidden
MyRibbonUserControl.StackpanelRT2.Visibility = Windows.Visibility.Hidden
MyRibbonUserControl.ribbonComboBoxColor.Visibility = Windows.Visibility.Hidden
MyRibbonUserControl.FontsDlgRibbonButton.Visibility = Windows.Visibility.Hidden
MyRibbonUserControl.GraphicAlignRibbonButton.Visibility = Windows.Visibility.Hidden
End Sub
Private Sub RichTextBox_Enter(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles WinformRichTextBox.MouseClick,
WinformRichTextBox.Enter, WinformRichTextBox.MouseEnter, _
WinformRichTextBox.TextChanged,
LogRichTextBox.MouseClick, LogRichTextBox.Enter, LogRichTextBox.MouseEnter, _
LogRichTextBox.TextChanged
cTextDataVM.ActiveRichTextBox = CType(sender, RichTextBox)
cTextDataVM.ActiveTextBox = Nothing
MyRibbonUserControl.StackpanelRT1.Visibility = Windows.Visibility.Visible
MyRibbonUserControl.StackpanelRT2.Visibility = Windows.Visibility.Visible
MyRibbonUserControl.ribbonComboBoxColor.Visibility = Windows.Visibility.Visible
MyRibbonUserControl.FontsDlgRibbonButton.Visibility = Windows.Visibility.Visible
MyRibbonUserControl.GraphicAlignRibbonButton.Visibility = Windows.Visibility.Visible
End Sub
Public Property UserControlWPFisMinimized As Boolean
Get
Return blnIsMinimized
End Get
Set
blnIsMinimized = Value
RibbonWPF_SetSize()
End Set
End Property
Public Sub RibbonWPF_SetSize()
Try
If blnIsMinimized = False Then
SplitContainer1.SplitterDistance = 160
Else
If blnIsMinimized = True Then SplitContainer1.SplitterDistance = 80
End If
Catch ex As Exception
IO.File.AppendAllText(sAppPath & "\Log.txt", _
String.Format("{0}{1}", Environment.NewLine, Now.ToString & "; " & ex.ToString()))
End Try
End Sub
Private Sub AppForm_Closing(sender As Object, e As CancelEventArgs) Handles Me.Closing
Try
If MyRibbonUserControl.AppCmdNewQAT.IsLoaded = False Then
My.Settings.AppCmdNewQAT_Visible = False
ElseIf MyRibbonUserControl.AppCmdNewQAT.IsLoaded = True Then
My.Settings.AppCmdNewQAT_Visible = True
End If
If MyRibbonUserControl.AppCmdOpenQAT.IsLoaded = False Then
My.Settings.AppCmdOpenQAT_Visible = False
ElseIf MyRibbonUserControl.AppCmdOpenQAT.IsLoaded = True Then
My.Settings.AppCmdOpenQAT_Visible = True
End If
If MyRibbonUserControl.AppCmdSaveAsQAT.IsLoaded = False Then
My.Settings.AppCmdSaveAsQAT_Visible = False
ElseIf MyRibbonUserControl.AppCmdSaveAsQAT.IsLoaded = True Then
My.Settings.AppCmdSaveAsQAT_Visible = True
End If
If MyRibbonUserControl.AppCmdCloseQAT.IsLoaded = False Then
My.Settings.AppCmdCloseQAT_Visible = False
ElseIf MyRibbonUserControl.AppCmdCloseQAT.IsLoaded = True Then
My.Settings.AppCmdCloseQAT_Visible = True
End If
Catch ex As Exception
IO.File.AppendAllText(sAppPath & "\Log.txt", _
String.Format("{0}{1}", Environment.NewLine, Now.ToString & "; " & ex.ToString()))
End Try
End Sub
Private Sub AppForm_KeyDown(ByVal sender As Object, _
ByVal e As System.Windows.Forms.KeyEventArgs) Handles MyBase.KeyDown
If e.Alt AndAlso (e.KeyCode = Keys.N) Then
If cTextDataVM.ActiveRichTextBox IsNot Nothing _
Then cTextDataVM.ActiveRichTextBox.Clear()
If cTextDataVM.ActiveTextBox IsNot Nothing Then cTextDataVM.ActiveTextBox.Clear()
End If
If e.Alt AndAlso (e.KeyCode = Keys.O) Then
cTextDataVM.OpenFileDlgCommand.Execute("OpenFileDlg")
End If
If e.Alt AndAlso (e.KeyCode = Keys.S) Then
cTextDataVM.SaveAsFileDlgCommand.Execute("SaveAsFileDlg")
End If
End Sub
End Class
用 cTextDataVM
替换了 _TextData
以用于 .ActiveTextBox
和 .activeRichTextBox
,同样也在 Private Sub AppForm_KeyDown
中,以纠正变量交换的问题。
我们无法在此处创建 WPF 窗口,因此标准对话框的窗口是一个 Winform(WinFormDialog
),它托管一个 WPF 对话框控件(UserControlDlgWindowWPF
)。
Mod_Public
包含一些全局对象,这些对象是运行演示所必需的。
Public sAppPath As String = Application.StartupPath
Public _textData As New TextData
Public cTextDataVM As New TextDataViewModel
MVVM 模式
从 WPF 功能区的角度来看,数据结构/模型很简单。
Usercontrol
具有菜单项/功能区按钮,这些按钮与 ActiveRichTextBox
或 ActiveTextBox
一起工作。
这就是我们在模型类 TextData
中看到的内容。
名为 TextData
的模型类中的更改。
变量/属性 AppPath()
已移回 mod_Public
。
PropertyChanged
的代码已被删除(这现在是 MVVM 工具包的一部分)。
新的是 Public
属性 NotifyTestbox
:它可用于测试 OnPropertyChanged
。
Public Class TextData
Private _NotifyTest As RibbonTextBox
Private _ActiveTextBox As TextBox
Private WithEvents _ActiveRichTextBox As RichTextBox
Dim _MyRibbonWPF As Ribbon
Dim _MyRibbonUserCtl As UserControlRibbonWPF
Public Sub New()
'
End Sub
Public Property MyRibbonUserCtl() As UserControlRibbonWPF
Get
Return _MyRibbonUserCtl
End Get
Set(value As UserControlRibbonWPF)
_MyRibbonUserCtl = value
End Set
End Property
Public Property MyRibbonWPF() As Ribbon
Get
Return _MyRibbonWPF
End Get
Set(value As Ribbon)
_MyRibbonWPF = value
End Set
End Property
Public Property NotifyTestbox As RibbonTextBox
Get
Return _NotifyTest
End Get
Set(value As RibbonTextBox)
_NotifyTest = value
'OnPropertyChanged("NotifyTestbox")
End Set
End Property
Public Property ActiveTextBox() As TextBox
Get
Return _ActiveTextBox
End Get
Set(value As TextBox)
_ActiveTextBox = value
End Set
End Property
Public Property ActiveRichTextBox() As RichTextBox
Get
Return _ActiveRichTextBox
End Get
Set(value As RichTextBox)
_ActiveRichTextBox = value
End Set
End Property
End Class
名为 TextDataViewModel 的 ViewModel 类中的更改
TextDataViewModel
类包含属性、ICommand
和方法。- 变量/属性
AppPath()
已移回mod_Public
。 PropertyChanged
的代码已被删除(这现在是 MVVM 工具包的一部分)。- 新的是
Public
属性NotifyTestbox
和Public
属性OnPropertyChangedTest
。两者都仅用于测试OnPropertyChanged
。 Filedialog
、Searchdialog
和其他对话框的方法现在已外包并作为服务运行。
名为 RichDataViewModel 的 ViewModel 类中的更改
- 名为
RichDataViewModel
的类包含属性、ICommand
和方法,用于Richtext
菜单命令/功能区按钮。 - 变量/属性
AppPath()
已移回mod_Public
。 PropertyChanged
的代码已被删除(这现在是 MVVM 工具包的一部分)。Printdialog
、Fontdialog
和其他对话框的方法现在已外包并作为服务运行。
整合 - WPF 用户控件概念和代码
用户控件背后的代码
在上述更改之后,usercontrol
的代码隐藏几乎是干净的。
Public Class UserControlRibbonWPF
#Region " constructors"
Public Sub New()
Me.DataContext = New TextDataViewModel
InitializeComponent()
End Sub
#End Region
#Region " Form Events"
Private Sub UserControlRibbonWPF_Loaded(sender As Object, e As RoutedEventArgs) _
Handles Me.Loaded
Try
_textData.MyRibbonUserCtl = Me
cTextDataVM.MyRibbonWPF = RibbonWPF
cTextDataVM.NotifyTestbox = ribbonTextBox
Catch ex As Exception
IO.File.AppendAllText(sAppPath & "\Log.txt", String.Format("{0}{1}", _
Environment.NewLine, Now.ToString & "; " & ex.ToString()))
Dim msgBoxService As IMsgBoxService = Ioc.[Default].GetService(Of IMsgBoxService)()
msgBoxService.Show("Unexpected error:" & vbNewLine & vbNewLine & ex.ToString,,, _
Windows.MessageBoxImage.Error)
End Try
End Sub
#End Region
DependencyInjection 或 ServiceInjection
如前所述,Winform 的代码隐藏中有一些用于此的代码。
Ioc.[Default].ConfigureServices(New ServiceCollection().AddSingleton _
(Of IMsgBoxService, MsgBoxService)().AddSingleton _
(Of IDialog)(New DialogVM()).AddSingleton _
(Of IOpenFileDlgVM)(New OpenFileDlgVM()).AddSingleton _
(Of IPasteImageDlgVM)(New PasteImageDlgVM()).AddSingleton _
(Of IRibbonStatusService)(New RibbonStatusService()).AddSingleton _
(Of ISaveAsFileDlgVM)(New SaveAsFileDlgVM()).AddSingleton _
(Of ISearchDlgVM)(New SearchDlgVM()).AddSingleton _
(Of IFontDlgVM)(New FontDlgVM()).BuildServiceProvider)
带有 ISaveAsFileDlgVM 的保存文件对话框示例
它使用接口 ISaveAsFileDlgVM
和服务/viewmodel SaveAsFileDlgVM
。
Public Class TextDataViewModel
...
Public Property SaveAsFileDlgCommand() As ICommand
...
Dim cmdSAFD As New RelayCommand(AddressOf SaveAsFileDialog)
SaveAsFileDlgCommand = cmdSAFD
...
Private Sub SaveAsFileDialog()
Dim dialog As ISaveAsFileDlgVM = Ioc.[Default].GetService(Of ISaveAsFileDlgVM)()
dialog.SaveAsFileDlg()
End Sub
...
而且,非常重要的一点是,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}"/>
PropertyChanged 测试
<RibbonGroup Header="PropertyChanged Test" Margin="0" Height="92" FontSize="14"
VerticalAlignment="Top" Width="120" FontFamily="Arial"
CanAddToQuickAccessToolBarDirectly="False">
<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>
<RibbonTextBox x:Name="NotifyTextBox" Text="{Binding OnPropertyChangedTest}"
HorizontalAlignment="Right"
Margin="0,0,-90,-55" TextWrapping="Wrap"
VerticalAlignment="Bottom" Width="120"
UndoLimit="10" FontSize="12">
</RibbonTextBox>
</RibbonGroup>
两个 textbox
通常只显示 activeTextbox
是否与“RichText
”或“PlainText
”相关。
但是,如果您手动编辑上面的一个,您会看到下面的内容立即被更改。
这是由 XAML 文件中的 UpdateSourceTrigger
=PropertyChanged
引起的。
Messenger 测试
添加 Inherits ObservableRecipient
很重要,其他详细信息可在 ObservableObject - Windows Community Toolkit | Microsoft Docs 中找到。
“应在视图的 Loaded 事件中注册视图特定的消息,并在 Unloaded 事件中取消注册,以防止内存泄漏和多次回调注册问题”。
我们从
Public Class RichDataViewModel
Inherits ObservableRecipient
Private msg As String
Public ReadOnly Property SendMsg As ICommand
Get
Return _cmdMsg
End Get
End Property
Private Sub SendMsgRibbonButton_Click()
Try
' DataExchange / Messenger
Dim msg = "Test Msg..."
SetStatus("TextDataViewModel", msg)
...
Public Sub SetStatus(ByVal r As String, ByVal m As String)
Try
'Call Messenger.Send(New StatusMessage(m), r)
Messenger.Send(New StatusMessage(m))
...
Public Class StatusMessage
Public Sub New(ByVal status As String)
NewStatus = status
MessageBox.Show(status)
End Sub
Public Property NewStatus As String
End Class
发送一条 Msg
到另一个 viewmodel
,只有当消息被注册时才可能。
Public Class TextDataViewModel
Inherits ObservableRecipient
Public Sub New()
Try
Messenger.Register(Of StatusMessage)(Me, Sub(r, m) r.StatusBarMessage = m.NewStatus)
...
Protected Overrides Sub Finalize()
Messenger.Unregister(Of StatusMessage)(Me)
MyBase.Finalize()
End Sub
在关闭 viewmodel
时,我们必须 unregister
该消息。
EventTrigger
要求: Microsoft.Xaml.Behaviors.Wpf
(NuGet 包)
xmlns:b="http://schemas.microsoft.com/xaml/behaviors"
...
<b:Interaction.Triggers>
<b:EventTrigger EventName="ContextMenuClosing">
<b:InvokeCommandAction Command="{Binding ContextMenuClosing}" />
</b:EventTrigger>
</b:Interaction.Triggers>
结论
这只是一个演示 - 它尚未达到生产标准。
但我希望这个演示表明,可以在不更改 WinForm
数据结构的情况下,通过添加 WPF Ribbon
来升级 Winforms 应用程序。
而且我认为,通过添加 MVVM 工具包,可以实现多种扩展。
最后说明
我对任何形式的反馈都非常感兴趣 - 问题、建议等。
参考文献
- [1] Using ICommand with MVVM pattern - CodeProject
- [2] Introduction to the MVVM Toolkit - Windows Community Toolkit | Microsoft Docs
- [3] C# WPF WYSIWYG HTML Editor - CodeProject
- [4] The big MVVM Template - CodeProject
历史
- 2022 年 1 月 28 日 - 首次提交