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.
The code contained in this chapter is example code only and will not be used in any other chapter of this book.
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.
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.
- https://platform.uno/docs/articles/platform-specific-csharp.htmlTable 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!
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 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”.
Code snippet to update the header TextBlock text for a Windows-specific message
LoginPage.xaml.cs complete code with a Windows-specific message
LoginPage.xaml.cs with platform-specific code for all the platforms
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.
Adds partial method declaration
LoginPage.xaml.cs completed code with partial method declaration
LoginPage.android.cs
LoginPage.ios.cs
LoginPage.macos.cs
LoginPage.skia.cs
LoginPage.windows.cs
LoginPage.wasm.cs
Android – LoginPage.xaml.cs empty partial class
Android – LoginPage.xaml.cs partial class with partial method implementation
iOS – LoginPage.xaml.cs partial class with partial method implementation
macOS – LoginPage.xaml.cs partial class with partial method implementation
Skia – LoginPage.xaml.cs partial class with partial method implementation
Windows – LoginPage.xaml.cs partial class with partial method implementation
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.
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.
xmlns Definitions for Platform-Specific XAML
Prefix | Platforms | Excluded Platforms | Namespace |
---|---|---|---|
win | Windows | Android, iOS, WASM, macOS, Skia | |
xamarin | Android, iOS, WASM, macOS, Skia | Windows | |
not_win | Android, iOS, WASM, macOS, Skia | Windows | |
android | Android | Windows, iOS, WASM, macOS, Skia | |
ios | iOS | Windows, Android, Web, macOS, Skia | |
wasm | WASM | Windows, Android, iOS, macOS, Skia | |
macos | macOS | Windows, Android, iOS, Web, Skia | |
skia | Skia | Windows, Android, iOS, Web, macOS | |
not_android | Windows, iOS, Web, macOS, Skia | Android | |
not_ios | Windows, Android, Web, macOS, Skia | iOS | |
not_wasm | Windows, Android, iOS, macOS, Skia | WASM | |
not_macos | Windows, Android, iOS, Web, Skia | macOS | |
not_skia | Windows, Android, iOS, Web, macOS | Skia |
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.
Android-specific xmlns definition
Only render TextBlock when using the Android platform via xmlns
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.
LoginPage.xaml – add platform-specific xmlns definitions for Android, iOS, macOS, Skia, and WASM
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
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.
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.
Removes pre-processor directives
Separates platform-specific code from shared code
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.
- 1.
Add <TargetFrameworks> you need to support.
- 2.
Set default compilation to false <EnableDefaultCompilationItems>.
- 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 .