How to write your own extensions
Many open source and 3rd-party libraries of Xamarin.Forms controls exist. To use other controls, a small amount of wrapper code is typically needed to define a corresponding view element using the incremental-update model used by Fabulous.
The following additional view elements are available as pre-built nuget libraries:
- FFImageLoading for cached images, as opposed to the built-in Image view that wastes time and memory
- Maps for platform maps
- SkiaSharp for drawing 2D graphics
- OxyPlot for charting
- VideoManager for playing audio and video
To use other Xamarin.Forms controls, a small amount of wrapper code must be written to convert the control to an Fabulous view element.
Please consider contributing your extensions to this repository.
The basic shape of an extension view component is shown below. Here we assume the Xamarin.Forms control defines one extra element
called ABC deriving from existing element kind BASE, and that ABC has one additional
collection property Prop1
and one primitive property Prop2
.
(A collection property is a one that may contain further sub-elements, e.g. children
for StackLayout, gestureRecognizers
for any View
and pins
in the Maps example further below.)
An view element simply defines a static member that extends View
and returns a ViewElement
.
The view element inherits attributes and update functionality from BASE via prototype inheritance.
NOTE: we are considering adding a code generator or type provider to automate this process, though the code is not complex to write.
NOTE: The API used to write these extensions is subject to change.
[<AutoOpen>]
module MyViewExtensions =
open Fabulous
open Fabulous.XamarinForms
// Define keys for the possible attributes
let Prop1AttribKey = AttributeKey<seq<ViewElement>> "ABC_Prop1"
let Prop2AttribKey = AttributeKey<bool> "ABC_Prop2"
// Fully-qualified name to avoid extending by mistake
// another View class (like Xamarin.Forms.View)
type Fabulous.XamarinForms.View with
/// Describes a ABC in the view
/// The inline keyword is important for performance
static member inline ABC(?prop1: seq<ViewElement>, ?prop2: bool, ... inherited attributes ... ) =
// Count the number of additional attributes
let attribCount = 0
let attribCount = match prop1 with Some _ -> attribCount + 1 | None -> attribCount
let attribCount = match prop2 with Some _ -> attribCount + 1 | None -> attribCount
// Unbox the ViewRef
let viewRef = match ref with None -> None | Some (ref: ViewRef<ABC>) -> Some ref.Unbox
// Populate the attributes of the base element
let attribs = ViewBuilders.BuildBASE(attribCount, ... inherited attributes (with ?ref=viewRef) ... )
// Add our own attributes.
match prop1 with None -> () | Some v -> attribs.Add (Prop1AttribKey, v)
match prop2 with None -> () | Some v -> attribs.Add (Prop2AttribKey, v)
...
// The creation method
let create () = new ABC()
// The incremental update method
let update (prev: ViewElement voption) (source: ViewElement) (target: ABC) =
ViewBuilders.UpdateBASE (prev, source, target)
source.UpdateElementCollection (prev, rop1AttribKey, target.Prop1)
source.UpdatePrimitive (prev, target, Prop2AttribKey, (fun target -> target.Prop2), (fun target v -> target.Prop2 <- v))
...
ViewElement.Create<ABC>(create, update, attribs)
The control is then used as follows:
View.ABC(Prop1 = [ View.Label("hello") ], prop2 = true, property3 = "Yo!")
The update
method of the extension is specified using:
source.UpdatePrimitive(prev, target, attribKey, setter, ?defaultValue)
- incrementally update a primitivesource.UpdateElement(prev, target, attribKey, getter, setter)
- incrementally update a nested elementsource.UpdateElementCollection(prev, attribKey, targetCollection)
- incrementally update a collection of nested elementssource.UpdateEvent(prev, target, attribKey, setter, ?defaultValue)
- incrementally update a primitive event
Sometimes it makes sense to “massage” the input values before storing them in attibutes, e.g. to apply a conversion from an F#-friendly value to a stored attribte value here:
match prop1 with None -> () | Some v -> attribs.Add(Prop1AttribKey, box (CONV v))
It is common to mark view extensions as inline
. This allows the F# compiler to create more optimized
element-creation code for each particular instantiation based on the small set of properties specified at a particular usage point.
In particular the compiler can statically determine the count of attributes and remove all allocations related to
optional arguments.
Example: Authoring the Xamarin.Forms.Maps Extension
The implementation of an extension for Xamarin.Forms.Maps
is shown below - this is the same extension as that
available in Fabulous.XamarinForms.Maps.dll
. The sample implements the extension for the types Map and
Pin.
[<AutoOpen>]
module MapsExtension =
open Fabulous.XamarinForms
open Xamarin.Forms
open Xamarin.Forms.Maps
let MapHasScrollEnabledAttribKey = AttributeKey "Map_HasScrollEnabled"
let MapIsShowingUserAttribKey = AttributeKey "Map_IsShowingUser"
let MapPinsAttribKey = AttributeKey "Map_Pins"
let MapTypeAttribKey = AttributeKey "Map_MapType"
let MapHasZoomEnabledAttribKey = AttributeKey "Map_HasZoomEnabled"
let MapRequestingRegionAttribKey = AttributeKey "Map_RequestedRegion"
let PinPositionAttribKey = AttributeKey "Pin_Position"
let PinLabelAttribKey = AttributeKey "Pin_Label"
let PinTypeAttribKey = AttributeKey "Pin_PinType"
let PinAddressAttribKey = AttributeKey "Pin_Address"
type Fabulous.XamarinForms.View with
/// Describes a Map in the view
static member inline Map(?pins: seq<ViewElement>, ?isShowingUser: bool, ?mapType: MapType,
?hasScrollEnabled: bool, ?hasZoomEnabled: bool, ?requestedRegion: MapSpan,
// inherited attributes common to all views
?horizontalOptions, ?verticalOptions, ?margin, ?gestureRecognizers, ?anchorX, ?anchorY, ?backgroundColor,
?heightRequest, ?inputTransparent, ?isEnabled, ?isVisible, ?minimumHeightRequest, ?minimumWidthRequest, ?opacity,
?rotation, ?rotationX, ?rotationY, ?scale, ?style, ?translationX, ?translationY, ?widthRequest,
?resources, ?styles, ?styleSheets, ?classId, ?styleId, ?automationId) =
// Count the number of additional attributes
let attribCount = 0
let attribCount = match pins with Some _ -> attribCount + 1 | None -> attribCount
let attribCount = match hasScrollEnabled with Some _ -> attribCount + 1 | None -> attribCount
let attribCount = match isShowingUser with Some _ -> attribCount + 1 | None -> attribCount
let attribCount = match mapType with Some _ -> attribCount + 1 | None -> attribCount
let attribCount = match hasZoomEnabled with Some _ -> attribCount + 1 | None -> attribCount
let attribCount = match requestedRegion with Some _ -> attribCount + 1 | None -> attribCount
// Count and populate the inherited attributes
let attribs =
ViewBuilders.BuildView(attribCount, ?horizontalOptions=horizontalOptions, ?verticalOptions=verticalOptions,
?margin=margin, ?gestureRecognizers=gestureRecognizers, ?anchorX=anchorX, ?anchorY=anchorY,
?backgroundColor=backgroundColor, ?heightRequest=heightRequest, ?inputTransparent=inputTransparent,
?isEnabled=isEnabled, ?isVisible=isVisible, ?minimumHeightRequest=minimumHeightRequest,
?minimumWidthRequest=minimumWidthRequest, ?opacity=opacity, ?rotation=rotation,
?rotationX=rotationX, ?rotationY=rotationY, ?scale=scale, ?style=style,
?translationX=translationX, ?translationY=translationY, ?widthRequest=widthRequest,
?resources=resources, ?styles=styles, ?styleSheets=styleSheets, ?classId=classId, ?styleId=styleId, ?automationId=automationId)
// Add our own attributes. They must have unique names which must match the names below.
match pins with None -> () | Some v -> attribs.Add(MapPinsAttribKey, v)
match hasScrollEnabled with None -> () | Some v -> attribs.Add(MapHasScrollEnabledAttribKey, v)
match isShowingUser with None -> () | Some v -> attribs.Add(MapIsShowingUserAttribKey, v)
match mapType with None -> () | Some v -> attribs.Add(MapTypeAttribKey, v)
match hasZoomEnabled with None -> () | Some v -> attribs.Add(MapHasZoomEnabledAttribKey, v)
match requestedRegion with None -> () | Some v -> attribs.Add(MapRequestingRegionAttribKey, v)
// The update method
let update (prevOpt: ViewElement voption) (source: ViewElement) (target: Map) =
ViewBuilders.UpdateView(prevOpt, source, target)
source.UpdatePrimitive(prevOpt, target, MapHasScrollEnabledAttribKey, (fun target v -> target.HasScrollEnabled <- v))
source.UpdatePrimitive(prevOpt, target, MapHasZoomEnabledAttribKey, (fun target v -> target.HasZoomEnabled <- v))
source.UpdatePrimitive(prevOpt, target, MapIsShowingUserAttribKey, (fun target v -> target.IsShowingUser <- v))
source.UpdatePrimitive(prevOpt, target, MapTypeAttribKey, (fun target v -> target.MapType <- v))
source.UpdateElementCollection(prevOpt, MapPinsAttribKey, target.Pins)
source.UpdatePrimitive(prevOpt, target, MapRequestingRegionAttribKey, (fun target v -> target.MoveToRegion(v)))
// The element
ViewElement.Create<Xamarin.Forms.Maps.Map>(Map, update, attribs)
/// Describes a Pin in the view
static member Pin(?position: Position, ?label: string, ?pinType: PinType, ?address: string) =
// Count the number of additional attributes
let attribCount = 0
let attribCount = match position with Some _ -> attribCount + 1 | None -> attribCount
let attribCount = match label with Some _ -> attribCount + 1 | None -> attribCount
let attribCount = match pinType with Some _ -> attribCount + 1 | None -> attribCount
let attribCount = match address with Some _ -> attribCount + 1 | None -> attribCount
let attribs = AttributesBuilder(attribCount)
// Add our own attributes. They must have unique names which must match the names below.
match position with None -> () | Some v -> attribs.Add(PinPositionAttribKey, v)
match label with None -> () | Some v -> attribs.Add(PinLabelAttribKey, v)
match pinType with None -> () | Some v -> attribs.Add(PinTypeAttribKey, v)
match address with None -> () | Some v -> attribs.Add(PinAddressAttribKey, v)
// The update method
let update (prevOpt: ViewElement voption) (source: ViewElement) (target: Pin) =
source.UpdatePrimitive(prevOpt, target, PinPositionAttribKey, (fun target v -> target.Position <- v))
source.UpdatePrimitive(prevOpt, target, PinLabelAttribKey, (fun target v -> target.Label <- v))
source.UpdatePrimitive(prevOpt, target, PinTypeAttribKey, (fun target v -> target.Type <- v))
source.UpdatePrimitive(prevOpt, target, PinAddressAttribKey, (fun target v -> target.Address <- v))
// The element
ViewElement.Create<Xamarin.Forms.Maps.Pin>(Pin, update, attribs)
In the above example, inherited properties from View
(such as margin
or horizontalOptions
) have been included in the facade for Map
. These properties
need not be added, you can set them on elements using the helper With
, usable for all View
properties:
View.Map(hasZoomEnabled = true, hasScrollEnabled = true).With(horizontalOptions = LayoutOptions.FillAndExpand)
Example: MasterDetailPage without a toolbar on UWP with custom ViewBuilders
Fabulous uses ViewBuilders to create the underlying Xamarin.Forms classes. Customizing ViewBuilders is not the recommended way for custom controls but it is a great solution for overridden controls like in the following example:
type MasterDetailPageWithoutToolbar() =
inherit Xamarin.Forms.MasterDetailPage()
override __.ShouldShowToolbarButton() = false
Fabulous.XamarinForms.ViewBuilders.CreateFuncMasterDetailPage <- fun () ->
upcast(new MasterDetailPageWithoutToolbar())
View.MasterDetailPage() // this now uses MasterDetailPageWithoutToolbar
See also: