diff --git a/Nuclex.Support b/Nuclex.Support index 54cab3a..e818fae 160000 --- a/Nuclex.Support +++ b/Nuclex.Support @@ -1 +1 @@ -Subproject commit 54cab3a63f4f678a3173dee3085d45268c560e8c +Subproject commit e818fae81012b0a819abcf2180b90eee1d998fb7 diff --git a/ReadMe.md b/ReadMe.md new file mode 100644 index 0000000..4f4c1f2 --- /dev/null +++ b/ReadMe.md @@ -0,0 +1,246 @@ +Nuclex Foundation Libraries +=========================== + +A set of clean and carefully designed utility classes for .NET. + +For specific documentation, see the individual projects in this package. + + +Settings (`.ini` / Windows Registry) +------------------------------------ + +Many applications have to store their settings in an external file or, +for pure Windows applications, in the registry. This can be tedious and +difficult to unit test, too. Nuclex.Support provides an autonomous ini +parser (which works cross-platform and does **not** rely on +`GetPrivateProfileString`). + +Furthermore, it uses an interface to provide the same functionality for +the Registry and in-memory settings. This lets you switch between storing +your settings in the registry, in an .ini file or constructing a settings +container in memory to appropriately unit-test your code with mock data. + +```csharp +static readonly string BasicCategoryName = "Basic"; +static readonly string HintsCategoryName = "Hints"; + +void saveSettings(ISettingStore settingsStore) { + settingsStore.Set(BasicCategoryName, "AskSaveOnQuit", this.askSaveOnQuit); + settingsStore.Set(BasicCategoryName, "ActivePanel", this.activePanelIndex); + settingsStore.Set(HintsCategoryName, "ShowNameHint", this.showNameHint); + // ... +} + +void saveSettingsToIni() { + var iniStore = new ConfigurationFileStore(); + saveSettings(iniStore); + + using(var writer = new StreamWriteR("awesome-app.ini")) { + iniStore.Save(writer); + writer.Flush() + } +} + +void saveSettingsToRegistry() { + using( + var registryStore = new WindowsRegistryStore( + RegistryHive.HKCU, "AwesomeApplication" + ) + ) { + saveSettings(registryStore); + } +} +``` + + +MVVM with Nuclex.Windows.Forms +------------------------------ + +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 (Ninject) to the MVVM Example +----------------------------------------------------- + +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) { + // ... + } + +} +``` + + +Observable Base Class for Data Binding and ORMs +----------------------------------------------- + +.NET provides the `INotifyPropertyChanged` interface for objects to expose +an event that reports when a property of the object has changed. This is +used by data binding UI controls and some ORMs to detect when an object has +been changed and the UI or database need to be updated. + +It is a bit tedious to implement, so here's a base class to make it much +more pleasant to use: + +```csharp +class CreateUserViewModel : Observable { + + public string FirstName { + get { return this.firstName; } + set { + if(value != this.firstName) { + this.firstName = value; + OnPropertyChanged(nameof(FirstName)); + } + } + } + + private string firstName; + +} +``` + +There's an extension method for the consuming side, too, with proper handling +of *wildcard* change notifications that are often overlooked: + +```csharp +CreateUserViewModel ViewModel { get; set; } + +void onPropertyChanged(object sender, PropertyChangedEventArgs arguments) { + if(arguments.AreAffecting(nameof(ViewModel.FirstName))) { + this.firstNameLine.Text = ViewModel.FirstName; + } +} +``` + + +Cloning Objects +--------------- + +Whether you use the prototye design patten on complex objects or have another +reason, sometimes a deep clone of an object tree is needed. This library +provides three complete solutions to cloning objects in .NET: + +- The `SerializationCloner`. It uses .NET's BinarySerializer in a way that + will serialize your object tree regardless of whether your objects have + the `Serializable` attribute or not. This is the slowest, least efficient + object cloner, but it relies on built-in .NET classes only. + +- The `ReflectionCloner` uses .NET's reflection capabilities (that means + interrogating an object what fields and properties it has) to create + complete clones of an object, including any arrays and referenced objects. + This serializer has no setup time and has pretty decent performance. + +- The `ExpressionTreeCloner` uses Linq expression trees to generate tailored + cloning code for your classes at runtime. This method of cloning has a setup + time (meaning it takes longer the first time it is confronted with a new + class), but from the second clone onwards, is much faster than the others. + +All three object cloners can create *shallow clones* (meaning any references +to other object will be kept without copying the referenced objects, too) and +*deep clones* meaning any refeferenced objects (and their referenced objects) +will be cloned as well. Careful, this means event subscribers, such a forms +and unexpected hangers-on will be cloned, too. + +Furthermore, all three object cloners can create *property-based clones* +(where only those settings exposed via properties are cloned), which may skip +the non-exposed parts of an object, as well as *field-based clones* which +replicate all the data of a class - any private field and hidden state. + +```csharp +class Example { + public Example(Example child = null) { + Child = child; + } + public Example Child { get; private set; } +} + +class Test { + public static void CloneSomething() { + var test = new Example(new Example()); + + var reflectionCloner = new ReflectionCloner(); + var clone = reflectionCloner.DeepFieldClone(test); + + // Clone is now a complete copy of test, including the child object + } +} +``` \ No newline at end of file diff --git a/foundation-package-old.nuspec b/foundation-package-old.nuspec new file mode 100644 index 0000000..f1658ad --- /dev/null +++ b/foundation-package-old.nuspec @@ -0,0 +1,35 @@ + + + + Nuclex.Foundation + 1.1.0 + Nuclex Foundation Libraries + Markus Ewald + + false + Apache 2.0 + Set of basic libraries offering tools for MVVM-based UIs, storage of settings, license keys, collections and observer/subscriber pattern helpers + en-US + mvvm,util + + + + + + + + + + + + + + + + + + + + + + diff --git a/foundation-package.nuspec b/foundation-package.nuspec index 7fcd394..daec482 100644 --- a/foundation-package.nuspec +++ b/foundation-package.nuspec @@ -13,26 +13,49 @@ Set of basic libraries offering tools for MVVM-based UIs, storage of settings, license keys, collections and observer/subscriber pattern helpers en-US mvvm,util + nuclex-logo.png - - + + + + + + + - + + + + + + + + + - - + + + + + + + + + + + - + \ No newline at end of file diff --git a/nuclex-logo.png b/nuclex-logo.png new file mode 100644 index 0000000..dd743e3 Binary files /dev/null and b/nuclex-logo.png differ