Converting a Windows .NET 6 WPF application to a Windows on Arm (WoA) application requires setting up the development environment and making some changes to the application to make it run on WoA without the emulator.
This article is a follow-up to Today’s Best Practices for Migrating Windows Apps to WoA.
In this article, we will port a sample WPF application to the WoA environment, using C# as our programming language, so some knowledge of C# and WPF/XAML will be necessary.
We will use Visual Studio 2019 as the IDE. You should know how to create a project, class, and WPF forms.
In the previous article we saw how WoA can execute almost every .NET application. It uses an x86-emulation layer to execute programs that are not compiled for the Aarch64 processor. Even though the emulation layer is made as performant as possible, we noticed that applications that are natively compiled for WoA can be up to 11 times faster.
Microsoft tried to squeeze WPF into the .NET 5 framework for Arm, but they could not finish it in time, so we created a UWP application that runs on WoA in native mode.
In .NET 6, WPF for Arm will be included, so let us see how we can use it.
Note: We will be using the most recent beta version available at the time of writing, so there will probably be some changes in the released version.
The Test Application
We will use a test application that will tackle a well-known problem: the Travelling Salesman Problem (TSP).
The main purpose of the application is to be able to perform speed benchmarks between the emulated version and the native .NET 6 WPF on Aarch64. So we need something CPU intensive, and preferably a bit graphical as well to test the capabilities of WPF in .NET 6.
To solve the TSP in a brute force way requires calculating all the possible paths (combinations) between all the points. This is still feasible with approximately 12 points, but then it becomes very slow.
To calculate all the possible paths between N points requires calculating N! paths. For example finding the optimal path between 20 points would require 20! (2,432,902,008,176,640,000) calculations. Even on a very fast computer, we will not live long enough to see the result.
We clearly need another solution. There are several possibilities, but we will implement a genetic algorithm (GA) to find the best local solution for this problem. The actual implementation is beyond the scope of this article. Most of how I have implemented it is described at Travelling Salesman - Genetic Algorithm. You can find the source code for the application at GVerelst/TravelingSalesman: Find the best path using a genetic algorithm.
These are the important points about the application:
- The "Generate Path" button generates a new random path. By default, the path will have 20 points.
- The population size determines the length of the population for the genetic algorithm. By default, it is set to 5,000. This means that for each iteration
- The distance of 5,000 paths is calculated.
- The 5,000 paths are ordered by their length.
- 5,000 new paths are generated by using crossover and mutation algorithms.
- The best path is displayed on the canvas.
- The random seed is used for the pseudo random generator. Starting from the same number will always generate the same sequence of numbers. This way we can better compare the times to complete the algorithm.
There is no error handling whatsoever in this version. Entering something non-numeric into one of the fields or starting the calculation without first generating the path will crash the program. If you want to verify for numeric input, check out the following Stack Overflow article: C# WPF How to use only numeric value in a textbox - Stack Overflow.
Here is the result of the algorithm after 1,000,000 calculations:
This is not guaranteed to be the best possible path, but it has been heavily optimized and will most likely be an acceptable solution.
The initial application is compiled for .NET 5, so it will not run on an Arm device.
Making the Application Run on an Aarch64 Device
Luckily for us, Microsoft is now implementing WPF in .NET 6. At the time of writing, version 6.0.0-preview.3 was available.
Installing Visual Studio 2019 directly on Windows on Arm is not supported yet and it is a faster dev | deploy | debug cycle to use an existing X64 developer machine
It would be possible to run Visual Studio on Windows on Arm, but it will run in x86-emulation mode. This is too slow to work comfortably, so we use an x64 Windows machine.
In my case, I am using a B4ms virtual machine in Microsoft Azure with 4 CPU cores, 16GB of RAM, 8 maximum disks attached (far more than we will need), and 2880 IOPS disk throughput to ensure good performance:
To prepare the development machine, first update Visual Studio to the latest version (version 16.11 or higher). The easiest way is to use the Visual Studio Installer. Then install the .NET 6 SDK.
If you want to use the Preview functionality in Visual Studio 2019, you need to explicitly say so. In the Options dialog (Tools > Options), enter "Preview" into the search field, then select Preview Features in the tree on the left. Search for "Use Preview of the .NET Core SDK" and select the checkbox. A restart of Visual Studio is required.
When you reopen Visual Studio, you will find .NET 6.0 in the dropdown for the target framework.
Set the target framework to .NET 6.0 in both the WPF and the GA projects, and optionally in the test project as well. Now we are ready to recompile the application.
This is a WPF application using the Model-View-ViewMode (MVVM) pattern, with some graphics (drawing the path) and a Background Worker thread. So it is not the most basic WPF application that is possible. Yet, the build does not give us any errors or warnings.
It runs without any problems, finding the resulting path in approximately the same time as the .NET 5 version of the program. This looks promising for the final version of the framework!
Before we can build and run an Aarch64-native version of our application, we first need to install .NET 6 preview on an Arm device, which you can download here. We don’t even need to restart the device after it is installed.
Note that if you already have an x86 .NET Core or .NET 5 SDK installed, the 64-bit Arm .NET SDK won’t overwrite the other SDK’s PATH variables – at least as of the most recent .NET 6 preview. This may change later in the .NET 6 release cycle. To check after you’ve installed the .NET 6 preview, run dotnet --info in a terminal. You’ll see something like this:
PS C:\> dotnet --info
.NET SDK (reflecting any global.json):
OS Name: Windows
OS Version: 10.0.19042
OS Platform: Windows
Base Path: C:\Program Files (x86)\dotnet\sdk\5.0.100\
Check the RID field. If it says win10-arm64, you’re good to go and you can skip to the Compiling for Aarch64 section below. If not, you’ll need to update your system path to include the arm64-native .NET SDK. To do this, open the Windows Settings app, and search for "environment variables":
Verify the Path variable in the User variables by double-clicking the variable:
Make sure you have an entry that reads C:\Program Files\dotnet and if you see an entry that reads C:\Program Files(x86)\dotnet, delete it. Now, if you run
dotnet –info, you should see:
PS C:\Users\gvere> dotnet --info
.NET SDK (reflecting any global.json):
OS Name: Windows
OS Version: 10.0.19042
OS Platform: Windows
Base Path: C:\Program Files\dotnet\sdk\6.0.100-preview.3.21167.6\
Host (useful for support):
This might have only been a problem on my device, but if you run into the same problem, you now know what to look for! I was not the only one with this issue, if you want more information check this: Windows can’t find the latest installed .NET SDK - Stack Overflow.
Compiling for Aarch64
Here are the final steps to get everything working.
On the development machine open the TSP.WPF project and add the <RuntimeIdentifiers> element as follows:
<ProjectReference Include="..\TSP.GA\TSP.GA.csproj" />
Next, we need to create a publish profile:
- Right-click the TSP.WPF project and select Publish…
- For the target, choose Folder and click Next.
- For the specific target, choose Folder as well and click Next.
- Select a location for the binaries to be published. Normally the defaults are fine. If you use a shared folder to deploy to your device, you can select that folder here so you can skip copying the files.
- Click Finish to create the initial publish profile.
- Under More options click Edit. Set the values as below and click Save.
- Click Publish.
Visual Studio now compiles a version of our project that is natively executable on Aarch64 devices:
Copy this folder to your device and run the application. Now it’s running in 64-bit native mode on the Arm device, using WPF on .NET 6!
The time to find the same path that we used before is now 1 minute and 23 seconds, which is about the same as the development VM. We can once again say running .NET 6 an Aarch64 device is quite performant.
There were some additional steps needed to build the project for WPF on .NET 6, but there were no code changes needed.
Installing .NET 6 on a 64-bit Windows on Arm machine was straightforward aside from the problem with the path. Make sure you check for this if you’re running both the win10-x86 and win10-arm64 .NET SDKs.
Running the WPF application natively only required a few changes in the .csproj files, and publishing it with the right Target runtime selected. Once this is done, the application runs without problems!
Although .NET 6 is still in preview, WPF support already works well. As we have seen, it is relatively simple to take an existing x86/x64 WPF application and run it natively on an Aarch64-based Windows machine.
You will need to use caution if your WPF application calls native C/C++ libraries via PInvoke or uses NuGet packages that wrap C/C++ libraries. If you do, you will need Aarch64 builds of these libraries to run your application natively.
If you run into issues, this thread is a good place to start looking for assistance.
For more information about .NET 6, check out Announcing .NET 6 Preview 3 | .NET Blog (microsoft.com).