© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2022
S. HoeflingGetting Started with the Uno Platform and WinUI 3https://doi.org/10.1007/978-1-4842-8248-9_5

5. Platform-Specific Code and XAML

Skye Hoefling1  
(1)
Rochester, NY, USA
 

Even with all the abstractions in Uno Platform, you will still need to write platform-specific code or XAML. When interacting with native APIs, you will always need to write code for those APIs. In the user interface, there may be spacing issues on controls not lining up just right, and you’ll need to add platform-specific XAML. In this chapter we are going to cover the various techniques of adding platform-specific code and XAML for Uno Platform.

In this chapter we will learn how to add platform-specific code in C# and XAML. We will be using the existing code that we have written up to now, but the completed code in this chapter will not be used in subsequent chapters. Our goal is to learn the concepts and apply them as we need them.

Note

The code contained in this chapter is example code only and will not be used in any other chapter of this book.

In Uno Platform you can write platform-specific code or XAML using several techniques:
  • Pre-processor directives: #if

  • XAML: xmlns

  • Multi-targeting project

Pre-processor Directives

A pre-processor directive is a compile-time symbol that will tell the compiler what code to omit or include in compiling that platform. The most common pre-processor directive is #if DEBUG, which only runs code if the binary has been compiled in debug mode.

Note

The DEBUG pre-processor directive is typically set in the debug configuration. In most if not all projects, it will always work. If the DEBUG constant is not set, the #if DEBUG will not work.

Uno Platform has multiple pre-processor directives that are included in the templates. There are symbols for each platform, so it easy enough to add it to your code, and it will only run on that platform.

XAML: XML Namespaces

XML namespaces or xmlns are defined in your XAML typically at the root node, which is typically your <Page> declaration. You are already using xmlns for loading different control libraries or even the default libraries that come with Uno Platform and WinUI 3.

Uno Platform allows you to define several namespaces that will only compile or load the XAML for a specific platform or set of platforms. This acts just like a pre-processor directive that we just learned about as we only have xmlns in XAML and in C# we have pre-processor directives.

Multi-targeting Project

Uno Platform development is best done using shared projects , but sometimes a multi-targeted project is going to work best for you. This could be an upstream library that you want to work with Uno Platform, or you want to use it for your shared code.

The advantage in multi-targeted projects is you will not need to use pre-processor directives or use them as much. You will create special C# code files that only compile for specific platforms. This can then leverage newer language features such as partial methods and have those methods implemented in the correct platform C# code files.

This is a very advanced technique and comes with a new set of risks. It should only be used if you are prepared to handle additional problems that may come up. It is a very powerful technique and is becoming more commonplace in the multi-platform .NET community.

The project template we are using is leveraging a multi-targeted project for the .NET MAUI target heads: Android, iOS, and macOS. Notice it is one project file but it allows you to compile under the various targets.

Pre-processor Directives

The pre-processor directive approach makes adding platform-specific C# code an easy process. The overview section explains the basic concepts of pre-processor directives, which you have probably seen as a #if DEBUG statement that only compiles the code if the binary is built in debug mode . A pre-processor directive is used at compile time and tells the compiler what code to omit or include depending on the values of the directive.

Uno Platform includes all the platform-specific pre-processor directives for you out of the box. Since it is recommended to use a shared project, you can simply just add them into any code file that you want to.

If you are using a multi-targeted project as opposed to a shared project, you will need to ensure the pre-processor directives are defined.

You can find all the documentation for platform-specific C# code at the Uno Platform official documentation page. It is a good idea to reference this for changes as Uno Platform continues to evolve :
  • https://platform.uno/docs/articles/platform-specific-csharp.html
    Table 5-1

    Pre-processor Directives for Platform-Specific Code

    Platform

    Symbol

    Windows

    NET6_0_OR_GREATER && WINDOWS

    Android

    __ANDROID__

    iOS

    __IOS__

    macOS

    __MACOS__

    WebAssembly (WASM)

    __WASM__

    Skia

    HAS_UNO_SKIA

    WPF

    HAS_UNO_SKIA_WPF

    Note The Windows platform needs to use NET6_0_OR_GREATER && WINDOWS as the WINDOWS constant is used for both UWP and WinUI.

    Tip It may be easier to manage to create a special constant in the Windows project head so you do not need to include verbose constants when you want to run Windows-specific C#.

Using Table 5-1 we can add a pre-processor directive to any C# file in the shared project, and it will only run on the corresponding platform. Let’s look at some code!

In our app, we have created a login page, which has a code behind file named LoginPage.xaml.cs . Let’s add a pre-processor directive to only run code on the Windows platform. See the code snippet in Listing 5-1.
public partial class LoginPage
{
  public LoginPage()
  {
    InitializeComponents();
#if NET6_0_OR_GREATER && WINDOWS
    Console.WriteLine("Login Page created for Windows");
#endif
  }
}
Listing 5-1

Add the Windows pre-processor directive to Console.WriteLine

The code in Listing 5-1 adds a statement to the console only for Windows as we are using the NET6_0_OR_GREATER && WINDOWS pre-processor directive. This is a good sample to get the basics, but let’s add some code to the XAML page so we can have more direct interaction with the platform-specific code.

Update the header TextBlock to have the attribute x:Name="header". This will allow us to access the object in the code behind and update the text. See code in Listing 5-2.
<TextBlock
  x:Name="header"
  Text="Welcome to UnoDrive"
  Style="{StaticResource Header}" />
Listing 5-2

Update the TextBlock control in LoginPage.xaml to use x:Name

Before we see the header property in the code behind, you must compile the application. This is because code generators need to run, which create the private variable. In the code behind, we can remove the Console.WriteLine() statement and update the text of our header TextBlock. Let’s update the header to say “Hello from Windows”.

Add the code snippet in Listing 5-3 into the pre-processor directive.
header.Text = "Hello from Windows";
Listing 5-3

Code snippet to update the header TextBlock text for a Windows-specific message

See complete code in Listing 5-4.
public partial class LoginPage
{
  public LoginPage()
  {
    InitializeComponents();
#if net6_0_OR_GREATER && WINDOWS
    header.Text = "Hello from Windows";
#endif
  }
}
Listing 5-4

LoginPage.xaml.cs complete code with a Windows-specific message

If you go and run the Windows application , you should see the header text updated, and if you run the other platforms, it is still the original value. See the screenshot in Figure 5-1.

A screenshot of the Windows user interface application which depicts the Hello from Windows message on the desktop and login to UnoDrive button.

Figure 5-1

Windows desktop application showing a Windows-only message

Now that we have the standard Windows pre-processor directive working, let’s add the remaining platforms in with similar messaging. See code in Listing 5-5.
public partial class LoginPage
{
  public LoginPage()
  {
    InitializeComponents();
#if NET6_0_OR_GREATER && WINDOWS
    header.Text = "Hello from Windows";
#elif __ANDROID__
    header.Text = "Hello from Android";
#elif __IOS__
    header.Text = "Hello from iOS";
#elif __MACOS__
    header.Text = "Hello from macOS";
#elif HAS_UNO_WASM
    header.Text = "Hello from WASM";
#elif HAS_UNO_SKIA
    header.Text = "Hello from Skia";
#endif
  }
}
Listing 5-5

LoginPage.xaml.cs with platform-specific code for all the platforms

Now you can go ahead and run the app for each platform and see the different Hello World messages. See Android and iOS screenshots in Figure 5-2. See the macOS screenshot in Figure 5-3. See the WASM screenshot in Figure 5-4. See the WPF screenshot in Figure 5-5. See the Linux screenshot in Figure 5-6.

A screenshot of Andriod and iOS interface which depicts the Hello from Android and Hello from iOS message on android with log in to UnoDrive button.

Figure 5-2

Android and iOS platform-specific header messages

A screenshot of the mac OS user interfaces application which depicts the Hello from macOS header message on the desktop with the login to UnoDrive button.

Figure 5-3

macOS application showing a macOS-only header message

A screenshot of the Web Assembly application which depicts a Hello from W A S M message on the desktop with the login to UnoDrive button by using the Uno platform.

Figure 5-4

WebAssembly displaying a web-only message

A screenshot of the Web Assembly application which depicts a Hello from W A S M message on the desktop with login to UnoDrive button by using the Uno platform.

Figure 5-5

WPF application showing the Skia-only message

A screenshot of a Linux user interface application that depicts a Hello from Skia message on the desktop with a login to UnoDrive button by using the Uno platform.

Figure 5-6

Linux application showing the Skia message

Code Organization

Adding many pre-processor directives like this can become messy quickly. It is best used for ad hoc platform-specific code . If you find yourself writing large chunks of code for each platform, you may want to create C# code files for that platform entirely.

When building cross-platform applications , you can use a partial method, which is a special C# language feature that allows you to define a method in a partial class and implement it in another file for that same partial class.

Update the code from earlier by removing all of the pre-processor directives and adding a partial method called SetHeaderText() . See the code snippet in Listing 5-6.
partial void SetHeaderText();
Listing 5-6

Adds partial method declaration

When defining a partial method, it has no implementation; it will be implemented in another file for the partial class. See completed LoginPage.xaml.cs code in Listing 5-7.
public partial class LoginPage
{
  public LoginPage()
  {
    InitializeComponents();
    SetHeaderText();
  }
  partial void SetHeaderText();
}
Listing 5-7

LoginPage.xaml.cs completed code with partial method declaration

Now you can create multiple files for the platform-specific implementations of this partial method :
  • LoginPage.android.cs

  • LoginPage.ios.cs

  • LoginPage.macos.cs

  • LoginPage.skia.cs

  • LoginPage.windows.cs

  • LoginPage.wasm.cs

In each file you will wrap the entire code in a pre-processor directive for that platform. For example, prior to implementation, our Android code will look like the snippet in Listing 5-8.
#if __ANDROID__
namespace UnoDrive
{
  public partial class LoginPage
  {
  }
}
#endif
Listing 5-8

Android – LoginPage.xaml.cs empty partial class

Since we have defined the partial method in our LoginPage.xaml.cs code behind file, we can now create the implementation of that method in each platform. Remember, if you try and compile the application and the partial method is not implemented, the build will fail. Update the partial classes as seen in Listing 5-9 for Android, Listing 5-10 for iOS, Listing 5-11 for macOS, Listing 5-12 for Skia, Listing 5-13 for Windows, and Listing 5-14 for WASM.
#if __ANDROID__
namespace UnoDrive
{
  public partial class LoginPage
  {
    partial void SetHeaderText()
    {
      header.Text = "Hello from Android";
    }
  }
}
#endif
Listing 5-9

Android – LoginPage.xaml.cs partial class with partial method implementation

#if __IOS__
namespace UnoDrive
{
  public partial class LoginPage
  {
    partial void SetHeaderText()
    {
      header.Text = "Hello from iOS";
    }
  }
}
#endif
Listing 5-10

iOS – LoginPage.xaml.cs partial class with partial method implementation

#if __MACOS__
namespace UnoDrive
{
  public partial class LoginPage
  {
    partial void SetHeaderText()
    {
      header.Text = "Hello from macOS";
    }
  }
}
#endif
Listing 5-11

macOS – LoginPage.xaml.cs partial class with partial method implementation

#if HAS_UNO_SKIA
namespace UnoDrive
{
  public partial class LoginPage
  {
    partial void SetHeaderText()
    {
      header.Text = "Hello from Skia";
    }
  }
}
#endif
Listing 5-12

Skia – LoginPage.xaml.cs partial class with partial method implementation

#if NET6_0_OR_GREATER && WINDOWS
namespace UnoDrive
{
  public partial class LoginPage
  {
    partial void SetHeaderText()
    {
      header.Text = "Hello from Windows";
    }
  }
}
#endif
Listing 5-13

Windows – LoginPage.xaml.cs partial class with partial method implementation

#if HAS_UNO_WASM
namespace UnoDrive
{
  public partial class LoginPage
  {
    partial void SetHeaderText()
    {
      header.Text = "Hello from WASM";
    }
  }
}
#endif
Listing 5-14

WASM – LoginPage.xaml.cs partial class with partial method implementation

This technique is a nice way to blend a multi-targeted csproj while still using the shared project. It is a convention-based technique , and there is nothing specific being done with file extensions at the compiler level. If you use the __ANDROID__ directive in a file that ends with .ios.cs, it will still run the code on Android and not iOS.

You and your team will need to ensure you follow good convention best practices when adding new code files.

Special Cases with Skia

When building your application for a Skia target , you need to ensure that you know your target audience and are careful with your code. The Skia target is used in both the GTK project, which can be run on Linux, macOS, and Windows, and the WPF project, which can only be run on Windows. This means the pre-processor directive of HAS_UNO_SKIA could be used on Linux, macOS, or Windows. If you need to access native Windows APIs using Skia, you will need to add the specific WPF directive HAS_UNO_SKIA_WPF.

XAML: XML Namespaces

Uno Platform allows you to write platform-specific XAML that makes it very easy to add controls or user interface changes for a specific platform. When you define any XAML class such as a page or control, you can add multiple XML namespaces or xmlns definitions. These definitions can be used throughout your XAML code file.

If you aren’t familiar with xmlns but have been working with XAML, you are probably using it and not even realizing it. Take the code snippet in Listing 5-15.
<Page
  x:Class="UnoDrive.LoginPage"
  xmlns:="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:local="using:UnoDrive"
  xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  mc:Ignorable="d">
  <!-- Omitted markup -->
</Page>
Listing 5-15

LoginPage.xaml with standard xmlns definitions

In this code snippet, we are using four different xmlns definitions: x, local, d, and mc. You are probably most familiar using the x namespace as it is used to add keys, names, and other items throughout your page.

In Uno Platform you can add xmlns definitions at the top of your page. When used, the XAML engine will include or omit the code depending on the platform. The Uno Platform official documentation provides details on what can be used:
Table 5-2 documents different namespaces you can include on your page to run platform-specific XAML. Many of the namespaces require to be placed in the mc:Ignorable attribute ; otherwise, you will get tooling errors with Visual Studio.
Table 5-2

xmlns Definitions for Platform-Specific XAML

Prefix

Platforms

Excluded Platforms

Namespace

win

Windows

Android, iOS, WASM, macOS, Skia

http://schemas.microsoft.com/winfx/2006/xaml/presentation

xamarin

Android, iOS, WASM, macOS, Skia

Windows

http://uno.ui/xamarin

not_win

Android, iOS, WASM, macOS, Skia

Windows

http://uno.ui/not_win

android

Android

Windows, iOS, WASM, macOS, Skia

http://uno.ui/android

ios

iOS

Windows, Android, Web, macOS, Skia

http://uno.ui/ios

wasm

WASM

Windows, Android, iOS, macOS, Skia

http://uno.ui/wasm

macos

macOS

Windows, Android, iOS, Web, Skia

http://uno.ui/macos

skia

Skia

Windows, Android, iOS, Web, macOS

http://uno.ui/skia

not_android

Windows, iOS, Web, macOS, Skia

Android

http://schemas.microsoft.com/winfx/2006/xaml/presentation

not_ios

Windows, Android, Web, macOS, Skia

iOS

http://schemas.microsoft.com/winfx/2006/xaml/presentation

not_wasm

Windows, Android, iOS, macOS, Skia

WASM

http://schemas.microsoft.com/winfx/2006/xaml/presentation

not_macos

Windows, Android, iOS, Web, Skia

macOS

http://schemas.microsoft.com/winfx/2006/xaml/presentation

not_skia

Windows, Android, iOS, Web, macOS

Skia

http://schemas.microsoft.com/winfx/2006/xaml/presentation

This table can be very confusing and difficult to understand when to use the various namespaces. Let’s break it down to the simple cases first.

If I want to create a page and display a special message only on Android and no other platforms, I will add the xmlns for Android, which is http://uno.ui/android . With that added, I can now append any control on the page with that namespace, and it will only be visible on Android. Add the Android-specific xmlns as seen in Listing 5-16.
xmlns:android="http://uno.ui/android
Listing 5-16

Android-specific xmlns definition

Add TextBlock to only render on Android as seen in Listing 5-17.
<android:TextBlock Text="Hello on Android" />
Listing 5-17

Only render TextBlock when using the Android platform via xmlns

See complete LoginPage.xaml code in Listing 5-18.
<Page
  x:Class="UnoDrive.LoginPage"
  xmlns:="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:local="using:UnoDrive"
  xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  xmlns:android="http://uno.ui/android"
  mc:Ignorable="d android">
  <!-- Omitted markup -->
  <android:TextBlock Text="Hello on Android" />
</Page>
Listing 5-18

LoginPage.xaml completed code with Android-specific xmlns

If you go and run this code, it will only display the message on Android and none of the other platforms.

This can make your XAML very verbose. If you need to change just one or two properties on a control, the same style syntax can be applied. However, instead of appending the namespace to the control, you can append it to the attribute. Given the preceding example, we can adjust the TextBlock to display different messages to different platforms.

Update the xmlns definitions to include the major platforms as seen in Listing 5-19.
xmlns:android="http://uno.ui/android"
xmlns:ios="http://uno.ui/ios"
xmlns:macos="http://uno.ui/macos"
xmlns:skia="http://uno.ui/skia"
xmlns:wasm="http://uno.ui/wasm"
Listing 5-19

LoginPage.xaml – add platform-specific xmlns definitions for Android, iOS, macOS, Skia, and WASM

Now update the TextBlock so it renders on all platforms and update the Text property to change depending on the platform. See code in Listing 5-20.
<TextBlock
  Text="Hello from Windows"
  android:Text="Hello from Android"
  ios:Text="Hello from iOS"
  macos:Text="Hello from macOS"
  skia:Text="Hello from Skia"
  wasm:Text="Hello from WASM" />
Listing 5-20

LoginPage.xaml – TextBlock control using xmlns to change the Text property

If you go and run the code, you will see a custom message displaying on the screen for each platform.

Native Controls

Adding controls or updating properties using the namespace definitions is a powerful technique to get your application looking just right. Uno Platform allows you to use this technique and add native platform code via XAML. Yes, if you are on Android and you want to add a native TextView or TextEdit, just add it to the XAML for your namespace:
<android:TextView
  Text="Hello native Android Control"
  TextSize="22 />

The TextView control is a native Android control that is part of the Android implementation for Uno Platform’s TextBlock. In practice it is best to use the Uno Platform control, but this demonstrates complex capabilities for using native controls. If you go and run the code, it will display a new native TextView control that is using the default styles for Android instead of the custom styles we implemented for this page.

There is no need for building any custom abstractions around the native controls. You can simply place them in your XAML as you would any other control. When Uno Platform creates the view, it will add the native one in the correct place.

Note

Native controls are most useful when there is a very specific native control you want to use. We used TextView as a concrete example to see it in action.

Multi-targeted Projects

The Uno Platform templates use a shared project as the recommended approach for building your application. If you want to create a multi-targeted project, you may run into problems that a shared project does not run into. There are still many benefits of using a multi-targeted project. We will not be using one in our application, but let’s go over some of the concepts for getting this to work and why you may want to use one.

Using a multi-targeted project will not solve all platform-specific problems, but it will certainly solve your C#-related ones.

Benefits
  • Removes pre-processor directives

  • Separates platform-specific code from shared code

Disadvantages
  • Complicated to configure

  • Difficult to maintain packages

  • Difficult to distinguish between WASM and WPF since they both use the .NET 6.0 target framework moniker

A multi-targeted project is a concept supported by csproj and modern .NET tooling. The idea is you add multiple <TargetFrameworks> for all the platforms you want to support. In Uno Platform you will still need to maintain all the project heads, and your shared code will just be a compiled binary instead of being included in the project head. This means you will effectively be creating two binaries, one with your shared code and the other with executable or application code. The shared project approach includes all code in the root executable or application code.

To isolate your platform-specific code, you will need to implement special <ItemGroup> definitions in your csproj file. These will tell the compiler what to compile for which platform:
  1. 1.

    Add <TargetFrameworks> you need to support.

     
  2. 2.

    Set default compilation to false <EnableDefaultCompilationItems>.

     
  3. 3.

    Add target framework–specific <ItemGroup> to define compilation strategy. For example, Android should compile all files that end in android.cs.

     

The goal of this section isn’t to fully document multi-targeting but to give you enough to understand how it works.

Conclusion

In this chapter we learned how to add platform-specific code in both C# and XAML, which allows us to ensure the application we are building looks just right between the various platforms. The code in this chapter will not directly be used in the following chapters but creates a foundation for the techniques we will be using later in the book.

If you had trouble with this chapter, you can view all the code on GitHub: https://github.com/SkyeHoefling/UnoDrive/tree/main/Chapter%205 .

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.17.79.60