Skip to content
Home » Basics of .NET MAUI – Part 13 – Platform Differences

Basics of .NET MAUI – Part 13 – Platform Differences

Spread the love

In the preceding parts of the series we were creating the GUI for our Slugrace application and it looks pretty decent. At least on Windows. In the previous part we created a CollectionView that displayed items consisting of a border, an image and a button with a number taken from the collection. It looked OK. But what about other platforms? Let’s test the app on Android. Make sure the TestPage is set as the starting page because we’re going to check this page first. Also, make sure you run the app on Android. This is what it looks like:

It doesn’t look bad, but it’s not perfect. For example, the borders have been cut off at both ends. This is because we set their WidthRequest property to 500, which is too much to fit on the screen. We could set it to a different value, like 300, but then it would look too small on Windows. Fortunately, we don’t have to pick just one platform to look good and neglect the others. We can set the properties to different values depending on which platform we’re using. To do that, we can use the OnPlatform markup extension.

There is one more markup extension that may come in handy, OnIdiom. This one enables us to set the properties to different values depending on the device idiom (whether it’s a phone, a tablet, a desktop, and so on). Let’s have a look at the OnPlatform markup extension first.

The source code for this article is available on Github.

The OnPlatform Markup Extension

We use the OnPlatform markup extension to define different values for different platforms. This markup extension has a couple properties that we use to set values for particular platforms. The most important properties are: Android, iOS, MacCatalyst and WinUI. You set them to values that are to be applied on the particular platforms. So, let’s say we want the WidthRequest property to be set to 500 like before, but only on Windows. On Android devices it should be set to 300. This is how we do it:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage ...>
    <CollectionView Margin="50">
        <CollectionView.ItemsSource>
            ...
        </CollectionView.ItemsSource>
        <CollectionView.ItemTemplate>
            <DataTemplate>
                <Border 
                    HeightRequest="100" 
                    WidthRequest="{OnPlatform WinUI=500, Android=300}">
                    <Grid>
                        ...

Now, if you run the app on Windows, it will look like before. But on Android devices the borders will be narrower:

There is one more property that we’re going to use a lot, Default. As the name suggests, it’s used to set a default value for all platforms except those which are explicitly specified. Our app is going to be deployed to Windows and Android, so we can use the Default property for Windows and explicitly specify the values for Android. Let’s rewrite the code then:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage ...>
    ...
                <Border 
                    HeightRequest="100" 
                    WidthRequest="{OnPlatform Default=500, Android=300}">
                    <Grid>
                        ...

As Default is the content property and it’s the first property, we can skip the name:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage ...>
    ...
                <Border 
                    HeightRequest="100" 
                    WidthRequest="{OnPlatform 500, Android=300}">
                    <Grid>
                        ...

This notation is more concise and I’m going to use it in the app. So, the default property will be used for Windows.

Before we actually start using the OnPlatform markup extension in our app, let’s have a look at a slightly different approach.

The OnIdiom Markup Extension

We’re not going to use it in our app, but it’s good to know that there is another markup extension, OnIdiom. It enables us to specify different values for different idioms. The most important properties correspond to the available idioms, so we have: Phone, Tablet, Desktop, TV and Watch. Let’s demonstrate it on the HeightRequest property:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage ...>
    ...
        <CollectionView.ItemTemplate>
            <DataTemplate>
                <Border 
                    HeightRequest="{OnIdiom Desktop=100, Phone=80}" 
                    ...
                    <Grid>
                        ...

Now the height of each item in the CollectionView will be 100 on desktop devices and 80 on phones. Here’s what it looks like on a phone:

There’s also the Default property, which works the same way as with the OnPlatform extension, so we could rewrite the code like this:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage ...>
    ...
        <CollectionView.ItemTemplate>
            <DataTemplate>
                <Border 
                    HeightRequest="{OnIdiom 100, Phone=80}" 
                    ...
                    <Grid>
                        ...

And now we can start to use the OnPlatform markup extension in our app to modify the appearance of the pages and content views on Android. We assume the values we set before are the default ones, so the ones that will be applied to Windows.

SettingsPage on Android

Let’s start with the SettingsPage and the PlayerSettings content view it contains. It looks OK on Windows, but if you run it on Android, you may be disappointed:

Half the stuff we should see here is missing. Let’s fix this. We’re going to modify three files, SettingsPage.xaml, PlayerSettings.xaml and App.xaml. Actually, let’s start with the last one. In App.xaml our global styles are defined. The first thing that maybe you have noticed in the screenshot above are the whitish squares inside the radio buttons. We see them because the BackgroundColor property of the inner circle inside the Unchecked visual state is set:

<?xml version = "1.0" encoding = "UTF-8" ?>
<Application ...>
    ...
            <ControlTemplate x:Key="RadioButtonTemplate">                
                ...
                    <VisualStateManager.VisualStateGroups>
                        ...
                                <VisualState x:Name="Unchecked">
                                    <VisualState.Setters>
                                        <Setter TargetName="check" Property="BackgroundColor" Value="#F3F2F1" />
                                        <Setter TargetName="check" Property="Opacity" Value="0" />
                                    ...

We don’t need it, so let’s just remove the whole line of code.

Still in the App.xaml file, let’s think about the styles that we apply to the particular elements in the SettingsPage and elsewhere. We’re not going to change the FontSize property of the labels because we’re going to change the text, but let’s reduce the FontSize property of the Entry objects. The text should be smaller on Android:

<?xml version = "1.0" encoding = "UTF-8" ?>
<Application ...>
    ...
            <Style TargetType="Entry">
                <Setter Property="BackgroundColor" Value="{StaticResource normalEntryColor}" />
                <Setter Property="FontSize" 
                        Value="{OnPlatform {StaticResource defaultFontSize}, Android=12}" />
                <Setter Property="CharacterSpacing" Value="1.5" />
                ...

On Windows the default value of 18 will be used.

Next, let’s have a look at the PlayerSettings.xaml file. We’ll make some modifications there:

<?xml version="1.0" encoding="utf-8" ?>
<ContentView ...>
    ...       
    <Grid
        RowDefinitions="*"         
        Margin="0, 10">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="{OnPlatform 150, Android=80}" />
            <ColumnDefinition Width="{OnPlatform 3*, Android=2.5*}" />
            <ColumnDefinition Width="10" />
            <ColumnDefinition Width="2*" />
        </Grid.ColumnDefinitions>

        <Label>
            <Label.Text>
                <OnPlatform x:TypeArguments="x:String">
                    <On Platform="WinUI" Value="Player Name" />
                    <On Platform="Android" Value="Player X" />
                </OnPlatform>
            </Label.Text>
        </Label>  
      
        <Entry 
            x:Name="nameEntry"
            Grid.Column="1"            
            WidthRequest="{OnPlatform 300, Android=130}"
            TextChanged="OnNameTextChanged">
        </Entry>

        <Label
            ...
        <Entry
            x:Name="initialMoneyEntry"                       
            Placeholder="1000" 
            Grid.Column="3" 
            WidthRequest="{OnPlatform 250, Android=100}"
            TextChanged="OnInitialMoneyTextChanged">            
        </Entry>
    </Grid>
</ContentView>

We can see some interesting changes here. Let’s have a look at them going from top to bottom.

So, we defined the ColumnDefinitions using property elements and setting different widths to the first two columns on Android. The last two columns will be the same on both platforms.

We also used property element syntax for the label below. We wouldn’t be able to set the Text property otherwise. Here you can see a different syntax for specifying platform-dependent values. We use the OnPlatform and On objects where we set the values for Windows and Android. As you remember, we don’t use quotation marks inside curly braces, so we must do it like this. We mustn’t forget to set the x:TypeArguments property to the type of the data.

Finally, we’ll make the two entries narrower on Android by setting their WidthRequest property to platform-specific values.

And now let’s have a look at a fragment of the SettingsPage.xaml file:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage ...>    
    ...    
    <Grid                
        Margin="{OnPlatform 10, Android=2}">             
        <Grid.RowDefinitions>
            <RowDefinition Height="{OnPlatform 40, Android=30}" />
            <RowDefinition Height="2.5*" />
            <RowDefinition Height="1.5*" />
            <RowDefinition Height="50" />
        </Grid.RowDefinitions>

        <!--the Settings label-->
        <Label 
            ...
            FontSize="{OnPlatform 24, Android=20}" />

        <!--the Players panel-->
        ...
            <Grid>
                <Image 
                    Source="all_slugs.png"                    
                    Aspect="{OnPlatform Fill, Android=AspectFill}"                    
                    Opacity=".5"/>

                <VerticalStackLayout VerticalOptions="{StaticResource defaultLayoutOptions}">                                                  
                    <Label                        
                        Style="{StaticResource labelSectionTitleStyle}">
                        <Label.Text>
                            <OnPlatform x:TypeArguments="x:String">
                                <On Platform="WinUI" Value="The Players" />
                                <On Platform="Android" Value="How many players?" />                                
                            </OnPlatform>
                        </Label.Text>
                    </Label>                    

                    <HorizontalStackLayout>
                        <RadioButton                             
                            GroupName="players">
                            <RadioButton.Content>
                                <Label>
                                    <Label.Text>
                                        <OnPlatform x:TypeArguments="x:String">
                                            <On Platform="WinUI" Value="1 player" />
                                            <On Platform="Android" Value="1" />
                                        </OnPlatform>
                                    </Label.Text>
                                </Label>                                
                            </RadioButton.Content>
                        </RadioButton>
                        ...
                    </HorizontalStackLayout>

                    <Grid
                        RowDefinitions="*"
                        ColumnDefinitions="150, 3*, 2*">               
                        
                        <Label
                            Grid.Column="1" 
                            Style="{StaticResource labelBaseStyle}">
                            <Label.Text>
                                <OnPlatform x:TypeArguments="x:String">
                                    <On Platform="WinUI" Value="Name (max 10 characters)" />
                                    <On Platform="Android" Value="Name" />
                                </OnPlatform>
                            </Label.Text>
                        </Label>
                        <Label
                            Grid.Column="2" 
                            Style="{StaticResource labelBaseStyle}">
                            <Label.Text>
                                <OnPlatform x:TypeArguments="x:String">
                                    <On Platform="WinUI" Value="Initial Money ($10 - $5000)" />
                                    <On Platform="Android" Value="Money" />
                                </OnPlatform>
                            </Label.Text>
                        </Label>
                    </Grid>  
                    ...
        <!--the Ending Conditions panel-->
        <Border
            Grid.Row="2">
            <VerticalStackLayout VerticalOptions="{StaticResource defaultLayoutOptions}">
                <Label
                    Text="Ending Conditions"
                    Style="{StaticResource labelSectionTitleStyle}" 
                    Margin="0, 0, 0, 10"/> 
                <Grid
                    RowDefinitions="*, *, *"
                    ColumnDefinitions="4*, 2*">
                    <RadioButton
                        IsChecked="True"
                        GroupName="endingConditions">
                        <RadioButton.Content>
                            <Label>
                                <Label.Text>
                                    <OnPlatform x:TypeArguments="x:String">
                                        <On Platform="WinUI" Value="The game is over when there is only one player with any money left." />
                                        <On Platform="Android" Value="last player left" />
                                    </OnPlatform>
                                </Label.Text>
                            </Label>                            
                        </RadioButton.Content>
                    </RadioButton>                                      
                    ...
                    <Entry
                        x:Name="maxRacesEntry"
                        Grid.Row="1"
                        Grid.Column="1" 
                        WidthRequest="{OnPlatform {StaticResource entryWidth}, Android=100}"
                        HorizontalOptions="Start"                         
                        Opacity="{StaticResource invisible}"
                        IsEnabled="False"                        
                        TextChanged="OnMaxRacesTextChanged">
                        <Entry.Placeholder>
                            <OnPlatform x:TypeArguments="x:String">
                                <On Platform="WinUI" Value="Set max number of races (1-100)" />
                                <On Platform="Android" Value="1-100 races" />
                            </OnPlatform>
                        </Entry.Placeholder>
                    </Entry> 
                    ...

So, again, let’s have a look at some of the changes.

First, we set platform-specific values for the Margin property on the main Grid. We also use property element notation for the RowDefinitions to make the first row different on each platform:

<Grid                
    Margin="{OnPlatform 10, Android=2}">             
    <Grid.RowDefinitions>
        <RowDefinition Height="{OnPlatform 40, Android=30}" />
        <RowDefinition Height="2.5*" />
        <RowDefinition Height="1.5*" />
        <RowDefinition Height="50" />
    </Grid.RowDefinitions>

We also want the image in the Players panel to be displayed differently:

<Image 
    Source="all_slugs.png"                    
    Aspect="{OnPlatform Fill, Android=AspectFill}"                    
    Opacity=".5"/>

Also in the Players panel, we want a different text to be displayed:

<Label                        
    Style="{StaticResource labelSectionTitleStyle}">
    <Label.Text>
        <OnPlatform x:TypeArguments="x:String">
            <On Platform="WinUI" Value="The Players" />
            <On Platform="Android" Value="How many players?" />                                
        </OnPlatform>
    </Label.Text>
</Label>

This way we’ll be able to put just numbers next to the radio buttons in that part of the UI. Here’s the first radio button:

<RadioButton                             
    GroupName="players">
    <RadioButton.Content>
        <Label>
            <Label.Text>
                <OnPlatform x:TypeArguments="x:String">
                    <On Platform="WinUI" Value="1 player" />
                    <On Platform="Android" Value="1" />
                </OnPlatform>
            </Label.Text>
        </Label>                                
    </RadioButton.Content>
</RadioButton>

The others look almost the same, so I omitted them in the code above.

The other changes are pretty much of the same type, so I’m not going to bore you with each and every one of them. You will find the complete code on Github. And here’s the final effect – the SettingsPage on Android:

As you can see, most of the labels are slightly different. Also the placeholder text in the Ending Conditions section is shorter. You can also see that when you enter data in the entries, the validation works just like on Windows.

Now let’s take care of the RacePage.

The Other Pages on Android

As far as the other pages are concerned, I’m not going to show you each and every change I made, because this would be too lengthy. You will find the entire modified code on Github, so go ahead, open each XAML file one by one and search for the occurrences of the OnPlatform markup extension. There also some other minor modifications. I also made some changes in the App.xaml file where the global styles are defined. These changes affect the entire app. There are some new styles and some of the old ones are now platform-specific:

<?xml version = "1.0" encoding = "UTF-8" ?>
<Application ...>
    ...
            <!--Numeric values-->
            <x:Double x:Key="defaultFontSize">18</x:Double>
            <x:Double x:Key="androidDefaultFontSize">14</x:Double>                        
            ...
            <Style TargetType="Border">
                ...
                <Setter Property="StrokeThickness" 
                        Value="{OnPlatform 5, Android=1}" />
                <Setter Property="Padding" Value="5" />
                <Setter Property="Margin" 
                        Value="{OnPlatform 5, Android=1}" />
            </Style>

            <Style TargetType="Button">
                ...
                <Setter Property="WidthRequest" 
                        Value="{OnPlatform 250, Android=150}" />
                <Setter Property="HeightRequest" 
                        Value="{OnPlatform 50, Android=40}" />                
                ...
            <Style x:Key="labelBaseStyle" ...
            <Style x:Key="androidLabelBaseStyle" TargetType="Label">
                <Setter Property="FontSize" Value="{StaticResource androidDefaultFontSize}" />
                ...
            <Style x:Key="labelSectionTitleStyle"...
            <Style x:Key="androidLabelSectionTitleStyle" 
                   TargetType="Label"
                   BasedOn="{StaticResource labelBaseStyle}">
                <Setter Property="FontSize" Value="16" />
                <Setter Property="FontAttributes" Value="{StaticResource emphasized}" />
            </Style>

            <Style TargetType="Entry">
                ...
                <Setter Property="FontSize" 
                        Value="{OnPlatform {StaticResource defaultFontSize}, Android=12}" />
                ...
            <ControlTemplate x:Key="RadioButtonTemplate">                
                ...                        
                        <Ellipse
                                Fill="{OnPlatform {StaticResource mainButtonColor}, 
                                    Android=White}" 
                                Stroke="{StaticResource mainButtonColor}"
                                HeightRequest="{OnPlatform 24, Android=20}"
                                WidthRequest="{OnPlatform 24, Android=20}"
                                ...
                        <Ellipse
                                x:Name="check" 
                                Fill="{OnPlatform {StaticResource buttonTextColor}, 
                                    Android={StaticResource mainButtonColor}}"
                                HeightRequest="{OnPlatform 12, Android=8}"
                                WidthRequest="{OnPlatform 12, Android=8}"
                                ...

For example I modified the border to be thinner on Android and the radio buttons to have different colors and sizes of the circles. This change affects also the SettingsPage, which eventually looks like this:

And now let’s have a look at the other pages. We’re going to see the RacePage and the GameOverPage as they would look without the above and further modifications, then how they look after the modifications and finally we’re going to discuss some interesting fragments of the code. As the RacePage contains several content views, these had to be modified as well.

So, here’s the RacePage before any modifications:

Looks great, right? But still, a few slight modifications wouldn’t be that bad. Here’s the same page after the modifications, with the Bets panel on the left and with the Results panel on the right:

with the Bets panel

with the Results panel

As you can see, I completely rearranged the elements of the page. The Bets panel is now near the top so that it doesn’t get hidden behind the keyboard when you type in the values in the entries.

And here’s the GameOverPage:

before

after

Here the changes are far less spectacular.

The changes in the code were made to target two different platforms. Let’s now have a look at some interesting places in the code, but this is not going to be a complete tour of all the changes I made, so make sure to view the XAML files in the Github repository.

Numeric Keyboard

When an entry gets focus on Android (A), a keyboard pops up (B):

This is great for the name entry where you need all alphanumeric characters. But for numeric inputs, like the initial money entry or the bet amount entry in the RacePage, we would either have to hold the button down for a while until the digit replaces the letter which is assigned to the same button:

or we would have to tap the ?123 button in the bottom-left corner and then use the keyboard with direct access to numeric keys:

This isn’t a problem if you have to do it once or twice, but in our application you will be entering numbers all the time, maybe not in the SettingsPage, but definitely in the Bets panel, so there must be a way to make our lives easier. And there is a very simple one. Just set the Keyboard property of the entries to Numeric. Here’s the entry in the PlayerSettings view:

<?xml version="1.0" encoding="utf-8" ?>
<ContentView xmlns...>
    ...        
        <Entry
            x:Name="initialMoneyEntry"                       
            Placeholder="1000" 
            Grid.Column="3" 
            WidthRequest="{OnPlatform 250, Android=100}"
            Keyboard="Numeric"
            TextChanged="OnInitialMoneyTextChanged">            
        ...

Now if the initial money entry gets focus, a numeric keyboard will show up:

The entry in the PlayerBet view should have this property set too.

Layouts

The layouts on the two platforms differ, especially in the RacePage. This can be achieved by setting the row heights and column widths to different values. To keep the same number of rows and columns, we can set some of them to 0, which will have the same effect as if they weren’t there at all. Then we set the Grid.Row and Grid.Column attached properties to different values where needed. Here’s an example from the RacePage:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage ...>

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="{OnPlatform 1.3*, Android=2.6*}" />
            <RowDefinition Height="{OnPlatform 2*, Android=.7*}" />
            <RowDefinition Height="{OnPlatform 2*, Android=*}" />
            <RowDefinition Height="{OnPlatform 0, Android=*}" />
        </Grid.RowDefinitions>

        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="{OnPlatform 3*, Android=*}" />
            <ColumnDefinition Width="{OnPlatform 3*, Android=*}" />
            <ColumnDefinition Width="{OnPlatform 3*, Android=0}" />
            <ColumnDefinition Width="{OnPlatform 2*, Android=0}" />
        </Grid.ColumnDefinitions>
        
        <!--Game Info-->        
        <Border
            Grid.Row="{OnPlatform 0, Android=3}"
            Grid.Column="{OnPlatform 0, Android=1}"
            HorizontalOptions="{OnPlatform Android=Fill}">
            <controls:GameInfo/>
        </Border>    
        ...

So, the Game Info content view is positioned in the first row and first column on Windows, but in the fourth row and second column on Android. It works the same for the other views.

Margin and Padding

Most of the remaining code is pretty straightforward, so I don’t think it’s necessary to discuss it in great detail. Let’s have a look at two properties, Margin and Padding, that may not be completely clear.

Well, what is actually the difference between the two?

Margin is used to set the distance between an element and its adjacent elements.

Padding is used to set the distance between and element and its child elements (its content).

Both Margin and Padding are of type Thickness, which is a structure. Depending on which constructor is called, we can define Thickness by one, two or four values.

If only one value is defined, it’s applied to all sides of the element uniformly.

If two values are defined, the first value is applied to the left and right sides of the element and the second value is applied to the top and bottom sides.

If four values are defined, they’re applied to the left, top, right and bottom sides of the element, in this exact order.

Here’s an example of Margin from the PlayerBet content view:

<?xml version="1.0" encoding="utf-8" ?>
<ContentView ...>
    ...
            <RadioButton 
                GroupName="player1">
                <RadioButton.Content>
                    <Label 
                        Text="Speedster"
                        Style="{OnPlatform {StaticResource labelBaseStyle},
                            Android={StaticResource androidLabelBaseStyle}}">
                        <Label.Margin>
                            <OnPlatform x:TypeArguments="Thickness">
                                <On Platform="WinUI" Value="0" />
                                <On Platform="Android" Value="-12, 0, 0, 0" />
                            </OnPlatform>
                        </Label.Margin>                        
                    </Label>
                ...

So, on Windows the label will have the same Margin on all four sides, on Android the distance on the left side will be decreased (hence the negative value), but it will be 0 on the other sides.

Now we have all the styles in place, our app looks good on both Windows and Android. There’s just one little topic as far as the visual appearance of our app is concerned, themes. We’ll be talking about themes in the next part of the series.


Spread the love

Leave a Reply