foundation-package/ReadMe.md

245 lines
7.8 KiB
Markdown
Raw Normal View History

2024-08-07 14:41:43 +00:00
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` 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 (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
}
}
2024-08-07 20:43:36 +00:00
```