Lighting up Native Platform Features in Xamarin Forms – Part 2

In the previous post I implemented a custom attached property to be used in Xamarin Forms XAML when a built-in accessory view is desired on a table cell. In this follow-up we will continue and build out the iOS renderer that is responsible for actually enabling the feature in our running application.

What about Android and Windows Phone – won’t they be affected too? That’s the beauty of the Xamarin Forms rendering model – it is up to each platform to decide how to natively implement the controls described in XAML markup. In this case, we have extended the existing XAML (not replaced it), and so the Android and Windows Phone renderers will simply just ignore the extensions unless we customize the renderers for those platforms as well.

Badge-Xamarin

TableView Rendering

Note: The following information comes through decompilation and inspection of the iOS renderers found in Xamarin.Forms.Platform.iOS (this assembly can be found in your Xamarin.iOS installation, or in your debug/bin output folder after compiling a Xamarin Forms project for iOS). I used JetBrains dotPeek to decompile the source from that assembly file.

The process of mapping a XAML TableView or ListView to a native UITableView/UITableViewSource and the various Xamarin Forms cell types to UITableViewCell objects is performed by a fairly complex orchestra of intertwined classes in the default iOS rendering system. It involves several moving parts, but ultimately the generation of native cell objects is handed off to an appropriate subclass of CellRenderer – depending upon which type of cell it is (text, image, etc.). Conveniently, this is handled by a public virtual method (GetCell()), which allows us to intercept and manipulate the generated cells.

Overriding the Default Cell Renderer with a Custom Renderer

The first step in creating our custom renderer is to subclass from the appropriate existing renderer. By subclassing an existing renderer, we can avoid having to implement everything from scratch (which would be a considerable task considering how relatively complex the table rendering code is). To override the default handling of TextCells, we want to subclass from TextCellRenderer as such:

  1. public class AccessorizedTextCellRenderer : TextCellRenderer
  2. {
  3.     public override UITableViewCell GetCell(Cell item, UITableViewCell reusableCell, UITableView tv)
  4.     {
  5.         var cell = base.GetCell(item, reusableCell, tv);
  6.         Apply(item, cell);
  7.         return cell;
  8.     }
  9.     public void Apply(Cell cell, UITableViewCell nativeCell)
  10.     {
  11.         // customize the generated cell here…
  12.     }
  13. }

And so that Xamarin Forms recognizes our new renderer, we need to register it with an assembly-level attribute. This can be placed just after the “using” statements at the top of the source code file (or in any other source code file in the iOS project):

  1. [assembly: ExportRenderer(typeof(TextCell),
  2.     typeof(DisclosureAccessoryDemo.iOS.AccessorizedTextCellRenderer))]

Refactoring the Custom Renderer to Handle All Cell Types

This is great, however we don’t want to only support Text Cells. It is perfectly legitimate to use Accessories on Image Cells and custom View Cells also. These each have their own default renderers – and it would not be ideal to have to implement our customization code three separate times and keep them in sync. So before we go further, let’s refactor this slightly by moving the Apply() method to a static class and adding the other two custom renderers:

  1. internal class CellAccessory
  2. {
  3.     public static void Apply(Cell cell, UITableViewCell nativeCell)
  4.     {
  5.         // customize the generated cell here…
  6.     }
  7. }
  8. public class AccessorizedTextCellRenderer : TextCellRenderer
  9. {
  10.     public override UITableViewCell GetCell(Cell item, UITableViewCell reusableCell, UITableView tv)
  11.     {
  12.         var cell = base.GetCell(item, reusableCell, tv);
  13.         CellAccessory.Apply(item, cell);
  14.         return cell;
  15.     }
  16. }
  17. public class AccessorizedImageCellRenderer : ImageCellRenderer
  18. {
  19.     public override UITableViewCell GetCell(Cell item, UITableViewCell reusableCell, UITableView tv)
  20.     {
  21.         var cell = base.GetCell(item, reusableCell, tv);
  22.         CellAccessory.Apply(item, cell);
  23.         return cell;
  24.     }
  25. }
  26. public class AccessorizedViewCellRenderer : ViewCellRenderer
  27. {
  28.     public override UITableViewCell GetCell(Cell item, UITableViewCell reusableCell, UITableView tv)
  29.     {
  30.         var cell = base.GetCell(item, reusableCell, tv);
  31.         CellAccessory.Apply(item, cell);
  32.         return cell;
  33.     }
  34. }

And of course we need to add the corresponding additional assembly-level attributes:

  1. [assembly: ExportRenderer(typeof(TextCell),
  2.     typeof(DisclosureAccessoryDemo.iOS.AccessorizedTextCellRenderer))]
  3. [assembly: ExportRenderer(typeof(ImageCell),
  4.     typeof(DisclosureAccessoryDemo.iOS.AccessorizedImageCellRenderer))]
  5. [assembly: ExportRenderer(typeof(ViewCell),
  6.     typeof(DisclosureAccessoryDemo.iOS.AccessorizedViewCellRenderer))]

Altering Renderer Behavior based on our Attached Property

At this point we are ready to implement the custom behavior. Our project should still build and run, but the application’s behavior so far should be unchanged. We have overridden the default renderers, but we aren’t altering their behavior yet. Let’s do that now.

In our Apply() method we have two incoming parameters – a reference to the source XAML cell element and a reference to the destination native UITableViewCell object that will be displayed. If you recall from my Part 1 post, we can inspect the value of our attached property by using the GetValue() method of the source cell element. If there is no value specified, then the default for that property will be returned:

  1. var acc = (AccessoryType)cell.GetValue(CellExtensions.AccessoryProperty);

Based on the value returned, we can either set the native cell’s Accessory type to a built-in value or (if there is none specified) show an empty view in its place. The reason for using an empty view is to preserve cell layout so that when adjacent cells have differing Accessory values, their text will still align from one cell to the next:

  1. switch (acc)
  2. {
  3.     case AccessoryType.None:
  4.         nativeCell.Accessory = UITableViewCellAccessory.None;
  5.         if (nativeCell.AccessoryView == null)
  6.             nativeCell.AccessoryView = new UIView(new CGRect(0, 0, 20, 40));
  7.         return;
  8.     case AccessoryType.Checkmark:
  9.         nativeCell.Accessory = UITableViewCellAccessory.Checkmark;
  10.         break;
  11.     case AccessoryType.DisclosureIndicator:
  12.         nativeCell.Accessory = UITableViewCellAccessory.DisclosureIndicator;
  13.         break;
  14.     case AccessoryType.DetailButton:
  15.         nativeCell.Accessory = UITableViewCellAccessory.DetailButton;
  16.         break;
  17.     case AccessoryType.DetailDisclosureButton:
  18.         nativeCell.Accessory = UITableViewCellAccessory.DetailDisclosureButton;
  19.         break;
  20. }
  21. if (nativeCell.AccessoryView != null)
  22.     nativeCell.AccessoryView.Dispose();
  23. nativeCell.AccessoryView = null;

At this point, we can run our application and the Accessory indicators should appear as expected.

image
Our App with native accessory views attached

What about Property Changes?

Mission Accomplished, right? Possibly not – while our solution thus far will work for static cases where cells are assigned an accessory value once and it never changes, it will not work for dynamic scenarios. For example, if you are building a checklist and you need to toggle individual cell checkmarks. With our current solution, the cell accessories are applied only once – when the cell is first scrolled into view. Luckily, this is a problem we can solve – BindableObject provides an implementation for INotifyPropertyChanged, and it not only raises this mechanism for normal properties, but for attached properties as well! Let’s start out by refactoring our renderer code slightly, in a way that will make it easier later. Here, I am simply extracting the “guts” of our existing Apply() method into a new Reapply() method:

  1. public static void Apply(Cell cell, UITableViewCell nativeCell)
  2. {
  3.     Reapply(cell, nativeCell);
  4. }
  5. private static void Reapply(Cell cell, UITableViewCell nativeCell)
  6. {
  7.     var acc = (AccessoryType)cell.GetValue(CellExtensions.AccessoryProperty);
  8.     switch (acc)
  9.     {
  10.         case AccessoryType.None:
  11.             nativeCell.Accessory = UITableViewCellAccessory.None;
  12.             if (nativeCell.AccessoryView == null)
  13.                 nativeCell.AccessoryView = new UIView(new CGRect(0, 0, 20, 40));
  14.             return;
  15.         case AccessoryType.Checkmark:
  16.             nativeCell.Accessory = UITableViewCellAccessory.Checkmark;
  17.             break;
  18.         case AccessoryType.DisclosureIndicator:
  19.             nativeCell.Accessory = UITableViewCellAccessory.DisclosureIndicator;
  20.             break;
  21.         case AccessoryType.DetailButton:
  22.             nativeCell.Accessory = UITableViewCellAccessory.DetailButton;
  23.             break;
  24.         case AccessoryType.DetailDisclosureButton:
  25.             nativeCell.Accessory = UITableViewCellAccessory.DetailDisclosureButton;
  26.             break;
  27.     }
  28.     if (nativeCell.AccessoryView != null)
  29.         nativeCell.AccessoryView.Dispose();
  30.     nativeCell.AccessoryView = null;
  31. }

And then we can change the Apply() method so that it now monitors for changes to the attached property:

  1. public static void Apply(Cell cell, UITableViewCell nativeCell)
  2. {
  3.     Reapply(cell, nativeCell);
  4.     cell.PropertyChanged += (s, e) =>
  5.     {
  6.         if (e.PropertyName == CellExtensions.AccessoryProperty.PropertyName)
  7.             Reapply(cell, nativeCell);
  8.     };
  9. }

If we test this, we will find that it works as expected. However, there is a slight problem…

Avoiding Memory Leaks

It might be subtle, but in our previous step we have managed to introduce a nasty memory leak into our application. To explain, we need to consider the context in which our code is being used… the source cells are owned by a XAML-based container (either a ListView or a TableView), and the native cells are generated on-demand as they are being scrolled into view. In order to monitor our source XAML objects for changes, we are attaching an event handler. And if the current cell scrolls out of view, and then back into view, we will end up adding a second event handler to the same cell instance – never do we clear out those handlers. That’s pretty bad, and by itself would lead to performance penalties…

— but it gets worse —

Our event handler itself is also doing something nefarious. If you look carefully, within the short lambda expression you will notice that we are referencing the “cell” and “nativeCell” outer variables from within the lambda itself. This will work; however it creates a closure on both variables – the lamba expression itself will keep those variables alive and ineligible for garbage collection, even if they are no longer referenced elsewhere. And since our lamba is the target of the event handlers we are adding (and never removing), this problem can be very disastrous.

This is a tricky problem to solve. We actually do want to retain references to those variables for longer than normal (so that we can update the native control if our attached property value changes). But we don’t want this to lead to performance degradation or a memory leak. Our best bet here is to use the WeakReference<T> class. WeakReference<T> allows you to retain a reference to an object without blocking garbage collection of that object. We can alter the CellAccessory class to contain the two weak references, and track them in a list so that we can also avoid linking a cell to two different native cells when it gets recycled. Additionally, we can remove dead entries from the tracked list if we encounter them. Here is the updated version of the CellAccessory class:

  1. internal class CellAccessory
  2. {
  3.     private static List<CellAccessory> _watchedCells =
  4.         new List<CellAccessory>();
  5.     internal static void Apply(Cell cell, UITableViewCell nativeCell)
  6.     {
  7.         CellAccessory watchedCell = null;
  8.         // look to see if we are already tracking this xaml cell
  9.         foreach (var item in _watchedCells.ToArray())
  10.         {
  11.             Cell cellRef;
  12.             if (item._cell.TryGetTarget(out cellRef))
  13.             {
  14.                 if (cellRef == cell)
  15.                 {
  16.                     watchedCell = item;
  17.                     break;
  18.                 }
  19.             }
  20.             else
  21.             {
  22.                 // remove dead entry from list
  23.                 _watchedCells.Remove(item);
  24.             }
  25.         }
  26.         // if not already tracking, set up new entry and monitor for property changes
  27.         if (watchedCell == null)
  28.         {
  29.             watchedCell = new CellAccessory { _cell = new WeakReference<Cell>(cell) };
  30.             cell.PropertyChanged += watchedCell.CellPropertyChanged;
  31.             _watchedCells.Add(watchedCell);
  32.         }
  33.         // update the target native cell of the tracked xaml cell
  34.         watchedCell._nativeCell = new WeakReference<UITableViewCell>(nativeCell);
  35.         // force immediate update of accessory type
  36.         watchedCell.Reapply();
  37.     }
  38.     private WeakReference<Cell> _cell;
  39.     private WeakReference<UITableViewCell> _nativeCell;
  40.     private void CellPropertyChanged(object sender, PropertyChangedEventArgs args)
  41.     {
  42.         // update native accessory type when xaml property changes
  43.         if (args.PropertyName == CellExtensions.AccessoryProperty.PropertyName)
  44.             Reapply();
  45.     }
  46.     private void Reapply()
  47.     {
  48.         Cell cell;
  49.         UITableViewCell nativeCell;
  50.         if (!_cell.TryGetTarget(out cell))
  51.         {
  52.             // remove dead entry from list
  53.             _watchedCells.Remove(this);
  54.             return;
  55.         }
  56.         if (!_nativeCell.TryGetTarget(out nativeCell))
  57.         {
  58.             // if a property change fires for a dead native cell (but xaml cell isn’t dead), then ignore it
  59.             return;
  60.         }
  61.         var acc = (AccessoryType)cell.GetValue(CellExtensions.AccessoryProperty);
  62.         switch (acc)
  63.         {
  64.             case AccessoryType.None:
  65.                 nativeCell.Accessory = UITableViewCellAccessory.None;
  66.                 if (nativeCell.AccessoryView == null)
  67.                     nativeCell.AccessoryView = new UIView(new CGRect(0, 0, 20, 40));
  68.                 return;
  69.             case AccessoryType.Checkmark:
  70.                 nativeCell.Accessory = UITableViewCellAccessory.Checkmark;
  71.                 break;
  72.             case AccessoryType.DisclosureIndicator:
  73.                 nativeCell.Accessory = UITableViewCellAccessory.DisclosureIndicator;
  74.                 break;
  75.             case AccessoryType.DetailButton:
  76.                 nativeCell.Accessory = UITableViewCellAccessory.DetailButton;
  77.                 break;
  78.             case AccessoryType.DetailDisclosureButton:
  79.                 nativeCell.Accessory = UITableViewCellAccessory.DetailDisclosureButton;
  80.                 break;
  81.         }
  82.         if (nativeCell.AccessoryView != null)
  83.             nativeCell.AccessoryView.Dispose();
  84.         nativeCell.AccessoryView = null;
  85.     }
  86. }

At this point we have functional implementation that will support dynamic changes to our attached property values, and will also support standard data-binding features of XAML.

Limitations of the Custom Renderer System

If you are familiar with the DetailButton and DetailDisclosureButton options for cell accessories, then you might be wondering about how to handle touch events on those. These two accessories capture their own touch events, and normally (in a traditional Xamarin.iOS application) you would override the AccessoryButtonTapped() method of a custom UITableViewSource to handle them. However the TableView renderer in Xamarin Forms uses its own UITableViewSource internally, which ignores this optional touch event. Because it is hidden internally, you don’t get an opportunity to intercept it or override it. This is where we run up against a limitation of the custom renderer system – to go any further we would need to not only provide our custom renderer for cells, but we would also need to provide a new renderer for TableView itself – and most likely from scratch. That’s beyond the scope of this article, but perhaps something to be explored again later.

Wrapping it up

While it may seem like a lot of effort went into this solution, keep in mind that it is fully reusable and will continue to be compliant with iOS User Interface Guidelines as that platform continues to evolve. A lot of the more complicated code was also implemented in order to make our solution more robust and to support runtime value changes – if you don’t need to support dynamic value changes in the attached property then the code doesn’t need to worry about attaching events and guarding against memory leaks at all.

Updated source code for the sample application can be found here on GitHub: https://github.com/Wintellect/XamarinSamples/tree/master/DisclosureAccessoryDemo

Need Xamarin Help?

Xamarin Consulting  Xamarin Training

Stay Informed

Sign up for the latest blogs, events, and insights.

We deliver solutions that accelerate the value of Azure.
Ready to experience the full power of Microsoft Azure?

Atmosera is thrilled to announce that we have been named GitHub AI Partner of the Year.

X