MVVM Toolkit Preview 3 & The Journey of an API

Michael Hawker

Michael

MVVM Toolkit Preview 3

TLDR;

๐ŸŽ‰ Today we’re happy to announce a new preview of our MVVM Toolkit as part of the Windows Community Toolkit. ๐ŸŽ‰

This update includes changes based on feedback from our community who’ve been using the initial preview of the library. It does contain some breaking changes, so be sure to read our notes about that here if you are updating.

๐Ÿ“ The highlights for the update to the library include:

  • Removing a direct dependency on the Microsoft.Extensions.DependencyInjection so developers are free to choose that or any library for their Inversion of Control needs.
  • Switching the Messenger to use weak references by default for MVVMLight migrators (as well as continuing to provide the option to the strong reference initial version).
  • Further optimizations and changes to the API surface for better usage and performance in different scenarios.

The rest of this post will cover our journey on how we set out to improve the usability of one of these API calls. Early on, we discovered an issue with compiling those changes on .NET Native for UWP; however, we ended up not only working around them, but also designing an even better API surface in the process! ๐Ÿš€

The Beginning

In our initial preview of the MVVM Toolkit, we provided a new API that allows a developer to more easily get notified in their UI when a Task is completed in their ViewModel. This makes it easier to directly obtain status on a task and remove IsLoading type properties to control UI flow while awaiting these Tasks. This API is called SetPropertyAndNotifyOnCompletion and had the following signature:

protected bool SetPropertyAndNotifyOnCompletion<TTask>(
    ref TTask? field,
    Expression<Func<TTask?>> fieldExpression,
    TTask? newValue,
    [CallerMemberName] string? propertyName = null)
    where TTask : Task;

While looking complex, it was still fairly straight-forward to use in practice as seen here:

private Task<string> myTask;
public Task<string> MyTask
{
     get => myTask;
     set => SetPropertyAndNotifyOnCompletion(ref myTask, () => myTask, value);
}

However, you’ll note that we’re effectively passing the same task twice into the function in different ways. ๐Ÿ˜• The first reference to the task, allowed the function to not need reflection in order to validate the initial task setup. The second lambda expression allows the method to check that the task is still the same upon completion (via reflection) before notifying the application that it was finished (as we couldn’t re-use the ref in the asynchronous method).

This pattern though, was still more complex than using a standard property change setup in the normal use case which just took a reference to the backing field:

private string query;
public string Query
{
    get => query;
    set => SetProperty(ref query, value);
}

The Initial Refactor

As we set out to improve the API after our initial preview of it. We wanted to try to simplify the need for passing the task twice to the SetPropertyAndNotifyOnCompletion method. Sergio Pedri, an active community member and developer of the MVVM Toolkit library, originally came up with an optimization using a custom ref returning delegate (a relatively newer feature introduced in C# 7):

protected delegate ref T FieldAccessor<T>();

protected bool SetPropertyAndNotifyOnCompletion<TTask>(
    FieldAccessor<TTask?> fieldAccessor,
    TTask? newValue,
    [CallerMemberName] string? propertyName = null)
    where TTask : Task;

This would simplify the syntax for utilizing the method by combining the ref and lambda (effectively) together:

private Task<string> myTask;
public Task<string> MyTask
{
    get => myTask;
    set => SetPropertyAndNotifyOnCompletion(() => ref myTask, value);
}

The added benefit to this approach was that it also removed the need for reflection entirely. ๐Ÿš€ You can see how this improved the performance of this one API here:

Initial API Performance Comparison

This led to a 7-14 times improvement in execution time and over 60% improvement to memory usage! ๐ŸŽ‰

Enter .NET Native

Even though our library is based on .NET Standard, one of the primary target groups for the Windows Community Toolkit are UWP developers! This of course means we want to ensure the library works well for them on .NET Native. When Sergio went to push his new changes to his open PR, he found our Continuous Integration build to have failed! What could have happened with this seemingly simple update using a feature of C#?

Unfortunately for us, we discovered the .NET Native compiler doesn’t support ref returning delegates. ๐Ÿ˜ฅ

We reached out to the .NET Native team, originally starting our discussion on GitHub here about what might be going on. They confirmed this was indeed a known issue with .NET Native.

In order to workaround this issue, we were going to have to try and change our method signature in another way to achieve our goal of simplifying its usage. Thankfully, the .NET Native team was able to help us investigate and suggest a potential solution to our problem. ๐ŸŽ‰

Their initial proposal was effectively the following API surface and usage:

// API Surface
protected class TaskAccessor<T> where T : Task
{
    public T Task { get; set; }
}

protected bool SetPropertyAndNotifyOnCompletion<TTask>(
    TaskAccessor<TTask> accessor,
    TTask newValue,
    [CallerMemberName] string? propertyName = null)
    where TTask : Task
{
    return default;
}

// Developer Usage
private TaskAccessor<Task<string>> myTask = new TaskAccessor<Task<string>>();
public Task<string> MyTask
{
    get => myTask.Task;
    set => SetPropertyAndNotifyOnCompletion(myTask, value);
}

While this would resolve the issue when compiling to .NET Native, it introduced some extra overhead on the developer compared to our standard property change notification pattern.

However, our first step was to at least implement the suggested pattern in the library to ensure it worked and compare the performance with the existing updated improvements we had already achieved.

During this implementation, we realized the backing TaskAccessor field would probably be best marked readonly as it shouldn’t be changed after initialization. This would be another step for developers using the API to have to know to setup.

In the end, we were concerned about having the extra wrapping type (including needing to specify Task as a generic type argument again), having to initialize the wrapper, and having to unwrap it again to retrieve it.

Side note though, this workaround did improve the memory usage even further! ๐Ÿ˜ฎ

Accessor Performance Improvement

The Journey

What followed was over a day long quest between Sergio and I to see if we could simplify the usage pattern of the API for developers, while still maintaining the goal of working with .NET Native. We really wanted to ensure this was an easy to use API without a lot of overhead for developers to understand and discover a complex nuanced syntax. ๐Ÿค”

The first issue we tackled was around needing to initialize the wrapper type and marking the backing field as readonly. By changing the API to accept the parameter by ref and leveraging TaskAccessor<T>‘s parameterless constructor, the API itself could automatically initialize the field with a new instance, if needed:

private TaskAccessor<Task<string>> myTask;
public Task<string> MyTask
{
    get => myTask.Task;
    set => SetPropertyAndNotifyOnCompletion(ref myTask, value);
}

Secondly, we improved the get accessor by adding an implicit cast operator to the TaskAccessor<T> field, which would automatically unwrap the inner T instance. This allows developers to have their property getters return the backing field directly (like a standard property setup), with the C# compiler taking care of invoking the implicit operator to retrieve the task instance stored there, if present:

private TaskAccessor<Task<string>> myTask;
public Task<string> MyTask
{
    get => myTask;
    set => SetPropertyAndNotifyOnCompletion(ref myTask, value);
}

We were getting closer to our goal of simplifying this API. ๐Ÿ‘จโ€๐Ÿ”ฌ The last thing that remained on our minds was the API still required including the generic type Task within the TaskAccessor itself. Not only was it more verbose, but it was especially odd if the Task was non-generic as you’d have to specify TaskAccessor<Task>. It’d be great if we could just use something like TaskAccessor directly and not re-iterate the Task within it.

While we (or rather Sergio) worked to thinking of a solution, an interesting anecdote occurred to me at this point. Thinking of the name TaskAccessor seemed to be very monolithic and indicative of the core problem we were trying to work around. This didn’t lead to feeling great about the solution, even though we had come so far at this point to get here. ๐Ÿค”

Lucky for us, we realized by just changing the name to TaskNotifier instead, it seemed to make things tie together more closely to the API it was related too, and just generally make us feel better about having to use it. Now, rather than seeming like a workaround to access a field, it feels like a piece of code that is actively working towards helping the developer with this scenario (even though it doesn’t actually do any of the real notifying. ๐Ÿคซ Shhhh, it’s a secret).

Finally, to implement a solution for the embedded Task type, Sergio decoupled the handling of both the generic and non-generic tasks by introducing two separate wrapping types: TaskNotifier and TaskNotifier<T>. The generic TaskNotifier<T> type automatically maps to Task<T> for the internal task instance, while TaskNotifier just contains a simple Task. We then exposed two different overloads of the SetPropertyAndNotifyOnCompletion method, one accepting a TaskNotifier and one accepting a TaskNotifier<T> parameter. This whole procedure is completely transparent for consumers of the library, but it allows us to completely remove the need to have those two type arguments being specified, making the code much less verbose.

๐Ÿ“ Note: There’s a bit more behind the scenes to this story in terms of implementation details and how it all functions. Of course, this is all open source code up on GitHub, so you can see how it works for yourself here.

The Final Result

We finally reached a state where we were truly happy with how developers could use this API. Now, it was not only simpler to use, but just as performant (indeed more so, see below), and closely mirrored the base property change API. This is the final result to the end developer in this new preview:

private TaskNotifier<string> myTask;
public Task<string> MyTask
{
    get => myTask;
    set => SetPropertyAndNotifyOnCompletion(ref myTask, value);
}

And when compared to the standard property backing, it becomes more apparent how nice this solution was:

private string query;
public string Query
{
    get => query;
    set => SetProperty(ref query, value);
}

You’ll see the only change a developer needs to enable Tasks is to use SetPropertyAndNotifyOnCompletion instead of SetProperty and change their Task backing type to TaskNotifier while still being able to leverage Task in their property itself. ๐ŸŽ‰

At the end of this adventure, we ended up with a better API than where we started, that was more performant, and easier to use. See the final comparison of performance metrics below.

Accessor Performance Improvement

๐Ÿฆ™๐Ÿ’— We’re thankful to the .NET Native team for working with us and proposing an initial solution to workaround our issue. It’s really amazing how this journey to improve an API led us down some unexpected paths, but in the end resulted in improvements for the MVVM Toolkit. ๐Ÿš€

Providing Feedback

Be sure to checkout our preview MVVM Toolkit sample and doc repo here. You can file issues against the samples and docs there, or provide input on the MVVM Toolkit code itself on our main repo’s tracking issue here. ๐Ÿ“ข

Thank you for sharing your feedback with us!

Co-written with Sergio Pedri, Windows Community Toolkit member and creator of the MVVM Toolkit.

3 comments

Leave a comment