This article combines YDock and the MVVM Toolkit. You will learn how to use docking framework, MVVM Toolkit and some of the features.
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 Dialog
s 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:
Microsoft.Extensions.DependencyInjection
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.
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 document
s and toolwindow
s with usercontrol
s.
My sample project is called MyWorksForYDock
.
The code behind Class MainWindow
is that:
namespace MyWorksForYDock
{
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)
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 usercontrol
s: RibbonView
, CreditsView
, DocView
and LogView
.
The other usercontrol
s are still dummies.
Registering the services / viewmodels is in the App
code behind.
namespace MyWorksForYDock
{
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 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
.
...
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.
<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:
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.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")
{
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:
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)
<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.
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 RibbonButton
s to format it. Many of these are EditingCommand
s 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.