Xamarin Forms includes an assortment of “views,” more commonly known as controls, to help you build cross-platform UIs using XAML. Each control has a default appearance, and each control exposes properties that allow you to customize its appearance. But what if you’re handed a set of UI requirements that can’t be achieved using those properties? What if, for example, you need to round the corners on an Entry control or wrap and truncate text in a Label? (The Xamarin Forms Label control supports either but not both.) The Xamarin forums are full of such questions, and the answer is almost always “you need to write a custom renderer.”
If you’re going to use Xamarin Forms in real-world projects, you’re going to need custom renderers. Custom renderers let you customize the logic that “renders” native controls from Xamarin Forms controls. Knowing how to write custom renderers – and when to use them – is one of the skills that separates expert Xamarin Forms developers from the rest of the pack.
The subject of custom renderers is a multifaceted one. Some renderers require little effort to write. Others are much more difficult and require intimate knowledge of the platforms on which they run. We’re going to start by using custom renderers to fix an annoyance regarding the Xamarin Forms Button control. In subsequent articles, we’ll build on what’s presented here not only to expand your knowledge of custom renderers, but to solve some of the common problems that Xamarin Forms developers encounter.
Why Custom Renderers?
To understand why custom renderers play an important role in many Xamarin Forms apps, consider the following XAML:
<ContentView Padding="50,200,50,200"> <Button Text="Click Me" BorderRadius="32" BorderWidth="8"> <Button.BackgroundColor> <OnPlatform x:TypeArguments="Color" WinPhone="#FF343434" /> </Button.BackgroundColor> <Button.BorderColor> <OnPlatform x:TypeArguments="Color" iOS="#FF6495ED" Android="#FF474747" WinPhone="#FF474747" /> </Button.BorderColor> </Button> </ContentView>
The goal is simple: to declare a Button control with a border and rounded corners by utilizing Button’s BorderWidth, BorderColor, and BorderRadius properties. Here’s what the output looks like on Windows Phone, Android, and iOS, in that order:
On iOS, you get exactly what you’d expect: a button with a thick blue border and rounded corners. But not so on Windows Phone and Android. Windows Phone honors the BorderWidth and BorderColor properties but ignores BorderRadius. On Android, none of the three Border properties are honored. Xamarin Forms promises you that if you declare a Button control – or a Label, or a ListView, or whatever – a native version of that control will be rendered onto each platform. But it doesn’t promise you that every property of every control is implemented on every platform.
What if you need borders with rounded corners on Windows Phone and Android, too? Are you out of luck, or can Button be customized to support these attributes on other platforms as well? The short answer is that Button can’t be customized to do that, but ButtonRenderer – the class that gets instantiated at run-time to turn Xamarin Forms Buttons into native button controls – can. We’re going to derive from ButtonRenderer to create two custom renderers – one for Windows Phone, and one for Android – and apply them so that BorderWidth, BorderColor, and BorderRadius are honored on all three platforms:
The beauty of it is that once you wrap your head around custom renderers, you’ll be able perform deeper customizations on Xamarin Forms controls, or, if you’d prefer, write controls of your own. Enticing? Then let’s move on.
Renderers 101
The Xamarin Web site has an excellent article titled “Customizing Controls for Each Platform” introducing custom renderers. The article warns that “the renderer APIs are not yet final,” so don’t be surprised if the renderers presented in this article have to be modified to work in a future version of Xamarin Forms. My button renderers were built and tested with Xamarin Forms 1.3. If necessary, I’ll update them to work with future versions as well.
Every control in Xamarin Forms is accompanied by a set of renderers. There is one renderer per control per platform. For example, when you declare a Button control and run the app on iOS, Xamarin instantiates a class named ButtonRenderer, which is found in the Xamarin.Forms.Platform.iOS namespace of the assembly of the same name. ButtonRenderer, in turn, instantiates a native iOS UIButton. On Android, Xamarin uses the ButtonRenderer class in the Xamarin.Forms.Platform.Android namespace of the Xamarin.Forms.Platform.Android assembly to create an Android Button control, and on Windows Phone, it uses the ButtonRenderer class in the Xamarin.Forms.Platform.WinPhone namespace of the Xamarin.Forms.Platform.WP8 assembly to create a Windows Phone Button. The following diagram illustrates the relationship between Button controls and per-platform ButtonRenderers:
Under normal circumstances, the renderers “just work” and you don’t even have to be aware that they’re there. But if you wish to customize the way a control is rendered, you can replace one or more of the built-in renderers with renderers of your own. Here’s a template for writing a custom renderer for buttons on the Windows Phone platform:
[assembly: ExportRenderer(typeof(Xamarin.Forms.Button), typeof(CustomButtonRenderer))] namespace ButtonRendererDemo.WinPhone { public class CustomButtonRenderer : ButtonRenderer { protected override void OnElementChanged(ElementChangedEventArgs<Xamarin.Forms.Button> e) { base.OnElementChanged(e); var button = e.NewElement; // Xamarin.Forms.Button control if (this.Control != null) { // Control refers to the instance of System.Windows.Controls.Button // created by the base renderer } } } }
The assembly attribute is important, because that’s what connects the custom renderer – CustomButtonRenderer in this example – to Xamarin Forms Buttons. Through reflection, Xamarin discovers the attribute and instead of instantiating a ButtonRenderer for each Button you create, it instantiates a CustomButtonRenderer. The latter derives from ButtonRenderer and overrides OnElementChanged, which is called when the Xamarin Forms Button is created. The call to the base class’s OnElementChanged method in the override instantiates a Windows Phone Button and assigns a reference to the renderer’s Control property. Now that you have a reference to the native control, you can manipulate it as needed.
ButtonRenderer also contains an overridable method named OnElementPropertyChanged that’s called when a property of the corresponding Xamarin Forms Button changes. It frequently (but not always) needs to be overridden as well. Specifically, if you need to manipulate the native control when a property of the Xamarin control changes, you’ll need to override OnElementPropertyChanged.
Remember that renderers are per-platform, which means that if you want to change the way a control is rendered on Windows Phone, Android, and iOS, you’ll be writing three renderers. The template is the same for each. Also remember that inside the renderer, Control refers to a native control. You’re not insulated from the native platform inside a renderer as you are in Xamarin Forms. But that’s the whole point of a renderer: getting access to the native control that Xamarin renders out so you can perform platform-specific customizations.
Writing a Custom Button Renderer for Windows Phone
Now let’s write a custom renderer to solve the problem that BorderRadius is ignored for Xamarin Forms Buttons on Windows Phone.
To do so, you have to know a bit about how controls work on Windows Phone. Each control on that platform is accompanied by a control template – basically a blueprint for the control itself in form of a collection of XAML primitives that are created when the control is created. Here’s an abbreviated version of the template that Windows Phone 8 uses to “inflate” a Button control:
<Grid Background="Transparent"> <Border x:Name="ButtonBackground" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" CornerRadius="0" Margin="{StaticResource PhoneTouchTargetOverhang}"> <ContentControl x:Name="ContentContainer" ContentTemplate="{TemplateBinding ContentTemplate}" Content="{TemplateBinding Content}" Foreground="{TemplateBinding Foreground}" HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" Padding="{TemplateBinding Padding}" VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"/> </Border> </Grid>
From this, we can discern that it is a Border element that forms the border a user sees around a Windows Phone Button. And while a Windows Phone Button control doesn’t have a BorderRadius property, a Border does (it’s called CornerRadius). If, after the Button is created, we could reach into its control template and get a reference to the Border, we could set the Border’s CornerRadius property equal to the Xamarin Forms Button’s BorderRadius property and voila! We’d have a button with round corners.
Here’s a custom renderer for Windows Phone that does precisely that:
[assembly: ExportRenderer(typeof(Xamarin.Forms.Button), typeof(CustomButtonRenderer))] namespace ButtonRendererDemo.WinPhone { public class CustomButtonRenderer : ButtonRenderer { protected override void OnElementChanged(ElementChangedEventArgs<Xamarin.Forms.Button> e) { base.OnElementChanged(e); var button = e.NewElement; if (Control != null) { button.SizeChanged += OnSizeChanged; } } private void OnSizeChanged(object sender, EventArgs e) { var button = (Xamarin.Forms.Button)sender; Control.ApplyTemplate(); var borders = Control.GetVisuals<Border>(); foreach (var border in borders) { border.CornerRadius = new CornerRadius(button.BorderRadius); } button.SizeChanged -= OnSizeChanged; } protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) { base.OnElementPropertyChanged(sender, e); if (e.PropertyName == "BorderRadius") { var borders = Control.GetVisuals<Border>(); foreach (var border in borders) { border.CornerRadius = new CornerRadius(((Xamarin.Forms.Button)sender).BorderRadius); } } } } static class DependencyObjectExtensions { public static IEnumerable<T> GetVisuals<T>(this DependencyObject root) where T : DependencyObject { int count = VisualTreeHelper.GetChildrenCount(root); for (int i = 0; i < count; i++) { var child = VisualTreeHelper.GetChild(root, i); if (child is T) yield return child as T; foreach (var descendants in child.GetVisuals<T>()) { yield return descendants; } } } } }
There’s a lot going on in this code, but here are the highlights. The OnElementChanged override waits for the Xamarin Forms Button to fire a SizeChanged event (experimentation proved that trying to apply a radius to the Border earlier in the control’s lifetime is fruitless), and then reaches into the control template, finds the Border, and copies the Button’s BorderRadius property to the Border’s CornerRadius property. To find the Border, it calls a local C# extension method named GetVisuals, which uses Windows Phones’ built-in VisualTreeHelper class to inspect the visual tree generated from the control template. The renderer also overrides OnElementPropertyChanged because if the Xamarin Forms Button’s BorderRadius property changes later in the lifetime of the app, the Border’s CornerRadius property must be modified, too. Try it: Write a little test code to change the BorderRadius when the button is clicked, and you’ll find that the border radius changes. Now repeat the test with OnElementPropertyChanged commented out and the radius won’t change.
Writing a Custom Button Renderer for Android
That solves the problem on Windows Phone, but what about Android? Alas, the custom renderer presented in the previous section won’t even compile on Android. And even if it did compile, it wouldn’t work because controls on Android are very different than controls on Windows Phone.
To tackle this problem on Android, you need to know that the appearance of Android controls is defined by drawables. When you create a button in an Android app, the system uses a set of predefined drawables to paint the button on the screen in various states (focused, pressed, etc.). A collection of drawables representing the button in different states is called a state list and is represented by instances of a class named StateListDrawable. Android programmers can customize the look of buttons by eschewing the built-in drawables and providing drawables of their own. Drawables (and state lists) are typically defined in XML, but can also be created programmatically.
The key to writing a custom renderer for Android that honors a Xamarin Forms Button’s BorderWidth, BorderColor, and BorderRadius properties is to create one or more drawables for the Button, and to apply these properties to the drawables themselves. Here’s the custom renderer I used to generate the Android screen shot featuring a button with rounded corners:
[assembly: ExportRenderer(typeof(Xamarin.Forms.Button), typeof(CustomButtonRenderer))] namespace ButtonRendererDemo.Droid { public class CustomButtonRenderer : ButtonRenderer { private GradientDrawable _normal, _pressed; protected override void OnElementChanged(ElementChangedEventArgs<Xamarin.Forms.Button> e) { base.OnElementChanged(e); if (Control != null) { var button = e.NewElement; // Create a drawable for the button's normal state _normal = new Android.Graphics.Drawables.GradientDrawable(); if (button.BackgroundColor.R == -1.0 && button.BackgroundColor.G == -1.0 && button.BackgroundColor.B == -1.0) _normal.SetColor(Android.Graphics.Color.ParseColor("#ff2c2e2f")); else _normal.SetColor(button.BackgroundColor.ToAndroid()); _normal.SetStroke((int)button.BorderWidth, button.BorderColor.ToAndroid()); _normal.SetCornerRadius(button.BorderRadius); // Create a drawable for the button's pressed state _pressed = new Android.Graphics.Drawables.GradientDrawable(); var highlight = Context.ObtainStyledAttributes(new int[] { Android.Resource.Attribute.ColorActivatedHighlight }).GetColor(0, Android.Graphics.Color.Gray); _pressed.SetColor(highlight); _pressed.SetStroke((int)button.BorderWidth, button.BorderColor.ToAndroid()); _pressed.SetCornerRadius(button.BorderRadius); // Add the drawables to a state list and assign the state list to the button var sld = new StateListDrawable(); sld.AddState(new int[] { Android.Resource.Attribute.StatePressed }, _pressed); sld.AddState(new int[] { }, _normal); Control.SetBackgroundDrawable(sld); } } protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) { base.OnElementPropertyChanged(sender, e); var button = (Xamarin.Forms.Button)sender; if (_normal != null && _pressed != null) { if (e.PropertyName == "BorderRadius") { _normal.SetCornerRadius(button.BorderRadius); _pressed.SetCornerRadius(button.BorderRadius); } if (e.PropertyName == "BorderWidth" || e.PropertyName == "BorderColor") { _normal.SetStroke((int)button.BorderWidth, button.BorderColor.ToAndroid()); _pressed.SetStroke((int)button.BorderWidth, button.BorderColor.ToAndroid()); } } } } }
The OnElementChanged override creates a pair of GradientDrawables, one representing the button in the “normal” state and the other in the pressed state, and initializes them using the Xamarin Forms Button’s Border properties. It then packages them together in a DrawableStateList and assigns the state list to the button. OnElementPropertyChanged ensures that the drawables are updated if any of these properties change on the Xamarin Forms Button. Different implementation, same result. Now you can have borders and rounded corners on Windows Phone and Android, too.
Summary
Of course, writing a custom renderer and attaching it to Button customizes all buttons on the platform. What if you wanted to customize some buttons, but not all? There’s a simple solution – one that I’ll introduce in a future article. In the meantime, check out Mark Smith’s excellent video on custom renderers over at Xamarin University. It’s chock full of great tips and will help deepen your understanding of Xamarin Forms’ rendering architecture.
Stay tuned for the next article in this series, which will use a custom renderer to add some flair to the RPN calculator app I introduced a few weeks ago. And feel free to download the source code for the app featured in this article. One thing you’ll notice is that the custom renderers belong to the Windows Phone and Android projects, not the shared PCL project. Makes sense when you think about it, because renderers, as you know, are platform-specific.
Need Xamarin Help?
Xamarin Consulting Xamarin Training