Warning! This post is dedicated to highly experimental technologies 😃
If you're into development in the Microsoft ecosystem, I'm sure you're familiar with .NET Core. It's a new framework, built from scratch, to bring all the goodies of the .NET Framework into the new modern world. Unlike the full .NET Framework, which has its roots deeply integrated into Windows, .NET Core is cross-platform, lightweight and easily extensible.
Until today, .NET Core has always been focused on supporting these new requirements. As such, its primary workload has always been web or back-end applications. Thanks to .NET Core, you can build web applications that can be hosted also on Linux servers; or that can be deployed inside a Docker container and easily scaled in complex micro services architectures.
At BUILD 2018 we have announced the next major release of .NET Core, 3.0, which is, without any doubts, the biggest and most ambitious release since the first version. On top of .NET Core 3.0, in fact, we'll be able to build new workloads:
As you can see from the image, for the first time .NET Core will support not just web and back-end applications, but also desktop ones which, until today, have always been part only of the full traditional .NET Framework.
Disclaimer! This doesn't mean that WPF and Windows Forms will become cross-platform and you'll be able to run a Windows desktop application, as it is, also on Linux and MacOS. The UI piece of the two frameworks still has a dependency on the Windows rendering system and, as such, it can't run on platforms which use instead a different visual rendering system.
Which are the main benefits of running a desktop application on top of .NET Core? Essentially performance and side-by-side support.
One of the key investments in .NET Core is around performances. Startup time is much faster and most of the APIs have been rewritten to be fully optimized. If you take a look at the popular TechEmpower website you will notice how, for most of the workloads, ASP.NET Core is much faster than many other popular web development frameworks like NodeJS.
One of the biggest blockers for enterprises to adopt newer versions of the .NET Framework is that it can be installed only at system level and it automatically comes with newer version of Windows. This means that if you have an application which targets .NET Framework 4.5 and you want to update it to take advantage of some of the improvements delivered by .NET Framework 4.7, you are forced to update all the applications (or, at least, to make sure they still run well) at the same time. The reason is that you can't install the .NET Framework 4.7 side-by-side with .NET Framework 4.5, but you have to update the existing 4.5 installation. This isn't a nightmare only for enterprises, but a big blocker also for Microsoft. If you look at the recent history of .NET Framework, you will notice how every upgrade brings mainly fixes and minor improvements. The reason is that, since we need to make sure to maintain backward compatibility, the team can't be agile and evolve the framework with changes that, potentially, can break older apps. Checking new code into the .NET Framework requires a long validation and testing period. You can read some thoughts from the team on this in the following article.
.NET Core, instead, can run side-by-side, with two different approaches:
- You can embed the runtime inside the application. This way you'll be able to deploy the app on any machine, even without the runtime installed, and make sure it will target the specific .NET Core version you have used to build it.
- You can install multiple .NET Core runtimes on the same machine. Unlike with the .NET Framework, you can have on the same machine .NET Core 1.0, .NET Core 2.0, .NET Core 3.0 and whatever .NET Core version will ship in the future. This means that if you deploy an application which runs on .NET Core 2.0, it will effectively leverage the .NET Core 2.0 runtime and not another runtime in backward compatibility mode.
Additionally, you will be able to leverage many of the benefits of the .NET Core ecosystem, like the opportunity to use the command line tools to create and build your projects or to use the improved .csproj format. In the end, .NET Core 3.0 will bring some specific benefits for desktop development, like a better support to high DPI screens or the opportunity to leverage all the UWP APIs.
Let's build our first project
.NET Core 3.0 is still at an experimental stage right now. This means that you won't find a public release, neither beta or stable, but you will have to download one of the daily builds.
You can get the latest one on the GitHub repository, specifically here. This page lists all the latest bits, including the SDK for all the supported platforms. In my case, since I have a Windows 10 PC, I downloaded the Windows x64 installer.
Once you have downloaded and installed the SDK, the easiest way to create a new project is to use the CLI. Visual Studio, in fact, doesn't support creating Windows Forms or WPF projects based on .NET Core yet.
First open File Explorer and create a folder to host your project. Then open a command prompt of your choice, move to the folder you have just created and then execute the following command:
dotnet new wpf
if you want to create a WPF application or:
dotnet new winforms
if you want to create a Windows Forms application. The CLI will create, inside the folder, an empty application using the standard template plus the project file.
For example, this is how the folder looks like when you create a WPF project:
If you want to try the application, you just need to execute the following command:
The project will be compiled and the template application will start:
If you want to start changing the code, you can open the .csproj file with Visual Studio. I strongly suggest you to install Visual Studio Preview. The coding experience is still far from perfect, but the preview version of Visual Studio 15.9 is able to handle .NET Core 3 projects much better than the stable 15.8 release.
The project will look like any other .NET Core project: you will have a Dependencies section, followed by the files of the application. If you expand the dependencies, you will notice how the WPF / Windows Forms framework is nothing more than a NuGet library called Microsoft.DesktopUI.App.
This configuration should help you to understand why WPF or Windows Forms application won't run on other operating systems, unlike ASP.NET Core. The core of the project is indeed a .NET Core application, but the Microsoft.DesktopUI.App library contains specific Windows features.
Right now Visual Studio doesn't fully support yet Windows applications running on top of .NET Core and many features are still missing. For example, you don't have a visual editor for the XAML files. Additionally, the debugging experience is a bit unstable. Sometimes you will see weird errors, sometimes the debugger will crash. For example, if you launch the template application and you press the Exit button, the debugger will hang for a while and then, after a few seconds, it will report the following error:
However, most of the times you should be able to compile and at least launch your application without facing too many issues.
Checking the compatibility of an existing app
As an experiment, I've decided to try to migrate to .NET Core a WPF application I often use for my demos called ExpenseIt. It's part of the official WPF samples collection from Microsoft on GitHub and it's a sample LOB application.
You can find the official WPF repository at https://github.com/Microsoft/WPF-Samples/. Once you have downloaded / cloned the repository, you can find the application under https://github.com/Microsoft/WPF-Samples/tree/master/Sample%20Applications/ExpenseIt.
The first step is to check if the application can be easily ported to .NET Core. For this task we can use a precious tool called .NET Portability Analyzer, which is available as a Visual Studio extension or as a command line tool.
In this post we're going to use the Visual Studio extension, so make sure to install it from the Marketplace.
Then open the ExpenseItDemo.csproj project under the Sample Applications/ExpenseIt/ExpenseItDemo folder of the repository you have previously downloaded / cloned. First we need to add another project to the solution, which contains a custom component leveraged by the main application. Right click on the solution, choose Add -> Existing project and look for the EditBoxControlLibrary.csproj file under the Sample Applications/ExpenseIt/EditBoxControlLibrary folder.
Just to make sure that everything is working as expected, try to compile and run the application. You should see the main window posted in the previous image.
Now we are ready to verify if this application can be indeed ported. Right click on the ExpenseItDemo project and choose Portability Analyzer Settings. Thanks to this window, you'll be able to choose which frameworks of the .NET family you want to target:
In our case we want our application to target .NET Core 3, so make sure to check only 3.0 under the .NET Core section. In my case, I've chosen also to enable .NET Standard, to check eventually how much code I would be able to move to a .NET Standard library.
Press OK, then right click again on the project and choose Analyze Project Portability (with project reference). This way the tool will analyze not only the ExpenseItDemo project, but also the EditBoxControlLibrary one, since it's referenced.
At the end of the process a window will popup with the list of all the reports that you have generated since you have started to use the tool. Open the most recent one and let's take a look at the outcome. Let's start from the first part:
Here we have a summary, which immediately tells us how much code we can reuse. As you can see, our application is perfectly compliant for .NET Core 3.0. The coverage of supported APIs is 100%, both for the main application and the library. This means that we can start our migration without fear!
We can also see that, if we would like to move some code to a .NET Standard library, we can do it but only partially. We would be able to move 33,33 % of the code from the EditControlLibrary project and 53,49 % from the main application.
If we scroll down the report, we can quickly see which are the APIs that aren't supported by .NET Standard:
As you can see, we're basically talking about all the APIs which are deeply rooted into the Windows ecosystem, like visual controls (Button, ComboBox, etc.) or event handlers. .NET Standard libraries, in fact, are meant to work with all the various frameworks of the .NET ecosystem, including cross-platform (Xamarin) or web (ASP.NET Core) and, as such, they can't support platform specific APIs.
However, for our purpose it isn't a blocker. What matter to us is that we get 100% compatibility with .NET Core 3.0, so let's move on!
Migrating the application
The plan of the team is to provide, once .NET Core will ship, some tools to easily migrate an existing Windows Forms or WPF project to .NET Core. However, right now we have to do it in the manual way.
Let's start from the template project we have created before. As first step, let's change the assembly name and the default namespace, in order to match the same of the original application. This way, we won't have to change any namespace in XAML or in code, but we can reuse the existing files as they are.
Right click on the original ExpenseItDemo project and let's look at its configuration:
The easiest way to achieve the same settings is to create a new WPF project inside a folder which name is ExpenseItDemo. This way, when you create the base app with the command dotnet new wpf, it will automatically get the right assembly name and default namespace. Otherwise, if you have created the project in a folder with a different name, just right click on it, choose Properties and make sure to configure the two fields with the value ExpenseItDemo. The main difference from the starting project is that, as Target framework, you will see .NET Core 3.0:
Now, in Solution Explorer, delete the existing App.xaml and MainWindow.xaml files, including the corresponding code-behind files.
Let's start to copy over the content of the EditBoxControlLibrary. For the sake of simplicity, while migrating to .NET Core we will merge everything inside the same project, instead of keeping two different projects. Copy the following content from the original project:
- The Themes folder
- The EditBox.cs file
- The EditBoxAdorner.cs file
Now, in Solution Explorer, expand the Themes folder, right click on the Generic.xaml file and:
- Set Page as Build action
- Remove the keyword MSBuild:Compile from the Custom tool field
This is how the configuration should look like:
If you try to compile the project now, you'll get an error since we have deleted the App.xaml file and, as such, the application doesn't have an entry point anymore.
Let's move to the folder which contains the original ExpenseItDemo project and, this time, copy the following files over the new project:
- The Validation folder
- The App.cs / App.xaml files
- The CreateExpenseReportDialogBox.xaml / CreateExpenseReportDialogBox.cs files
- The ExpenseReport.cs file
- The LineItem.cs file
- The LineItemCollection.cs file
- The MainWindow.xaml / MainWindow.cs files
- The ViewChartWindow.xaml / ViewChartWindow.cs files
- The Watermark.png image
The next step is to right click on each XAML file we have just added and to repeat the same steps we have done before in the Properties panel: set Page as Build action and remove the keyword MSBuild:Compile from the Custom tool field.
The only exception is the App.xaml file, which Build action must be set to Application definition, since it's the entry point of our application.
Lastly, you need to change the properties also of the image we have imported, the Watermark.png file. In this case, you need to set the Build action to Content and the Copy to Output Directory field to Copy if newer.
Now rebuild the project and launch the debugging. The application will normally start. We have fully migrated our solution to .NET Core 3.0!
Some issues, here and there
As anticipated at the beginning of the post, .NET Core 3.0 is still in an early stage and, as such, you can experience some quirks. Here are the two biggest ones I've found so far during the migration of the project.
The first one is about the debugging experience, which isn't perfect yet. Do you remember the error that appeared when we tried to close the base template app? You will get a similar behavior in the ExpenseIt app when you choose a name from the list and you press the Create expense report button. However, it isn't an application problem. If you launch the application without attaching the debugger (by pressing CTRL+F5 in Visual Studio) and you repeat the same steps, the Create expense report window will appear without issues.
The second one is about the CLI, which still doesn't support all the APIs exposed by WPF and Windows Forms. If you try to build or run this project from the command line (using dotnet build or dotnet run) you will get the following error:
PS C:\Users\mpagani\Desktop\WPF> dotnet run C:\Users\mpagani\Desktop\WPF\App.xaml(22,46): error MC3087: Cannot set content property 'XmlSerializer' on element 'XmlDataProvider'. 'XmlSerializer' has incorrect access level or its assembly does not allow access. Line 22 Position 46. [C:\Users\mpagani\Desktop\WPF\WPF.csproj] The build failed. Please fix the build errors and run again.
However, also in this case, it isn't an application problem. If you compile the project through the Visual Studio / MSBuild toolchain, as you have seen, the application builds and starts just fine. Thanks to Oren Novotny for pointing this!
Deploying the application
The best way to try the side-by-side support is to publish the application as self-contained. This means that we'll be able to distribute it on any machine, even if it doesn't have the .NET Core 3.0 runtime installed.
To start the process right click on the project in Visual Studio and choose Publish. Press Start and then open the dropdown at the bottom and choose Create profile instead of Publish immediately.
This way, in the next step, you'll be able to customize the profile by clicking on Configure. Here you'll be able to set the Deployment mode to Self-contained, which means that the output folder will contain also the .NET Core 3.0 runtime.
Save and press Publish. Visual Studio will build the project and, if you have left the default folder, at the end of the process you will find a folder called publish under bin\Debug inside the project's folder.
Now copy it over a clean Windows machine (like a VM), which doesn't have the .NET Core 3.0 SDK or runtime installed, and launch it. You will notice how the application will start without problems. Cool, isn't it?
In this blog post we have moved our first baby steps into the .NET Core 3.0 world for Windows desktop applications. The purpose of this post is mainly to have fun testing a new technology. As you have seen, there are still issues and the debugging experience is far from perfect. .NET Core 3.0 hasn't officially reached yet the preview phase, which means that it's totally not meant for production purposes.
However, if you are a Windows developer, it's a good idea to start familiarizing with .NET Core and, with the help of Portability Analyzer, start to understand how much code from your existing applications can be easily ported to this new platform.
If you want to play with the sample project I've built for this article, you can get it from GitHub.