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

WPF MVVM RichText Demo using YDock[Panel]

5.00/5 (2 votes)
23 May 2024CPOL5 min read 20.9K   219  
Use docking framework, RichText, MVVM Toolkit and some more features
This article combines YDock and the MVVM Toolkit. You will learn how to use docking framework, MVVM Toolkit and some of the features.

 

Image 1

Introduction

This article and the demo are about getting started using YDock together with CommunityToolkit.Mvvm and some self-created interfaces / services for MessageBox and some dialogs.

Background

There are many CodeProject articles about other docking frameworks, but nothing with YDock. So I started to combine YDock and the CommunityToolkit.Mvvm.

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 do not describe and explain every detail of the complete demo project. The focus is how to use docking framework, MVVM Toolkit and 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 are as listed in the Credits / Reference section.

Quick Overview of the Content

  • YDock
  • CommunityToolkit.Mvvm and .NET 4.8
    • RelayCommand
    • OnPropertyChanged
    • ObservableRecipient (Messenger and ViewModelBase)
    • DependencyInjection (to run MsgBox and Dialogs as a service)
    • ObservableCollection (for Credits Listbox)
  • Ribbon Menu
  • Services / dialogs
  • Using 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:

C#
Microsoft.Extensions.DependencyInjection

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.

Usage of the Docking Framework

Found the excellent YDock and the YDockTest projects @GitHub.

For a reusable sample, we have to replace some of the dummy documents and toolwindows with usercontrols.

My sample project is called MyWorksForYDock.

The code behind Class MainWindow is that:

C#
namespace MyWorksForYDock
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            Loaded += MainWindow_Loaded;
            Closing += MainWindow_Closing;

            _Init();
        }

        static string SettingFileName { get { return string.Format(@"{0}\{1}", 
                      Environment.CurrentDirectory, "Layout.xml"); } }

        public DocView doc_new;
        public DocView doc_0;
        private Doc left;
        private Doc right;
        private RibbonView top;
        public LogView bottom;
        private Doc left_1;
        private Doc right_1;
        private CreditsView bottom_1;
        private MainWindow wnd;

        private void _Init()
        {
            doc_new = new DocView("New_doc");
            doc_0 = new DocView("doc_0");
            left = new Doc("left");
            right = new Doc("right");
            top = new RibbonView("Ribbon");
            bottom = new LogView("Log");
            left_1 = new Doc("left_1");
            right_1 = new Doc("right_1");
            bottom_1 = new CreditsView("Credits");

            DockManager.RegisterDocument(doc_0);
            DockManager.RegisterDock(top, DockSide.Top);
            DockManager.RegisterDock(bottom, DockSide.Bottom);
            DockManager.RegisterDock(left, DockSide.Bottom);
            DockManager.RegisterDock(right, DockSide.Right);
            DockManager.RegisterDock(left_1, DockSide.Left);
            DockManager.RegisterDock(right_1, DockSide.Right);
            DockManager.RegisterDock(bottom_1, DockSide.Bottom);
        }

        private void MainWindow_Loaded(object sender, RoutedEventArgs e)
        {
            wnd = this;
            if (File.Exists(SettingFileName))
            {
                var layout = XDocument.Parse(File.ReadAllText(SettingFileName));
                foreach (var item in layout.Root.Elements())
                {
                    var name = item.Attribute("Name").Value;
                    if (DockManager.Layouts.ContainsKey(name))
                        DockManager.Layouts[name].Load(item);
                    else DockManager.Layouts[name] = 
                         new YDock.LayoutSetting.LayoutSetting(name, item);
                }

                DockManager.ApplyLayout("MainWindow");
            }
            else
            {
                doc_0.DockControl.Show();
                top.DockControl.Show();
                bottom.DockControl.Show();
                left.DockControl.Show();
                right.DockControl.Show();
                left_1.DockControl.Show();
                right_1.DockControl.Show();
                bottom_1.DockControl.Show();
            }
        }

        private void MainWindow_Closing(object sender, CancelEventArgs e)
        {
            DockManager.SaveCurrentLayout("MainWindow");

            var doc = new XDocument();
            var rootNode = new XElement("Layouts");
            foreach (var layout in DockManager.Layouts.Values)
                layout.Save(rootNode);
            doc.Add(rootNode);

            doc.Save(SettingFileName);

            DockManager.Dispose();
        }

        private void OnClick(object sender, RoutedEventArgs e)
        {
            var item = sender as MenuItem;
            if (item.Header.ToString() == "left")
                left.DockControl.Show();
            if (item.Header.ToString() == "left_1")
                left_1.DockControl.Show();
            if (item.Header.ToString() == "Ribbon (top)")
                top.DockControl.Show();
            if (item.Header.ToString() == "right")
                right.DockControl.Show();
            if (item.Header.ToString() == "right_1")
                right_1.DockControl.Show();
            if (item.Header.ToString() == "Log (bottom)")
                bottom.DockControl.Show();
            if (item.Header.ToString() == "Credits")
                bottom_1.DockControl.Show();
            if (item.Header.ToString() == "doc_0")
                doc_0.DockControl.Show();
            if (item.Header.ToString() == "New_doc" && (doc_new.DockControl is object))
                 if (doc_new.DockControl == null)
                    // TODO
                    System.Windows.MessageBox.Show
                    ("Unexpected error: New_doc n.a. " + Environment.NewLine );
                else if (doc_new.DockControl is object)
                                    doc_new.DockControl.Show();
        }
    }

    public class Doc : TextBlock, IDockSource
    {
        public Doc(string header)
        {
            _header = header;
        }

        private IDockControl _dockControl;
        public IDockControl DockControl
        {
            get
            {
                return _dockControl;
            }

            set
            {
                _dockControl = value;
            }
        }

        private string _header;
        public string Header
        {
            get
            {
                return _header;
            }
        }

        public ImageSource Icon
        {
            get
            {
                return null;
            }
        }
    }
}

Views Concept and Code

MainWindow shows the usercontrols: RibbonView, CreditsView, DocView and LogView.

The other usercontrols are still dummies.

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

C#
namespace MyWorksForYDock
{
    /// <summary>
    /// Interaction logic for App.xaml
    /// </summary>
    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 MyWorksForYDock.Views;
using System.ComponentModel;
using System.Windows.Controls;
using System.Windows.Controls.Ribbon;

namespace MyWorksForYDock
{

    public class TextData : ObservableRecipient, INotifyPropertyChanged
    {
        private string _text;
        private string _richText;
        private RibbonTextBox _NotifyTest;
        private string _readText;
        private TextBox _ActiveTextBox;

        private RichTextBox _ActiveRichTextBox;
        private Ribbon _MyRibbonWPF;
        private RibbonView _MyMainWindow;

        public TextData()
        {
            // 
        }

        public RibbonView MyMainWindow
        {
            get { return _MyMainWindow; }
            set { _MyMainWindow = 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, 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.

ViewModel class called DocViewModel

This class is used for DocView.

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.

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 RibbonView 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, Mode=OneWay, 
             UpdateSourceTrigger=PropertyChanged}"/>

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

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 use Class TestingViewModel for RibbonView, LogView and CreditsView.

But even if we use same VM for different Views, we have different instances of the VM running and have to send a Msg from Class TestingViewModel for data exchange:

C#
using CommunityToolkit.Mvvm.Messaging;

namespace MyWorksForYDock
{ public class OpenFileDlgVM : ObservableRecipient, IOpenFileDlgVM
    {
        private string msg;
        private RichTextBox myRTB;
        private TextBox myTB;

        public OpenFileDlgVM()
        {
            // 
        }

        public string OpenFileDlg(object ActiveTBox)
        {
            FileDialog dialog = new OpenFileDialog();
            try
            {
                dialog.Filter = "All Files(*.*)|*.*|RTF Files (*.rtf)|*.rtf";
                dialog.FilterIndex = 1;
                dialog.Title = "RTE - Open File";
                dialog.DefaultExt = "rtf";
                // dialog.Filter = "Rich Text Files|*.rtf|" &
                // "Text Files|*.txt|HTML Files|" &
                // "*.htm|All Files|*.*"
                dialog.ShowDialog();
                if (string.IsNullOrEmpty(dialog.FileName))
                    return default;
                string strExt;
                strExt = System.IO.Path.GetExtension(dialog.FileName);
                strExt = strExt.ToUpper();

                string FileName = Path.GetFileName(dialog.FileName);
                SetStatus("TestingViewModel", FileName);

                if (ActiveTBox.GetType().ToString() == 
                   "System.Windows.Controls.RichTextBox") // IsNot Nothing Then
                {
                    myRTB = (RichTextBox)ActiveTBox;

                    switch (strExt ?? "")
                    {
                        case ".RTF":
                            {
                                var t = new TextRange
                                        ((TextPointer)myRTB.Document.ContentStart, 
                                        (TextPointer)myRTB.Document.ContentEnd);
                                var file = new FileStream
                                           (dialog.FileName, FileMode.Open);
                                t.Load(file, DataFormats.Rtf);
                                file.Close();
                                break;
                            }

                        default:
                            {
                                var t = new TextRange((TextPointer)
                                        myRTB.Document.ContentStart, 
                                        (TextPointer)myRTB.Document.ContentEnd);
                                var file = new FileStream
                                           (dialog.FileName, FileMode.Open);
                                t.Load(file, DataFormats.Text);
                                file.Close();
                                break;
                            }
                    }
                    string currentFile = dialog.FileName;
                    dialog.Title = "Editor: " + currentFile.ToString();
                }
                else if (ActiveTBox.GetType().ToString() == 
                                    "System.Windows.Controls.TextBox")
                {
                    myTB = (TextBox)ActiveTBox;
                    string currentFile = dialog.FileName;
                    myTB.Text = Mod_Public.ReadTextLines(dialog.FileName);
                }
            }
            catch (Exception ex)
            {
                File.AppendAllText(Mod_Public.sAppPath + @"\Log.txt", 
                    string.Format("{0}{1}", Environment.NewLine, 
                    DateAndTime.Now.ToString() + "; " + ex.ToString()));
                var MsgCmd = new RelayCommand<string>
                             (m => MessageBox.Show("Unexpected error:" + 
                              Constants.vbNewLine + 
                              Constants.vbNewLine + ex.ToString()));
                MsgCmd.Execute("");
            }

            return default;
        }

        public void SetStatus(string r, string m)
        {
            try
            {
                var s = Messenger.Send(new StatusMessage(m));
            }
            catch (Exception ex)
            {
                SetStatus("OpenFileDlgVM", ex.ToString());
                Mod_Public.ErrHandler(ex.ToString());
            }
        }

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.Register<PassActiveRTBoxMsg>(this, 
               (r, m) => PassActiveRTBoxMsg = m.ActiveRTBox);
     Messenger.Register<GetCreditsMsg>(this, (r, m) => GetCreditsMsg = m.AddCredit);
...
    ~TestingViewModel()
        {
        Messenger.Unregister<StatusMessage>(this);
        Messenger.Unregister<DialogMessage>(this);
        Messenger.Unregister<PassActiveRTBoxMsg>(this);
        Messenger.Unregister<GetCreditsMsg>(this);
        }

On closing the viewmodel, we have to unregister the message.

The message appears on StatusBar and the Ribbon.

EventTrigger

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

XAML
<UserControl
   xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
   xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
   xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
   xmlns:local="clr-namespace:MyWorksForYDock.Views" 
   xmlns:b="http://schemas.microsoft.com/xaml/behaviors"
   xmlns:MyWorksForYDock="clr-namespace:MyWorksForYDock" 
   x:Class="MyWorksForYDock.Views.DocView"
   mc:Ignorable="d" 
   d:DesignHeight="450" d:DesignWidth="800">
   
<Grid>
    <RichTextBox x:Name="myRichTextBox" HorizontalScrollBarVisibility="Auto" 
                 VerticalScrollBarVisibility="Auto" 
                 Margin="2,2,2,0" IsDocumentEnabled="True" AcceptsTab="True">
        <RichTextBox.DataContext>
            <MyWorksForYDock:DocViewModel/>
        </RichTextBox.DataContext>
        <FlowDocument>
             <Paragraph>
                <Run Text="RichTextBox"/>
             </Paragraph>
        </FlowDocument>
        <b:Interaction.Triggers>
            <b:EventTrigger EventName= "MouseWheel">
                <b:InvokeCommandAction Command=
                                "{Binding ParameterisedCommand, Mode=OneWay}" 
                                  CommandParameter="{Binding ElementName=myRichTextBox, 
                                  Mode=OneWay}"/>
            </b:EventTrigger>
            <b:EventTrigger EventName= "MouseDoubleClick">
                <b:InvokeCommandAction Command=
                                "{Binding ParameterisedCommand, Mode=OneWay}" 
                                  CommandParameter="{Binding ElementName=myRichTextBox,
                                  Mode=OneWay}"/>
            </b:EventTrigger>
            <b:EventTrigger EventName= "TextChanged">
                <b:InvokeCommandAction Command=
                                "{Binding ParameterisedCommand, Mode=OneWay}" 
                                  CommandParameter="{Binding ElementName=myRichTextBox,
                                  Mode=OneWay}"/>
            </b:EventTrigger>
                <b:EventTrigger EventName= "MouseEnter">
                    <b:InvokeCommandAction Command=
                                "{Binding ParameterisedCommand, Mode=OneWay}" 
                                  CommandParameter="{Binding ElementName=myRichTextBox,
                                  Mode=OneWay}"/>
            </b:EventTrigger>
        </b:Interaction.Triggers>
    </RichTextBox>

</Grid>
</UserControl>

This is used to get the ActiveRichTextBox.

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 Community 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 = "YDock",
              Note = "GitHub",
              Link = "https://github.com/yzylovepmn/YDock"
           },
       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"
           },
       new Credits()
           {
              Item = "Find/ReplaceDialog",
              Note = "Forum Msg",
              Link = "https://www.itcodar.com/csharp/
                      changing-font-for-richtextbox-without-losing-formatting.html"
           }
       };
...
public class Credits
    {
        public string Item { get; set; }
        public string Note { get; set; }
        public string Link { get; set; }
    }

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 .xaml file.

 

Update to version 1.4

Redesigned all ViewModel classes:

Using RelayCommand instead of ICommand or ParamCommand in many cases.

Cleaned up code and removed most of the disabled code segments.

 

Conclusion

This is only a demo application – it is not production ready.

But I think the YDock framework and the MVVM 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

  • 1st March, 2023 - Version 1.0 
  • 3rd March, 2023 - Version 1.1
  • 5th March, 2023 - Version 1.2 fixed OpenFileDialog bug
  • 5th May, 2024 - Version 1.3 redesigned class called TextData
  • 9th May, 2024 - Version 1.4 redesigned all ViewModel classes
  • 23rd  May, 2024 - new title for the article

Points of Interest / License

The projects CommunityToolkit.Mvvm, YDock and the YDockTest are filed under MIT license.

License

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