Architecting .NET Desktop and Mobile applications

Posted by: Damir Arh , on 10/15/2020, in Category Patterns & Practices
Views: 556620
Abstract: This tutorial introduces several architectural and design patterns that can be used to implement common scenarios in .NET desktop and mobile applications.

Desktop and Mobile Applications – Architectural Similarities and Differences

Although it might not seem so at the first glance, desktop and mobile applications have a lot in common which is the reason why I decided to cover the application architecture for both types of applications in the same article.

Desktop applications used to be always connected fat clients which implemented all the business logic locally and directly communicated with the data store (typically a relational database management system, such as SQL Server).

Today, most of them are much more like mobile applications:

  • Instead of having direct access to the data store, they communicate through intermediary services which can be securely exposed over public networks.
  • A significant part of the business logic is implemented in the services so that it can be shared between different client applications (desktop, mobile and web). Local processing in desktop applications is often limited only to local data validation, presentation and other highly interactive tasks that benefit from quick response times.
  • They don’t rely on being always connected to services. Public networks are not as reliable as internal ones. Increasingly, applications are expected to work on laptops in locations where there might be no connectivity at all.

If you look carefully, haven’t these been the properties of mobile applications since their beginnings?

On the other hand, the performance of mobile devices is getting better and better. Today, they are mostly at par with desktop and laptop computers, allowing mobile applications to do local processing that’s comparable to desktop applications.

The most notable differences between the two application types today are the size of the screen (typically smaller for mobile apps) and the input methods (typically mouse and keyboard for desktop applications, and touch for mobile applications). These mostly affect the application user interface, not its architecture.

In addition to that, at least in the .NET ecosystem, the technologies and frameworks used to develop desktop and mobile applications are very similar, and parts of them, are at times the same.

Are you new to Architecting .NET Applications? Make sure to read this guide

Application Architecture – Getting started

Decoupling application code from UI

Loose coupling is a key property of well-architected applications. When dealing with the user interface, this means that no application code must directly depend on the user interface. Instead of being placed in the user interface code-behind files, it should be organized in standalone classes that can be fully used and tested without the user interface.

The architectural patterns used to achieve that depend on the UI framework.

In this article, I’m going to focus on the MVVM (model-view-view model) pattern which works well with all XAML-based UI frameworks: WPF (Windows Presentation Foundation), UWP (Universal Windows Platform) and Xamarin.Forms (cross-platform UI framework for Xamarin mobile applications).

You can read more about other UI frameworks for desktop and mobile applications in two of my DNC Magazine articles:

Developing Desktop Applications in .NET

Developing Mobile Applications in .NET

As the name implies, the MVVM pattern distinguishes between three types of classes:

  • Models implement the domain logic and are in no way affected by the application user interface.
  • Views implement the application user interface and consist of the declarative description of the user interface in the XAML markup language and the imperative code in the corresponding code-behind file. Depending on the UI framework, these are either Window or Page classes.
  • View models are the intermediate classes that orchestrate the interaction between models and views. They expose the model functionality in a way that can be easily consumed by the views through data binding.

mvvm-pattern-classes

Figure 1: Types of classes in the MVVM pattern

By default, bindings in the view refer to properties of the object assigned to a control property named DataContext (or BindingContext in Xamarin.Forms). Although each UI control has its own DataContext property, the value is inherited from the parent control when it isn’t set.

Because of this, the DataContext property only needs to be set at the view level when using MVVM. Typically, a view model object should be assigned to it. This can be done in different ways:

Declaratively in XAML:

<Window.DataContext>
    <local:MainWindowViewModel />
</Window.DataContext>

In the view code-behind file in the constructor:

this.DataContext = new MainWindowViewModel();

In the external code constructing the view:

var view = new MainWindow();
view.DataContext = new MainWindowViewModel();

With the view model set as the view’s DataContext, its properties can be bound to properties of controls, as in the following example with InputValue and InputDisabled view model properties:

<TextBox Text="{Binding InputValue}" IsReadOnly="{Binding InputDisabled}" />

The control will read the values when it initializes. Typically, we want the values to be re-read when they change in the view model. For this to work, the view model must implement the INotifyPropertyChanged interface, so that the PropertyChanged event is raised whenever a property value changes:

public class MainWindowViewModel : System.ComponentModel.INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    private void OnPropertyChanged([CallerMemberName] string name = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
    }

    private string inputValue;
    public string InputValue 
    { 
        get => inputValue;
        set
        {
            var oldValue = inputValue;
            inputValue = value;
            if (oldValue != value) OnPropertyChanged();
        }
    }

    // ...
}

The binding subscribes to the event and updates the control when the event is raised for the bound view model property.

Bindings can either be one-way (i.e. controls only display the value) or two-way (i.e. controls also write values that changed because of user interaction back to the view model). Each control property has its own default value for binding mode which can be changed in the binding declaration:

<TextBox Text="{Binding InputValue, Mode=TwoWay}" IsReadOnly="{Binding InputDisabled}" />

Apart from displaying and modifying values of the view model properties, the view also needs to invoke methods on the view model (e.g. when a button is clicked). This can be achieved by binding to a command that invokes that method.

<Button Command="{Binding ClearCommand}">Clear</Button>

The command exposed by the view model must implement the ICommand interface:

public MainWindowViewModel()
{
    ClearCommand = new DelegateCommand(_ => Clear());
}

private void Clear()
{
    InputValue = string.Empty;
}

In the view model above, the DelegateCommand class accepts an action to specify the view model method to be invoked. The code below is a simplistic implementation of such a DelegateCommand:

public class DelegateCommand : ICommand
{
    public event EventHandler CanExecuteChanged;
    private Action<object> Action { get; }
    public bool CanExecute(object parameter) => true;
    public void Execute(object parameter) => Action(parameter);

    public DelegateCommand(Action<object> action) 
    {
        Action = action;
    }
}

Not all components support binding to a command for every type of interaction. Many only raise events which can’t be directly bound to a command.

An example of such a control is the ListView which only raises an event when the selected item changes. Behaviors (originally introduced by Microsoft Expression Blend design tool for XAML) can be used to bind events to commands. In WPF, the InvokeCommandAction from the Microsoft.Xaml.Behaviors.Wpf NuGet package can be used today:

<ListView ItemsSource="{Binding ListItems}">
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="SelectionChanged">
            <i:InvokeCommandAction Command="{Binding SelectionChangedCommand}" />
        </i:EventTrigger>
    </i:Interaction.Triggers>
</ListView>

There’s one more type of interaction between the UI and the view model: opening other windows (or pages). This can’t be achieved through binding because it doesn’t affect an existing UI control but requires a new view to be created instead, e.g.:

var view = new SecondWindow();
view.DataContext = new SecondWindowViewModel(InputValue);
view.ShowDialog();

The above code would have to be placed in a command action to be executed in response to an event – for example a button click. This allows it to pass a value from the current window (i.e. the view model itself) to the new window as a parameter of the new view model constructor. Although this works, it requires the view model to directly interact with the view (i.e. create a new view instance corresponding to the new window) which the MVVM pattern is trying to avoid.

To keep decoupling between the two classes, a separate class is typically introduced to encapsulate the imperative interaction with the UI framework. This would be a minimalistic implementation of such a class:

public class NavigationService
{
    public void OpenDialog<TWindow, TViewModel>(object parameter)
        where TWindow: Window, new()
        where TViewModel: IViewModel, new()
    {
        var view = new TWindow();
        var viewModel = new TViewModel();
        viewModel.Init(parameter);
        view.DataContext = viewModel;
        view.ShowDialog();
    }
}

The view model could then simply call the method on the NavigationService class with correct arguments (view and view model types, view model parameter):

NavigationService.OpenDialog<SecondWindow, SecondWindowViewModel>(InputValue);

To get rid of the only remaining coupling between the View Model and the View (i.e. the type of the view to open), there’s usually a convention on how to determine the type of the view from the type of the view model. In our case, the view type could match the view model type with its ViewModel postfix removed. This would allow the view to be instantiated using reflection:

public void OpenDialog<TViewModel>(object parameter)
    where TViewModel: IViewModel, new()
{
    var viewModelType = typeof(TViewModel);
    var viewTypeName = viewModelType.FullName.Replace("ViewModel", string.Empty);
    var viewType = Assembly.GetExecutingAssembly().GetType(viewTypeName);

    var view = (Window)Activator.CreateInstance(viewType);
    var viewModel = new TViewModel();
    viewModel.Init(parameter);
    view.DataContext = viewModel;
    view.ShowDialog();
}

Now, a window can be opened without being referenced in the originating view model at all. Only its view model is required since the Navigate class finds the corresponding view based on a naming convention:

NavigationService.OpenDialog<SecondWindowViewModel>(InputValue);

Although there’s a lot of interaction between the view and the view model, all of it is in the same direction. View is fully aware of the view model, but the view model is not aware of the view at all.

view-viewmodel-interaction

Figure 2: Interaction between the view and the view model

As you can see, there’s some plumbing code involved to implement the MVVM pattern in an application that could easily be shared between applications.

This is where MVVM frameworks come into play.

They include all the necessary plumbing so that you don’t have to develop your own. Although they also have their own conventions which you will need to adopt. The most popular MVVM frameworks are (in no particular order): Caliburn.Micro, MvvmCross, MVVM Light, and Prism.

 

Dependency injection

To decouple the view model from the UI framework, we moved the code that interacts with the UI framework into a separate class. However, the newly created NavigationService class for that purpose is still instantiated inside the view model:

public NavigationService NavigationService { get; set; } = new NavigationService();

This makes the view model fully aware of it. The property has a public setter on purpose so that the NavigationService class could be replaced with a mock or a different implementation in unit tests, but this doesn’t really solve the issue.

To make it easier to replace the NavigationService class in tests, we should first introduce an interface, that the NavigationService class and any potential replacement classes will implement:

public interface INavigationService
{
    void OpenDialog<TViewModel>(object parameter)
        where TViewModel : IViewModel, new();
}

Now, we can change the type of the NavigationService property to the INavigationService interface so that the replacement implementations can also be assigned to it:

public INavigationService NavigationService { get; set; } = new NavigationService();

One last step to fully decouple the view model from the NavigationService class is to avoid creating the instance inside the view model:

public INavigationService NavigationService { get; set; };

Instead, we will create the instance of the NavigationService in the code responsible for creating the instance of the view model and assign it to the property there:

var viewModel = new MainWindowViewModel();
viewModel.NavigationService = new NavigationService();

This pattern is called dependency injection because the dependencies (the NavigationService class in our case) of a class (the view model in our case) are injected into that class from the outside so that the class doesn’t depend on the concrete implementation.

The approach of injecting the dependencies through property assignment is called property injection. A more common approach is to inject the dependencies as constructor parameters instead. This is called constructor injection:

private readonly INavigationService navigationService;

public MainWindowViewModel(INavigationService navigationService)
{
    this.navigationService = navigationService;

    // ...
}

In a typical view model, a class responsible for navigating between the views will not be the only injected dependency. Other common dependencies to be injected can be grouped into the following categories:

  • Domain layer dependencies, such as classes responsible for remote service calls and communication with the data store.
  • Application diagnostics services, such as logging, error reporting and performance measurement.
  • OS or device-level services, such as file system, camera, time, geolocation, and similar.

Also, view models aren’t the only classes with external dependencies that should be injected so that the class doesn’t directly depend on other concrete implementations. The same approach is used for most classes, even for services that are injected into the view model. For example, a class responsible for calling a specific remote service could depend on a more generic class responsible for performing HTTP calls which in turn could depend on a logging class.

class-dependencies-architecture

Figure 3: Hierarchical nature of class dependencies

There are libraries available to make the process of dependency injection in an application easier to manage. In addition to automatically providing all the required dependencies when a class is initialized, they also include features for controlling the lifetime of an instance of a dependency.

The common choices for the lifetime of a dependency are:

  • A new instance can be created every time a dependency is required.
  • The same instance could be used throughout the application as long as it is running, making the class effectively a singleton (you can read more about singletons in my DNC Magazine article: Singleton in C# – Pattern or Anti-pattern).
  • Other custom lifetimes can be defined, e.g. for a duration of a save operation to ensure transactional consistency in the data store.

There’s an abundance of dependency injection libraries available for .NET. Their APIs are slightly different, but they all have the same core feature set. Based on the NuGet download statistics, the most popular ones at the time of writing were: Autofac, Unity, and Ninject.

Since dependency injection is an integral part of creating new instances of view models which the MVVM frameworks are responsible for, all MVVM frameworks include a built-in dependency injection framework which you can use. In Prism, for example, you would register your dependencies and view models with calls to its own dependency injection container instance:

containerRegistry.RegisterSingleton<INavigationService, NavigationService>();

This configuration would then automatically be used when instantiating the view model classes for views. All the necessary plumbing for that is provided by the framework.

You can read more about dependency injection in general in Craig Berntson’s DNC Magazine article Dependency Injection – SOLID Principles.

Communicating with remote services

Many of desktop and mobile applications today primarily communicate with a remote service to get and manipulate data. Most commonly these are REST (representational state transfer) services which communicate over the HTTP protocol.

To interact with these services, the client-side code typically uses wrapper methods that map to individual REST endpoints and hide the details of the underlying HTTP requests. This is an implementation of the proxy design pattern, a remote proxy to be exact.

Usually, a single class will contain methods for all endpoints of a single REST service. Each method will serialize any parameters as query parameters or request body and use them to make an HTTP request to the remote service URL endpoint using an HTTP client.

The response received from the remote service will usually be serialized in JSON format. The proxy method will deserialize it into local DTO (data-transfer object) classes.

rest-service-remote-proxy-communication

Figure 4: Communication with a REST service using a remote proxy

These wrapper methods are very similar to each other and are very error-prone to write because of the repetitive code they contain. This makes them a good candidate for code generation.

If a REST service is documented using the OpenAPI specification (originally called Swagger specification), there are several tools available for doing that:

  • OpenAPI Generator is the official highly configurable command line tool with support for many programming languages and runtime environments. It has 4 different built-in templates for generating C# code and supports custom templates as well.
  • AutoRest is Microsoft’s command line tool which can also generate code for multiple programming languages. However, it is much more focused on the Microsoft ecosystem and has very good documentation for the generated C# code.
  • NSwag only supports C# and TypeScript code generation but can also generate OpenAPI specification files for ASP.NET Web API services. Its primary use case is to generate both the server-side specification files and the client-side proxy classes, allowing it to better support C# and .NET specifics.

Of course, all tools generate not only the client-side proxies but also the DTO classes describing the data returned by the remote service. Especially in cases when the REST service is maintained by a different team or company and changes frequently, automated code generation with these tools can save a lot of time.

In applications which not only display data from remote services but also allow data manipulation, validation of data input is an important topic.

At minimum, the remote services will validate any posted data and return potential validation errors they encounter. These validation errors can then be exposed to the views through the view models so that they are presented to the user.

However, a lot of validations that are done by the remote service could also be performed on the client before the data is sent to the remote service (e.g. to check if a piece of data is required or if it matches the requested format).

Of course, the remote service would still have to repeat the same validations because it can’t rely on the data sent from the clients. But the users would nevertheless benefit from shorter response times for errors which are detected without a roundtrip to the remote service.

When implementing data validation, the visitor pattern is often used. This allows the validation logic to be kept separate from the data transfer objects which is beneficial to keep the concerns separate.

It also makes it easier to have multiple different types of validators for the same type of data, based on how it is going to be used.

data-validation-visitor-pattern

Figure 5: Data validation implemented using the visitor pattern

The validator interface will have the role of the visitor with a Validate method accepting the data transfer object to validate and returning the validation result including any potential validation errors:

public interface IValidator<T>
{
    ValidationResult Validate(T data);
}

To strictly follow the visitor pattern, the object being validated could have its own Validate method accepting a concrete validator. To keep validation decoupled from the data transfer object, this method could also be implemented as an extension method:

public static ValidationResult Validate<T>(this T data, IValidator<T> validator)
{
    return validator.Validate(data);
}

But such a method is not required, and a validator can be efficiently used without it:

var validator = new FormValidator();
var result = validator.Validate(formData);

Although validation doesn’t require as much plumbing code as some of the other concerns covered earlier in the article, a good third-party library can still make a developer’s life easier.

The most popular library for validation in the .NET ecosystem is FluentValidation. Its main advantages are a well-thought-out API and a large collection of built-in rules to be used in your validators.

Conclusion

In this article, I have described common architecture patterns to be used in desktop and mobile applications.

I started with the role of the MVVM pattern in decoupling the application code from the user interface. In the next part, I explained how dependency injection can further help to decouple the view model classes from the rest of the application logic.

I concluded with patterns used for interaction with remote data services: the remote proxy for communicating with the remote service and the visitor pattern for implementing local data validation. I accompanied each pattern with popular libraries and tools in the .NET ecosystem which can make their implementation easier.

This article was technically reviewed by Yacoub Massad.

This article has been editorially reviewed by Suprotim Agarwal.

Absolutely Awesome Book on C# and .NET

C# and .NET have been around for a very long time, but their constant growth means there’s always more to learn.

We at DotNetCurry are very excited to announce The Absolutely Awesome Book on C# and .NET. This is a 500 pages concise technical eBook available in PDF, ePub (iPad), and Mobi (Kindle).

Organized around concepts, this Book aims to provide a concise, yet solid foundation in C# and .NET, covering C# 6.0, C# 7.0 and .NET Core, with chapters on the latest .NET Core 3.0, .NET Standard and C# 8.0 (final release) too. Use these concepts to deepen your existing knowledge of C# and .NET, to have a solid grasp of the latest in C# and .NET OR to crack your next .NET Interview.

Click here to Explore the Table of Contents or Download Sample Chapters!

What Others Are Reading!
Was this article worth reading? Share it with fellow developers too. Thanks!
Share on LinkedIn
Share on Google+

Author
Damir Arh has many years of experience with software development and maintenance; from complex enterprise software projects to modern consumer-oriented mobile applications. Although he has worked with a wide spectrum of different languages, his favorite language remains C#. In his drive towards better development processes, he is a proponent of Test-driven development, Continuous Integration, and Continuous Deployment. He shares his knowledge by speaking at local user groups and conferences, blogging, and writing articles. He is an awarded Microsoft MVP for .NET since 2012.


Page copy protected against web site content infringement 	by Copyscape




Feedback - Leave us some adulation, criticism and everything in between!