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 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.
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
.
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.
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
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.
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
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
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.
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:
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.
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.