A toolkit help to build MVVM client applications with Blazor, WPF, UWP, Xamarin or Windows Forms. Making ViewModelBase<TInheritor>
the base class of your view model type (TInheritor
) your View Model class is empowered with Property Notification, Validations, Property Coertion, Editing Scopes and Undo/Redo; all configured by Fluent API as you can see in the following examples:
- View Model base
- Property declaration
- Propagation of Property change notification
- Automatic property value coercion
- Validation
- Commands
- Scopes
- Putting it all together
Start by inheriting from ViewModelBase<YourClass>
like this:
public class ContactEditor : ViewModelBase<ContactEditor>
{
...
}
Note that the generic parameter T
on ViewModelBase<T>
is substituted by your view model (ContactEditor
in the example)
The class ViewModelBase<T>
implements INotifyPropertyChanged
and provides a set of methods to support property validation, coerce and so on.
Use GetValue<TPropertyType>()
and SetValue<TPropertyType>(TPropertyType value)
to define get and set accessors on properties.
public class ContactEditor : ViewModelBase<ContactEditor>
{
public string FirstName
{
get => GetValue<string>();
set => SetValue(value);
}
}
By doing this, the property change is notified whenever the new value is different from old value and validations succedded.
The RegisterProperty
method can be use to defined the behaviors of a property about change notifications, validations, etc. Is the starting point of the Fluent API on properties. For example, in the following code:
public class ContactEditor : ViewModelBase<ContactEditor>
{
public static ContactEditor()
{
RegisterProperty(vm => vm.FirstName)
.Coerce(name => name.Trim())
}
public string FirstName
{
get => GetValue<string>();
set => SetValue(value);
}
}
The Coerce
method guaranties that the value is trimmed before set. This configuration is made by using the Fluent API starting with the RegisterProperty
method
Computed properties is defined as usual like FullName in the following code:
public class ContactEditor : ViewModelBase<ContactEditor>
{
public string FirstName { get => GetValue<string>(); set => SetValue(value); }
public string LastName { get => GetValue<string>(); set => SetValue(value); }
public string FullName => $"{FirstName} {LastName}";
}
By marking computed properties with the [DependOn(dependencyProperty)]
attribute, every property-change notification happening on the dependencyProperty is propagated to this property, which means that another change notification will be raised for this property. In the following code, FullName is attributed with [DependOn]
:
public class ContactEditor : ViewModelBase<ContactEditor>
{
public string FirstName { get => GetValue<string>(); set => SetValue(value); }
public string LastName { get => GetValue<string>(); set => SetValue(value); }
[DependOn(nameof(FirstName))]
[DependOn(nameof(LastName))]
public string FullName => $"{FirstName} {LastName}";
}
Such that the following code:
var editor = new ContactEditor();
editor.FirstName = "John";
will notify PropertyChanged
twice: one for the property FirstName and the other for the property FullName
Propagation happens from one property to another. Such that in the following code
public class ContactEditor : ViewModelBase<ContactEditor>
{
public string FirstName { get => GetValue<string>(); set => SetValue(value); }
public string LastName { get => GetValue<string>(); set => SetValue(value); }
public string Prefix { get => GetValue<string>(); set => SetValue(value); }
[DependOn(nameof(FirstName))]
[DependOn(nameof(LastName))]
public string FullName => $"{FirstName} {LastName}";
[DependOn(nameof(Prefix))]
[DependOn(nameof(FullName))]
public string FormalName => $"{Prefix} {FullName}";
}
Any change made on FirstName or LastName will raise PropertyChanged
3 times: one for the original porperty, another for FullName and because FormalName depends on FullName another notification is raised for FormalName too.
Note that any change on the Prefix property will raised PropertyChanged
only for 2 properties Prefix and FormalName
The property dependencies can be defined also with a fluent api starting from RegisterProperty
method and continuing with the DependOn
fluent declaration as follows:
public class ContactEditor : ViewModelBase<ContactEditor>
{
public static ContactEditor()
{
RegisterProperty(vm => vm.FullName)
.DependOn(nameof(FirstName), nameof(LastName));
RegisterProperty(vm => vm.FormalName)
.DependOn(nameof(Prefix))
.DependOn(nameof(FullName));
}
public string FirstName { get => GetValue<string>(); set => SetValue(value); }
public string LastName { get => GetValue<string>(); set => SetValue(value); }
public string Prefix { get => GetValue<string>(); set => SetValue(value); }
public string FullName => $"{FirstName} {LastName}";
public string FormalName => $"{Prefix} {FullName}";
}
Note that you can even use a single call to DependOn with many property names or even chain many DependOn calls
This way the declarations of the attribute [DependOn(...)]
are not longer required on every computed property.
Consider to use fluent api from static constructor to prevent performance overhead because of continuous redeclarations
Use fluent API to coerce any value assigned to the properties:
public class ContactEditor : ViewModelBase<ContactEditor>
{
public static ContactEditor()
{
RegisterProperty(c => c.FirstName)
.Coerce(name => name.Trim(),
name => char.ToUpper(name[0]) + name.Substring(1).ToLower());
}
}
...
contactEditor.FirstName = " john "; // contactEditor.FirstName == "John"
This can be useful to automaticaly "fix" values introduced through declarative two-way bindings:
<TextBox Text="{Binding FirstName, Mode=TwoWay}"/>
Coerce transformation will occur in the same order they are defined
Use Validate
to add validation rules that will be applied before the value is set to the property:
public class ContactEditor : ViewModelBase<ContactEditor>
{
public static ContactEditor()
{
RegisterProperty(x => x.LastName)
.Validate((viewModel, value, validationResultCollection)
=> validationResultCollection.Error(string.IsNullOrWhiteSpace(val),
"LastName cannot be empty or whitespace"));
}
}
Validate
has the following optional parameters:
notifyChangeOnValidationError=true
: see Putting it all together: Property change with validationBehaviorOnCoerce=ValidationErrorBehavior.AfterCoerce
): see Putting it all together: Coerce with validationContinueOnValidationError=true
: If this param is set to false then any error detected during validation will (silently) abort the property change.
Validations are applied in the same order they are defined
Validation Results can be of type: Error, Warning or Information. Validation is considered succeded if there is no error in the collection when validation finishes.
Each validation rule can add 1, many or 0 validation results. Use the following ViewModelBase<T>
methods:
GetValidationResults(propertyName)
to know the validation results at any moment. Then, useValidate()
of thisValidationResultCollection
if you want to apply additional validations from imperative codeClearValidationResult()
to reset validation results
ViewModelBase<T>
implements INotifyDataErrorInfo
, so it is integrated with binding and validation system of WPF and UWP, and
its members can be used to know whether is it some validation errors (HasError
), whether the appear os disappear (ErrorChanged
)
and what validation error is happening per property.
Warnings and Informations is considered Errors too in the
INotifyDataErrorInfo
logic
Use RegisterCommand
to add commands to the View Model:
public override Task InitializeAsync(IDispatcher dispatcher = null)
{
ICommand cmd = RegisterCommand("RegisterNewUser",
execute: x => RegisterUser(x),
canExecuteCondition: x => !ExistsUser(x));
return base.InitializeAsync(dispatcher);
}
This way commands can be bound in WPF and UWP like this:
<Button Command="{Binding ViewModel.Commands.RegisterNewUser}"/>
Note that this method creates an object which is
System.Windows.Input.ICommand
It is recommended to register commands from theInitializeAsync
method
Every command registered this way will be invalidated when any property changes. See: Putting it all together: Invalidate commands on property changes
Use Scopes to work with the View Model features in a transactional way. You can optimize property changes notifications, record changes to enable Undo/Redo operations, and so on...
Scopes are blocks of code defined using BeginScope
and executed through StartAsync
:
var scope = vm.BeginScope(sc =>
{
vm.FirstName = "John";
vm.LastName = "Smith";
});
await scope.StartAsync();
Scopes are IObservable<object>
The way of notify partial results is using: Yield(object)
:
var scope = await vm.BeginScope(sc =>
{
foreach (var item in myList)
sc.Yield(item);
})
.Observe(x => x.Subscribe(myObserver))
.StartAsync();
Use Observe(Action<IObservable<object>>)
to react to scope yield-results, or even to apply Reactive extensions queries:
var scope = await vm.BeginScope(sc =>
{
foreach (var item in myContactList)
sc.Yield(item);
})
.Observe(c => c.Where(c => c.Age >= 18)
.DistinctUntilChanged()
.Subscribe(Console.WriteLine))
.StartAsync();
Using UndoAsync
in the scope you can undo every changes applied in the scope to those properties registered with recording enabled:
RegisterProperty(vm => vm.FirstName)
.EnableRecording();
RegisterProperty(vm => vm.LastName);
Then you can apply UndoAsync()
as follows:
(vm.FirstName, vm.LastName) = ("Unknown", "Unknown");
var scope = vm.BeginScope(sc =>
{
...
vm.FirstName = "John";
vm.LastName = "Wood";
});
await scope.StartAsync();
var (first, last) = (vm.FirstName, vm.LastName); // ("John", "Wood")
await scope.UndoAsync();
var (first, last) = (vm.FirstName, vm.LastName); // ("Unknown", "Wood")
From this example, see that because EnableRecording
is not applied explicitly on LastName the change on this property during the scope was not undone through UndoAsync
Property changes and its propagation will happen always after the coercions are applied on the source property. E.g:
contactEditor.PropertyChanged += (s.e) =>
{
if (e.PropertyName == "FullName")
Console.WriteLine(contactEditor.FullName); // "John"
};
contactEditor.FirstName = " john ";
Setting optional parameter: notifyChangeOnValidationError
of the Validate
method, you can decide whether the property change notification is raise even when validation fails.
The Validate
method has the parameter BehaviorOnCoerce
used to indicate whether some specific validation rule should be applied before or after the Coerce, or both.
public class ContactEditor : ViewModelBase<ContactEditor>
{
public static ContactEditor()
{
RegisterProperty(x => x.Name)
.Coerce(x => x[0].ToUpper() + x.Substring(1).ToLower())
.Validate((t, v, vr) => vr.Error<NotNull>(v) && v.Length > 1,
continueOnValidationError: false,
when: ValidationErrorBehavior.BeforeCoerce);
}
}
Because the validation will apply before coerce and because continueOnValidationError=false
it is safe to apply the Coerce with no errors
Every time one property is changed all registered commands are invalidated so their CanExecute
method will be called and their event CanExecuteChanged
will be raised.
var scope = vm.BeginScope(sc =>
{
vm.FirstName = "Johnn";
vm.FirstName = "John";
vm.LastName = "Smit";
vm.LastName = "Smith";
});
await scope.StartAsync();
Doing this, the PropertyChanged event is raised once for FirstName
, another time for LastName
, and even when FullName
is depending on both, it will be notified only once from the scope.
Just add devoft.ClientModel as a dependency and start coding
devoft.Client.Test is the main testing project
This project exists thanks to all the people who contribute: mariodvm, johnnamdez
If you want to learn more about this and other projects visit us at devoft