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

WinForm VB.NET 托管 WPF 功能区 - 第 3 部分 - 现已集成 MVVM 工具包及更多内容

starIconstarIconstarIconstarIconstarIcon

5.00/5 (4投票s)

2022年1月27日

CPOL

5分钟阅读

viewsIcon

5806

downloadIcon

143

我的想法是在 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
  • ObservableRecipientMessengerViewModelBase
  • DependencyInjection(将 MsgBoxDialogs 作为服务运行)

对于 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 具有菜单项/功能区按钮,这些按钮与 ActiveRichTextBoxActiveTextBox 一起工作。

这就是我们在模型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 属性 NotifyTestboxPublic 属性 OnPropertyChangedTest。两者都仅用于测试 OnPropertyChanged
  • FiledialogSearchdialog 和其他对话框的方法现在已外包并作为服务运行。

名为 RichDataViewModel 的 ViewModel 类中的更改

  • 名为 RichDataViewModel 的类包含属性、ICommand 和方法,用于 Richtext 菜单命令/功能区按钮。
  • 变量/属性 AppPath() 已移回 mod_Public
  • PropertyChanged 的代码已被删除(这现在是 MVVM 工具包的一部分)。
  • PrintdialogFontdialog 和其他对话框的方法现在已外包并作为服务运行。

整合 - 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 工具包,可以实现多种扩展。

最后说明

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

参考文献

历史

  • 2022 年 1 月 28 日 - 首次提交
© . All rights reserved.