Native intercompatibility
📖 Overview
The Uno Platform allows you to reuse views and business logic across platforms. Sometimes though you may want to write different code per platform, either because you need to access platform-specific native APIs and 3rd-party libraries, or because you want your app to look and behave differently depending on the platform.
In this module, you'll discover the platform-specific escape hatches within the Uno Platform and build a custom control to learn how a different native control can be used on a per-platform basis.
💡 What happens when you compile
With Roslyn, Microsoft open-sourced the C# compiler, but they also exposed a powerful API for code analysis. Roslyn provides easy access to all the syntactic and semantic information that the compiler possesses. The folks behind Uno created a source generator that leverages this power for code generation and, like the Uno platform, it's free and open-source.
At compile-time, there are four main things under the hood that Uno does:
- Parses XAML files and generates C# code to create the information needed to build your application's visual tree.
- Generation of dependency objects that are optimized for static type-checking where possible.
- Generation of native constructors for iOS and Android.
- Generation of Localizable.strings (iOS) and Strings.xml (Android) files from
.resw
resources.
Inside of the source generator, you'll find an MSBuild task that allows you to easily add generated code based on Roslyn's analysis of your application. As the generator has a full set of semantic information from Roslyn, it can do this in a smart way. This might be partial class definitions that augment existing types, or it might be entirely new classes. For instance, if the source generator detects that the class is a view type, the source generator adds methods to update binding information when the view is loaded or unloaded.
If you are curious to see files generated from XAML, you should click on View all files and check under the obj
/ [config] / [platform] / g
/ XamlCodeGenerator
. Each generator is getting a separate folder under g
.
💡 DependencyObject is an interface with a supplied implementation
Part of the power of Uno on Android and iOS is the ability to mix UWP view types with purely native views easily. This is possible because, in Uno, all views inherit from the native base view type.
This however posed a challenge for reproducing UWP's inheritance hierarchy as UIElement
is the primitive view type in UWP, which derives from the DependencyObject
class which is the base class for anything that has DependencyProperties
, that is, anything that supports databinding such as views, as well as some non-view framework types like Transforms and Brushes.
Since Uno can't change the design of the iOS or Android frameworks, Uno chose to make DependencyObject
an interface with an implementation that is supplied by code generation. This design decision allows an Uno FrameworkElement
to be a UIView
and at the same time to be a DependencyObject
.
Most statically-typed languages, except C++, don't permit multiple base classes on account of the added complexity it brings, a.k.a. the 'diamond problem'. In dynamically-typed languages, it's quite common to bolt on extra functionality to a class in a reusable way with mixins. As C# is a statically-typed language, it doesn't support mixins as a first-class language feature.
Uno can, however, generate code.
💡 Native Constructors are automatically provided
As views in Uno inherit directly from native views on Android/iOS, they need to have special constructors that are called under-the-hood by Xamarin. Writing these by hand would be tedius and particularly painful when porting existing UWP code, so Uno generates them for you automatically if they don't already exist.
💡 Platform-specific code in Uno
There are two code layout techniques that can be used to implement platform-specific code with the Uno Platform:
- Place platform-specific code in a file that is only included in the desired
platform head
. - Use conditionals within a file within the
shared project
to conditionally implement platform-specific code.
In Visual Studio, a shared project
is just a list of files. Referencing a shared project
in an ordinary .csproj
project causes those files to be included in the project. They're treated in exactly the same way as the files inside the project.
The Uno Platform provides you with two techniques to conditionally implement platform-specific code within a shared project:
💡 Platform-specific controls in Uno
Tag the platform-specific control with the : DependencyObject
interface and the implementation of the interface will be automatically supplied by the source generator.
Below you'll find an example of how to wrap an Android.Widget.ProgressBar
as new control and usage of typical object-oriented programming techniques to customize the behavior of the new control:
#if __ANDROID__
public partial class CustomControl : Android.Widget.ProgressBar, DependencyObject { }
// On Android, use Uno.UI.ContextHelper.Current to obtain Android.Content.Context
public CustomControl() : base(Uno.UI.ContextHelper.Current)
{
Indeterminate = false;
Min = 0;
Progress = 50;
Max = 100;
}
#endif
Some platform-specific controls (UIKit.*
) only expose properties through methods, therefore, making them impossible to use from XAML. To improve bindability and to provide a way to make two-way binding possible, the Uno Platform provides a growing number of bindable controls
under the Uno.UI.Controls namespace. If a native wrapper exists for your control, you should use it. If a native wrapper does not exist, please create it and contribute it back to the project in a pull-request.
#if __ANDROID__
public partial class YourControl : Uno.UI.Controls.BindableProgressBar { }
// On Android, use Uno.UI.ContextHelper.Current to obtain Android.Content.Context
public CustomControl() : base(Uno.UI.ContextHelper.Current)
{
Indeterminate = false;
Min = 0;
Progress = 50;
Max = 100;
}
#endif
🎯 Build the NativeProgress control
The implementation of iOS and Android has been done for you. You'll need to do the WebAssembly control.
<Page ...
xmlns:controls="using:TodoApp.Shared.Controls">
<controls:NativeProgress Value="{Binding Value}" />
</Page>
- [ ] Review TodoApp/TodoApp.Shared/*.xaml
- [ ] Review TodoApp/TodoApp.Shared/*.xaml.cs
- [ ] Review TodoApp/TodoApp.SharedControls/NativeProgress.md
- [ ] Review TodoApp/TodoApp.SharedControls/NativeProgress.cs
- [ ] Review TodoApp/TodoApp.SharedControls/NativeProgress.Droid.cs
- [ ] Review TodoApp/TodoApp.SharedControls/NativeProgress.iOS.cs
- [ ] Review TodoApp/TodoApp.SharedControls/NativeProgress.UWP.cs
- [ ] Implement TodoApp/TodoApp.SharedControls/NativeProgress.WASM.cs
📚 Additional Reading Material
- Platform-specific C# code in Uno.
- Platform-specific XAML markup in Uno.
- Bindable controls.
- Talkin’ ‘bout my generation: How the Uno Platform generates code, part 1.
- Talkin’ ‘bout my generation: How the Uno Platform generates code, part 2.
⏭️ What's next
In the next module you'll learn about the internals of the Uno Platform code-base. The team has built-in plenty of escape hatches that enables you to be autonomous without being dependant on pull-requests being merged. We hope that that this knowledge will enable you to become self-reliant, to not push your pull-requests if you are ever caught in a jam and ultimately become a regular contributor to the open-source project. A growing open-source project is a healthy open-source project. 💖