There are many CodeProject articles about other MVVM frameworks, but almost nothing with WPF and the MVVM Toolkit. So I started to create this document.
Download CsMvvmToolkit_CP.zip
Introduction
This article and the demo are about getting started using the MVVM Toolkit and some self-created interfaces / services for MessageBox
and some dialogs.
Background
There are many CodeProject articles about other MVVM frameworks, but almost nothing with WPF and the MVVM Toolkit. So I started to create this document.
The Model, View and ViewModel (the MVVM pattern) is a good way to organize or structure your code and helps you to simplify, develop and test (e.g. unit testing) your application.
The Model holds the data and has nothing to do with the application logic.
The ViewModel acts as the connection between Model and View.
The View is the User Interface.
I will not describe and explain every detail of the complete demo project. The focus is how to test some of the features.
Using the Code
MVVM Structure / Features
The MVVM Toolkit is from Microsoft and also some of the other used features are not my own: Sources as listed in the Credits / Reference section.
Quick Overview of the Content
- MVVM Toolkit and .NET 4.7.2
RelayCommand
OnPropertyChanged
ObservableRecipient
(Messenger
and ViewModelBase
) DependencyInjection
(to run MsgBox
and Dialog
s as a service) ObservableCollection
(for Credits Listbox
)
- Ribbon Menu
- Services / dialogs
- Using
RelayCommand
and ICommand
to bind buttons to the ViewModel
Installation of the MVVM Toolkit
With the installation of the NuGet package of the MVVM Toolkit, it installs 6 or 7 other packages.
For DependencyInjection
, we need to install another NuGet package:
And I made interfaces / services for MsgBox
and some dialogs.
DependencyInjection
manages to start the following dialogs independent from the viewmodel
s:
FontDlgVM
MsgBoxService
DialogVM
OpenFileDlgVM
RibbonStatusService
SaveAsFileDlgVM
SearchDlgVM
This allows the usage of custom Messagebox
es / dialogs as well as Unit Testing.
MainWindow Concept and Code
MainWindow
shows the Ribbon
.
Below that there is a Tabcontrol
, with tabs for MVVM Toolkit Testing and RichText
.
The code behind Class MainWindow
is that:
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new TestingViewModel();
}
}
}
Application
Registering the services / viewmodels is in the App code behind.
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
includes:
public static void ErrHandler(string sErr)
and:
public static string ReadTextLines(string FileName)
MVVM Pattern - Details
From the WPF Ribbon
’s point of view, the data structure / model is simple:
The Ribbon
has menu items / ribbon buttons which work together with the ActiveRichTextBox
or the ActiveTextBox
.
That is what we see in model class TextData
.
Model class called TextData
:
using CommunityToolkit.Mvvm.ComponentModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Controls;
using System.Windows.Controls.Ribbon;
namespace CsMvvmToolkit_CP
{
public class TextData : ObservableRecipient, INotifyPropertyChanged
{
private string _text;
private string _richText;
private RibbonTextBox _NotifyTest;
private string _readText;
private TextBox _ActiveTextBox;
private RichTextBox __ActiveRichTextBox;
private RichTextBox _ActiveRichTextBox
{
[MethodImpl(MethodImplOptions.Synchronized)]
get { return __ActiveRichTextBox; }
[MethodImpl(MethodImplOptions.Synchronized)]
set { __ActiveRichTextBox = value; }
}
private Ribbon _MyRibbonWPF;
private MainWindow _MyMainWindow;
public TextData()
{
}
public MainWindow MyMainWindow
{
get { return _MyMainWindow; }
set { _MyMainWindow = (MainWindow)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; }
}
}
}
ViewModel class
called TestingViewModel
The class called TestingViewModel
contains properties, RelayCommands
, ICommands
and methods for the testing of some MVVM features. It contains also code for the ObservableCollection
(Of Credits), which is used for the Listview
with the References / Credits for this article.
Putting Things Together - WPF Concept and Code
QAT (QuickAccessToolbar)
You can remove buttons from the QAT (on right click, a context menu appears for that). And you can show QAT below the Ribbon
. You can Restore QAT from Settings Tab as well. And you can change backcolor
of the Ribbon
.
DependencyInjection or ServiceInjection
As already mentioned, there is some code for this in code behind of the App.
Save File Dialog Example with ISaveAsFileDlgVM
It uses interface ISaveAsFileDlgVM
and service
/ viewmodel SaveAsFileDlgVM
.
public class TestingViewModel
: ObservableRecipient
, INotifyPropertyChanged
...
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);
}
}
...
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"
="Save As" Command="{Binding SaveAsFileDlgCommand}"/>
From the Ribbon
, you can start and test other dialogs or the messagebox
with:
- Open Dialog
- Search (Source as listed in Credits/References)
OpenFileDialog
- Tab Help > Info
FontDialog
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 can send a Msg
from Class
TestingViewModel
:
Imports Microsoft.Toolkit.Mvvm.Messaging
public class TestingViewModel : ObservableRecipient, INotifyPropertyChanged
private string msg;
…
_cmdMsg = new Command(SendMsgRibbonButton_Click);
…
public ICommand SendMsg
{
get { return _cmdMsg; }
}
…
private void SendMsgRibbonButton_Click()
{
try
{
string msg = "Test Msg...";
SetStatus("TestingViewModel", msg);
}
catch (Exception ex)
{
SetStatus("TestingViewModel", ex.ToString());
Mod_Public.ErrHandler(ex.ToString());
}
}
...
public void SetStatus(string r, string m)
{
try
{
Messenger.Send(new DialogMessage(m));
}
catch (Exception ex)
{
SetStatus("TestingViewModel", ex.ToString());
Mod_Public.ErrHandler(ex.ToString());
}
}
...
public class StatusMessage
{
public StatusMessage(string status)
{
NewStatus = status;
}
public string NewStatus { get; set; }
}
Send Msg
is only possible if the message is registered:
using Microsoft.Toolkit.Mvvm.Messaging;
...
Messenger.Register<DialogMessage>(this, (r, m) => DialogMessage = m.NewStatus);
Messenger.Register<StatusMessage>(this, (r, m) => StatusBarMessage = m.NewStatus);
...
Messenger.Unregister<StatusMessage>(this);
Messenger.Unregister<DialogMessage>(this);
On closing the viewmodel
, we have to unregister the message.
The message appears on StatusBar
and the Ribbon
.
PropertyChanged Test
<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 x:Name="NotifyTextBox" Text="{Binding OnPropertyChangedTest,
UpdateSourceTrigger=PropertyChanged}" HorizontalAlignment="Right" Margin="0,0,-90,-53"
TextWrapping="Wrap" VerticalAlignment="Bottom" Width="120"
UndoLimit="10" FontSize="12"/>
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.
EventTrigger
Requirements: Microsoft.Xaml.Behaviors.Wpf
(NuGet package)
xmlns:b="http://schemas.microsoft.com/xaml/behaviors"
...
<b:Interaction.Triggers>
<b:EventTrigger EventName= "MouseWheel">
<b:InvokeCommandAction Command="{Binding ParamCommandTBx}"
CommandParameter="{Binding ElementName=myTextBox, Mode=OneWay}"/>
</b:EventTrigger>
<b:EventTrigger EventName= "MouseDoubleClick">
<b:InvokeCommandAction Command="{Binding ParamCommandTBx}"
CommandParameter="{Binding ElementName=myTextBox, Mode=OneWay}"/>
</b:EventTrigger>
<b:EventTrigger EventName= "TextChanged">
<b:InvokeCommandAction Command="{Binding ParamCommandTBx}"
CommandParameter="{Binding ElementName=myTextBox, Mode=OneWay}"/>
</b:EventTrigger>
<b:EventTrigger EventName= "MouseEnter">
<b:InvokeCommandAction Command="{Binding ParamCommandTBx}"
CommandParameter="{Binding ElementName=myTextBox, Mode=OneWay}"/>
</b:EventTrigger>
</b:Interaction.Triggers>
...
This is used when the Ribbon
is minimized via ContextMenu
and for other stuff.
ObservableCollection
It is part of viewmodel TestingViewModel
and used for the listbox
with Credits / References.
public class TestingViewModel : ObservableRecipient, INotifyPropertyChanged
#Region " fields"
...
private ObservableCollection<Credits> _credit = new ObservableCollection<Credits>();
...
#End Region
...
_credit = new ObservableCollection<Credits>()
{
new Credits()
{
Item = "MVVM 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 = "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"
}
};
...
public class Credits
{
public string Item { get; set; }
public string Note { get; set; }
public string Link { get; set; }
}
Test with the ObservableCollection
Click on Clear Listbox the delete the credits.
Read XML to Listbox
restores the references.
The advantage of the ObservableCollection
is that we need no UpdateTrigger
for the Listbox
.
RichText
From tab 'RichText
', you can select some text within the RichTextBox
and use the RibbonButton
s to format it. Many of these are EditingCommand
s and appear only in the UserCtlRibbonWPF.xaml file.
RelayCommands replacing some ICommands with project version 2.2
#region RelayCommands
NewFile = new RelayCommand(New_Click);
ExitApp = new RelayCommand(Exit_Click);
Print = new RelayCommand(Print_Click);
Info = new RelayCommand(Info_Click);
GreenBackground =
new RelayCommand(BackgroundGreenRibbonButton_Click);
WhiteBackground =
new RelayCommand(BackgroundWhiteRibbonButton_Click);
RestoreQAT = new RelayCommand(RestoreQAT_Click);
Apploaded = new RelayCommand(App_Loaded);
ClearListbox = new RelayCommand(ClearListboxButton_Click);
SaveXml = new RelayCommand(SaveXml_Click);
ReadXml = new RelayCommand(ReadXml_Click);
ReadLog = new RelayCommand(ReadLog_Click);
SendMsg = new RelayCommand(SendMsgRibbonButton_Click);
GetError = new RelayCommand(GetErrorButton_Click);
#endregion
The RelayCommand
of the CommunityToolkit allows slimmer code than my previous version with ICommand
. Here is one example:
...
public IRelayCommand NewFile { get; }
...
NewFile = new RelayCommand(New_Click);
...
private void New_Click()
{
try
{
if (ActiveRichTextBox is object)
{
ActiveRichTextBox.Document.Blocks.Clear();
}
if (ActiveTextBox is object)
{
GetText = Constants.vbNullString;
}
}
...
==========================================
Upgrade to NET8 with project NET8CsMvvmToolkit version 1.1
Possibility for RelayCommands using Source Generator
The following description is based on MS Learn link
MVVM source generators - Community Toolkits for .NET | Microsoft Learn:
Starting with version 8.0, the MVVM Toolkit includes brand new Roslyn source generators that will help greatly reduce boilerplate when writing code using the MVVM architecture. They can simplify scenarios where you need to setup observable properties, commands and more. If you're not familiar with source generators, you can read more about them here.
This was not possible with .Net Framework 4.8, that's why I've created a NET8 version of this project.
Here is a old and a new source version for a relay command:
OLD:
public IRelayCommand<RichTextBox> RTBoxCommand { get; }
RTBoxCommand = new RelayCommand<RichTextBox>(DoParameterisedCommand);
private void DoParameterisedCommand(object parameter)
{
_textData.ActiveRichTextBox = (RichTextBox)parameter;
_textData.ActiveTextBox = null;
OnPropertyChangedTest = "RichText";
}
NEW:
[RelayCommand]
private void ParameterRichTBox(object parameter)
{
_textData.ActiveRichTextBox = (RichTextBox)parameter;
_textData.ActiveTextBox = null;
OnPropertyChangedTest = "RichText";
}
You can see that the two lines for the command can be disabled and replaced with [RelayCommand].
For better reading I've renamed the method name from DoParameterisedCommand
to ParameterRichTBox
.
The commands are created by the source generater and saved somewhere in
.../users/{UserName}/AppData/Local/VSGeneratedDocuments/...
or in the Project Subfolders:
\obj\net8.0\generated\.....
In my project we have three types of commands to update.
Normal commands, commands which pass a parameter and commands which start a dialog.
More info from RelayCommand attribute - Community Toolkits for .NET | Microsoft Learn:
In order to work, annotated methods need to be in a partial class. If the type is nested, all types in the declaration syntax tree must also be annotated as partial. Not doing so will result in a compile errors, as the generator will not be able to generate a different partial declaration of that type with the requested command.
The name of the generated command will be created based on the method name. The generator will use the method name and append "Command" at the end, and it will strip the "On" prefix, if present. Additionally, for asynchronous methods, the "Async" suffix is also stripped before "Command" is appeneded.
Conclusion
This is only a demo – it is not production ready.
But I think the MVVM Community Toolkit will allow you a variety of extensions.
Final note: I am very interested in feedback of any kind - problems, suggestions and other.
Credits / Reference
History
- 16th May, 2024 - Added New Chapter: Upgrade to NET8 with project NET8CsMvvmToolkit
- 7th May, 2024 - Version 2.2 - Relay Commands replacing some ICommands
- 6th May, 2024 - Version 2.1 - Redesigned Classes
TextData
and TestingViewModel
- 23rd Feb, 2023 - Version 1.1 - Because
Microsoft.ToolKit.Mvvm
has been deprecated, we now must use this alternate package: CommunityToolkit.Mvvm
- 8th June, 2022 - Added the Model, View and ViewModel (the MVVM pattern) explanation
- 19th May, 2022 - Initial submission