diff --git a/Documents/ViewModels.uml b/Documents/ViewModels.uml new file mode 100644 index 0000000..4ae6575 Binary files /dev/null and b/Documents/ViewModels.uml differ diff --git a/Documents/WindowManager.svg b/Documents/WindowManager.svg new file mode 100644 index 0000000..2f0ad49 --- /dev/null +++ b/Documents/WindowManager.svg @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + + (Optional) + + + + + + + IActiveWindowTracker + <<interface>> + Attributes + Methods + + ActiveWindow : Form + + + IWindowManager + <<interface>> + Methods + + OpenRoot<TViewModel>(viewModel : TViewModel = null) : Form + + ShowModal<TViewModel>(viewModel : TViewModel = null) : bool? + + CreateView<TViewModel>(viewModel : TViewModel = null) : Control + + CreateViewModel<TViewModel>() : TViewModel + + + + WindowManager + Attributes + Methods + + ActiveWindow : Form + + OpenRoot<TViewModel>(viewModel : TViewModel = null) : Form + + ShowModal<TViewModel>(viewModel : TViewModel = null) : bool? + + CreateView<TViewModel>(viewModel : TViewModel = null) : Control + + CreateViewModel<TViewModel>() : TViewModal + # LocateViewForViewModel(viewModelType : Type) : Type + # CreateInstance(type : Type) : object + + + + IView + <<interface>> + Attributes + Methods + + DataContext : object + + + + ViewForm + Attributes + Methods + + DataContext : object + # OnDataContextChanged(sender : object, oldDataContext : object, newDataContext : object) + # InvalidateAllViewModelProperties() + + + + MultiPageViewForm + Attributes + Methods + + DataContext : object + # OnDataContextChanged(sender : object, oldDataContext : object, newDataContext : object) + # InvalidateAllViewModelProperties() + # IdentifyPageContainer() : Control + # OnViewModelPropertyChanged(sender : object, arguments : PropertyChangedEventArgs) + # ActivePageView : Control + # ActivePageViewModel : object + + + + YourViewModel + Attributes + Methods + - ... + + ... + + + + INotifyPropertyChanged + <<interface>> + Attributes + Methods + + <<event>> PropertyChanged + + + + ViewControl + Attributes + Methods + + DataContext : object + # OnDataContextChanged(sender : object, oldDataContext : object, newDataContext : object) + # InvalidateAllViewModelProperties() + diff --git a/Documents/WindowManager.uml b/Documents/WindowManager.uml new file mode 100644 index 0000000..a64378f Binary files /dev/null and b/Documents/WindowManager.uml differ diff --git a/ReadMe.md b/ReadMe.md index dc6edfe..bbd2af8 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -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 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`. + +- 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()); + } +} +``` + +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) { + // ... + } + +} +```