Click here to Skip to main content
16,020,628 members
Articles / Desktop Programming / WPF

C# MVVM Toolkit Demo

Rate me:
Please Sign up or sign in to vote.
4.94/5 (28 votes)
16 May 2024CPOL6 min read 66.6K   4K   88   19
This article and the demo are about getting started using the MVVM Community Toolkit and some self-created interfaces / services for Message Box and some Dialogs.
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.

NEW: Download NET8CsMvvmToolkit.zip

Download CsMvvmToolkit_CP.zip

Image 1

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 Dialogs 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:

Image 2

And I made interfaces / services for MsgBox and some dialogs.

DependencyInjection manages to start the following dialogs independent from the viewmodels:

  • FontDlgVM
  • MsgBoxService
  • DialogVM
  • OpenFileDlgVM
  • RibbonStatusService
  • SaveAsFileDlgVM
  • SearchDlgVM

This allows the usage of custom Messageboxes / 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:

C#
{
    /// <summary>
    /// Interaktionslogik für MainWindow.xaml
    /// </summary>

    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            this.DataContext = new TestingViewModel();
        }
    }
}

Application

Registering the services / viewmodels is in the App code behind.

C#
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:

C#
public static void ErrHandler(string sErr)

and:

C#
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:

C#
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, RelayCommandsICommands 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

C#
...    
       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.

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"
 ="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:

C#
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
            {
                // DataExchange / Messenger
                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:

C#
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

XAML
<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 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.

EventTrigger

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

XAML
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.

C#
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 RibbonButtons to format it. Many of these are EditingCommands and appear only in the UserCtlRibbonWPF.xaml file.

RelayCommands replacing some ICommands with project version 2.2

C#
#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:

C#
...    
    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:

C#
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

License

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


Written By
Engineer
Germany Germany

Comments and Discussions

 
QuestionMix .NET and Framework in one Solution possible? Pin
Winfried Rothenberg1-Jun-24 18:55
Winfried Rothenberg1-Jun-24 18:55 
NewsPossibility for RelayCommands replacing some ParamCommands Pin
Jo_vb.net7-May-24 13:31
mvaJo_vb.net7-May-24 13:31 
Questionupdate Pin
Robert Stefanz22-Feb-23 23:02
Robert Stefanz22-Feb-23 23:02 
AnswerRe: update Pin
Jo_vb.net6-May-24 3:32
mvaJo_vb.net6-May-24 3:32 
GeneralRe: update Pin
Jaroslav Mitrovic14-May-24 1:54
Jaroslav Mitrovic14-May-24 1:54 
GeneralRe: update Pin
Jo_vb.net14-May-24 2:39
mvaJo_vb.net14-May-24 2:39 
GeneralRe: update Pin
Jaroslav Mitrovic15-May-24 8:03
Jaroslav Mitrovic15-May-24 8:03 
GeneralRe: update Pin
Jo_vb.net15-May-24 8:10
mvaJo_vb.net15-May-24 8:10 
GeneralRe: update Pin
Jo_vb.net15-May-24 11:49
mvaJo_vb.net15-May-24 11:49 
GeneralRe: update Pin
Jaroslav Mitrovic16-May-24 2:22
Jaroslav Mitrovic16-May-24 2:22 
GeneralRe: update Pin
Jo_vb.net16-May-24 21:42
mvaJo_vb.net16-May-24 21:42 
GeneralRe: update Pin
Winfried Rothenberg1-Jun-24 23:19
Winfried Rothenberg1-Jun-24 23:19 
GeneralRe: update Pin
Jo_vb.net1-Jun-24 23:58
mvaJo_vb.net1-Jun-24 23:58 
SuggestionCommunityToolkit Pin
Member 155580291-Sep-22 3:29
Member 155580291-Sep-22 3:29 
GeneralHyperbole Indeed Pin
Massey Ferguson20-May-22 11:37
Massey Ferguson20-May-22 11:37 
GeneralRe: Hyperbole Indeed Pin
Jo_vb.net21-May-22 3:35
mvaJo_vb.net21-May-22 3:35 
GeneralRe: Hyperbole Indeed Pin
IrishJoe997-May-24 5:05
IrishJoe997-May-24 5:05 
To be fair, the article did not promise to explain the concept, but to demonstrate its execution.
GeneralMy vote of 5 Pin
Ștefan-Mihai MOGA19-May-22 4:58
professionalȘtefan-Mihai MOGA19-May-22 4:58 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.