Creating a Xamarin.Forms Custom Renderer for a FlexGrid Cell Template
With the Xuni FlexGrid control you can create custom cells by defining some UI elements as a cell template. The cell template is essentially a reused component, rendered on every row of the grid for that particular column, and you can customize the cell template for any number of columns. The following markup shows a FlexGrid with three bound columns and one column with a custom cell template.
<xuni:FlexGrid x:Name="grid1" AutoGenerateColumns="False">
<xuni:FlexGrid.Columns>
<xuni:GridColumn Binding="Symbol" Width="*"/>
<xuni:GridColumn Binding="Open" Format="n2" Width="*"/>
<xuni:GridColumn Binding="Close" Format="n2" Width="*"/>
<xuni:GridColumn Header="Change" Width="2*">
<xuni:GridColumn.CellTemplate>
<DataTemplate>
<Label Text="My Custom Content" />
</DataTemplate>
</xuni:GridColumn.CellTemplate>
</xuni:GridColumn>
</xuni:FlexGrid.Columns>
</xuni:FlexGrid>
The Problem – Layout Controls within a Cell Template hurt Performance
When FlexGrid is rendered on each platform, we actually leverage a native version of FlexGrid to get the best performance. The FlexGrid control is not really created in Xamarin.Forms – you just access it from the Forms project. Your custom cell template does not have a corresponding component at the native level, so the best way we can “pass” the template to the native level is as a View. It’s no different than if it were outside of FlexGrid and sitting on your Forms page. If your grid has a lot of rows the number of Views can quickly pile up and begin to visibly affect performance. Item-bound controls like the ListView and FlexGrid perform best when they have an optimized way to render the items. A ListView with 1,000 text cells will perform better than a ScrollView containing 1,000 labels because it’s been optimized. FlexGrid tries to help as best as it can. For instance, if your cell template contains just a single control (such as a Label, an Image, or a Xuni gauge), the grid can reliably pass this to the native platform, and you get great performance. But if your cell template contains multiple controls in a layout (such as a Grid or StackLayout), there is no reliable way for FlexGrid to optimize it using the Xamarin.Forms renderer so each cell has to be passed as a View.
The Solution – Create a Custom Renderer for your Cell Template
There is a way to work around the performance limitation that comes with custom cell templates containing complex layouts. You can write your own custom renderer for your cell. Underneath Xamarin.Forms every control has a native renderer on each platform that determines how the control is created. And luckily for us, Xamarin has exposed an entry point for developers to create their own native renderers and therefore, create custom controls written completely in C#. A custom renderer can be used to implement platform-specific customizations on each platform. The intended purpose is to actually deliver a different experience on each platform. But in most cases, developers write custom renderers to fill in a gap. For instance, Xamarin.Forms doesn’t provide any way to save an image, so we can go a level deeper and write code within each platform project that handles the action. Basically, when you go deeper you unlock more capabilities. In our case we want to create a custom renderer for a FlexGrid cell to get the best performance by getting closer to the native level. For example, let’s create this custom cell layout that is a horizontal layout containing an image and a colored label (in the Change column). You can continue reading the implementation or download the sample on GitHub.
Creating the Custom Renderer
The Xamarin.Forms documentation has very nice examples of creating a variety of custom renderers that start extending various controls. Since we aren’t customizing an existing control, we will follow the steps under implementing a custom view. There are three steps defined in the documentation for creating a custom renderer. I won’t cover everything in as much detail as the documentation. Instead, I will just interject what is necessary to create the exact cell layout as shown above.
Step 1 – Create a Custom Xamarin.Forms Control
In this step we define the object model for our custom control. To help understand this process, approach it like you are creating a reusable control that could be put anywhere in your app. My custom cell layout is kind of like a stock ticker with a value. So I will need just a bindable value property. I could add an image property, but in this case the image (and text color) is determined from the value. I will name my control CustomChangeView, and I will add this class to the Xamarin.Forms portable project.
public class CustomChangeView : View
{
public static readonly BindableProperty ValueProperty = BindableProperty.Create(propertyName: "Value", returnType: typeof(double), declaringType: typeof(double), defaultValue: 0.0);
public double Value
{
get { return (double)GetValue(ValueProperty); }
set { SetValue(ValueProperty, value); }
}
}
Step 2 – Consume the Custom Control in Xamarin.Forms
In this step we will consume the control somewhere in our UI. We can define a custom cell template for FlexGrid in XAML, so we can easily consume, and bind to, our custom control there. I will add a local XML namespace at the top of my XAML page, and then define the cell template for the 4th column using the CustomChangeView control.
xmlns:local="clr-namespace:FlexGridCustomRenderer;assembly=FlexGridCustomRenderer"
xmlns:xuni="clr-namespace:Xuni.Forms.FlexGrid;assembly=Xuni.Forms.FlexGrid"
…
<xuni:FlexGrid x:Name="grid1" AutoGenerateColumns="False" IsReadOnly="True">
<xuni:FlexGrid.Columns>
<xuni:GridColumn Binding="Symbol" Width="*"/>
<xuni:GridColumn Binding="Open" Format="n2" Width="*"/>
<xuni:GridColumn Binding="Close" Format="n2" Width="*"/>
<xuni:GridColumn Header="Change" Width="2*">
<xuni:GridColumn.CellTemplate>
<DataTemplate>
<local:CustomChangeView Value="{Binding Change}"/>
</DataTemplate>
</xuni:GridColumn.CellTemplate>
</xuni:GridColumn>
</xuni:FlexGrid.Columns>
</xuni:FlexGrid>
Step 3 – Create the Custom Renderer for each Platform
This is the most complex step, as we now have to leave the Xamarin.Forms portable project and write some code in each platform project we plan to support. So far Xamarin.Forms sees the CustomChangeView but has no idea how to create it. The idea is that we tell Xamarin.Forms, hey – use this code to render this control in each platform. The process is to create a subclass of the ViewRenderer<T1, T2> class where the first type argument is our custom control, and the second type is the underlying native control that will be used to create it. For Windows Phone we will use a StackPanel. On Android we will use a LinearLayout. On iOS we could use a UIStackView, but since it was just introduced in iOS9 we can be a bit more primitive and just use a basic UIView. Within our subclass we will override the OnElementChanged method and inject our logic to instantiate and set our custom content. This method is called when Xamarin.Forms attempts to create our custom control. Finally, we add the ExportRenderer attribute to the top of each custom renderer class to register the control with Xamarin.Forms. CustomChangeViewRenderer iOS:
using System;
using Xamarin.Forms;
using FlexGridCustomRenderer.iOS;
using Xamarin.Forms.Platform.iOS;
using UIKit;
[assembly: ExportRenderer (typeof(FlexGridCustomRenderer.CustomChangeView), typeof(CustomChangeViewRenderer))]
namespace FlexGridCustomRenderer.iOS
{
public class CustomChangeViewRenderer : ViewRenderer<CustomChangeView, UIKit.UIView>
{
UIView layoutPanel;
UILabel textValue;
UIImageView imgArrow;
protected override void OnElementChanged(ElementChangedEventArgs<FlexGridCustomRenderer.CustomChangeView> e)
{
base.OnElementChanged (e);
if (Control == null) {
// initialize layout
layoutPanel = new UIView ();
// initialize image
imgArrow = new UIImageView ();
imgArrow.Frame = new CoreGraphics.CGRect (0, 0, 25, 25);
layoutPanel.AddSubview (imgArrow);
// initialize label
textValue = new UILabel ();
textValue.Font = UIFont.FromName ("Helvetica", 14f);
textValue.Frame = new CoreGraphics.CGRect (25, 0, 100, 25);
layoutPanel.AddSubview (textValue);
// set control
SetNativeControl (layoutPanel);
}
if (e.OldElement != null) {
// unsubscribe
}
if (e.NewElement != null) {
// subscribe
textValue.Text = e.NewElement.Value.ToString ("n2");
if (e.NewElement.Value < 0) { textValue.TextColor = UIColor.FromRGB (231, 76, 60); imgArrow.Image = UIImage.FromFile ("Arrow-Down-64.png"); } else if (e.NewElement.Value > 0) {
textValue.TextColor = UIColor.FromRGB (46, 204, 113);
imgArrow.Image = UIImage.FromFile ("Arrow-Up-64.png");
}
}
}
}
}
CustomChangeViewRenderer Android:
using Android.App;
using Android.Content;
using Android.OS;
using Android.Runtime;
using Android.Views;
using Android.Widget;
using Xamarin.Forms;
using FlexGridCustomRenderer.Droid;
using Xamarin.Forms.Platform.Android;
[assembly: ExportRenderer(typeof(FlexGridCustomRenderer.CustomChangeView), typeof(CustomChangeViewRenderer))]
namespace FlexGridCustomRenderer.Droid
{
public class CustomChangeViewRenderer : ViewRenderer<FlexGridCustomRenderer.CustomChangeView, Android.Widget.LinearLayout>
{
LinearLayout layoutPanel;
TextView textValue;
ImageView imgArrow;
protected override void OnElementChanged(ElementChangedEventArgs<FlexGridCustomRenderer.CustomChangeView> e)
{
base.OnElementChanged(e);
if (Control == null)
{
layoutPanel = new LinearLayout(Context);
layoutPanel.Orientation = Orientation.Horizontal;
imgArrow = new ImageView(Context);
layoutPanel.AddView(imgArrow);
textValue = new TextView(Context);
textValue.SetTextSize(Android.Util.ComplexUnitType.Dip, 16);
textValue.SetPadding(0, 10, 0, 0);
layoutPanel.AddView(textValue);
SetNativeControl(layoutPanel);
}
if (e.OldElement != null)
{
// Unsubscribe
}
if (e.NewElement != null)
{
// Subscribe
textValue.Text = e.NewElement.Value.ToString("n2");
if (e.NewElement.Value < 0) { imgArrow.SetImageResource(Resource.Drawable.arrowdown); textValue.SetTextColor(Android.Graphics.Color.Argb(255, 231, 76, 60)); } else if (e.NewElement.Value > 0)
{
imgArrow.SetImageResource(Resource.Drawable.arrowup);
textValue.SetTextColor(Android.Graphics.Color.Argb(255, 46, 204, 113));
}
}
}
}
}
CustomChangeViewRenderer WinPhone:
using FlexGridCustomRenderer;
using FlexGridCustomRenderer.WinPhone;
using System;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using Xamarin.Forms;
using Xamarin.Forms.Platform.WinPhone;
[assembly: ExportRenderer(typeof(CustomChangeView), typeof(CustomChangeViewRenderer))]
namespace FlexGridCustomRenderer.WinPhone
{
public class CustomChangeViewRenderer : ViewRenderer<CustomChangeView, System.Windows.Controls.StackPanel>
{
StackPanel layoutPanel;
TextBlock textValue;
System.Windows.Controls.Image imgArrow;
protected override void OnElementChanged(ElementChangedEventArgs<CustomChangeView> e)
{
base.OnElementChanged(e);
if (Control == null)
{
layoutPanel = new StackPanel();
layoutPanel.Orientation = Orientation.Horizontal;
imgArrow = new System.Windows.Controls.Image();
layoutPanel.Children.Add(imgArrow);
textValue = new TextBlock();
layoutPanel.Children.Add(textValue);
//InitializeAsync();
SetNativeControl(layoutPanel);
}
if (e.OldElement != null)
{
// Unsubscribe
imgArrow.Source = null;
}
if (e.NewElement != null)
{
// Subscribe
textValue.Text = e.NewElement.Value.ToString("n2");
if(e.NewElement.Value < 0) { imgArrow.Source = new BitmapImage(new Uri("Toolkit.Content/Arrow-Down-64.png", UriKind.RelativeOrAbsolute)); textValue.Foreground = new SolidColorBrush(System.Windows.Media.Color.FromArgb(255, 231, 76, 60)); } else if(e.NewElement.Value > 0)
{
imgArrow.Source = new BitmapImage(new Uri("Toolkit.Content/Arrow-Up-64.png", UriKind.RelativeOrAbsolute));
textValue.Foreground = new SolidColorBrush(System.Windows.Media.Color.FromArgb(255, 46, 204, 113));
}
}
}
}
}
Compare Custom Renderers to Alternative
The alternative to creating a custom renderer is to create the custom cell content within the Xamarin.Forms portable project. The XAML would look something like this:
...
<xuni:GridColumn.CellTemplate>
<DataTemplate>
<StackLayout Orientation="Horizontal" Spacing="0">
<Image Source="{Binding ChangeImageSource}"/>
<Label Text="{Binding Change, StringFormat='{0:n2}'}" TextColor="{Binding Change, Converter={StaticResource changeTextColorConverter}}" VerticalOptions="Center">
<Label.FontSize>
<OnPlatform x:TypeArguments="x:Double" iOS="14" Android="16" WinPhone="18"/>
</Label.FontSize>
</Label>
</StackLayout>
</DataTemplate>
</xuni:GridColumn.CellTemplate>
...
Note that we also need a text color converter that makes the label text red or green. You can see the full implementation of the alternative, and compare performance side-by-side in the sample. Download the sample on GitHub And as I’ve explained, the downside is that since Xamarin.Forms provides no optimization for layout controls, your app performance will blow up much quicker as the only way to pass each cell to the native platform is as a View.
Conclusion – When do you use Custom Renderers?
When should you create a custom renderer for a FlexGrid cell? The answer is very simple – anytime the cell content includes multiple controls in a grid or stack layout. If your custom cell is just a single control (like a single Label or Image), then there is no need to create a custom renderer because FlexGrid can reliably render these on the native platforms in an efficient manner. Creating a custom renderer allows you to take advantage of better performance. As you can see in the comparison, the renderer is not required but the FlexGrid without it performs more slowly when you run it on a physical device. Creating custom renderers have more uses as well. If you’ve worked with Xamarin.Forms you may find that the control set is limited, and even suites like Xuni may not have the controls you need. You may find that you can create or customize your control as a renderer. The brilliant thing about custom renderers in Xamarin.Forms is that they are still written entirely in C#. This is because Xamarin.Forms is really just a layout on top of Xamarin.Android and Xamarin.iOS, all of which use C#. You can read more about custom renderers in the Xamarin.Forms documentation.