Hosting Native Controls – Moving from Xamarin Forms Renderers to Uno Platform Controls

In this blog post we will look at a common technique in Xamarin Forms to customise native platform controls and how we can achieve the equivalent result using Uno Platform. 

You can follow along with the source code for both the Xamarin Forms and Uno Platform projects used in this blog.

What are Renderers?

First let’s look at what a custom renderer is and why you might use one. Xamarin Forms provides a hierarchy of UI controls which can be created in code or XAML. The framework contains controls for all the common controls on each supported platform even though the functionality varies from iOS, Android, and other operating systems. Therefore, each control contains firstly the cross-platform control code, which defines the dependency properties, methods and events which form the public API for the control. Additionally, there is a renderer required for each native platform.

The job of the renderer is to map the native API to the public Xamarin Forms API and handle drawing and appearance so that the control behaves correctly in the Xamarin Forms layout system. The amount of work a renderer must do depends on how closely the native control matches the cross-platform API. In some cases, the renderer can add functionality on top of the control where it isn’t built into the native control. Most of the in-box renderers don’t attempt to change the look and feel of the native control – the default appearance of Xamarin Forms is to match the platform theme. The Xamarin Forms control acts as a lowest common denominator and it is not possible to access additional platform functionality from the public UI except via a few platform-specific attached properties. 

For anything more complex, a developer can supply a custom renderer which can change the behaviour of an existing control (or add support for a new native control). This could be done to, for example, provide custom styling which cannot be set using the Xamarin properties, or to use a completely different native control instead of the in-box implementation. 

All renderers are derived from ViewRenderer<TView, TNativeView> where TView is the type of the Xamarin Forms view class and TNativeView is a platform specific type for the native control. On iOS the native control must be a UIView and on Android an Android.Views.View. There are two methods you must override – OnElementChanged is called when the Xamarin Forms element is assigned (or freed up) and you use this to setup, or clean-up, the native control and any event handlers. 

OnElementPropertyChanged is called whenever a dependency property on the Xamarin Forms element is changed. For example, if the Text property of your custom label changes you will want to update the native control with the new value. This provides a single point for handling all property changes and you can use a switch statement to handle different properties. There is a magic property name of “Renderer” which is not a property exposed in the public UI but is set when setting up the renderer for the element. Handling this change allows you to perform actions when the control is first displayed. One area where the OnElementPropertyChanged differs from WinUI dependency properties is that the change handler is not called when an initial value is set for the property, only when it subsequently changes. Therefore, we will handle the Renderer property change and set all the required properties to ensure that the control starts with the correct values. 

Our Example Control

We created a RichLabel control to demonstrate how the above works. This has the functionality of a Label (in WinUI, the equivalent control is the TextBlock) but adds automatic handling of links within the label text. This control will allow you to tap a phone number and dial it or tap a web link to open the default browser. IOS and Android support this functionality, but it is not exposed through Xamarin Forms, so we can use a custom renderer to add this behaviour. To show the basics of this approach, our renderer just handles two features – the Text and the Font. A more complex control would follow the same pattern but with many more properties. 

Implementing our Renderer for iOS

To get this functionality on iOS we need to use a different native control from the Xamarin LabelRenderer, so we have to create a new renderer derived from ViewRenderer<TView,TNativeView>. Xamarin’s iOS LabelRender uses UILabel, and we are switching to UITextView for more functionality. We have to add our own functionality to create the control, and set the link features at the same time:

				
					protected override UITextView CreateNativeControl() 
        { 
            var view = new UITextView(); 
            view.Editable = false; 
            view.DataDetectorTypes = UIDataDetectorType.All; 
            return view; 
        } 
				
			

The completed iOS property changed handler looks like this

				
					protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
        { 
            System.Diagnostics.Debug.WriteLine(e.PropertyName); 
            base.OnElementPropertyChanged(sender, e); 
            switch(e.PropertyName) 
            { 
                case "Renderer": 
                    UpdateText(); 
                    UpdateFont(); 
                    break; 

                case "Text": 
                    UpdateText();  
                    break; 

                case "FontFamily": 
                case "FontSize": 
                    UpdateFont();  
                    break; 
            } 
        } 
				
			

UpdateText is quite straightforward, passing the string to the native control. UpdateFont uses an iOS specific method to convert given font properties to a native UIFont and then setting this on the native control. 

Implementing our Renderer for Android

The Android equivalent is simpler – because we don’t need to change the type of native control. We can simply create a class which inherits from LabelRenderer, and we get all the built-in functionality. Then we add additional code to the property changed handler to call Linkify when either the Renderer or Text changes to build the hyperlinks from the new Text value. 

				
					protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
        { 
            base.OnElementPropertyChanged(sender, e); 
            
            if(e.PropertyName == "Text" || e.PropertyName == "Renderer") 
            { 
                Linkify.AddLinks(Control, MatchOptions.All); 
            } 
        } 
				
			

Implementing Native Controls in Uno Platform

Uno platform, understandably, takes a very different approach. The public API in this case is designed to exactly match WinUI so that you can use the same code across WinUI on Windows and on any Uno supported platform. One of the ways this becomes apparent is that many of the core controls are sealed in WinUI, and there is no access to the internal platform implementation, which means you cannot create a derived control and just add the extra bits of functionality. Instead, you need to create a custom Control and use this to host a native control. The Control must contain both the public functionality, such as methods and dependency properties and the code to interoperate with the native APIs. For this reason, you’ll need to make use of conditional compilation to have multiple sections of code, each appearing only for a particular platform. 

When you create a new custom control in an Uno project, it contains a bare minimum of code – the constructor sets a default style key for the control, and by default, this is the type of the custom control itself. A style is generated in the generic.xaml resource dictionary, and you can use this to apply a template to all instances of the control. If you were building a custom control consisting of other controls and WinUI primitives, this is where you would define the appearance of the control. However, when hosting a native control, you generally specify just a root layout control, e.g., a Border or a Grid, and then the native control is applied as a child of this in the code behind. In our example, the default template looks like this:

				
					<Style TargetType="local:RichLabel" > 
        <Setter Property="Template"> 
            <Setter.Value> 
                <ControlTemplate TargetType="local:RichLabel"> 
                    <Border x:Name="RootBorder" 
                        Background="{TemplateBinding Background}" 
                        BorderBrush="{TemplateBinding BorderBrush}" 
                        BorderThickness="{TemplateBinding BorderThickness}" 
                        Padding="{TemplateBinding Padding}"> 
                        <win:TextBlock Text="{TemplateBinding Text}"/> 
                    </Border> 
                </ControlTemplate> 
            </Setter.Value> 
        </Setter> 
    </Style> 
				
			

To test that it is working we’ve used Uno’s support for platform specific XAML to add a child TextBlock control on Windows only which will display the contents as plain text. On mobile platforms the native control will sit within the Border. 

We’ve implemented the native control by overriding the OnApplyTemplate method. This method is called when the template is applied, and the visual tree is being created. Once complete we can access named elements from the template using GetTemplateChild and cast the result to the correct type. Now we can get the RootBorder element from the template above and add a child control to it. A note of caution here – because the control template can be replaced it may break this code which depends on there being a Border element in the template with the defined name. 

Wrapping Native Controls

Uno provides a helper method VisualTreeHelper.AdaptNative which wraps a native control in a WinUI element which can be set as the Child of the Border. This wrapper handles the layout and works like a regular FrameworkElement. The native control itself is platform specific so we must create separate code for iOS and Android. We can do this by using conditional compilation using an #if block. The IOS and ANDROID constants ensure that the platform specific code is only visible to the compiler in the specified platform build. You can toggle the platform in Visual Studio and see that the code will be greyed out for the unselected platforms. 

To add a UITextView with automatic linking, like our Xamarin custom renderer, we add code inside the #if IOS block. Firstly, we create a UITextView, then set the DataDetectorTypes to All. We wrap this control using AdaptNative then set it as the child of the Border defined in the control template. The Android process is similar except using a TextView. This gets the control into the UI tree, however, we need to populate the native control with the property values from the WinUI control. We also need to ensure that changes to the properties are passed on to the native control. This approach differs from the Xamarin Forms renderer, but we can create change handlers for each DependencyProperty we added to the custom control. From these we fire a method to update the native control. As with the control creation, the exact nature of the process is platform specific, so we must again use conditional compilation. In our example, we expose only a Text property. In a real-world control, you may have other properties to customize the font, colour, etc, but each will follow the same process. 

In the control code we create an UpdateText method – this is to be called every time the Text property is set. Inside this property, we use conditional compilation to call the appropriate method on the native control to set the text. On Android, we must also call Linkify to scan the text for links and apply these. The method to support iOS and Android looks like the following:

				
					private void UpdateText() 
        { 
            if (_textView != null) 
            { 
#if IOS 
                _textView.Text = this.Text; 
#elif ANDROID 
                _textView.Text = this.Text; 
                Linkify.AddLinks(_textView, MatchOptions.All); 
#endif 
            } 
        } 
				
			

In the case of a Color property the platform specific code would also be responsible for converting a WinUI Color into the native equivalent. With these two aspects completed we now have the basics working to create the native control and set properties on it. The sample project displays the result you can tap the embedded links in the label and it will open web links in the default browser and phone links with the phone app. 

About Uno Platform

For those new to the Uno Platform, it allows for creating pixel-perfect, single-source C# and XAML apps that run natively on Windows, iOS, Android, macOS, Linux and Web via WebAssembly. In addition, it offers Figma integration for design-development handoff and a set of extensions to bootstrap your projects. Uno Platform is free, open-source (Apache 2.0), and available on GitHub.

Next Steps

Now that we have created a control showing how to wrap and customize a native control, you can follow the same process to wrap any native control and create something specific to your requirements just as you could with Xamarin Forms custom renderers. The source code is available for both the Xamarin Forms and Uno projects used for this blog. 

To upgrade to the latest release of Uno Platform, please update your packages to 4.8 via your Visual Studio NuGet package manager! If you are new to Uno Platform, following our official getting started guide is the best way to get started. (5 min to complete)

Join the Uno Platform 5.2 LIVE Webinar – Tomorrow, April 30th at 3 PM EST – Save to calendar