How to Upgrade a UWP Application to WinUI 3.0

As we get close to preview 4 of WinUI 3.0 (it’s Feb 2021 already so where’s my WinUI preview4!), now’s the time to start looking at how you’re going to upgrade your UWP application to WinUI3.0. In this post we’ll look at a migration approach that will allow you to keep your UWP app running … Continue reading “How to Upgrade a UWP Application to WinUI 3.0”

As we get close to preview 4 of WinUI 3.0 (it’s Feb 2021 already so where’s my WinUI preview4!), now’s the time to start looking at how you’re going to upgrade your UWP application to WinUI3.0. In this post we’ll look at a migration approach that will allow you to keep your UWP app running in parallel to a WinUI 3.0 desktop app.

WinUI Desktop v UWP

Before we get on and look at the migration process, I’m going to point a big bright red flag at the elephant in the room. For those who are waiting for WinUI3.0 in UWP to ship, you’re going to be sorely disappointed. Support for WinUI3.0 in UWP has slipped from a scheduled release into the wish list bucket (see published roadmap). DO NOT hold out for this!

As you can see from the published roadmap WinUI3 in UWP is pushing into the .NET 6 timeframe at earliest. What’s more likely at this stage is for Project Reunion to light up some, if not all, the key UWP features inside of a .NET 5/6 WinUI Desktop application. This is what you should be planning on supporting as you look to migrate your UWP application to WinUI3.0.

The rest of this post will be focused on migrating a UWP application to WinUI3.0 for Desktop.

Migration Isn’t a One-Time Process

One of the features of WinUI that has been touted is that switching from UWP to WinUI is going to be as simple as switching out some namespaces. Well, this clearly isn’t going to be the case since WinUI3.0 for Desktop is missing some core pieces of the UWP app model, such as application lifecycle, so those things won’t easily migrate.

Even if we ignore the features that won’t migrate currently, the proposed approach is a one way migration (eg. find and replace the Windows.UI.Xaml namespaces with Microsoft.UI.Xaml namespaces). Given we’re still a way out from WinUI being released, it’s likely that you’ll have to run this migration process multiple times. In fact, you’ll probably want to run your UWP application in parallel with the new WinUI application whilst you check stability, features, performance etc.

With this in mind, let’s work on the assumption that we need to be able to build and run either, or both, the UWP app and the WinUI app. In order to do this, we’re going to take an existing UWP application, move all the UI elements (pages, resources etc) into a separate multi-targeted library and then add a WinUI Desktop (and packaging) project. This will allow us to pick either, or both, projects for debugging.

Let’s walk through this in more details.

The UWP Application

Starting with the UWP application we’re going to migrate. As you can see it’s a simple application that has two pages, MainPage and SecondPage.

The MainPage has a single button that triggers navigation to the SecondPage.

The SecondPage has a single button that triggers navigation back to MainPage.

Setting up the Solution

We’ll go ahead and add both the WinUI Desktop project (Blank App, Packaged (WinUI in Desktop) project template)

and the multi-targeted library (use the Class Library (WinUI in Desktop) project template) to the solution.

The solution structure should now look like this – you can see that there’s a Universal Windows project (i.e. the UWP target) and a WinUI with Package project (i.e. the WinUI Desktop target – set the (Package) project to be the startup project to debug the WinUI Desktop target).

We need to make sure that both the UWP and the WinUI Desktop projects have a reference to the UI multi-targeted library. The UI library is where all the pages and other XAML resources are going to reside, allowing us to multi-target both UWP (i.e. uap) and WinUI Desktop (i.e. net5.0-windows)

XAML Pages and Resources

The first thing we’re going to do is to move both MainPage and SecondPage (both XAML and XAML.cs files) to the UI project. I also have an implicit Button style that I used to center the buttons on both pages, which was defined in the Application.Resources in App.xaml. I’ll refactor this style into a ResourceDictionary called StyleResources, which I’ll also add to the UI project.

Because the UI project was created as a WinUI Desktop class library, it only has a single target framework defined, being net5.0-windows10.0.18362.0. As we noted earlier, the UI project needs to be multi-targeted to generate builds for both UWP and WinUI. To do this we need to switch to use the MSBuild.Sdk.Extras as the Sdk defined in the Project element in the csproj. This also requires a global.json file with the following content:

{
  "msbuild-sdks": {
    "MSBuild.Sdk.Extras": "3.0.23"
  }
}

Now we can replace the TargetFramework element with TargetFrameworks and specify both uap and net5.0-windows. Here’s the complete csproj file for your review….

<Project Sdk="MSBuild.Sdk.Extras">
  <PropertyGroup>
    <TargetFrameworks>uap10.0.16299;net5.0-windows10.0.18362.0</TargetFrameworks>
    <TargetPlatformMinVersion>10.0.17134.0</TargetPlatformMinVersion>
    <RootNamespace>WinUIMultiTarget.UI</RootNamespace>
    <RuntimeIdentifiers>win10-x86;win10-x64;win10-arm64</RuntimeIdentifiers>
    <IsWinUI>false</IsWinUI>
  </PropertyGroup>

  <PropertyGroup Condition="$(TargetFramework.Contains('net5.0'))">
    <IsWinUI>true</IsWinUI>
  </PropertyGroup>

  <PropertyGroup Condition="$(IsWinUI)">
    <DefineConstants>WinUI</DefineConstants>
  </PropertyGroup>

  <PropertyGroup Condition="$(IsWinUI)==false">
    <TargetPlatformIdentifier>UAP</TargetPlatformIdentifier>
    <TargetPlatformVersion Condition=" '$(TargetPlatformVersion)' == '' ">10.0.19041.0</TargetPlatformVersion>
  </PropertyGroup>

  <ItemGroup Condition="$(IsWinUI)">
    <PackageReference Include="Microsoft.WinUI" Version="3.0.0-preview3.201113.0" />
  </ItemGroup>

  <ItemGroup Condition="$(IsWinUI)==false">
    <PackageReference Include="Microsoft.UI.Xaml" Version="2.5.0" />
  </ItemGroup>

  <ItemGroup>
    <None Remove="MainPage.xaml" />
    <None Remove="SecondPage.xaml" />
    <None Remove="StyleResources.xaml" />
  </ItemGroup>

  <ItemGroup Condition="$(IsWinUI)">
    <Page Update="MainPage.xaml">
      <Generator>MSBuild:Compile</Generator>
    </Page>
    <Page Update="SecondPage.xaml">
      <Generator>MSBuild:Compile</Generator>
    </Page>
    <Page Update="StyleResources.xaml">
      <Generator>MSBuild:Compile</Generator>
    </Page>
  </ItemGroup>

  <ItemGroup Condition="$(IsWinUI)==false">
    <Page Include="MainPage.xaml">
      <SubType>Designer</SubType>
      <Generator>MSBuild:Compile</Generator>
    </Page>
    <Compile Update="MainPage.xaml.cs">
      <DependentUpon>MainPage.xaml</DependentUpon>
    </Compile>
    <Page Include="SecondPage.xaml">
      <SubType>Designer</SubType>
      <Generator>MSBuild:Compile</Generator>
    </Page>
    <Compile Update="SecondPage.xaml.cs">
      <DependentUpon>SecondPage.xaml</DependentUpon>
    </Compile>
    <Page Include="StyleResources.xaml">
      <SubType>Designer</SubType>
      <Generator>MSBuild:Compile</Generator>
    </Page>
    <Compile Update="StyleResources.xaml.cs">
      <DependentUpon>StyleResources.xaml</DependentUpon>
    </Compile>
  </ItemGroup>
</Project>

So it’s worth noting that the logic detects whether the target framework is net5.0-windows and defines a property IsWinUI. This in turn defines a conditional constant, WinUI, which can be used in the code behind to include/exclude code based on whether it’s being built for WinUI or not.

Unfortunately there are some subtle differences in the default includes for XAML and CS files between UWP and WinUI, so the IsWinUI property is used to change how files are included.

Note also that the IsWinUI property is used to selectively include the NuGet package for WinUI3 (i.e. Microsoft.WinUI) or WinUI2 (i.e. Microsoft.UI.Xaml).

For each of the Xaml.cs files, we then need to adjust the import statements to switch between Microsoft.UI.Xaml and Windows.UI.Xaml depending on whether it’s being built for WinUI or not.

#if WinUI
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
#else
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
#endif

And now our UI project is good to go – building this project should generate a build in the uap10.0.16299 and net5.0-windows10.0.18362.0 folders for UWP and WinUI respectively.

Startup Page

The only thing left to do is to update the startup page for both applications.

In the UWP application, we simply need to change the page that is first navigated to in the App.xaml.cs from MainPage to UI.MainPage (the MainPage from the UI project):

rootFrame.Navigate(typeof(UI.MainPage), e.Arguments);

In the WinUI application we need to first add a Frame to the MainWindow, and then when the Frame is loaded, navigate to the UI.MainPage.

<Window
    x:Class="UWPtoWinUIMigration.WinUI.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
    mc:Ignorable="d">
    <Frame Loaded="FrameLoaded" />
</Window>

And the FrameLoaded method that is invoked by the Loaded event on the Frame:

private void FrameLoaded(object sender, RoutedEventArgs e)
{
    (sender as Frame).Navigate(typeof(UI.MainPage));
}

And that’s it – you can now run both applications and they’ll navigate to MainPage, followed by SecondPage. The sample project is available on GitHub for full source code.

Leave a Reply

Your email address will not be published. Required fields are marked *