A collection of MVVM helpers for WPF, Xamarin.Forms and .NET MAUI.
I wanted to create this library for my self, however, I'll be stoked if anyone else finds it useful. Feel free to request more features or changes.
Code & inspiration:
I've used the ObservableObject
, ObservableRangeCollection
and ICommand
implementations from James Montemagno's MVVM-Helpers library, with some minor tweaks here and there. Thank you James!
Status:
General package, includes all the interfaces and some shared implementations.
Install-Package Divis.DarkHelpers
Platform specific implementations (base view, navigation service, etc) for WPF.
Install-Package Divis.DarkHelpers.WPF
Platform specific implementations (base view, navigation service, etc) for Xamarin.Forms.
Install-Package Divis.DarkHelpers.XF
Platform specific implementations (base view, navigation service, etc) for .NET MAUI.
Install-Package Divis.DarkHelpers.Maui
Simple implementation of INotifyPropertyChanged that any class can inherit from.
public class MyObject : DarkObservableObject
{
private string _firstName;
public string FirstName
{
get => _firstName;
set => SetProperty(ref _firstName, value, onChanged: DoSomething);
}
private void DoSomething()
{
//react to changes
}
}
A base view model class that implements INotifyPropertyChanged
and has all the properties and events you would usually need.
Properties:
IsBusy
IsNotBusy
CanLoadMore
IsLoadingMore
Events (virtual methods):
OnInitializeAsync
- One time initialzation method, to be called when the view model is first usedOnRefreshAsync
- To be called whenever the view model is brought to user's attention, i.e. anytime the corresponding view is shownOnBeforeExitAsync
- To be called before a view is exited, determines whether the exit should be continued or cancelledOnExitAsync
- To be called when a view is exiting, useful for any cleanup work
Event support:
WPF | Xamarin.Forms | .NET MAUI | |
---|---|---|---|
OnInitializeAsync |
Yes | Yes | Yes |
OnRefreshAsync |
No | Yes | Yes |
OnBeforeExitAsync |
Yes | No | No |
OnExitAsync |
Yes | Yes | Yes |
A ObservableCollection that adds important methods such as: AddRange, RemoveRange, Replace, and ReplaceRange.
public DarkObservableCollection<Item> Items { get; set; } = new DarkObservableCollection<Item>();
private void ReloadItems(){
var items = _dataService.GetItems();
Items.ReplaceRange(items);
}
Collection synchronization can be enabled using the static DarkObservableCollectionSettings
class. Since collection synchronization is implemented differently for WPF and Xamarin.Forms, the initialization differs a bit.
WPF (App.xaml.cs)
using DarkHelpers.Collections;
protected override void OnStartup(StartupEventArgs e)
{
DarkObservableCollectionSettings.RegisterSynchronizer<DarkWpfSynchronizer>();
//other code
}
Xamarin.Forms (App.xaml.cs)
using DarkHelpers.Collections;
public App()
{
DarkObservableCollectionSettings.RegisterSynchronizer<DarkXfSynchronizer>();
//other code
}
.NET MAUI (App.xaml.cs)
using DarkHelpers.Collections;
public App()
{
DarkObservableCollectionSettings.RegisterSynchronizer<DarkMauiSynchronizer>();
//other code
}
Navigate freely, even from a class library where you might be storing your view models.
It works like this:
- register the ViewModel & View pairs to the instance of the platform specific nagivation service (
DarkXfNavigationService
for Xamarin.Forms andDarkWpfNavigationService
for WPF). The view model has to implement theDarkViewModel
class and the view has to implement the specific view base class (DarkWpfViewBase
for WPF andDarkXfViewBase
class for Xamarin.Forms). More on that in the "Custom base view" section - store the platform specific implementation as an instance of
IDarkNavigationService
in a DI container (or wherever) - only use the
IDarkNavigationService
instance to perform navigation
WPF:
using DarkHelpers.Abstractions;
using DarkHelpers.WPF;
var nav = new DarkWpfNavigationService();
nav.Register<HomeViewModel, HomeView>();
nav.Register<ObservableCollectionViewModel, ObservableCollectionView>();
nav.Register<CommandsViewModel, CommandsView>();
someContainer.RegisterSingleton<IDarkNavigationService>(nav);
Xamarin.Forms:
using DarkHelpers.Abstractions;
using DarkHelpers.XF;
var nav = new DarkXfNavigationService();
nav.Register<HomeViewModel, HomeView>();
nav.Register<ObservableCollectionViewModel, ObservableCollectionView>();
nav.Register<CommandsViewModel, CommandsView>();
someContainer.RegisterSingleton<IDarkNavigationService>(nav);
.NET MAUI:
using DarkHelpers.Abstractions;
using DarkHelpers.Maui;
var nav = new DarkMauiNavigationService();
nav.Register<HomeViewModel, HomeView>();
nav.Register<ObservableCollectionViewModel, ObservableCollectionView>();
nav.Register<CommandsViewModel, CommandsView>();
someContainer.RegisterSingleton<IDarkNavigationService>(nav);
using DarkHelpers;
var nav = someContainer.Get<IDarkNavigationService>();
await nav.PushAsync(new HomeViewModel());
await nav.PopAsync();
Synchronous and asynchronous ICommand implementations, plus a DarkEventManager to help your events be garbage collection safe
Custom base view that allows the navigation by view model.
NOTE: you can now use my Visual Studio extension that contains view templates for WPF, Xamarin.Forms and .NET MAUI for easier view creation. The extension can be found HERE
Create a new Window
and change the code to look like this.
I'm using the SomeApp as my example project namespace
SomeView.xaml
<darkViews:DarkWpfViewBase
x:Class="SomeApp.Views.SomeView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:darkViews="clr-namespace:DarkHelpers.WPF;assembly=DarkHelpers.WPF"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:viewModels="clr-namespace:SomeApp.ViewModels;assembly=SomeApp"
Title="SomeView"
x:TypeArguments="viewModels:SomeViewModel"
mc:Ignorable="d">
<Grid />
</darkViews:DarkWpfViewBase>
SomeView.xaml.cs
using DarkHelpers.WPF;
using SomeApp.ViewModels;
namespace SomeApp.Views
{
public partial class SomeView : DarkWpfViewBase<SomeViewModel>
{
public HomeView(SomeViewModel vm) : base(vm)
{
InitializeComponent();
}
}
}
Create a new ContentPage
and change the code to look like this.
I'm using the SomeApp as my example project namespace
SomeView.xaml
<?xml version="1.0" encoding="utf-8" ?>
<darkViews:DarkXfViewBase
x:Class="SomeApp.Views.SomeView"
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:darkViews="clr-namespace:DarkHelpers.XF;assembly=DarkHelpers.XF"
xmlns:viewModels="clr-namespace:SomeApp.ViewModels;assembly=SomeApp"
Title="Home"
x:TypeArguments="viewModels:SomeViewModel">
<ContentPage.Content>
<Grid />
</ContentPage.Content>
</darkViews:DarkXfViewBase>
SomeView.xaml.cs
using DarkHelpers.XF;
using SomeApp.ViewModels;
using Xamarin.Forms.Xaml;
namespace SomeApp.Views
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class SomeView : DarkXfViewBase<SomeViewModel>
{
public HomeView(SomeViewModel vm) : base(vm)
{
InitializeComponent();
}
}
}
SomeView.xaml
<?xml version="1.0" encoding="utf-8" ?>
<darkViews:DarkMauiViewBase
x:Class="Sample.Maui.Views.SomeView"
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:darkViews="clr-namespace:DarkHelpers.Maui;assembly=DarkHelpers.Maui"
xmlns:viewModels="clr-namespace:SomeApp.ViewModels;assembly=SomeApp"
Title="Home"
x:TypeArguments="viewModels:SomeViewModel">
<VerticalStackLayout>
<Label Text="Hey!" />
</VerticalStackLayout>
</darkViews:DarkMauiViewBase>
SomeView.xaml.cs
using DarkHelpers.Maui;
using SomeApp.ViewModels;
namespace SomeApp.Views;
public partial class SomeView : DarkMauiViewBase<SomeViewModel>
{
public SomeView(SomeViewModel vm) : base(vm)
{
InitializeComponent();
}
}
Safely fire and forget a Task
while being able to handle exceptions. Sometimes you don't care about waiting for a Task
to complete or you can't await it (in an event). That's what this extension is for.
using DarkHelpers;
protected override void OnAppearing()
{
//use an Action to handle exceptions
Action<Exception> errorHandler = (ex) => Console.WriteLine(ex);
DoSomethingAsync().SafeFireAndForget(errorHandler);
//or use the ITaskErrorHandler to handle exceptions
ITaskErrorHandler taskErrorHandler = null; //this might get injected by DI
DoSomethingAsync().SafeFireAndForget(taskErrorHandler);
}
private async Task DoSomethingAsync()
{
//simulate work
await Task.Delay(500);
}
This is useful for executing ICommand
s manually. It checks the CanExecute
and calls the Execute
method if CanExecute
returns true
.
using DarkHelpers;
//this might be called from a code behind of a page if there's no option to bind the command in XAML
Book book = null;
viewModel.LoadItemCommand.TryExecute(book);
Simplify your converters with the DarkConverterBase
(IValueConverter
) and DarkMultiConverterBase
(IMultiValueConverter
) base classes.
Simply inherit from either of these base classes and only override the methods you need (Convert
or ConvertBack
or both). The rest will throw a verbose NotSupportedException
.
Before:
public class ToUpperTextConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if(value is string text)
{
return text.ToUpper();
}
return value;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotSupportedException($"The {nameof(ConvertBack)} method is not supported in {nameof(ToUpperTextConverter)}.");
}
}
After:
public class ToUpperCaseTextConverter : DarkConverterBase
{
public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is string text)
{
return text.ToUpper();
}
return value;
}
}