Today,
the .NET platform is a truly universal tool — it can be used to solve a wide range of tasks, including the development of application applications for popular operating systems such as Windows, Linux, MacOS, Android, and iOS.
In this article, we consider the architecture of cross-platform .NET applications using the MVVM design pattern and
reactive programming . Let's get acquainted with the
ReactiveUI and
Fody libraries, learn how to implement the INotifyPropertyChanged interface using attributes, touch on the basics of
AvaloniaUI ,
Xamarin Forms ,
Universal Windows Platform ,
Windows Presentation Foundation and
.NET Standard , learn effective tools for unit testing of model layers and application presentation models.
The material is an adaptation of the articles "
Reactive MVVM For The .NET Platform " and "
Cross-Platform. NET Apps Via Reactive MVVM Approach ", published by the author earlier on Medium. Code samples
are available on GitHub .
')
Introduction MVVM architecture and cross-platform .NET
When developing cross-platform applications on the .NET platform, it is necessary to write portable and supported code. When working with frameworks that use XAML dialects, such as UWP, WPF, Xamarin Forms, and AvaloniaUI, this can be achieved using the MVVM design pattern, reactive programming, and the separation of the .NET Standard code. This approach improves application portability by allowing developers to use a common code base and common program libraries on various operating systems.

In more detail we will consider each of the layers of an application built on the basis of the MVVM architecture - a model (Model), a view (View) and a view model (ViewModel). The model layer consists of domain services, data transfer objects, database entities, repositories — all of the business logic of our program. The view is responsible for displaying user interface elements on the screen and depends on the specific operating system, and the view model allows the two layers described above to interact, adapting the model layer for user interaction.
The MVVM architecture assumes a division of responsibility between the three software layers of the application, so these layers can be moved to separate assemblies aimed at the .NET Standard. The formal .NET Standard specification allows developers to create portable libraries that can be used in different implementations of .NET using one unified set of API interfaces. Strictly following the MVVM architecture and the separation of the .NET Standard code, we will be able to use ready-made model layers and presentation models when developing the user interface for various platforms and operating systems.

If we have written an application for the Windows operating system using the Windows Presentation Foundation, we can easily port it to other frameworks, such as, for example, Avalonia UI or Xamarin Forms - and our application will work on platforms such as iOS, Android, Linux, OSX, and the user interface will be the only thing you need to write from scratch.
Traditional MVVM implementation
View models typically include properties and commands to which XAML markup elements can be bound. In order for the data bindings to work, the view model must implement the INotifyPropertyChanged interface and publish the PropertyChanged event whenever any properties of the view model change. The simplest implementation might look like this:
public class ViewModel : INotifyPropertyChanged { public ViewModel() => Clear = new Command(() => Name = string.Empty); public ICommand Clear { get; } public string Greeting => $"Hello, {Name}!"; private string name = string.Empty; public string Name { get => name; set { if (name == value) return; name = value; OnPropertyChanged(nameof(Name)); OnPropertyChanged(nameof(Greeting)); } } public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged(string name) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); } }
XAML describing the application's UI:
<StackPanel> <TextBox Text="{Binding Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/> <TextBlock Text="{Binding Greeting, Mode=OneWay}"/> <Button Content="Clear" Command="{Binding Clear}"/> </StackPanel>
And it works! When the user enters his name in the text field, the text below instantly changes, welcoming the user.

But wait! Our UI needs only two synchronized properties and one command, why do we need to write more than twenty lines of code for our application to work correctly? What happens if we decide to add more properties that reflect the state of our view model? The code will be more, the code will become more complicated and more complicated. And we still support him!
Recipe # 1. Template "Observer". Short getters and setters. ReactiveUI
In fact, the problem of a verbose and confusing implementation of the INotifyPropertyChanged interface is not new, and there are several solutions. First of all, you should pay attention to
ReactiveUI . This is a cross-platform, functional, reactive MVVM framework that allows .NET developers to use reactive extensions when developing presentation models.
Reactive extensions are an implementation of the “Observer” design pattern defined by the interfaces of the .NET standard library - “IObserver” and “IObservable”. The library also includes more than fifty statements that allow you to transform event streams — filter, merge, group them — with a syntax similar to
LINQ structured query language. Read more about reactive extensions
here .
ReactiveUI also provides a base class that implements INotifyPropertyChanged - ReactiveObject. Let's rewrite our sample code using the possibilities provided by the framework.
public class ReactiveViewModel : ReactiveObject { public ReactiveViewModel() { Clear = ReactiveCommand.Create(() => Name = string.Empty); this.WhenAnyValue(x => x.Name) .Select(name => $"Hello, {name}!") .ToProperty(this, x => x.Greeting, out greeting); } public ReactiveCommand Clear { get; } private ObservableAsPropertyHelper<string> greeting; public string Greeting => greeting.Value; private string name = string.Empty; public string Name { get => name; set => this.RaiseAndSetIfChanged(ref name, value); } }
This view model does exactly the same thing as the previous one, but the code in it is smaller, it is more predictable, and all the links between the properties of the view model are described in one place, using the
LINQ to Observable syntax. Of course, we could stop at this, but the code is still quite a lot - we have to explicitly implement getters, setters and fields.
Recipe # 2. INotifyPropertyChanged Encapsulation. ReactiveProperty
An alternative solution could be to use the
ReactiveProperty library, which provides wrapper classes responsible for sending notifications to the user interface. With
ReactiveProperty, the view model should not implement any interfaces; instead, each property implements INotifyPropertyChanged itself. Such reactive properties also implement IObservable, which means that we can subscribe to their changes, as if we were using
ReactiveUI . Let's change our view model using ReactiveProperty.
public class ReactivePropertyViewModel { public ReadOnlyReactiveProperty<string> Greeting { get; } public ReactiveProperty<string> Name { get; } public ReactiveCommand Clear { get; } public ReactivePropertyViewModel() { Clear = new ReactiveCommand(); Name = new ReactiveProperty<string>(string.Empty); Clear.Subscribe(() => Name.Value = string.Empty); Greeting = Name .Select(name => $"Hello, {name}!") .ToReadOnlyReactiveProperty(); } }
We just need to declare and initialize the reactive properties and describe the connections between them. There is no need to write any template code, apart from property initializers. But this approach has a flaw - we have to change our XAML in order for the data bindings to work. Reactive properties are wrappers, so the UI must be tied to its own property for each such wrapper!
<StackPanel> <TextBox Text="{Binding Name.Value, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/> <TextBlock Text="{Binding Greeting.Value, Mode=OneWay}"/> <Button Content="Clear" Command="{Binding Clear}"/> </StackPanel>
Recipe # 3. Modify build at compile time. PropertyChanged.Fody + ReactiveUI
In a typical presentation model, each public property should be able to send notifications to the user interface when its value changes.
PropertyChanged.Fody doesn't have to worry about it. The only thing that is required of the developer is to mark the class of the view model with the
AddINotifyPropertyChangedInterface attribute - and the code responsible for publishing the PropertyChanged event will be written to the setters automatically after building the project, together with the implementation of the INotifyPropertyChanged interface, if it is not present. If necessary, turn our properties into streams of changing values, we can always use the
WhenAnyValue extension
method from the
ReactiveUI library. Let's rewrite our sample for the third time, and see how much more concise our model of presentation will be!
[AddINotifyPropertyChangedInterface] public class FodyReactiveViewModel { public ReactiveCommand Clear { get; } public string Greeting { get; private set; } public string Name { get; set; } = string.Empty; public FodyReactiveViewModel() { Clear = ReactiveCommand.Create(() => Name = string.Empty); this.WhenAnyValue(x => x.Name) .Select(name => $"Hello, {name}!") .Subscribe(x => Greeting = x); } }
Fody changes the project IL code at compile time. The
PropertyChanged.Fody add-on searches for all classes marked with the
AddINotifyPropertyChangedInterface attribute or implements the INotifyPropertyChanged interface and edits the setters of such classes. You can learn more about how code generation works and what other tasks it can solve, see Andrei Kurosh’s report "
Reflection.Emit. Practice of Use ".
Although PropertyChanged.Fody allows us to write clean and expressive code, outdated versions of the .NET Framework, including 4.5.1 and
older , are no longer supported. This means that you can, in fact, try using ReactiveUI and Fody in your project, but at your own peril and risk, and considering that all the errors found will never be fixed! Versions for .NET Core are supported
by Microsoft support policy .
From theory to practice. Validation of forms with ReactiveUI and PropertyChanged.Fody
Now we are ready to write our first reactive presentation model. Let's imagine that we are developing a complex multi-user system, while thinking about UX and want to collect feedback from our customers. When a user sends us a message, we need to know whether it is a bug report or a suggestion for improving the system, and we also want to group the reviews into categories. Users should not send emails until they have filled out all the necessary information correctly. A presentation model that satisfies the above conditions may look like this:
[AddINotifyPropertyChangedInterface] public sealed class FeedbackViewModel { public ReactiveCommand<Unit, Unit> Submit { get; } public bool HasErrors { get; private set; } public string Title { get; set; } = string.Empty; public int TitleLength => Title.Length; public int TitleLengthMax => 15; public string Message { get; set; } = string.Empty; public int MessageLength => Message.Length; public int MessageLengthMax => 30; public int Section { get; set; } public bool Issue { get; set; } public bool Idea { get; set; } public FeedbackViewModel(IService service) { this.WhenAnyValue(x => x.Idea) .Where(selected => selected) .Subscribe(x => Issue = false); this.WhenAnyValue(x => x.Issue) .Where(selected => selected) .Subscribe(x => Idea = false); var valid = this.WhenAnyValue( x => x.Title, x => x.Message, x => x.Issue, x => x.Idea, x => x.Section, (title, message, issue, idea, section) => !string.IsNullOrWhiteSpace(message) && !string.IsNullOrWhiteSpace(title) && (idea || issue) && section >= 0); valid.Subscribe(x => HasErrors = !x); Submit = ReactiveCommand.Create( () => service.Send(Title, Message), valid ); } }
We label our view model with the
AddINotifyPropertyChangedInterface attribute — in this way, all properties will notify the UI of changes in their values. Using the
WhenAnyValue method, we will subscribe to changes to these properties and will update other properties. The team responsible for submitting the form will remain off until the user completes the form correctly. Let's save our code to the class library, aimed at .NET Standard, and move on to testing.
Unit testing of presentation models
Testing is an important part of the software development process. With tests, we will be able to trust our code and stop being afraid of refactoring it - after all, to check the correctness of the program's work, it will be enough to run the tests and make sure they are successful. An application that uses the MVVM architecture consists of three layers, two of which contain platform-independent logic — and we can test it using .NET Core and the
XUnit framework.
To create mocks and
stubs , the
NSubstitute library is useful to us, providing a convenient API for describing reactions to system actions and values returned by “fake objects”.
var sumService = Substitute.For<ISumService>(); sumService.Sum(2, 2).Returns(4);
To improve the readability of both the code and error messages in our tests, we use the
FluentAssertions library. With it, we will not only not have to remember which argument in Assert. Equal goes with the actual value, and with what the expected one, but our IDE will write the code for us!
var fibs = fibService.GetFibs(10); fibs.Should().NotBeEmpty("because we've requested ten fibs"); fibs.First().Should().Be(1);
Let's write a test for our presentation model.
[Fact] public void ShouldValidateFormAndSendFeedback() {
UI for Universal Windows Platform
Well, now our presentation model has been tested and we are sure that everything works as expected. The process of developing the presentation layer of our application is quite simple - we need to create a new platform-specific Universal Windows Platform project and add a link to the .NET Standard library containing the platform-independent logic of our application. Then it’s easy to declare controls in XAML, bind their properties to the properties of the view model and remember to
specify the data context in any convenient way. Let's do it!
<StackPanel Width="300" VerticalAlignment="Center"> <TextBlock Text="Feedback" Style="{StaticResource TitleTextBlockStyle}"/> <TextBox PlaceholderText="Title" MaxLength="{Binding TitleLengthMax}" Text="{Binding Title, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/> <TextBlock Style="{StaticResource CaptionTextBlockStyle}"> <Run Text="{Binding TitleLength, Mode=OneWay}"/> <Run Text="letters used from"/> <Run Text="{Binding TitleLengthMax}"/> </TextBlock> <TextBox PlaceholderText="Message" MaxLength="{Binding MessageLengthMax}" Text="{Binding Message, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/> <TextBlock Style="{StaticResource CaptionTextBlockStyle}"> <Run Text="{Binding MessageLength, Mode=OneWay}"/> <Run Text="letters used from"/> <Run Text="{Binding MessageLengthMax}"/> </TextBlock> <ComboBox SelectedIndex="{Binding Section, Mode=TwoWay}"> <ComboBoxItem Content="User Interface"/> <ComboBoxItem Content="Audio"/> <ComboBoxItem Content="Video"/> <ComboBoxItem Content="Voice"/> </ComboBox> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition /> <ColumnDefinition /> </Grid.ColumnDefinitions> <CheckBox Grid.Column="0" Content="Idea" IsChecked="{Binding Idea, Mode=TwoWay}"/> <CheckBox Grid.Column="1" Content="Issue" IsChecked="{Binding Issue, Mode=TwoWay}"/> </Grid> <TextBlock Visibility="{Binding HasErrors}" Text="Please, fill in all the form fields." Foreground="{ThemeResource AccentBrush}"/> <Button Content="Send Feedback" Command="{Binding Submit}"/> </StackPanel>
Finally, our form is ready.

UI for Xamarin.Forms
For the application to work on mobile devices running Android and iOS, you need to create a new
Xamarin.Forms project and
describe the UI using Xamarin controls adapted for mobile devices.

UI for Avalonia
Avalonia is a cross-platform framework for .NET that uses the XAML dialect common for WPF, UWP or Xamarin.Forms developers. Avalonia supports Windows, Linux and OSX and is developed by a
community of enthusiasts on GitHub . To work with
ReactiveUI, you must install the
Avalonia.ReactiveUI package.
Let's write the presentation layer on Avalonia XAML!

Conclusion
As we see, .NET in 2018 allows us to write
truly cross-platform software - using UWP, Xamarin.Forms, WPF and AvaloniaUI we can provide support for our application operating systems Android, iOS, Windows, Linux, OSX. The MVVM design pattern and libraries such as
ReactiveUI and
Fody can simplify and speed up the development process, allowing you to write understandable, supported, and portable code. Developed infrastructure, detailed documentation and good support in code editors make the .NET platform more and more attractive for software developers.
If you are writing desktop or mobile applications on .NET and are not yet familiar with ReactiveUI, be sure to pay attention to it - the framework uses
one of the most popular GitHub clients for iOS , the
Visual Studio extension
for GitHub and
Slack for Windows 10 Mobile .
Cycle of articles on ReactiveUI on Habré can be an excellent starting point. Developers on Xamarin will certainly come in handy with the course "
Building an iOS app with C # " from one of the authors of ReactiveUI. You can learn more about the experience of developing on AvaloniaUI
from the article about Egram , an alternative client for Telegram on .NET Core.
The sources of the cross-platform application described in the article and demonstrating the possibilities of form validation with ReactiveUI and Fody
can be found on GitHub . An example of a cross-platform application running on Windows, Linux, macOS and Android, and demonstrating the use of ReactiveUI, ReactiveUI.Fody and
Akavache is also available on GitHub .