An online markdown blog and knowledge repository.
A collection of notes related to developing and building upon MVVM in DotNET WPF.
TargetProperty
must be a dependency property.BindingSource
.Data flows from target properties to binding source objects by default:
MSFT documentation on WPF Data Bindings in .NET 8.
Adds lots of common Types that support:
See MVVM Toolkit Introduction for the detailed list of Types.
Adds Roslyn source generators to reduce boilerplate coding.
[ObservableProperty]
to Fields. The Property with SetProperty(ref name, value)
is generated during build.ICommand
type Field and associated ICommand
type Property needed to initiate new RelayCommand(DoCommandMethod)
.MVVM Toolkit is a collection of tools and types:
How well supported and active is the core project? Seems to be fairly active with recent Issues resolved, and several PRs waiting and recently closed. One hitch is the CommunityToolkit Samples Repo is relatively complex and does a poor job supporting comprehension of the Toolkit syntax and capabilities. My humble opinion.
Does the MVVM Toolkit have a lifecycle timeline published? The CommunityToolkit dotnet repo points to a Milestones section of the repo, and it is empty. The CommunityToolkit Documentation does not provide any additional Milestone or Lifecycle information at all.
The two things that it has going for it:
[ObservableProperty]
to a Field.partial void
methods using Field name, like OnNameChanging(string?)
and OnNameChanged(string?)
.string? oldValue, string? newValue
versions.The [ObservableProperty]
attribute can be applied to partial class fields:
ObservableCollection<T>
where T could be any Type.There is a StackOverflow question about NotifyPropertyChanged not working on Observable Object.
[NotifyPropertyChangedFor()]
attribute applies to the container of the complex (nested) object.PropertyChanged
events for the nested object properties will be required.private readonly IMyNestedIocService _myService;
public MyViewModel(IMyNestedIocService myService)
{
_myService = myService;
_myService.PropertyChanged += (s,e) =>
{
OnPropertyChanged(nameof(TargetPropertyToNotify));
}
}
Raise a notification whena property changes by using NotifyPropertyChangedFor
attribute.
Example code from [MSFT MVVM Toolkit Official Documentation]:
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(FullName))]
private string? name;
To re-compute whether a Command Enabled
state should be changed when a property is changed, leverage [NotifyCanExecuteChangedFor(nameof(MyCommand))]
as an attribute above the depended Field.
CanExecute
type methods to set them true/false depending on the Field state every time it changes.IRelayCommand
property.Warning: Adding custom attributes on a field will require using a traditional manual property (i.e. You write the code yourself). This is true of fields that have custom validation, rather than relying on ValidationAttribute
alone.
Use the following attributes on the Field:
[ObservableProperty]
[NotifyDataErrorInfo]
[Required]
[MinLength()]
This results in:
ValidateProperty(value, "Value2");
in the getter.ObservableValidator
object.Note: Adding more cuatom attributes will not apply them to the generated property. Write the code yourself to work around this limitation.
Properties whose Type inherits/implements ObservableRecipient
can use [NotifyPropertyChangedRecipients]
attribute.
PropertyChanged()
message notifications.IMessenger
instance at the current ViewModel.Decorate the Field with [ObservableProperty]
and [NotifyPropertyChangedRecipients]
attributes:
IMessenger
.Broadcast(oldValue, value)
in the Property setter.Use:
[property: ]
target over annotated fields.Eliminates relay command property boilerplate for annotated methods.
[RelayCommand]
Parameters can be included, and will convert the generated Command code into the generic IRelayCommand<T>
version.
Async commands are wrapped in IAsyncRelayCommand
or IAsyncRelayCommand<T>
when defined with a Task
return type.
void
as an async return type.Enable CanExecute
using the Relay Command attribute: [RelayCommand(CanExecute = nameof(CanGreetUser))]
Control the state of a Command by adding the following to the Field definition:
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(MyCustomCommand))]
Note about updating the visual state of a button:
CanExecute
method.IRelayCommand.NotifyCanExecuteChanged
to invalidate the command and signal CanExecute
to re-evaluate.Async command?
AllowConcurrentExecuteions
property.CancellationToken
is supported, however any previously pipelines command will run without being awaited (although I could be mis-understanding the documentation comment here).Handling Async Exceptions:
[RelayCommand(FlowExceptionsToTaskScheduler = bool)]
IAsyncRelayCommand.ExecutionTask
and will bubble up to TaskScheduler.UnobservedTaskException
. This is more complex to use but allows UI configuration to respond to Exceptions.Note: When set to true
, unrelated Exceptions might not be rethrown automatically!
Cancel Commands:
[RelayCommand(IncludeCancelCommand = bool)]
ICommand
wrapped async relay command.Custom Attributes:
See Custom Attributes subsection above - it applies here.
Method to insert MVVM support code into existing types:
How to do it, per [Community Toolkit MVVM Documentation]:
[INotifyPropertyChanged]
public partial class MyViewModel : InheritedType
{
// the partial class-level attribute identifies it as
// the recipient of the code that will be generated.
// Other helpers will be included:
// Implements:
// INotifyPropertyChanged
// ObservableObject
// ObservableRecipient
}
Features:
INotifyPropertyChanged
INotifyPropertyChanging
SetProperty
methods to set property values from types inheriting from ObservableObject.SetPropertyAndNotifyOnCOmpletion
method that can set Task
properties to support completion notification.OnPropertyChanged
and OnPropertyChanging
, which can be overridden to customize raising notification events.Wrap a custom class with ObservableObject
:
public class ObserableMyClass : ObservableObject
{
private readonly Location location;
public ObservableMyClass(Location location) => this.location = location;
public string CityName
{
get => location.CityName;
set => SetProperty(location.CityName, value, location, (l, c) => l.CityName = c);
}
}
In the above example code, ObservableMyClass.SetProperty
signature overload is SetProperty<TModel, T>(T, T, TModel, Action<TModel, T>, string)
.
TModel
: Type argument of the model to be wrapped.T
: Type of the property to be set. Infers TModel.T oldValue
: Pass the current value of the wrapped object property.T newValue
: New value to set to the property.TModel model
: Target model that is being wrapped.Action<TModel, T> callback
: Function to invoke if new value does not equal current one, which sets the new value if not equal to existing value. Be sure to only use the input params for this and not the wrapped object Fields (for performance reasons).Always raise a notification event if a property is of a Task type.
Note: This implementation is meant to replace NotifyTaskCompletion<T>
from Microsoft.Toolkit
package.
Dependency Injection is a common solution to attaining IoC, but creating services that are injected into backend classes (as parameters to viewmodel constructors).
Microsoft.Extensions.DependencyInjection
package instead.IServiceProvider
setup and use.How to integrate this into Community Toolkit?
See CommunityToolkit.Mvvm.DependencyInjection documentation.
How To:
Microsoft.Extensions.DependencyInjection
CommunityToolkit.Mvvm
IServiceProvider ConfigureServices()
method and implement a new ServiceCollection()
instance. This is where services and ViewModels will get injected. Return the ServiceCollection
instance.IServiceProvider Services
with a single getter.App Current()
method that returns an Application.Current
cast as an App
.ConfigureServices()
to Services
and calls this.InitializeComponent
.this.DataContext = App.Current.Services.GetService<TViewModel>();
with the named ViewModel as the type.IServiceProvider
(probably as a Transient service): Add a private field for each service that the ViewModel will consume, and use App.Current.Services.GetService<IServiceType>();
to inject it via the IoC. IServiceType
is the interface name that the service implements.Visual Studio 2019 or newer.
DotNET Framework 4.7+
Reference: WPF with MVVM Project Setup video by Tim Corey.
Install NuGet Package Caliburn.Micro (Tim used 3.2.0 but a newer version is currently available that might not support DotNET 4.7 Framework.
The UI Layer is broken-out into Models, ViewModels, and Views.
Bootstrapper.cs code:
using Caliburn.Micro;
using System.Windows;
using MyDesktopApp.ViewModels;
namespace MyNamespace
{
public class Bootstrapper : BootstrapperBase
{
public Bootstrapper()
{
Initialize();
}
protected override void OnStartup(object sender, StartupEventArgs e)
{
// tell Caliburn Micro to use ShellViewModel as the base view
// similar to how XAML StartupUri tells WPF to use MainWindow.xaml
// so be sure to remove StartupUri from App.xaml and add a new
// ResourceDictionary
DisplayRootViewFor<ShellViewModel>();
}
}
}
Add the following <ResourceDictionary>
hive to <Application.Resources>
in App.xaml:
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary>
<local:Bootstrapper x:Key="Bootstrapper" />
</ResourceDictionary>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
Caliburn.Micro:
x:Name
that matches the ViewModel's Property.WPF OneWay Binding:
Text
for a TextBlock) to {Binding Path=OtherProp, Mode=OneWay}
OtherProp
refers to the value to copy from.NotifyOfPropertyChanged
in the ViewModel Property so edits to a OneWay Binding control also get updated.NotifyOfPropertyChanged code example:
public string LastName {
// field
// prop getter
set {
_lastname = value;
NotifyOfPropertyChange(() => LastName);
NotifyOfPropertyChange(() => Fullname);
}
}
Keyword x:Name="ActiveItem"
enables parenting a child View for display.
Nest User Control (WPF) inside of a parent Window (WPF).
Caliburn.Micro 'Screen' is the simplest display model, but there are others:
ActivateItem():
Screen
.Load___()
methods with ActivateItem(new ChildnameViewModel());
to control child view instantiation.<Content Control x:Name="ActivateItem" />
to the parent View to placehold where a child View will appear.x:Name
attributes that call LoadPageN
where N is a name that makes sense for the View that will get initialized.ActivateItem()
in the LoadPageN()
method(s) that instantiate a new instance of the child ViewModel that will drive the View.public namespace MyNamespace
{
public class MyClass : Conductor<object>
{
// members...
public void LoadPageOne()
{
ActivateItem(new FirstChildViewModel());
}
public void LoadPageTwo()
{
ActivateItem(new SecondChildViewModel());
}
}
}
<!-- Buttons trigger calls to LoadPageOne() and LoadPageTwo() methods in parent ViewModel -->
<Button x:Name="LoadPageOne" Grid.Row="5" Grid.Column="1">Load First Page</Button>
<Button x:Name="LoadPageTwo" Grid.Row="5" Grid.Column="2">Load Second Page</Button>
<!-- ContentControl calls ActivateItem -->
<ContentControl Grid.Row="6" Grid.Column="1" Grid.ColumnSpan="5" x:Name="ActiveItem" />
Use BindableCollection<T>
for multi-element controls like ComboBoxes.
private BindableCollection<PersonModel> _people = new BindableCollection<PersonModel>();
public BindableCollection<PersonModel> People
{
get { return _people; }
set { _people = value; }
}
OneWayToSource
binds from the Control to the Property in the ModelView.<!-- In the control, binding by x:Name -->
<TextBlock x:Name="SelectedPerson_LastName" />
Use the naming convention 'Can' + PropertyName. For example, to implement the ability to enable a button only under certain conditions on the state of a Property:
public string FirstName
{
get { return _firstName;}
set {
_firstName = value;
NotifyOfPropertyChange(() => FirstName);
}
}
public ClearText(string firstName)
{
FirstName = string.Empty;
}
public bool CanClearText(string firstName)
{
if (String.IsNullOrWhiteSpace(firstName))
{
return false;
}
return true;
}
I'll overview Tosker's Corner demonstrations of using input validation in the next four subsections.
Remember: Updates to properties must include notifications, for example IObservableCollection
, or INotifyPropertyChanged
, etc implementations.
Throw an Exception type when the property value does not meet specific requirements.
It appears this is a handy way to deal with input validation upon Control submission during debug (because a thrown exception can be handled or ignored), but is useful in a fully developed app.
Attributes can be added to the Binding statement that will cause the App to update the input control decoration when the exception is thrown: ValidatesOnExceptions=True
and UpdateSourceTrigger
(set to Explicit, LostFocus, or PropertyChanged).
Use IDataErrorInfo interface to define to implement methods that support input validation and response.
Provide a property to evaluate using an indexer property.
Use a Switch-Case construct to identify each property to validate, the requirement to meet (e.g. Username.Length < 5
), and what to return in both true and false cases.
Returning null
tells WPF that there is no error. Returning a value indicates an error.
Attributes must be added to the input Binding: ValidatesOnDataErrors
and UpdateSourceTrigger
. This enables the control decoration (thin red border on error).
To allow displaying the return message from the indexing property, a Dictionary can be used to add items that the WPF attribute ToolTip
can be bound to that provides feedback on the index return (if not null).
Perhaps one thing for me to try is to use a separate Label to show the error condition rather than a Tool Tip, to remain A11y compatible.
This method utilizes a newly implemented class that inherits from ValidationRule
and overrides Validate(object value, CultureInfo cultureInfo)
method, performs the comparison for validation, and returns a ValidationResult(bool, string)
for WPF to consume.
A valid result is identified by the Validate()
method returning new ValidationResult(true, null)
.
WPF must include Attached Properties with a Binding that identifies the Binding Path, ValidatesOnDataErrors
, and UpdateSourceTrigger
(just like the previous examples).
A Binding.ValidationRules
Attached Property must be added that defines the rule argument (in Validate()
method).
To display an error message, use a ControlTemplate
in Application.Resources
to override "errorTemplate", and define a new Control that includes an AdornedElementPlaceholder
with a TextBlock
that Binds to the first error (identified as [0]
) and its property ErrorContent
e.g. {Binding [0].ErrorContent}
.
Then, in the Control that needs the in-line error message, add Validation.ErrorTemplate="{StaticResource errorTemplate}"
so the error message(s) will appear in-line. This might not be a great A11y solution either, but the simplicity in implementation and effectiveness for sighted users is pretty compelling.
Requires adding a reference to System.ComponentModel.DataAnnotations
(this might be a .NET Framework 4.x requirement - in .net6 and newer, the library might be available in a using
statement without adding by library reference).
A ControlTemplate
in Application.Resources
will be needed here as well.
Ensure the ViewModel (or the data managing class) is inheriting from ObservableObject
.
Add the Annotations [Required(ErrorMessage=string)]
and [Rule...(args, args, ErrorMessage(string))]
to the properties that require validation. TaskersCorner used [StringLength(50, MinimumLength=5, ErrorMessage="Must be at least 5 characters.")]
.
As in previous methods, the XAML will need to be updated to include special Binding properties. In ToskersCorner, the example used a TextBox with the Text property set to {Binding Username, ValidatesOnExceptions=True, UpdateSourceTrigger=PropertyChanged, Validation.ErrorTemplate="StaticResource errorTemplate}
.
The ViewModel (or data class) will need its Setter updated to include ValidateProperty(value, string Property)
so that WPF can access a public Property that updates the validation data.
YouTube Tosker's Corner.
WPF with Caliburn.Micro Project Setup.
Warning might be dead MSFT MVVM Community Toolkit Samples Project Home.
MSFT MVVM Community Toolkit Official Documentation.
.NET Foundation Community Toolkit Github Repo which is the parent container to CommunityToolkit.Mvvm components in 'src' folder.
Julian Ewers-Peters has a Blog that is occasionally updated with CommunityToolkit and DetNET MAUI technical details.
Return to ContEd Index
Return to Root README