Converting the WeatherTwentyOne app from dotnet Maui to Uno

As the dotnet Maui team continue to churn out each preview, it’s great to see some of the showcase apps coming together. One of these is the WeatherTwentyOne app that David Ortinau has been working on. Whilst I’ve been doing a lot of work with the Uno platform recently, I haven’t been spending as time … Continue reading “Converting the WeatherTwentyOne app from dotnet Maui to Uno”

As the dotnet Maui team continue to churn out each preview, it’s great to see some of the showcase apps coming together. One of these is the WeatherTwentyOne app that David Ortinau has been working on. Whilst I’ve been doing a lot of work with the Uno platform recently, I haven’t been spending as time with Maui. As such, I wanted to take this opportunity to see what would be involved in migrating the WeatherTwentyOne app from dotnet Maui to WinUI (Windows App Sdk) and Uno. This post covers various aspects of migrating Maui XAML across to WinUI and Uno XAML. Most of the points apply equally to migrating a Xamarin.Forms application to WinUI and Uno.

Running the WeatherTwentyOne App

Before I started the migration process I wanted to make sure I could build and run the WeatherTwentyOne app. Specifically I wanted to run it in both phone and desktop mode as I was already aware that there were some subtle layout differences based on the form factor.

To get things running, the process was relatively simple

Note there are still some bugs with the single project tooling – I had to unload and reload the main WeatherTwentyOne project in order for the tooling to show me the full list of items in the run dropdown.

You can run the iOS and Android applications by setting the WeatherTwentyOne project as the startup project, and you can run the Windows (WinUI for desktop) application by setting the WeatherTwentyOne.WinUI project as the startup project. I would imagine that these will eventually be merged into a single project.

Creating the Uno Solution

For the purpose of this migration I’m going to start with the WinUI solution template provided by the Uno team (Install dotnet template, then run “dotnet new unoapp-winui”). From there I’m going to add a UWP target following the instructions in my post, How to Upgrade a UWP Application to WinUI 3.0. This will NOT give me a UWP app running WinUI3, it will give me a UWP app running WinUI2.x. I’m adding this to show that it’s possible to use all the same techniques to port a maui app to both WinUI3 and backport to WinUI2.x running on UWP.

Application Structure

The first things we’re going to do in migrating across all the functionality of the WeatherTwentyOne Maui app is to copy all the views, logic etc into our Uno app. This includes the Converters, Models, Pages, Resources, Services, ViewModels and Views folders, which we’ll copy into the Shared project of our Uno app. Do this using Windows Explorer so that we can control which files are included in the application – if you try to include them all at once you’ll just have way to many build errors to deal with.

Pages and Views

Next we’re going to add all the pages and views into the application (from the Pages and Views folders). Each of these items includes a XAML file and a code behind. As you add each item, you’ll of course start getting a bunch of build errors – the XAML for Maui follows a different structure than for UWP/WinUI/Uno.

So that we can maintain a running app, and avoid a thousand build errors, my suggestion is that for each item (i.e. the pair of XAML and code files), you include them in the Shared project (right-click the item and select Include in Project). For each page (i.e. root element is ContentPage) you’ll need to change the root element from ContentPage to Page (don’t forget the closing tag too), for example:

Maui
<ContentPage
    xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:m="clr-namespace:WeatherTwentyOne.Models"
    xmlns:v="clr-namespace:WeatherTwentyOne.Views"
    xmlns:ios="clr-namespace:Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific;assembly=Microsoft.Maui.Controls"
    ios:Page.UseSafeArea="True"
    Title="Redmond, WA"
    x:Class="WeatherTwentyOne.Pages.HomePage">

WinUI
<Page
    x:Class="WeatherTwentyOne.Pages.HomePage"
    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">

For other views, you’ll need to wrap the root element with a UserControl element, for example (note that in this case I haven’t ported the “StackLayout” yet, this code just illustrates wrapping using a UserControl):

Maui
<StackLayout xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             Spacing="15"
             x:Class="WeatherTwentyOne.Views.CurrentWidget">


WinUI
<UserControl
    x:Class="WeatherTwentyOne.Views.CurrentWidget"
    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">
    <StackLayout Spacing="15">

In both cases, make sure that the root element defines the x:Class attribute and that namespaces are only defined on the root element (and not any child element in the page or view).

As you go, I recommend commenting out the code that you’ve replaced so that you’ve still got it there as a reference – this will be useful later to debug any UI layout related issues.

Rather than attempting to fix up all the XAML as you go, I first focus on making sure that the architecture of the app is working. By this I mean being able to run the application and navigate to each of the pages/views. To do this, we’ll comment out all the XAML in each of the pages/views, except the root element. This should quickly get us to the point where we have all the XAML views included and that the application should be able to be built and run.

In the code behind files for the pages and views, you’ll need to remove any Maui related namespaces, remove the inherited base class from the class definition (this isn’t required since the base class is defined in XAML) and then comment out any logic that doesn’t immediately build. You may feel that you’re being overly aggressive with commenting out code but it’s significantly quicker to start with a building application, than attempting to fix each build error in order.

The default app template from Uno uses MainPage as the start page for the application, whereas the WeatherTwentyOne app starts at HomePage. You’ll need to adjust the logic in App.xaml.cs to navigate to HomePage initially, and then you can remove MainPage.

ViewModels, Services and Models

Before we go further in enabling parts of the UI by uncommenting elements on the pages and view, we need to include the logic of the application. For this we’re going to include the contents of the ViewModels, Services and Models folders. Again, if you come across code that doesn’t compile, comment it out and we’ll come back later and fix up the code. The parts I had to comment out at this point were:

  • FavoritesViewModel – comment out the Microsoft.Maui.Controls namespace that isn’t used
  • SettingsViewModel – comment out Microsoft.Maui.Controls namespace and the SelectUnits property
  • ServiceProvider – comment out this class – we’ll discuss how we do service provision later in the post

At this point you should be able to run the application on each of the Uno platforms. Unfortunately, it’ll just display a blank page but hey, it’s progress!

Migrating XAML

Ok, so this is going to be the painful bit – we have to methodically go through each page and enable each XAML element. Some elements, for example a Grid, will translate smoothly, since the spec for a Maui Grid is similar to that of WinUI. There are other controls, such as the StackLayout or the ScrollView, that you’ll need to exchange with the WinUI equivalent, which is a StackPanel and ScrollViewer (don’t confuse the WinUI StackLayout, which is designed for rendering items inside of a list based control, with the Maui StackLayout which is more similar to the StackPanel control). There are some controls that don’t have an exact translation, so you’ll need to go with something similar. For example there is no FlexLayout in WinUI but you should be able to a WrapPanel to achieve similar results (the WrapPanel comes from the CommunityToolkit for WinUI).

A lot of the attributes for Maui XAML elements also map across to the same attributes in WinUI XAML. There are still some anomalies, such as HeightRequet and WidthRequest, where you’ll need to adjust the name of the attribute (in this case to just Height and Width). Of course, there are other attributes, particularly when you changing the XAML element, that won’t translate.

My approach to this process is to start with the first page of the app, the HomePage, and gradually uncomment the XAML. Once you’ve uncommented the XAML on the HomePage, you can move onto to some of the views that are embedded on the HomePage, for example the CurrentWidget and WindLiveWidget.

As you go, periodically build and run the application – unfortunately just because the XAML may compile, doesn’t necessarily mean it will run without throwing a runtime exception. Furthermore the exceptions that are raised are often unhelpful “something XAML related” style errors – the best way to resolve these exceptions is to backtrack to the last XAML that ran successfully and incrementally change the XAML until you find the culprit.

OnPlatform and OnIdiom

As you go through converting XAML elements from Maui XAML to WinUI XAML you’ll come across both OnPlatform and OnIdiom override values for some elements. We’re going to handle these in different ways:

  • OnPlatform – this translates nicely to the Uno platform specific XAML. You’ll need to import the platform namespace (eg xmlns:ios=”http://uno.ui/ios”) and then specify the platform specific attribute (eg ios:Margin=”0,70,0,0″)
  • OnIdiom – this should be handled using visual states with triggers based on the width and/or orientation of the device. We’ll come back to implementing these visual states a bit later on – I suggest for the initial pass to simply set the default value (eg ColumnDefinitions=”{OnIdiom Phone=’‘, Default=’68,,480′}” would convert to just ColumnDefinitions=”68,,480″)

Images

The WeatherTwentyOne app has a number of images that are included in the application, for example icons that reflect the various weather conditions. These are located in the Resources/Images folder. This folder can be included in your project, but you’ll need to make sure that the Build Action for each image (mostly svg files) is set to Content, to ensure the image will be packaged alongside your application.

Referencing images in XAML can be done by providing the path to the image file based on the location within the package (eg ms-appx:///Resources/Images/weather_partly_coudy_day.svg)

Styles

In the WeatherTwentyOne app, some styles have been applied using the Xamarin.Forms/Maui class attribute (instead of using the normal Style property). To convert this to WinUI XAML, change the “class” attribute to “Style” and make sure that the value is referenced as a static resource. For example class=”Title1″ changes to Style=”{StaticResource Title1}”.

When you encounter the first style reference, you’ll need to include the DefaultTheme XAML and code behind, which includes all the Styles. Then you’ll of course need to fix up the XAML, starting with the root node, for example:

Maui
<?xml version="1.0" encoding="utf-8" ?>
<ResourceDictionary xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
                    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" 
                    xmlns:app="clr-namespace:WeatherTwentyOne.Resources.Styles"
                    x:Class="WeatherTwentyOne.Resources.Styles.DefaultTheme"
                    xmlns:android="clr-namespace:Microsoft.Maui.Controls.PlatformConfiguration.AndroidSpecific;assembly=Microsoft.Maui.Controls">


WinUI
<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    x:Class="WeatherTwentyOne.Resources.Styles.DefaultTheme">

Again, my recommendation with the DefaultTheme is to comment out the majority of the XAML and only enable the styles that you come across. There are some implicit styles which we’ll need to enable at the end in order to fix up the layout. Fixing a style will often involve correcting the TargetType (eg changing Label to TextBlock) and making sure the style has a key (eg changing Class=”Title1″ to x:Key=”Title1″)

In order for the DefaultTheme to be used within the application it needs to be loaded when the application starts. To do this, add the DefaultTheme element to the Resources defined in App.xaml

<Application
    x:Class="WeatherTwentyOne.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:styles="using:WeatherTwentyOne.Resources.Styles">
	<Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
                <styles:DefaultTheme />
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Application.Resources>
</Application>

At this point, where I’ve uncommented the XAML in HomePage and the first couple of elements in CurrentWidth, I’m starting to see the UI appear for my application.

A couple of other needed adjustment to styles.

  • There is an implicit Style for the Page type. WinUI won’t pick up implicit styles for the Page element, so this needs to be changed to an explicit style by specifying the x:Key attribute (eg x:Key=”PageStyle”). Each page will need to explicitly reference this style.
  • The Maui application defines implicit styles for some elements (eg Label) and then uses the class attribute to override any attributes for particular elements. The equivalent way to do this in WinUI is to define an explicit base style (eg BaseTextBlockStyle) and an implicit style that is based on it (eg <Style TargetType=”TextBlock” BasedOn=”{StaticResource BaseTextBlockStyle}”/>). Any other Style for that element should also be based on the explicit base style. After making this change you will find that some of the Styles have keys that conflict with the color resources. This can be fixed by appending a suffix, eg TextBlockStyle to make them unique – don’t forget that you’ll need to adjust the value of the Style attribute that references these styles.

SVG Images

The Maui application includes images in SVG format. However, in the XAML it refers to them as PNGs. As part of the transition to XAML we’ll change the image file name to refer to the actual svg image. However, as you may have noticed in the early screenshot, part of the svg is being cut off. This is because the svg has a fixed height and width.

To fix this, for each SVG we’ll replace the static width and height attributes with a preserveAspectRatio. We can do a bulk find and replace in Visual Studio across all the svg files included in the project

Animations

The wind indicator is artificially animated in code in the WeatherTwentyOne application to simulate what the actual wind indicator might look like. To convert this, rather than define the animation in code, we’re going to define the animation in XAML using a Storyboard and then invoke the storyboard from code. The Maui code calls the RotateTo function periodically to adjust the wind direction

Needle.RotateTo(WindValues[direction], 200, Easing.SpringOut);

In WinUI there are three parts to creating this animated effect. Firstly, we need to define a RotateTransform on the Image element itself.

<Image x:Name="Needle"
Source="ms-appx:///Resources/Images/compass_needle.svg" >
    <Image.RenderTransform>
        <RotateTransform CenterX="100" CenterY="100" Angle="0"/>
    </Image.RenderTransform>
</Image>

Next, we need to define the Storyboard that will drive the animation. Note here that I’ve picked the BackEase predefined easing function – you can pick whichever easing function you think is closest to the SpringOut easing function from Maui.

<Storyboard x:Name="NeedleRotation">
    <DoubleAnimation x:Name="NeedleAnimation"  
                    Storyboard.TargetName="Needle" 
                    Storyboard.TargetProperty="(Image.RenderTransform).(RotateTransform.Angle)">
        <DoubleAnimation.EasingFunction>
            <BackEase EasingMode="EaseOut"/>
        </DoubleAnimation.EasingFunction>
    </DoubleAnimation>
</Storyboard>

Lastly we need to trigger the Storyboard to run. We’re also going to modify the angle (represented as the To value on the NeedleAnimation) and the Duration (to make this animation slightly more random).

NeedleAnimation.To = WindValues[direction];
NeedleAnimation.Duration = TimeSpan.FromMilliseconds(rand.Next(100, 300));
NeedleRotation.Begin();

It appears that we’ve had to add a lot more code to do the similar animation, which for this simple animation is the case. There are two important reasons why this method is preferable: Firstly, that the animation, is declared in XAML along with your layout. Secondly, a single Storyboard can define multiple animations which can be useful when you need to represent more complex animations. UWP developers may be familiar with Blend which allows you to design our your animations, abstracting away the complexity of defining a Storyboard in XAML.

StringFormat

In the Next24HrWidget the data binding makes use of the StringFormat argument to modify the way the data is displayed. I want to show a couple of different options so that you can make your mind up as to which you prefer.

The first is used to convert a DateTime into a string value (eg StringFormat='{0:h tt}’). For this, we’re going to expose an additional property on the underlying data source that is the formatted date eg

public string FormattedDateTime => DateTime.ToString("h tt");

The other alternative is to use a value converter to apply the string format – this is a more generic option allowing you to define the string formatting in XAML, similar to the way it is done in Maui. Here’s the basic code for a StringFormatConverter.

public class StringFormatConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, string language)
    {
        if (value == null)
            return null;

        if (parameter == null)
            return value;

        return string.Format((string)parameter, value);
    }

    public object ConvertBack(object value, Type targetType, object parameter, string language)
    {
        throw new NotImplementedException();
    }
}

And using this in XAML

Note that you need to define the StringFormatConverter as a resource either at the application, page or usercontrol level. I’ve defined it as a resource for the Next24HrWidget for the timebeing, until I need it elsewhere, in which case I will move it to be an application resource.

BindableLayout

The Next23H4Widget makes use of a BindableLayout to define a sequence of hourly forecasts. This translates nicely to a ItemsRepeater in WinUI XAML, as shown in this example.

Maui
<StackLayout
    BindableLayout.ItemsSource="{Binding Hours}">
    <BindableLayout.ItemTemplate>
        <DataTemplate>
            <StackLayout>
            ......
            </StackLayout>
        </DataTemplate>
    </BindableLayout.ItemTemplate>
</StackLayout>


WinUI
<ScrollViewer 
    ScrollViewer.HorizontalScrollBarVisibility="Auto"
    ScrollViewer.HorizontalScrollMode="Enabled"
    ScrollViewer.VerticalScrollMode="Disabled" >
    <ItemsRepeater 
    ItemsSource="{Binding Hours}">
        <ItemsRepeater.Layout>
            <StackLayout x:Name="VerticalStackLayout" Orientation="Horizontal" Spacing="12"/>
        </ItemsRepeater.Layout>
        <ItemsRepeater.ItemTemplate>
            <DataTemplate>
                <StackPanel>
                 ........ 
                </StackPanel>
            </DataTemplate>
        </ItemsRepeater.ItemTemplate>
    </ItemsRepeater>
</ScrollViewer>

IValueConverter

There are four value converters that are included in the WeatherTwentyOne app which can be included in the WinUI application. There is just a slight change that’s required because of a subtle difference in the IValueConverter interface. The following code shows the IValueConverter interface for the two platforms. The only difference is the final parameter which is CultureInfo for Maui and a string for WinUI.

Maui
    public interface IValueConverter
    {
        object Convert(object value, Type targetType, object parameter, CultureInfo culture);
        object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture);
    }

WinUI
    public interface IValueConverter
    {
        object Convert(object value, Type targetType, object parameter, string language);
        object ConvertBack(object value, Type targetType, object parameter, string language);
    }

The final parameter isn’t used by any of the converters in this application, so it’s just a matter of changing the method signature.

XAML Array of Items

The WidgetsPanel makes use of an array of items as the ItemsSource for a BindableList. Defining an array of items in XAML (eg <x:Array Type=”{x:Type m:Metric}”> ) isn’t supported in WinUI, so this needs to be moved to codehind, similar to the other sets of data that are exposed by the HomeViewModel.

ImageButton

Both the WidgetsPanel and the NavBarView make use of the ImageButton control. This doesn’t exist in WinUI. However, it’s easy to recreate such a control by specifying a ContentTemplate for a regular Button.

<DataTemplate x:Key="ImageButtonContentTemplate">
    <Image Stretch="Uniform" 
        Source="{Binding}" 
        VerticalAlignment="Center" 
        HorizontalAlignment="Center"/>
</DataTemplate>

To use this template with a Button you need to specify the path to the image as the Content and the template as the ContentTemplate.

<Button 
    Content="ms-appx:///Resources/Images/add_icon.svg" 
    ContentTemplate="{StaticResource ImageButtonContentTemplate}" />

OnIdiom and VisualStates

So far we’ve covered off most of the significant changes you’ll need to make to convert Maui XAML to WinUI XAML – you’ll just need to go through each XAML page and apply these changes in sequence. However, there’s one point that I said we’d come back to, and that’s dealing with the OnIdiom modifiers that you’ll see in the Maui XAML. For example on the HomePage the root Grid has different column and row definitions for the Phone versus Default idioms.

<Grid
    Margin="{OnPlatform iOS='0,70,0,0', Default='0'}"
    ColumnDefinitions="{OnIdiom Phone='*', Default='68,*,480'}"
    RowDefinitions="{OnIdiom Phone='*,68', Default='*'}">

Since WinUI doesn’t have the notion of OnIdiom, the best way to implement this adaptive layout is using visual states. Here’s the equivalent visual states for the HomePage – I’ve only included the property setters for the root grid for clarify.

<Grid
    x:Name="RootGrid"
    Margin="0"
    ios:Margin="0,70,0,0"
    ColumnDefinitions="68,*,480"
    RowDefinitions="*">
    <VisualStateManager.VisualStateGroups>
        <VisualStateGroup>
            <VisualState x:Name="Narrow">
                <VisualState.StateTriggers>
                    <AdaptiveTrigger MinWindowWidth="0"/>
                </VisualState.StateTriggers>
                <VisualState.Setters>
                    <Setter Target="RootGrid.ColumnDefinitions" Value="*"/>
                    <Setter Target="RootGrid.RowDefinitions" Value="*,68"/>
                </VisualState.Setters>
            </VisualState>
            <VisualState x:Name="Wide">
                <VisualState.StateTriggers>
                    <AdaptiveTrigger MinWindowWidth="720"/>
                </VisualState.StateTriggers>
            </VisualState>
        </VisualStateGroup>
    </VisualStateManager.VisualStateGroups>

This is another of the situations where the WinUI XAML is much more verbose than the Maui equivalent. However, these visual states will eventually include the changes for all elements that need to adapt between different layouts. Defining these layout changes in XAML also makes it possible for tooling to assist with the design process. For example Blend (for UWP only at the moment) offers the ability to switch into each state and adjust the layout in the designer.

Summary

As you can see from the following image in this post we’ve come a long way to porting the WeatherTwentyOne app across to WinUI/Uno.

You can also see from this image that there is still some work to be done to tidy up the alignment, sizing and to make sure it all works cross platform with Uno. I’ll be covering the progress in a future post and I’ll share a link to the source code once I’ve completed the migration.

One thought on “Converting the WeatherTwentyOne app from dotnet Maui to Uno”

Leave a Reply

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