How we built Xamarin.Forms wrapper for SciChart mobile
Andrew Bt
Posted on April 5, 2024
Our customers often ask if we support Xamarin.Forms: support for xamarin forms, SciChart in Xamarin.Forms shared code, examples to use xamarin Android and iOS in Xamarin.Forms) , but until now we supported only Xamarin Native that includes Xamarin.Android and Xamarin.iOS.
Xamarin Native does allow our customers to use power of SciChart on both Xamarin Native and Forms, but since we did not provide Xamarin.Forms wrappers before it required some additional work to be done on the customers’ side.
Xamarin.Forms allow to fully use all features provided by Forms (XAML and DataBinding) and now we are glad to announce the release of open-source wrappers for Xamarin.Forms which should cover this gap.
When making a decision about releasing Xamarin.Forms we were mindful about the enormous amount of boilerplate code we have to write to wrap SciChart code for each platform and furthermore support it, with all the breaking changes that could be introduced by new releases. Honestly, that scared us!
The time we needed to write the wrappers could have been used on developing new features and new platforms, or even products, that is how laborious this task was.
So we decided to use another approach and invest some time into developing some tools which would help us to avoid pain of writing same code for wrappers for each platform and more importantly help us to reduce time required to support this code in future.
Code generation and requirements
The approach that we choose is to create code generator for Xamarin.Forms wrappers which writes most of the code for us. It does so based on some rules and some manually written metadata.
In this case, if there will be some breaking change in future release of Xamarin.Forms or Microsoft decides to deprecate it in favour of .NET MAUI we can simply update the generator! We can regenerate most of the code instead of manually locating and updating all problematic places.
We can simply update the generator and most of the code will be regenerated instead of manually locating and updating all problematic places.
We all agreed that the code generation is a cool idea and it solves a problem with supporting code, allowing to automate writing of repetitive code. But how to implement it and what exactly should it do?
We did a research from which we created a list of requirements:
- We don't want to spend a lot of time on writing generator to support 100% code generation, because the cost of time resources to do so would not be justified. Therefore, we only require to automate generation of repetitive code parts. For the code that is used in one place only it is ok to write it manually.
- Use existing tools/libraries if it's possible
- It should be possible to add handwritten code at any class when necessary.
- Metadata used by generator should be readable and if possible, support autocomplete and automatic scheme validation.
Decision Making
First of all, let's talk about tools that we have used in our generator.
The first tool we needed was to generate the C# code. We did not want to write this part on our own unless it is really necessary. Obviously, at first we took a look on Roslyn. But it looked too complicated for our purpose, because its main purpose is to perform code analysis and not to generate code. There were some other open source options available, but in the end we choose CodeDOM. It's developed by Microsoft, pretty easy to use and does exactly what we wanted to do.
Next, we had to decide what should be included into metadata, that we will used as input for our generator. To generate code, we needed to get information about type names, properties, methods, arguments, relations between types (base class, implemented interfaces, etc).
At first we thought that ideal source of such information would be using reflection - we already have Xamarin Native assemblies for iOS and Android, so why don't use it. We used Mono.Cecil for this part, because System.Reflection which comes with .NET was unable to load some platform specific types and produced many exceptions.
Unfortunately there were alot of problems with reflection only approach - it was very hard to merge information about types from different platforms to generate list of common methods and properties, and that is why we decided to declare what types, methods, properties should be wrapped in Xamarin.Forms explicitly and use reflection only as additional source of information.
Next question was how to write information about types. There were several potential solutions like use JSON, XML, YAML formats to describe types, then deserialize information and generate required source files. However, the problem with this approach was the lack of autocomplete for existing system types and there was no way to tell if there is an error in file until you run the generator.
That is why we decided to take a different approach and use .NET type system to store information about how wrappers should be generated. The idea is to define an interface for each Xamarin.Forms wrapper class that should be generated and then annotate it and its parts using special attributes, which tell generator how it should generate wrapper for each platform. In this way we would get autocomplete for defining properties, methods for these interfaces and if there is some error in code .NET compiler will let us know when we try to compile this code. Then generator, using reflection reads information from these annotated types and generates Xamarin.Forms class which can be used in application and platform specific wrapper for SciChart class for each platform.
Project structure
Based on the above research we organized our Xamarin.Forms project like this:
Xamarin.Forms wrapper consist of 4 libraries:
- SciChart.Xamarin.Views.Core where we store information which then is used by generator
- SciChart.Xamarin.Views where we have Xamarin.Forms classes, which are used as public API by target applications
- SciChart.Xamarin.Android.Renderer and SciChart.Xamarin.iOS.Renderer that contain platform specific code (renderers, wrappers for native code etc) and reference native libraries for iOS and Android platforms
Simple example
To create a simple example let's bind a simple SciChart class and few properties and methods. For this example we'll bind Camera3D class which is used by 3D chart. We start from declaring ICamera3D interface in Core assembly where we declare metadata used by generator. Then we add some attributes like ClassDeclarartion which tells that this interface is used for generating of Xamarin.Forms around specified native type, and InjectNativeSciChartObject which tells that generator should inject native object via constructor:
[ClassDeclaration("Camera3D", typeof(View))] [InjectNativeSciChartObject] public interface ICamera3D : INativeSciChartObjectWrapper { }
This should be enough to generate wrappers around native Camera3D class for each platforms and Xamarin.Forms wrapper class which uses Xamarin.Forms View as base class ( this allows to declare camera in XAML ).
Now let's declare some properties and methods by using BindablePropertyDefinition and MethodDeclaration attributes:
[ClassDeclaration("Camera3D", typeof(View))] [InjectNativeSciChartObject] public interface ICamera3D : INativeSciChartObjectWrapper { [BindablePropertyDefinition()] float FieldOfView { get; set; } [BindablePropertyDefinition()] float NearClip { get; set; } [BindablePropertyDefinition()] float FarClip { get; set; } [MethodDeclaration()] void ZoomToFit(); }
This is simple case with binding for primitive type.
Next let's add more complex and try to bind ProjectionMode property which uses Enum type. First we need to declare this type in Core so it can be used in ICamera3D interface:
[EnumDefinition] public enum CameraProjectionMode { Perspective, Orthogonal, }
This should tell generator to generate helper class for converting Xamarin.Forms CameraProjectionMode to native CameraProjectionMode which are called CameraProjectionModeToXamarin() and CameraProjectionModeFromXamarin(). In our case values for native enum have the same name as enum used by Xamarin.Forms so there is no need to add anything else, but it's possible to customize generation if it's required. Now we can declare property in ICamera3D interface:
[ClassDeclaration("Camera3D", typeof(View))] [InjectNativeSciChartObject] public interface ICamera3D : INativeSciChartObjectWrapper { [BindablePropertyDefinition()] float FieldOfView { get; set; } [BindablePropertyDefinition()] float NearClip { get; set; } [BindablePropertyDefinition()] float FarClip { get; set; } [BindablePropertyDefinition()] [NativePropertyConverterDeclaration("CameraProjectionMode")] CameraProjectionMode ProjectionMode { get; set; } [MethodDeclaration()] void ZoomToFit(); }
Code is similar, but now we have used additional attribute which is called NativePropertyConverterDeclaration. This attribute tells generator to generate additional XXXToXamarin() and XXXFromXamarin() call for property getter and setter which converts Xamarin.Forms type to native type. In our case it will insert CameraProjectionModeToXamarin() and CameraProjectionModeFromXamarin() calls which were automatically generated for CameraProjectionMode enum.
Other attributes
Our generator has support of some other attributes which can be used for more complex cases:
- AbstractClassDefinition. Should be used to tell generator that this class is abstract. In this case generator only generates Xamarin.Forms class and there is no need to generate native wrapper.
- EnumValueDefinition. Used to adjust enum mappings in case if enum values have different names on native platforms.
- GenericParamsDeclaration. Used to tell generator that target wrapper should be generic class.
- InjectAndroidContext. Android specific attribute for classes which require Android Context class in their constructors.
- PropertyDeclaration. Attribute which tells generator to generate simple .NET property without creating BindableProperties.
- TypeConverterDeclaration. Attribute which tells generator that Xamarin.Forms property should use specified TypeConverter ( used in XAMl )
Limitations and handwriting code
The code above allows to cover most of the cases, but not all of them. In some rare cases it is still too hard to implement auto generating or simply is not necessary as the code is used in one place. In those cases we write code on our own and that's why all generated classes are generated as partial classes. This allows us to create a separate file with class in same namespace as auto generated one and add code which can't be generated. Then thanks to partial classes feature auto generated and handwritten code is combined and compiled into Xamarin.Forms wrappers. Currently this is how we implemented some events exposed by SciChart, some mappings for types which should be handled with extreme caution because of complexity (Ranges, DataSeries, Styles etc).
Chart example
Everything above allowed us to use SciChart with Xamarin.Forms. Let's create a simple chart (code will be familiar to our WPF users).
First of all we need to declare it. In this example we'll do it in XAML, but you can do it in code behind:
<?xml version="1.0" encoding="utf-8" ?> <ContentPage xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:d="http://xamarin.com/schemas/2014/forms/design" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:visuals="clr-namespace:SciChart.Xamarin.Views.Visuals;assembly=SciChart.Xamarin.Views" xmlns:renderableSeries="clr-namespace:SciChart.Xamarin.Views.Visuals.RenderableSeries;assembly=SciChart.Xamarin.Views" xmlns:axes="clr-namespace:SciChart.Xamarin.Views.Visuals.Axes;assembly=SciChart.Xamarin.Views" xmlns:modifiers="clr-namespace:SciChart.Xamarin.Views.Modifiers;assembly=SciChart.Xamarin.Views" mc:Ignorable="d" x:Class="TestApp.UI.Examples.LineChart"> <ContentPage.Content> <visuals:SciChartSurface HorizontalOptions="Fill" VerticalOptions="Fill"> <visuals:SciChartSurface.RenderableSeries> <renderableSeries:FastLineRenderableSeries x:Name="LineSeries" StrokeStyle="#FF279B27, 2"/> </visuals:SciChartSurface.RenderableSeries> <visuals:SciChartSurface.XAxes> <axes:NumericAxis/> </visuals:SciChartSurface.XAxes> <visuals:SciChartSurface.YAxes> <axes:NumericAxis/> </visuals:SciChartSurface.YAxes> <visuals:SciChartSurface.ChartModifiers> <modifiers:ModifierGroup> <modifiers:PinchZoomModifier/> <modifiers:ZoomPanModifier ReceiveHandledEvents="true"/> <modifiers:ZoomExtentsModifier/> </modifiers:ModifierGroup> </visuals:SciChartSurface.ChartModifiers> </visuals:SciChartSurface> </ContentPage.Content> </ContentPage>
Here we declare SciChartSurface with one numeric XAxis and one numeric YAxis. Also we add line series and add some interactivity using ChartModifier API.
Next step is to introduce some data in code behind:
public partial class LineChart : ContentPage { public LineChart() { InitializeComponent(); } protected override void OnAppearing() { base.OnAppearing(); var dataSeries = new XyDataSeries<double, double>(); for (int i = 0; i < 100; i++) { var y = Math.Sin(i * 0.05); dataSeries.Append(i, y); } LineSeries.DataSeries = dataSeries; } }
This code declares DataSEries with sinewave and assigns it to line series, that we declared in XAML before.
The last step is to add license key. To do this we suggest to add next code in App class of your application:
public partial class App : Xamarin.Forms.Application { public App () { InitializeComponent(); // Setup SciChart Licenses string iosAndroidLicense = ...; using (var manager = new SciChartLicenseManager()) { manager.AddLicense(SciChartPlatform.Android, iosAndroidLicense); manager.AddLicense(SciChartPlatform.iOS, iosAndroidLicense); } ... } ... }
When you run application on Android/iOS device you should see something like this:
Link to the Xamarin Forms binding generator on Github
We've open sourced the Xamarin.Forms binding generator for SciChart iOS/Android and published it to Github. Please find a link to the project above.
Posted on April 5, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.