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.
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
ListView to a native
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:
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):
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:
And of course we need to add the corresponding additional assembly-level attributes:
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.
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:
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:
At this point, we can run our application and the Accessory indicators should appear as expected.
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
And then we can change the
Apply() method so that it now monitors for changes to the attached property:
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> 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
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
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