This is no richtext editor but only a demo with example menu items. Host a WPF user control with Ribbon within a WinForm VB.NET project and try to use MVVM pattern. Part 3 of my article / tip series now features the MVVM Toolkit and more.
Introduction
This article / tip and the demo were inspired by the CodeProject article, Using ICommand with MVVM pattern, which creates and uses ICommands
according to MVVM pattern.
I used code from there as a template for my own classes / interface.
Part 3 is about the part usage of the MVVM Toolkit together with self made interfaces / services for MsgBox
and some dialogs.
Background
My first article WinForm VB.NET hosting WPF Ribbon was focused on basic work to get the demo running.
But I wasn‘t happy about the interface / communication between the WinForm and the WPF usercontrol. That has been changed in WinForm VB.NET Hosting WPF Ribbon - Part 2 - Using ICommand with MVVM Pattern.
Here, presenting Part 3, we will only discuss things that were missing in the first two articles / tips and / or are new or have been changed.
New MVVM Structure / Features
ICommand
as used in article / tip Part 2 was an important step for me, but not the perfect solution.
It is good for menuitem
and button single click events; but for any other EventTrigger
like a .MouseDoubleClick
or .ContextMenuClosing
, it is not very helpful.
Therefore, I decided to use a current framework.
Installation of the MVVM Toolkit
With the installation of the NuGet package of the MVVM Toolkit, it installs 6 or 7 other packages.
Introduction to the MVVM Toolkit - Windows Community Toolkit | Microsoft Docs
Usage of MVVM Toolkit Parts
RelayCommand
OnPropertyChanged
ObservableRecipient
(Messenger
and ViewModelBase
) DependencyInjection
(run MsgBox
and Dialogs
as a service)
For DependencyInjection
, we need to install another NuGet package:
I made interfaces / services for MsgBox
and some dialogs.
DependencyInjection
manages to start the following dialogs independent from the viewmodels:
FontDlgVM
PasteImageDlgVM
MsgBoxService
DialogVM
OpenFileDlgVM
RibbonStatusService
SaveAsFileDlgVM
SearchDlgVM
This allows the usage of custom Messagebox
es / dialogs as well as Unit Testing.
Winform Concept and Code
AppForm
hosts the WPF Usercontrol
with the Ribbon
.
With that configuration, we have no Application.xaml and therefore the code for registering the services / viewmodels is in the Winform code behind.
There is also code for Restore QAT State when Winform Loads and saving QAT State when closing the app.
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")
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
Replaced _TextData
with cTextDataVM
for .ActiveTextBox
and .activeRichTextBox
, same in Private Sub AppForm_KeyDown
, to correct an issue with swapped variables.
We cannot create a WPF window here, therefore the window for the standard dialog is a winform (WinFormDialog
) which hosts a WPF dialog control (UserControlDlgWindowWPF
).
Mod_Public
includes some global objects which are needed to get the demo running:
Public sAppPath As String = Application.StartupPath
Public _textData As New TextData
Public cTextDataVM As New TextDataViewModel
MVVM Pattern
From the WPF Ribbon’s point of view, the data structure / model is simple:
The Usercontrol
has menu items / ribbon buttons which work together with the ActiveRichTextBox
or the ActiveTextBox
.
That is what we see in model class TextData
.
Changes in model class called TextData
.
Variable/Property AppPath()
has been moved back to mod_Public
.
The code for PropertyChanged
has been removed (this is now part of the MVVM Toolkit).
New is Public
Property NotifyTestbox
: It can be used to test 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
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
Changes in ViewModel Class called TextDataViewModel
- Class
TextDataViewModel
contains properties, ICommand
s and methods. - Variable/Property
AppPath()
has been moved back to mod_Public
. - The code for
PropertyChanged
has been removed (this is now part of the MVVM Toolkit). - New are
Public
Property NotifyTestbox
and Public
Property OnPropertyChangedTest
. Both are only used to test OnPropertyChanged
. - Methods for
Filedialog
, Searchdialog
and other dialogs are now outsourced and running as a Service.
Changes in ViewModel class called RichDataViewModel
- The class called
RichDataViewModel
contains properties, ICommand
s and methods for the Richtext
menu commands / Ribbon Buttons. - Variable/Property
AppPath()
has been moved back to mod_Public
. - The code for
PropertyChanged
has been removed (this is now part of the MVVM Toolkit). - Methods for
Printdialog
, Fontdialog
and other dialogs are now outsourced and running as a Service.
Putting Things Together - WPF Usercontrol Concept and Code
The Code behind the usercontrol
After the above mentioned changes, the usercontrol
is almost clean of code behind.
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 or ServiceInjection
As already mentioned, there is some code for this in code behind of the 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)
Save File Dialog Example with ISaveAsFileDlgVM
It uses interface ISaveAsFileDlgVM
and service / 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
...
And, very important, Command="{Binding SaveAsFileDlgCommand}"
in the XAML file.
<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 Test
<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>
Both textbox
es normally show only if the activeTextbox
is related to „RichText
“ or „PlainText
“.
But if you edit the upper one manually, you can see that the lower one‘s content is changed immediately.
This is caused by UpdateSourceTrigger
=PropertyChanged
in the XAML file.
Messenger Test
It is important to add Inherits ObservableRecipient
, this and other details are described in ObservableObject - Windows Community Toolkit | Microsoft Docs.
„View specific messages should be registered In the Loaded Event Of a view And deregistered In the Unloaded Event To prevent memory leaks And problems multiple callback registrations“.
We send a Msg
from:
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
Dim msg = "Test Msg..."
SetStatus("TextDataViewModel", msg)
...
Public Sub SetStatus(ByVal r As String, ByVal m As String)
Try
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
to another viewmodel
, which is only possible if the message is registered:
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
On closing the viewmodel
, we have to unregister
the message.
EventTrigger
Requirements: Microsoft.Xaml.Behaviors.Wpf
(NuGet package):
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>
Conclusion
This is only a demo – it is not production ready.
But I hope this demo shows that it would be possible to upgrade a Winforms app with a WPF Ribbon
without too many changes regarding the data structure on the WinForm
.
And I think with the addition of the MVVM Toolkit, it allows a variety of extensions.
Final Note
I am very interested in feedback of any kind - problems, suggestions and other.
References
History
- 28th January, 2022 - Initial submission