Extensions for Third-Party Controls
FmgLib.MauiMarkup's fluent methods are produced by a Roslyn source generator, and the same generator can create fluent extensions for controls from any library — SkiaSharp, ZXing.Net.Maui, UraniumUI, InputKit, Syncfusion, DevExpress, your own control library… Three mechanisms exist:
[MauiMarkup(typeof(...))]— opt-in per control[MauiMarkupAttachedProp(...)]— for attached propertiesMauiMarkupSourceGeneratorMSBuild property — fully automatic scanning
1. [MauiMarkup] — opt-in generation#
Put the attribute on any class in your project (the class itself is irrelevant — it is just an anchor for the attribute). For each type passed, the generator emits fluent methods for its bindable properties and events.
using FmgLib.MauiMarkup;
namespace GeneratedExam;
[MauiMarkup(typeof(ZXing.Net.Maui.Controls.BarcodeGeneratorView))]
public class MyBarcodeGeneratorView { }
[MauiMarkup(typeof(ZXing.Net.Maui.Controls.CameraView))]
public class MyCameraView { }
[MauiMarkup(typeof(SkiaSharp.Extended.UI.Controls.SKLottieView))]
public class MySkLottieView { }
Rules:
- The constructor accepts 1..N types — batch several controls per attribute.
- Multiple attributes may sit on one class.
- Any class works as the anchor —
MauiProgramis a popular single location:
using FmgLib.MauiMarkup;
using SkiaSharp.Extended.UI.Controls;
using ZXing.Net.Maui.Controls;
using UraniumUI.Material.Controls;
namespace MauiApp1;
[MauiMarkup(typeof(CameraView))]
[MauiMarkup(typeof(SKLottieView), typeof(SKFileLottieImageSource), typeof(DataGrid))]
[MauiMarkup(typeof(SKConfettiView), typeof(BarcodeGeneratorView), typeof(InputField), typeof(EditorField), typeof(TextField))]
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
});
return builder.Build();
}
}
What gets generated#
Given ZXing's CameraView (which declares IsTorchOnProperty, CameraLocationProperty and a FrameReady event), the generator produces a CameraViewExtension class in the FmgLib.MauiMarkup namespace containing, for each bindable property, the standard four overloads:
new CameraView()
.CameraLocation(CameraLocation.Front) // direct value
.IsTorchOn(e => e.Path("TorchEnabled")) // property builder (bindings, OnLight/OnDark, …)
.OnFrameReady((s, e) => Analyze(e)) // event handler
…and for each event, the two On<Event> shapes (Event Handlers). Styles work too, because the SettersContext overloads are generated:
new Style<CameraView>(e => e.IsTorchOn(false))
Usage after generation#
Generated methods live in the FmgLib.MauiMarkup namespace, so your existing using covers them:
new TextField()
.Title("Password")
.TitleColor(Colors.LightGray)
.AccentColor(Colors.CadetBlue)
.TextColor(Colors.White)
.IsPassword(true),
new SKLottieView()
.Source(new SKFileLottieImageSource().File("iconapp.json"))
.RepeatCount(-1)
.HeightRequest(250)
.WidthRequest(250)
2. [MauiMarkupAttachedProp] — attached properties#
For attached properties declared on third-party classes, use MauiMarkupAttachedProp. Constructor parameters, in order:
| # | Parameter | Meaning |
|---|---|---|
| 1 | controlType |
The class declaring the attached property |
| 2 | propertyName |
The BindableProperty field name (use nameof) |
| 3 | returnType |
The property's value type |
| 4 | declaringType |
The type the property will be applied to (the extension's target) |
Example — InputKit's FormView.IsSubmitButton, applied to Button:
[MauiMarkupAttachedProp(typeof(InputKit.Shared.Controls.FormView),
nameof(InputKit.Shared.Controls.FormView.IsSubmitButtonProperty),
typeof(bool),
typeof(Button))]
[MauiMarkup(typeof(InputKit.Shared.Controls.FormView))]
public class MyFormView { }
Generated usage (naming: owner class + property name, as with built-in attached properties):
new Button()
.Text("Login")
.FontAttributes(FontAttributes.Bold)
.FormViewIsSubmitButton(true)
3. Automatic Mode — MauiMarkupSourceGenerator#
To skip attributes entirely, enable assembly scanning in your app project's .csproj:
<PropertyGroup>
<MauiMarkupSourceGenerator>true</MauiMarkupSourceGenerator>
</PropertyGroup>
When enabled, the generator scans referenced third-party assemblies and creates fluent extensions for every eligible public BindableObject control automatically. (The required CompilerVisibleProperty wiring is injected automatically by the NuGet package via buildTransitive props — nothing else to configure.)
Trade-off: automatic mode is convenient but generates code for everything it finds, which can increase compile time in large solutions. The attribute approach keeps generation scoped to what you use.
Base Classes Are Generated Automatically#
Annotating a leaf control also generates fluent extensions for its eligible third-party base classes — most of a control's bindable surface usually lives there. For example, Syncfusion's SfButton inherits Command, FontSize, FontAttributes, Text… from ButtonBase; [MauiMarkup(typeof(SfButton))] alone produces both SfButtonExtension and ButtonBaseExtension, so every inherited property is fluently available:
[MauiMarkup(typeof(SfButton))]
public class Markup { }
new SfButton()
.Text("Login")
.Command(vm.LoginCommand) // declared on ButtonBase — generated automatically
.FontSize(15) // declared on ButtonBase
.TextColor(Colors.White) // redeclared identically on SfButton — served by ButtonBase
.StrokeThickness(1)
(MAUI core base classes are never re-generated — their extensions already ship inside the library.)
Redefined-Property Rules#
Controls sometimes re-declare a base class's property. The generator handles the two cases differently:
- Same property type (the common "convenience redeclaration") — e.g.
SfButton.TextColorre-exposingButtonBase.TextColor: the derived class gets no duplicate method and no suffix. The base class's generic extension (TextColor<T>() where T : ButtonBase) already serves the derived control; duplicating it would make every call ambiguous (CS0121). - Different property type (a genuine
newredefinition) — e.g.SfAvatarView.BackgroundchangingBrushtoColor: the derived method gets theNewsuffix, because same-named overloads differing only in the builder's generic argument would break lambda call sites and could silently target the wrongBindableProperty:
new SfAvatarView()
.BackgroundNew(Colors.LightBlue) // SfAvatarView's own (Color) Background property
.Background(someBrush) // inherited VisualElement (Brush) Background
If a method you expect seems "missing" on the leaf type, it is almost always served by a base-class extension under the same name — IntelliSense will still offer it; only truly type-changing redefinitions carry the New suffix.
Members That Are Deliberately Skipped#
A control's public surface is not always reachable from your assembly, and generating a fluent wrapper for a member that cannot be used would break your build instead of the library's. The generator therefore skips a member when any of the following holds — the control still generates, just without that one method:
| Member shape | Why it is skipped |
|---|---|
Setter is protected, internal, protected internal or private protected (e.g. DrawnUi's SkiaControl.IsMeasuring) |
self.Prop = value is not accessible outside the declaring assembly (CS0272). If the control also declares a matching BindableProperty, the fluent method IS generated and routes through SetValue. |
init-only setter |
Assignable only inside an object initializer (CS8852). |
Read-only collection with no callable Add — Queue<T> (Enqueue), LinkedList<T> (explicit ICollection<T>.Add), ReadOnlyCollection<T> |
The generated body is a foreach (…) Prop.Add(item); loop that would not compile (CS1929). |
static property or event |
An instance-fluent call would silently write to the type, and several shapes cannot compile at all (CS0176). Set it with a plain assignment instead. |
Member type (or a generic argument of it) is not public |
A public extension method cannot expose it (CS0053). |
Member — or its type — is [Obsolete(…, error: true)] |
Any mention is a hard error (CS0619). |
ref-returning property, ref struct type (e.g. Span<T>) |
Cannot be captured by the generated overloads. |
If one of these is exactly the member you need, reach for a hand-written custom extension method — you can access it however the control actually allows.
Practical Notes#
- Generation happens in the project where the attribute (or MSBuild property) lives — typically your app project, or a shared UI project referenced by the app.
- Regeneration is automatic on build; generated sources are visible under Analyzers → FmgLib.MauiMarkup.Generator in the IDE.
- The generator only needs the type to derive from
BindableObject; behaviors, image sources, and non-visual bindable objects (e.g.SKFileLottieImageSourceabove) are all fair game. - Combine with custom extension methods for anything the generator can't infer (regular CLR properties, methods, animations).
Related Topics#
- Fluent Properties — the overload family being generated
- Attached Properties
- Custom Extension Methods