Jon Rumsey

An online markdown blog and knowledge repository.


Project maintained by nojronatron Hosted on GitHub Pages — Theme by mattgraham

WPF MVVM Learnings

A collection of notes related to developing and building upon MVVM in DotNET WPF.

Table of Contents

WPF Reference Notes

WPF Bindings Overview

Data flows from target properties to binding source objects by default:

MSFT documentation on WPF Data Bindings in .NET 8.

Community Toolkit - MVVM Toolkit

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.

MVVM Toolkit is a collection of tools and types:

Community Toolkit - Concerns and Possible Gotchas

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:

  1. MSFT has dedicated money and employees to the toolkit.
  2. The DotNET Foundation supports the toolkit (among many others).

Community Toolkit - Observables

Community Toolkit - ObservableProperty Attribute

The [ObservableProperty] attribute can be applied to partial class fields:

There is a StackOverflow question about NotifyPropertyChanged not working on Observable Object.

private readonly IMyNestedIocService _myService;

public MyViewModel(IMyNestedIocService myService)
{
  _myService = myService;
  _myService.PropertyChanged += (s,e) =>
  {
    OnPropertyChanged(nameof(TargetPropertyToNotify));
  }
}

Basic Notification

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;

Evaluate Command CanExecute

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.

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.

Requesting Property Validation

Use the following attributes on the Field:

This results in:

Note: Adding more cuatom attributes will not apply them to the generated property. Write the code yourself to work around this limitation.

Sending Notification Messages

Properties whose Type inherits/implements ObservableRecipient can use [NotifyPropertyChangedRecipients] attribute.

Decorate the Field with [ObservableProperty] and [NotifyPropertyChangedRecipients] attributes:

Adding Custom Attributes

Use:

Community Toolkit - RelayCommand Attribute

Eliminates relay command property boilerplate for annotated methods.

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.

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:

Note about updating the visual state of a button:

Handling Concurrency

Async command?

Handling Async Exceptions:

Note: When set to true, unrelated Exceptions might not be rethrown automatically!

Cancel Commands:

Custom Attributes:

See Custom Attributes subsection above - it applies here.

Community Toolkit - INotifyPropertyChanged

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
}

Community Toolkit - ObservableObject

Features:

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).

Handling Task Properties

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.

Community Toolkit - ObservableRecipient

Community Toolkit - Inversion of Control

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 Dependency Injection

How to integrate this into Community Toolkit?

See CommunityToolkit.Mvvm.DependencyInjection documentation.

How To:

  1. Install Microsoft.Extensions.DependencyInjection
  2. Install CommunityToolkit.Mvvm
  3. Create a helper class that will need to be shared between ViewModels.
  4. Extract an interface for the helper class.
  5. In App.xaml.cs: Implement a private static IServiceProvider ConfigureServices() method and implement a new ServiceCollection() instance. This is where services and ViewModels will get injected. Return the ServiceCollection instance.
  6. In App.xaml.cs: Implement a public property IServiceProvider Services with a single getter.
  7. In App.xaml.cs: Implement a static method App Current() method that returns an Application.Current cast as an App.
  8. In App.xaml.cs: Create a CTOR that assigns ConfigureServices() to Services and calls this.InitializeComponent.
  9. In each View CTOR that needs binding to its ViewModel: Add this.DataContext = App.Current.Services.GetService<TViewModel>(); with the named ViewModel as the type.
  10. In each ViewModel registered in 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.

Caliburn Micro - Requirements

Visual Studio 2019 or newer.

DotNET Framework 4.7+

Caliburn Micro - WPF with MVVM Project Setup

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.

Caliburn Micro - Setup Steps

  1. Create a ViewModel
  2. Create a View
  3. Create a class called Bootstrapper that will inherit from BootstrapperBase (using Caliburn.Micro) (see code below)
  4. Remove MainWindow.xaml (and its backing cs)
  5. Update App.xaml and add a Merged ResourceDictionary that points to '<local:Bootstrapper x:Key="Bootstrapper" />' (see code below)

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 - Uses Naming Conventions

Caliburn Micro - Binding Values in ViewModel to a View

Caliburn.Micro:

  1. Create a public Property in ViewModel for the data. Use a full Property not just a backing Field or 'auto get-set' notation.
  2. Add a Control in the View and give it an x:Name that matches the ViewModel's Property.

WPF OneWay Binding:

Caliburn Micro - NotifyOfPropertyChanged

NotifyOfPropertyChanged code example:

public string LastName {
  // field
  // prop getter
  set {
    _lastname = value;
    NotifyOfPropertyChange(() => LastName);
    NotifyOfPropertyChange(() => Fullname);
  }
}

Caliburn Micro - Child Forms

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():

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" />

Caliburn Micro - Binding Collections

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; }
        }
<!-- In the control, binding by x:Name -->
<TextBlock x:Name="SelectedPerson_LastName" />

Caliburn Micro - Binding Events

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;
}

WPF Input Validation

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.

WPF Input Validation By Exception

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).

WPF Input Validation by IDataErrorInfo

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.

WPF Input Validation by ValidationRule

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.

WPF Input Validation by Annotations

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.

Random Notes

Resources

YouTube Tosker's Corner.

WPF with Caliburn.Micro Project Setup.

Warning might be dead :arrow_right: 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