Back in 2002, the world was exciting for Windows desktop application developers: Microsoft released the .NET Framework! With it came the completely new language C# (do you remember it was called Cool prior to the release?) as well as various new subsystems. WinForms was the tool of choice, meant to take over from previous efforts by Microsoft (C++/ActiveX/VB based development, as well as MFC) as well as competing dev platforms like Delphi. “Meant to take over”, according to some, at least — as we know, time moved on and things became more complex instead of simpler, and the success of individual platform technologies harder to judge.
On the basis of the .NET Framework, the next important client UI technology was of course WPF, and to this day both WinForms and WPF remain relevant to all devs who create native UI apps for Windows. In fact, these technologies can cross over to platforms other than Windows, for instance by using Wine or Avalonia UI.
Ever since 2001, somewhat ahead of the first public .NET Framework release, DevExpress has produced high quality components for desktop apps. We never stopped these developments, and many of our customers maintain such applications today and want to continue doing so. If you are one of them, this post is for you. If you are curious for any other reason, this post is of course for you as well!
The New Complexity
In the last twenty years, many things have changed about the way software is commonly developed. This is not a bad thing, it means that we learned from mistakes that were made and gained a better understanding of best practices that help us avoid issues we recognized. On the other hand, it takes time and effort to follow such developments and changes, and the practical problem of moving from point A (an existing implementation) to point B (the new and hopefully better plan) is never easy to solve.
In this article, I will begin by providing an overview of technical areas where important changes have happened over the years, and of the support DevExpress offers for the required steps that move your apps into the future. I intend to follow this up in the near future with detailed posts to cover each specific topic.
For this overview, here are the main points:
- The separation of backend and frontend in application systems has changed. Where simple structures like client/server used to be the norm, distributed systems of varying complexity are typical today.
- For data persistence, the number of commonly used options has increased. It now includes NoSQL solutions as well as distributed structures such as Event Sourcing backends.
- UI apps need to take the new architectural concerns into account and work asynchronously, which creates technical challenges for developers.
A Changed Understanding Of Backend And Frontend In Desktop Apps
There was a time when desktop apps did everything themselves: UI, (business?) logic, data storage. These kinds of apps certainly still exist today, though they are usually utility applications rather than full-blown business applications. A new wave of such apps has become ubiquitous on mobile devices, although even those tend to integrate cloud storage and backup mechanisms.
For a typical business application with a desktop focus, a two-tier “client/server” type architecture was the most common structure for a long time.
Some small changes to this architecture arrived a long time ago. For instance, we learned that it wasn’t safe to expose SQL database systems to the internet directly, and this introduced the requirement of either a VPN connection or a safe proxy service. From a logical standpoint however, simple client/server architectures seemed sufficient, and with concepts like the integration of a .NET CLR into SQL Server this approach enjoy lots of support for years.
Things began to change slowly when it became apparent that applications often needed support on the side of the data storage server, in order to implement certain functionality efficiently. Some business logic algorithms can be expensive to run on a client that uses a potentially slow connection to the database server — if millions of roundtrips are necessary, you can either spend lots of time optimizing the algorithm or choose the easy and efficient way of implementing the logic near the data. This idea often resulted in architectures where select services ran in the data server environment, shortening access times, while the direct interaction of the desktop app with the data server remained the same for most other purposes.
Clearly one problem with this type of structure is that various different components need to work together in complicated ways. One important aspect is security: if Alice is logged into the desktop app, how do we ensure that Alice’s data access on the storage side is limited according to business rules, and that all separate services, when they act on Alice’s behalf, are subjected to the same restrictions? Maintenance also became a concern, since various endpoints had to stay in sync with aspects such as ORM based “clever” business objects and event based logic implementations on that level.
A typical “middle tier” architecture was the next evolutionary step to combine all data access and related logic in one place.
This brings us forward to the final step, at least for the time being, which was again based on an obvious maintenance issue with the monolithic middle tier. Functionality was separated into individual services which could be maintained more easily, while the logical structure was kept the same, without direct access to data storage for the frontend app(s). It is important to keep in mind that implementation aspects also moved on in the same timeframe, for instance resulting in loose coupling more often than not, perhaps using REST or gRPC interfaces, which in turn makes such structures very flexible and very open, or at least either open or flexible depending on requirements.
This summary certainly misses a few aspects and details of what happened over many years, but it shall suffice to provide some background. For developers who focus on desktop apps, here are the most important changes to consider:
- Business logic is not primarily implemented in the desktop app itself, but largely in services elsewhere. This may mean that such logic is not our responsibility, but more likely we need to interact with it, possibly augment it (for instance for validation that usually splits, shares and duplicates responsibilities between services and frontend apps).
- Data access requires defined interfaces. It is possible to integrate fully dynamic query scenarios, for devs or even for end users, but since this requires special solutions it should be done only if requested as a business use case. Backend-for-frontend (BFF) endpoints are more efficient at runtime, and they make data access both safer and faster!
- Authentication and authorization of users should happen in cooperation with services. In reality this is easy since it only needs some interaction with a token service on login and logout, and the passing of tokens with all API calls to services.
- To make applications future safe, it is a good practice to separate UI from logic. Patterns exist to support this effort, with MVVM the most common one today. Applying this pattern means that your frontend app can interact with any middle tier or service based backend today, and move on towards whatever changes tomorrow will bring.
In a follow-up post, I will describe in some detail how DevExpress tooling and library functionality helps with the implementation of MVVM patterns for WinForms and WPF, and with the generation of forms required for security system integration.
Data Access
In the past, many desktop applications — and others, such as ASP.NET web apps! — were written with a SQL backend in mind. This approach reflected the basic client/server architecture described above, although solutions with automated synchronization of multiple SQL Server instances, or similar approaches using other RDBMS, were widely used for complex deployment scenarios. Persisting data in relational systems required the usual exercises in normalization, and for many years now ORM products were commonly used in object oriented environments such as .NET (but also Java) to save developers some work.
One development of recent years has been that NoSQL database systems have become more common in all types of software systems. There are many reasons for this, and of course SQL is still available. But the enhanced flexibility of many NoSQL systems, greater scalability, higher availability, and the ease of use from object oriented environments are strong arguments that favor, for instance, document databases. Since many software solutions have backends that run entirely in cloud environments, a good choice of such systems is easily available, and solutions like Microsoft’s Azure Cosmos DB even provide multiple APIs in one cloud product.
Document databases are very useful since they allow OO devs to drop objects into databases in a more “natural” structure than that required by RDMBS, and they resolve all the common issues around data “versioning” in normalized structures at the same time.
In some environments, frontend applications do not interact with data storage directly at all, and they may be completely unaware of the structures used internally. The pattern CQRS is important since it promotes the understanding that there are two separate channels of information flow: the “write side” receives commands with data, for instance new or changed objects, from frontend apps and stores it somewhere. The “read side” receives queries and responds by returning data. The data “models” that are sent and received by frontend apps are process-specific and don’t necessarily represent either objects used in the frontend app, or persistent structures used by the backend.
Event Sourcing is a separate pattern that is often used in conjunction with CQRS, and its main effect on the architectural structure is that data services tend to be even more granular, data models more purpose-specific. Typically, “backend for frontend” (BFF) data models are used in this context, often in Microservices deployments, which makes for great maintainability and fantastic performance.
DevExpress data aware components on all UI platforms support
data binding through interface layers. .NET has its own such
layers, of course, whether you consider
BindingList<T>
(commonly used in WinForms) or
ObservableCollection<T>
(more typical for WPF), or the interface based architecture of
LINQ with its Expression Trees and
IQueryable<T>
. Since many DevExpress data-bound controls have specific
runtime querying requirements, they support their own
translation systems from user-space interactive UI features to
server-side data queries using LINQ or any custom
implementation.
To make the component data binding features easier to use, wizards help you create bindings for many ORM and API based scenarios. Additionally, the Backend Web API Service or XAF Middle Tier Security provides a ready-made service application project for data access and security, accessible from any .NET application as well as web or Blazor projects.
As above, these are the most important changes to keep in mind for desktop app developers:
- A shift towards object-based data persistence may remove the need to model data in a desktop application. If you are responsible for all parts of your application system, you may need to implement such models elsewhere.
- The same shift can also mean that relational concerns, and/or ORM, are not part of the picture anymore.
- In teams where complex backend architectures are built, this work will usually be separate from frontend development. If you specialize in the frontend work, you may be presented with API requirements which have no recognizable data persistence aspects.
- You can expect DevExpress UI components to supply APIs that allow you to interface with application backends — in some cases this may be fully automatic, in other cases you will need to build adapters.
I will publish a follow-up post that details the technical features in DevExpress components to support binding to any backend data architecture.
Asynchronous Frontend Apps
Technically, even client/server apps are asynchronous — or at least they can be. The same is true for apps that work locally and store data only in the file system. The question is, what does the app do while it’s waiting for data? It can either block execution, so that the user can’t do anything else — displaying a modal status bar is the same thing from a user’s perspective! — or it can remain “live”, allowing the user to keep working, or to potentially cancel a running operation.
The big difference is that with filesystem operations the potential wait time is often measured in milliseconds or less. With database operations it depends where the server is, how many clients it serves, whether it’s well optimized for the query at hand, ... it may return quickly or it may not, and there is an unfortunate number of apps out there which simply make the user wait until there’s a response, as if devs can’t do any better!
Yes, we can do better, and this becomes more and more important the more distributed the systems are with which a frontend app communicates. We can’t rely on ... anything, really. When a query is sent, it can receive its response immediately — but it may also take a while, or it may never return, and we need to deal with that. This sounds like a challenge, and it is, but it’s also an opportunity to deliver apps that are more responsive for the user, allow work on multiple jobs at the same time, are open to users to define their own workflows.
The technical side may be quite simple, as long as we
understand C# features including
async
and
await
, and the use of
Task<T>
. There are of course other aspects to multi-threading in UI
apps:
here’s a starting point for WinForms
and
a similar one for WPF. On top of all that, it may be important to consider the
architecture of the overall application system — it could be
one where calls are made through HTTP(S) and receive responses
in the near future, but it may also be message based so that
queries and responses are completely decoupled.
Needless to say, DevExpress UI controls can work with any scenario summarized above. A further future post will include detailed samples to illustrate how you can create modern desktop apps that take full advantage of distributed architectures and deployments of modern software.