Tuesday, March 16, 2021

I updated a Xamarin Forms project to Maui

xxx
Tuesday, March 16, 2021

The very early Maui samples are out. I wanted to see if I could create a Xamarin Forms project and then update it to Maui. Here is how it went and what I learned.

For my sample project I just created the blank Xamarin Forms Template.


It’s a very simple app so we won’t be seeing how to update native renderers, custom styles, or anything fun like that. But it’ll give us an early insight into just how different Forms and Maui are.

I’m doing this on an old MacBook Pro that I don’t really use anymore just in case installing dotNet 6 breaks something. So, first I updated Visual Studio Mac to the latest version. Then I created the project from the template and made sure it ran on iOS and Android. Everything looks good.


Now that we’ve got the Xamarin Forms project, let’s get a Maui project.

Preview 1

Preview 1 of dotNet 6 came out a little while ago. I gave it a shot but I couldn’t quite get it working. The sample projects only had iOS, Android, and Forms and I really wanted to play with Maui. Maui was available if you switched to the /dev branch, but when I did that everything stopped building. Even my Xamarin Forms 5 project stop building, so good thing I was using an old laptop! But, this could easily have been something I screwed up.

Preview 2

Fast forward to a couple days ago and Preview 2 has been released. You need to install the Preview 2 version of the dotNet 6 SDK, as well as the Preview 2 Android, iOS, and Mac Catalyst workloads. All are linked on here on the samples page.

Then download the sample themselves (same page) and you’re ready. On Mac, at least, you can’t yet run from the IDE. It’s all terminal based for now. So open a terminal and go to the directory were HelloMaui.csproj resides. Run one of these:

dotnet build HelloMaui -t:run -f net6.0-android --no-restore
dotnet build HelloMaui -t:run -f net6.0-ios --no-restore
dotnet build HelloMaui -t:run -f net6.0-maccatalyst --no-restore

Note that once dotnet bug 15485 is fixed you can drop the --no-restore.

Hello Maui!

Now we have a Maui app.

iOS and MacOS worked great, at first Android showed a blank white screen and then just crashed. Not sure why and I didn’t dig too deep. The next day I tried again and magically it was just working.

Maui on iOS
Maui on MacOS
Maui crashing on Android
Maui working on Android
(the next day)

You can edit the code in Visual Studio / VSCode, but it doesn’t seem to Hot Reload or Hot Restart. I’m assuming this will change once it’s starting from Visual Studio and not the terminal.

You can also just do a build if you want to check your code changes, like this:

dotnet build HelloMaui -t:build -f net6.0-ios --no-restore

Checking On Forms 5

Reopening Visual Studio with my sample Xamarin 5 project, it now compiles and runs just fine. So it seems like it should be safe to install dotNet 6 on your normal work laptop. I mean, I’m not going to. But I’ve got this spare laptop sitting around so I don’t need to. You can also just install it inside a VM of course.

Comparing Forms 5 and Maui

If we compare the Maui project structure to the Forms project structure we can see several difference. The most obvious, of course, is that Maui is using a single project.

Maui is targeting a different framework, so let’s open up the .csproj files and take a look.

Here is the .csproj for Forms:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
    <ProduceReferenceAssembly>true</ProduceReferenceAssembly>
  </PropertyGroup>

  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
    <DebugType>portable</DebugType>
    <DebugSymbols>true</DebugSymbols>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Xamarin.Forms" Version="5.0.0.2012" />
    <PackageReference Include="Xamarin.Essentials" Version="1.6.1" />
  </ItemGroup>
</Project>

And here it is in Maui:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFrameworks>net6.0-android;net6.0-ios</TargetFrameworks>
    <TargetFrameworks Condition=" '$(OS)' != 'Windows_NT' ">$(TargetFrameworks);net6.0-maccatalyst</TargetFrameworks>
    <OutputType>Exe</OutputType>
    <SingleProject>true</SingleProject>
    <ApplicationId>com.microsoft.hellomaui</ApplicationId>
    <ApplicationTitle>MAUI</ApplicationTitle>
    <ApplicationVersion>1.0</ApplicationVersion>
    <AndroidVersionCode>1</AndroidVersionCode>
    <RuntimeIdentifier Condition="'$(TargetFramework)' == 'net6.0-ios'">ios-x64</RuntimeIdentifier>
    <RuntimeIdentifier Condition="'$(TargetFramework)' == 'net6.0-maccatalyst'">maccatalyst-x64</RuntimeIdentifier>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.Maui" Version="6.0.100-*" />
  </ItemGroup>
  <!-- NOTE: these are needed only for Release builds -->
  <ItemGroup Condition=" '$(Configuration)' == 'Release' ">
    <PackageReference Include="System.CodeDom" Version="6.0.0-*" />
    <PackageReference Include="System.Configuration.ConfigurationManager" Version="6.0.0-*" />
    <PackageReference Include="System.Diagnostics.PerformanceCounter" Version="6.0.0-*" />
    <PackageReference Include="System.Diagnostics.EventLog" Version="6.0.0-*" />
    <PackageReference Include="System.Drawing.Common" Version="6.0.0-*" />
    <PackageReference Include="System.IO.Ports" Version="6.0.0-*" />
    <PackageReference Include="System.Threading.AccessControl" Version="6.0.0-*" />
  </ItemGroup>
  <ItemGroup>
    <SharedImage Include="Resources\AppIcons\appicon.svg" ForegroundFile="Resources\AppIcons\appiconfg.svg" IsAppIcon="true" />
    <SharedImage Include="Resources\Images\*" />
    <SharedFont Include="Resources\Fonts\*" />
  </ItemGroup>
</Project>

Looks like we can just copy all of that if we change the ApplicationId.

Also, note that the AppIcon is just a single SVG! Looks like Maui will just automatically take care of generating all the different icon sizes for us.

SingleProject is interesting. This implies we should be able to just keep the existing iOS and Android projects and not worry about merging everything into a single project. However, I tried setting it to false and running dotnet build TestXamarinForms5 -t:build -f net6.0-ios --no-restore and got this error: error : Info.plist not found.. So maybe something else needs to be set. Something to play around with later, I want to try and get everything into one project anyway.

Now let’s look at how we can move the Forms files to make them match up to what Maui would expect. Here, I’ve made this handy, and not at all confusing, graphic:


Let’s break it out into steps. I’m just guessing at what needs to be done based on the differences in folder stucture.

1) Copy the Maui .csproj for the Forms project and rename the ApplicationId

2) App.xaml.cs becomes Application.cs

3) Android

  • AndroidManifest.xml and MainActivity.cs move to the /Android folder
  • Everything in the platform /Resources/Values moves to the Forms /Android/Resources/Values folder
  • We need to create a MainApplication class under the Forms /Android folder

MainApplication.cs

using System;
using Android.App;
using Android.Runtime;
using Microsoft.Maui;

namespace HelloMaui
{
	[Application]
	public class MainApplication : MauiApplication<Application>
	{
		public MainApplication(IntPtr handle, JniHandleOwnership ownerShip) : base(handle, ownerShip)
		{
		}
	}
}

4) iOS

  • AppDelegate.cs, Entitlements.plist, Info.plist, and LaunchScreen.storyboard move to the Forms /iOS folder
  • Main.cs gets renamed Program.cs and moves to the Forms /iOS folder

5) Create a new file MainWindow.cs

MainWindow.cs

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Maui;

namespace HelloMaui
{
	public class MainWindow : IWindow
	{
		public IPage Page { get; set; }
		public IMauiContext MauiContext { get; set; }

		public MainWindow()
		{
			Page = App.Current.Services.GetService<MainPage>();
		}
	}
}

With those steps done the project isn’t building. Comparing the file contents now, and not just the folder structure, and doing some trial and error: here is what I’ve found needs to be done.

6) Delete AssemblyInfo.cs (not 100% sure this is needed, but I did it)

7) In MainPage.xaml.cs remove using Xamarin.Forms, add an inheritance from IPage, and add this function:

public IView View
{
  get => (IView)Content;
  set => Content = (View)value;
}

It’ll end up looking like this.

8) In iOS/AppDelegate.cs replace everything with this (again, don’t forget to change the namespace):

using Foundation;
using Microsoft.Maui;

namespace HelloMaui
{
	[Register("AppDelegate")]
	public class AppDelegate : MauiUIApplicationDelegate<Application>
	{
	}
}

9) In Android/MainActivity.cs same thing:

using Android.App;
using Microsoft.Maui;

namespace HelloMaui
{
	[Activity(Theme = "@style/AppTheme.NoActionBar", MainLauncher = true)]
	public class MainActivity : MauiAppCompatActivity
	{
	}
}

10) As a quick cheat - just copy the /Resources and /MacCatalyst directories from the HelloMaui project. For a real project, you’d need to convert your AppIcon to an SVG and move all your iOS and Android resources here. You’d also need to customize the MacCatalyst info.plist file but we don’t need to go into that now.

11) Since Maui is now generating the app icon, remove it from the iOS/info.plist

<key>XSAppIconAssets</key>
<string>Assets.xcassets/AppIcon.appiconset</string>

12) App.xaml.cs (which is now Application.cs) ended up looking much different. Here is the new format

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Maui;
using Microsoft.Maui.Controls.Compatibility;
using Microsoft.Maui.Hosting;

namespace HelloMaui
{
	public class Application : MauiApp
	{
		public override IAppHostBuilder CreateBuilder() => 
			base.CreateBuilder()
				.RegisterCompatibilityRenderers()
				.ConfigureServices((ctx, services) =>
				{
					services.AddTransient<MainPage>();
					services.AddTransient<IWindow, MainWindow>();
				})
				.ConfigureFonts((hostingContext, fonts) =>
				{
					fonts.AddFont("ionicons.ttf", "IonIcons");
				});

		public override IWindow CreateWindow(IActivationState state)
		{
			Microsoft.Maui.Controls.Compatibility.Forms.Init(state);
			return Services.GetService<IWindow>();
		}
	}
}

13) In the root folder where the .sln file lives add these files: global.json and NuGet.config

global.json

{
    "sdk": {
        "version": "6.0.100-preview.2.21155.3",
        "rollForward": "disable",
        "allowPrerelease": true
    }
}

NuGet.config

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <packageSources>
    <clear />
    <!-- ensure only the sources defined below are used -->
    <add key="dotnet6" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet6/nuget/v3/index.json" />
    <add key="xamarin" value="https://pkgs.dev.azure.com/azure-public/vside/_packaging/xamarin-impl/nuget/v3/index.json" />
    <add key="public"  value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public/nuget/v3/index.json" />
  </packageSources>
  <config>
    <add key="globalPackagesFolder" value="packages" />
  </config>
</configuration>

Now if we run dotnet build TestXamarinForms5 it builds! 🎉

And we we do dotnet build TestXamarinForms5 -t:build -f net6.0-ios --no-restore it runs:

It doesn’t look right, though. At first I thought maybe Maui didn’t support all the components that page was using, but it’s just a StackLayout and labels. The problem is the Maui sample app specifies colors for everything, whereas the default Xamarin Forms template does not. If we simply add a background to MainPage.xaml…

BackgroundColor="Gray"

…then we get a much better output

And now our Forms 5 app (now a Maui app) works on Mac Catalyst:

Android was doing the same thing it did before with the sample Maui app. It would launch a white blank app and then crash. I tried an emulator and a physical device and the same thing. If it’s the same pattern as the sample app then it’ll work tomorrow, but who knows.

Take Aways

This was a very simple test project so there were some things we didn’t explore and are outside the scope of this blog post:

  • What happened to App.xaml? (Where do we define the application wide ResourceDictionary, Converters, Styles, etc.)
  • Where do we put our native renderers?
  • How do application lifecycles work now?

These are the kind of things that will be fun to learn as we play with the previews and hear more from the Maui team.

A lesson learned already: make sure you are explicitily adding colors to all elements now so that you don’t have to add them when Maui comes out. If you’ve already implemented dark mode with AppThemeBinding then you are probably already there.

Real World Maui Updates

When Maui does come out, this won’t actually be needed. There will be an update tool. I imagine there will be some manual tweaking needed, especially for more complex projects, so hopefuly this post has helped to show the differences between the two project types.