Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / XAML

WinForm VB.NET Hosting WPF Ribbon - Part 3 - Now Feat. MVVM Toolkit and More

5.00/5 (2 votes)
27 Jan 2022CPOL5 min read 5.4K   111  
My idea was to host a WPF user control with Ribbon within a WinForm VB.NET project and try to use MVVM pattern.
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.

Image 1

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

Image 2

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:

Image 3

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 Messageboxes / dialogs as well as Unit Testing.

Image 4

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.

VB.NET
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

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:

VB.NET
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.

VB.NET
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

Changes in ViewModel Class called TextDataViewModel

  • Class TextDataViewModel contains properties, ICommands 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, ICommands 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.

VB.NET
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.

VB.NET
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.

VB.NET
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.

XML
<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

XML
<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 textboxes 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.

Image 5

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:

VB.NET
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

to another viewmodel, which is only possible if the message is registered:

VB.NET
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.

Image 6

EventTrigger

Requirements: Microsoft.Xaml.Behaviors.Wpf (NuGet package):

XML
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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)