Platform-specific C# code
Uno Platform allows you to reuse views and business logic across platforms. Sometimes though, you may want to write different code per platform. You may need to access platform-specific native APIs and 3rd-party libraries, or want your app to look and behave differently depending on the platform.
This guide covers multiple approaches to managing per-platform code in C#. See this guide for managing per-platform XAML.
Project structure
There are multiple ways to restrict code or XAML markup to be used only on a specific platform:
- Use conditional code based on the
OperatingSystem.IsXXXmethod - Use conditionals in a source file
- Place the code in a separate file which is only included in the desired platform.
The structure of an Uno Platform app created with the default Visual Studio template is explained in more detail here.
OperatingSystem.IsXXX
The Uno Platform templates differentiate platforms using the current target framework, yet it's possible to determine the running platform at runtime using:
if (OperatingSystem.IsBrowser())
{
// Do something WebAssembly specific
}
In a class library, a single net10.0 target framework combined with these runtime checks keeps build times down and produces one asset every Uno Platform head can consume. Add net10.0-ios or net10.0-android target frameworks when the library needs types from a platform SDK, which a net10.0 assembly cannot reference.
Note
JSImport/JSExport and BrowserHtmlElement are available on all platforms targeting .NET 7 and later, and code using those APIs to be conditionally excluded at compile time, and should only use OperatingSystem conditions.
#if conditionals
The most basic means of authoring platform-specific code is to use #if conditionals:
#if __UNO__
Console.WriteLine("Uno Platform - Pixel-perfect WinUI apps that run everywhere");
#else
Console.WriteLine("Windows - Built with Microsoft's own tooling");
#endif
If the supplied condition is not met, e.g. if __UNO__ is not defined, then the enclosed code will be ignored by the compiler.
The following conditional symbols are predefined for each Uno platform. Application heads and class libraries get the same set, whether or not they use the Uno.Sdk:
| Platform | Symbol | Remarks |
|---|---|---|
| Android | __ANDROID__ |
net10.0-android |
| iOS | __IOS__ |
net10.0-ios |
| tvOS | __TVOS__ |
net10.0-tvos |
| iOS or tvOS | __APPLE_UIKIT__ |
net10.0-ios and net10.0-tvos |
| WebAssembly | __WASM__ |
net10.0-browserwasm. The .NET SDK also defines BROWSERWASM for this target framework |
| Desktop | __DESKTOP__ |
net10.0-desktop. The .NET SDK also defines DESKTOP for this target framework |
| Non-Windows | __UNO__ |
Every target framework except the WinAppSDK one. This is the C# equivalent of the not_winappsdk: XAML prefix and of *.crossruntime.cs. To learn about symbols available when __UNO__ is not present, see below |
| Non-Windows | HAS_UNO |
Identical to __UNO__ |
Symbols such as __SKIA__, __CROSSRUNTIME__ or WINAPPSDK appear in the Uno Platform source code but are never defined in your projects. See Preprocessor symbols in the Uno Platform repository for what they mean and what to use instead.
Legacy symbols
These symbols are defined wherever __UNO__ is, so existing code keeps compiling. Use __UNO__ in new code.
| Symbol | Remarks |
|---|---|
UNO_REFERENCE_API |
Despite the name, it has nothing to do with reference assemblies |
HAS_UNO_WINUI |
Dates from when Uno Platform shipped both a UWP and a WinUI flavor |
HAS_UNO_SKIA, __UNO_SKIA__ |
Deprecated, planned for removal in Uno Platform 8.0. They do not name a drawing backend: Uno.UI is compiled once and resolves its backend at run time |
Each platform symbol is only defined in the target framework that provides it. In an application head, which targets net10.0-ios, net10.0-android, net10.0-browserwasm, and net10.0-desktop directly, #if blocks behave as described above.
In a class library, #if blocks behave the same way. The asset built for net10.0-ios or net10.0-android is the one an iOS or Android head consumes, so its conditional code is the code that runs — see Implications for iOS/Android class libraries. A library that targets only net10.0 has none of the platform symbols defined; use OperatingSystem.IsXXX runtime checks there.
Tip
Conditionals can be combined with boolean operators, e.g. #if __ANDROID__ || __IOS__. It is also possible to define custom conditional compilation symbols per project in the 'Build' tab in the project's properties.
Uno Platform version
UNO_<major>_<minor>_OR_GREATER is defined for the Uno Platform release a project builds against and for every release before it, in the style of .NET's NET8_0_OR_GREATER. A library that compiles against more than one Uno Platform version can use it to branch:
#if UNO_7_0_OR_GREATER
// Uno Platform 7.0 and later
#else
// Uno Platform 6.x
#endif
The symbols come with the Uno.WinUI package and exist from Uno Platform 7.0 onwards, so an older release defines none of them: treat a missing symbol as an older version. Like __UNO__, they are not defined in the Windows App SDK target framework.
Windows-specific code
On net10.0-windows10.0.xxxxx target framework, an Uno Platform application isn't using Uno.UI at all. It's compiled using Microsoft's own tooling. For that reason, the __UNO__ symbol is not defined on Windows. This aspect can optionally be leveraged to write code specifically intended for Uno.
Apps targeting Windows use Windows App SDK. The following symbols are available there:
| App model | Symbol | Remarks |
|---|---|---|
| Windows App SDK | WINDOWS |
Defined by the .NET SDK for net10.0-windows10.0.xxxxx |
| Windows App SDK | WINDOWS10_0_18362_0_OR_GREATER |
Depending on the TargetFramework value, the 18362 part may need adjustment |
| Windows App SDK, packaged | WINAPPSDK_PACKAGED |
Defined by the Uno.Sdk when the app is packaged as MSIX (WindowsPackageType is MSIX, the default when a Package.appxmanifest is present) |
UWP heads, and the NETFX_CORE symbol they defined, are not supported as of Uno Platform 7.0.
Type aliases
Defining a type alias with the using directive, in combination with #if conditionals, can make for cleaner code. For example:
#if __ANDROID__
using _View = Android.Views.View;
#elif __IOS__
using _View = UIKit.UIView;
#else
using _View = Microsoft.UI.Xaml.UIElement;
#endif
...
public IEnumerable<_View> FindDescendants(FrameworkElement parent) => ...
Partial class definitions
Heavy usage of #if conditionals in shared code makes it hard to read and comprehend. A better approach is to use partial class definitions to split shared and platform-specific code.
Starting from Uno Platform 5.2, in project or class libraries using the Uno.Sdk, a set of implicit file name conventions can be used to target specific platforms:
*.wasm.csis built only fornet10.0-browserwasm*.browserwasm.csis built only fornet10.0-browserwasm*.desktop.csis built only fornet10.0-desktop*.iOS.csis built only fornet10.0-ios*.tvOS.csis built only fornet10.0-tvos*.UIKit.csis built only fornet10.0-iosandnet10.0-tvos*.Android.csis built only fornet10.0-android*.WinAppSDK.csis built only fornet10.0-windows10(eg.net10.0-windows10.0.22621)*.crossruntime.csand*.skia.csare built for every target framework exceptnet10.0-windows10
*.crossruntime.cs and *.skia.cs are equivalent, and hold code for the target frameworks Uno Platform renders itself as opposed to the WinAppSDK one. Neither names a drawing backend: Uno.UI is compiled once and resolves its backend at run time.
Note
Before Uno Platform 7.0, *.skia.cs and *.crossruntime.cs were built only for net10.0-desktop, which made *.skia.cs an alias of *.desktop.cs. A file that is genuinely desktop-only should be renamed to *.desktop.cs.
Note
*.reference.cs, *.iOSmacOS.cs and *.Apple.cs are no longer recognized. *.reference.cs and
*.iOSmacOS.cs named build flavors that no longer exist and can be removed; rename *.Apple.cs to
*.UIKit.cs, which always had the identical rule.
Using file name conventions allows for reducing the use of #if compiler directives.
A simple example
Shared code in PROJECTNAME/NativeWrapperControl.cs:
public partial class NativeWrapperControl : Control {
...
protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
_nativeView = CreateNativeView();
}
Platform-specific code in PROJECTNAME/NativeWrapperControl.Android.cs:
#if __ANDROID__
public partial class NativeWrapperControl : Control {
...
private View CreateNativeView() {
... //Android-specific code
}
Platform-specific code in PROJECTNAME/NativeWrapperControl.iOS.cs:
#if __IOS__
public partial class NativeWrapperControl : Control {
...
private UIView CreateNativeView() {
... //iOS-specific code
}
You can use partial methods when only one platform needs specialized logic.
Tip
To place the native view this pattern creates into the visual tree, see Embedding native controls.