Added a bit of documentation for the WindowManager and its MVVM concept

This commit is contained in:
cygon 2024-07-27 00:09:35 +02:00
parent 15a45b6434
commit 7a0ae3b3c7
4 changed files with 224 additions and 0 deletions

BIN
Documents/ViewModels.uml Normal file

Binary file not shown.

109
Documents/WindowManager.svg Normal file
View File

@ -0,0 +1,109 @@
<?xml version="1.0" standalone="no" ?>
<svg width="720px" height="1240px" version="1.1" xmlns="http://www.w3.org/2000/svg">
<path d="M 235,160 L 235,122" fill="none" stroke="#000000" />
<path d="M 225,142 L 235,122 245,142 Z" fill="#e0e0e0" stroke="#000000" />
<path d="M 235,360 L 235,306" fill="none" stroke="#000000" stroke-dasharray="5, 5" />
<path d="M 225,326 L 235,306 245,326 Z" fill="#e0e0e0" stroke="#000000" />
<path d="M 460,500 L 510,500" fill="none" stroke="#000000" />
<path d="M 494,492 L 510,500 494,508" fill="none" stroke="#000000" />
<path d="M 300,640 L 300,610 610,610 610,570" fill="none" stroke="#000000" stroke-dasharray="5, 5" />
<path d="M 600,590 L 610,570 620,590 Z" fill="#e0e0e0" stroke="#000000" />
<path d="M 620,830 L 620,610 610,610 610,570" fill="none" stroke="#000000" stroke-dasharray="5, 5" />
<path d="M 600,590 L 610,570 620,590 Z" fill="#e0e0e0" stroke="#000000" />
<path d="M 610,450 L 610,400" fill="none" stroke="#000000" />
<path d="M 602,418 L 610,402 618,418" fill="none" stroke="#000000" />
<path d="M 460,370 L 510,370" fill="none" stroke="#000000" />
<path d="M 494,362 L 510,370 494,378" fill="none" stroke="#000000" />
<path d="M 610,290 L 610,230" fill="none" stroke="#000000" stroke-dasharray="5, 5" />
<path d="M 600,252 L 610,232 620,252 Z" fill="#e0e0e0" stroke="#000000" />
<path d="M 610,290 L 610,230" fill="none" stroke="#000000" stroke-dasharray="5, 5" />
<text x="645" y="267" font-size="11" font-weight="400" font-family="Arial" fill="#000000" text-anchor="middle">(Optional)</text>
<path d="M 602,248 L 610,232 618,248" fill="none" stroke="#000000" />
<path d="M 680,1100 L 680,610 610,610 610,570" fill="none" stroke="#000000" stroke-dasharray="5, 5" />
<path d="M 600,590 L 610,570 620,590 Z" fill="#e0e0e0" stroke="#000000" />
<rect x="10" y="10" width="450" height="112" fill="#c8ffc8" stroke="#000000" />
<line x1="10" y1="50" x2="460" y2="50" stroke="#000000" />
<line x1="10" y1="96" x2="460" y2="96" stroke="#000000" />
<text x="235" y="45" font-size="13" font-weight="700" font-family="Arial" font-style="italic" fill="#000000" text-anchor="middle">IActiveWindowTracker</text>
<text x="235" y="27" font-size="13" font-weight="400" font-family="Arial" fill="#000000" text-anchor="middle">&lt;&lt;interface&gt;&gt;</text>
<text x="235" y="62" font-size="11" font-weight="700" font-family="Arial" fill="#808080" text-anchor="middle">Attributes</text>
<text x="235" y="108" font-size="11" font-weight="700" font-family="Arial" fill="#808080" text-anchor="middle">Methods</text>
<text x="20" y="81" font-size="13" font-weight="400" font-family="Arial" fill="#640000">+ ActiveWindow : Form</text>
<rect x="10" y="160" width="450" height="146" fill="#c8ffc8" stroke="#000000" />
<line x1="10" y1="200" x2="460" y2="200" stroke="#000000" />
<text x="235" y="195" font-size="13" font-weight="700" font-family="Arial" font-style="italic" fill="#000000" text-anchor="middle">IWindowManager</text>
<text x="235" y="177" font-size="13" font-weight="400" font-family="Arial" fill="#000000" text-anchor="middle">&lt;&lt;interface&gt;&gt;</text>
<text x="235" y="212" font-size="11" font-weight="700" font-family="Arial" fill="#808080" text-anchor="middle">Methods</text>
<text x="20" y="231" font-size="13" font-weight="400" font-family="Arial" fill="#000064">+ OpenRoot&lt;TViewModel&gt;(viewModel : TViewModel = null) : Form</text>
<text x="20" y="251" font-size="13" font-weight="400" font-family="Arial" fill="#000064">+ ShowModal&lt;TViewModel&gt;(viewModel : TViewModel = null) : bool?</text>
<text x="20" y="271" font-size="13" font-weight="400" font-family="Arial" fill="#000064">+ CreateView&lt;TViewModel&gt;(viewModel : TViewModel = null) : Control</text>
<text x="20" y="291" font-size="13" font-weight="400" font-family="Arial" fill="#000064">+ CreateViewModel&lt;TViewModel&gt;() : TViewModel</text>
<rect x="10" y="360" width="450" height="212" fill="#c8ffc8" stroke="#000000" />
<line x1="10" y1="380" x2="460" y2="380" stroke="#000000" />
<line x1="10" y1="426" x2="460" y2="426" stroke="#000000" />
<text x="235" y="375" font-size="13" font-weight="700" font-family="Arial" fill="#000000" text-anchor="middle">WindowManager</text>
<text x="235" y="392" font-size="11" font-weight="700" font-family="Arial" fill="#808080" text-anchor="middle">Attributes</text>
<text x="235" y="438" font-size="11" font-weight="700" font-family="Arial" fill="#808080" text-anchor="middle">Methods</text>
<text x="20" y="411" font-size="13" font-weight="400" font-family="Arial" fill="#640000">+ ActiveWindow : Form</text>
<text x="20" y="457" font-size="13" font-weight="400" font-family="Arial" fill="#000064">+ OpenRoot&lt;TViewModel&gt;(viewModel : TViewModel = null) : Form</text>
<text x="20" y="477" font-size="13" font-weight="400" font-family="Arial" fill="#000064">+ ShowModal&lt;TViewModel&gt;(viewModel : TViewModel = null) : bool?</text>
<text x="20" y="497" font-size="13" font-weight="400" font-family="Arial" fill="#000064">+ CreateView&lt;TViewModel&gt;(viewModel : TViewModel = null) : Control</text>
<text x="20" y="517" font-size="13" font-weight="400" font-family="Arial" fill="#000064">+ CreateViewModel&lt;TViewModel&gt;() : TViewModal</text>
<text x="20" y="537" font-size="13" font-weight="400" font-family="Arial" fill="#000064"># LocateViewForViewModel(viewModelType : Type) : Type</text>
<text x="20" y="557" font-size="13" font-weight="400" font-family="Arial" fill="#000064"># CreateInstance(type : Type) : object</text>
<rect x="510" y="450" width="200" height="120" fill="#c8ffc8" stroke="#000000" />
<line x1="510" y1="490" x2="710" y2="490" stroke="#000000" />
<line x1="510" y1="536" x2="710" y2="536" stroke="#000000" />
<text x="610" y="485" font-size="13" font-weight="700" font-family="Arial" font-style="italic" fill="#000000" text-anchor="middle">IView</text>
<text x="610" y="467" font-size="13" font-weight="400" font-family="Arial" fill="#000000" text-anchor="middle">&lt;&lt;interface&gt;&gt;</text>
<text x="610" y="502" font-size="11" font-weight="700" font-family="Arial" fill="#808080" text-anchor="middle">Attributes</text>
<text x="610" y="548" font-size="11" font-weight="700" font-family="Arial" fill="#808080" text-anchor="middle">Methods</text>
<text x="520" y="521" font-size="13" font-weight="400" font-family="Arial" fill="#640000">+ DataContext : object</text>
<rect x="10" y="640" width="580" height="132" fill="#c8ffc8" stroke="#000000" />
<line x1="10" y1="660" x2="590" y2="660" stroke="#000000" />
<line x1="10" y1="706" x2="590" y2="706" stroke="#000000" />
<text x="300" y="655" font-size="13" font-weight="700" font-family="Arial" fill="#000000" text-anchor="middle">ViewForm</text>
<text x="300" y="672" font-size="11" font-weight="700" font-family="Arial" fill="#808080" text-anchor="middle">Attributes</text>
<text x="300" y="718" font-size="11" font-weight="700" font-family="Arial" fill="#808080" text-anchor="middle">Methods</text>
<text x="20" y="691" font-size="13" font-weight="400" font-family="Arial" fill="#640000">+ DataContext : object</text>
<text x="20" y="737" font-size="13" font-weight="400" font-family="Arial" fill="#000064"># OnDataContextChanged(sender : object, oldDataContext : object, newDataContext : object)</text>
<text x="20" y="757" font-size="13" font-weight="400" font-family="Arial" fill="#000064"># InvalidateAllViewModelProperties()</text>
<rect x="70" y="830" width="580" height="212" fill="#c8ffc8" stroke="#000000" />
<line x1="70" y1="850" x2="650" y2="850" stroke="#000000" />
<line x1="70" y1="896" x2="650" y2="896" stroke="#000000" />
<text x="360" y="845" font-size="13" font-weight="700" font-family="Arial" fill="#000000" text-anchor="middle">MultiPageViewForm</text>
<text x="360" y="862" font-size="11" font-weight="700" font-family="Arial" fill="#808080" text-anchor="middle">Attributes</text>
<text x="360" y="908" font-size="11" font-weight="700" font-family="Arial" fill="#808080" text-anchor="middle">Methods</text>
<text x="80" y="881" font-size="13" font-weight="400" font-family="Arial" fill="#640000">+ DataContext : object</text>
<text x="80" y="927" font-size="13" font-weight="400" font-family="Arial" fill="#000064"># OnDataContextChanged(sender : object, oldDataContext : object, newDataContext : object)</text>
<text x="80" y="947" font-size="13" font-weight="400" font-family="Arial" fill="#000064"># InvalidateAllViewModelProperties()</text>
<text x="80" y="967" font-size="13" font-weight="400" font-family="Arial" fill="#000064"># IdentifyPageContainer() : Control</text>
<text x="80" y="987" font-size="13" font-weight="400" font-family="Arial" fill="#000064"># OnViewModelPropertyChanged(sender : object, arguments : PropertyChangedEventArgs)</text>
<text x="80" y="1007" font-size="13" font-weight="400" font-family="Arial" fill="#000064"># ActivePageView : Control</text>
<text x="80" y="1027" font-size="13" font-weight="400" font-family="Arial" fill="#000064"># ActivePageViewModel : object</text>
<rect x="510" y="290" width="200" height="112" fill="#c8ffc8" stroke="#000000" />
<line x1="510" y1="310" x2="710" y2="310" stroke="#000000" />
<line x1="510" y1="356" x2="710" y2="356" stroke="#000000" />
<text x="610" y="305" font-size="13" font-weight="700" font-family="Arial" fill="#000000" text-anchor="middle">YourViewModel</text>
<text x="610" y="322" font-size="11" font-weight="700" font-family="Arial" fill="#808080" text-anchor="middle">Attributes</text>
<text x="610" y="368" font-size="11" font-weight="700" font-family="Arial" fill="#808080" text-anchor="middle">Methods</text>
<text x="520" y="341" font-size="13" font-weight="400" font-family="Arial" fill="#640000">- ...</text>
<text x="520" y="387" font-size="13" font-weight="400" font-family="Arial" fill="#000064">+ ...</text>
<rect x="510" y="120" width="200" height="112" fill="#c8ffc8" stroke="#000000" />
<line x1="510" y1="160" x2="710" y2="160" stroke="#000000" />
<line x1="510" y1="206" x2="710" y2="206" stroke="#000000" />
<text x="610" y="155" font-size="13" font-weight="700" font-family="Arial" font-style="italic" fill="#000000" text-anchor="middle">INotifyPropertyChanged</text>
<text x="610" y="137" font-size="13" font-weight="400" font-family="Arial" fill="#000000" text-anchor="middle">&lt;&lt;interface&gt;&gt;</text>
<text x="610" y="172" font-size="11" font-weight="700" font-family="Arial" fill="#808080" text-anchor="middle">Attributes</text>
<text x="610" y="218" font-size="11" font-weight="700" font-family="Arial" fill="#808080" text-anchor="middle">Methods</text>
<text x="520" y="191" font-size="13" font-weight="400" font-family="Arial" fill="#640000">+ &lt;&lt;event&gt;&gt; PropertyChanged</text>
<rect x="130" y="1100" width="580" height="132" fill="#c8ffc8" stroke="#000000" />
<line x1="130" y1="1120" x2="710" y2="1120" stroke="#000000" />
<line x1="130" y1="1166" x2="710" y2="1166" stroke="#000000" />
<text x="420" y="1115" font-size="13" font-weight="700" font-family="Arial" fill="#000000" text-anchor="middle">ViewControl</text>
<text x="420" y="1132" font-size="11" font-weight="700" font-family="Arial" fill="#808080" text-anchor="middle">Attributes</text>
<text x="420" y="1178" font-size="11" font-weight="700" font-family="Arial" fill="#808080" text-anchor="middle">Methods</text>
<text x="140" y="1151" font-size="13" font-weight="400" font-family="Arial" fill="#640000">+ DataContext : object</text>
<text x="140" y="1197" font-size="13" font-weight="400" font-family="Arial" fill="#000064"># OnDataContextChanged(sender : object, oldDataContext : object, newDataContext : object)</text>
<text x="140" y="1217" font-size="13" font-weight="400" font-family="Arial" fill="#000064"># InvalidateAllViewModelProperties()</text>
</svg>

After

Width:  |  Height:  |  Size: 12 KiB

BIN
Documents/WindowManager.uml Normal file

Binary file not shown.

115
ReadMe.md
View File

@ -15,3 +15,118 @@ and it will figure out what the correct view is by name (i.e. `ExampleForm`).
There are unit tests for the whole library, so everything is verifiably There are unit tests for the whole library, so everything is verifiably
working on all platforms tested (Linux, Windows, Raspberry). working on all platforms tested (Linux, Windows, Raspberry).
MVVM
----
MVVM stands for Model-View-ViewModel. It is a way to design GUI applications
that improves testability and keeps the UI separate from the business logic.
- The Model is the underlying data the application is working with,
not designed in any way to facilitate any GUI display or editing
- The ViewModel is like an adapter - it presents the data in the model in
a way that can be easily dealt with by the view. For example, sets of
items may be exposed as `BindingList<Item>`.
- The View is the UI itself - for example, the main window or a modal dialog
that is displayed would be a view, too. Each view has a connected ViewModel
and the only code in the view class should be what directly handles the UI,
like updating button states when the ViewModel signals a change.
Basic Design
------------
This library implements the MVVM pattern through its `WindowManager` class:
![The WindowManager and its related classes](./Documents/WindowManager.svg)
The `WindowManager` keeps track of all open windows and their view models,
so your basic `Main()` method, which normally looks like this:
```csharp
[STAThread]
static void Main() {
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new MainForm());
}
```
Now becomes this:
```csharp
[STAThread]
static void Main() {
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
using(var windowManager = new WindowManager()) {
Application.Run(windowManager.OpenRoot<MainViewModel>());
}
}
```
As you can see, we no longer mention the `MainForm` by name, instead we ask
the `WindowManager` to construct a new `MainViewModel` and also create a view
that displays it.
It does so by using a "convention over configuration" approach, meaning it
assumes that if you request a view for `FlurgleSettingsViewModel`, it will
look for a view named `FlurgleSettingsView`, `FlurgleSettingsForm`,
`FlurgeSettingsWindow` or `FlurgleSettingsDialog` class and try to construct
an instance of that class.
Furthermore, if that class implements the `IView` interface, the view model
will be assigned to its `DataContext` property, establishing
the View/ViewModel relationship.
Adding an IoC Container
-----------------------
In the previous example, the view and its view model were constructed using
`Activator.CreateInstance()` - a method provided by .NET that creates a new
instance via a type's default constructor.
Most of the time, ViewModels have constructor parameters, however. For example
to provide the ViewModel with the data it is supposed to be an adapter for.
You can achieve that by constructing the ViewModel yourself and passing it
to the `WindowManager.OpenRoot()` or `WindowManager.ShowModal()` methods.
A much better approach is to use a dependency injector - an IoC container with
automatic constructor parameter injection. My favorite one is Ninject (due to
its neat setup with a fluent interface), but you can use any container you
wish, simply by inheriting your own `WindowManager` class:
```csharp
public class NinjectWindowManager : WindowManager {
public NinjectWindowManager(IKernel kernel, IAutoBinder autoBinder = null) :
base(autoBinder) {
this.kernel = kernel;
}
protected override object CreateInstance(Type type) {
return this.kernel.Get(type);
}
private IKernel kernel;
}
```
Your `NinjectWindowManager` will now use `IKernel.Get()` to construct its
ViewModels, allowing their constructors to require any services and instances
you have set up in your Ninject kernel.
```csharp
class MainViewModel {
public MainViewModel(IMyService myService, IMySettings mySettings) {
// ...
}
}
```