doc/articles/guides/xf-migration/custom-controls.md
This guide explores how to migrate custom controls from Xamarin.Forms to Uno Platform. As you migrate your Xamarin.Forms applications, you'll need to bring forward your custom controls. This article demonstrates how to port a Xamarin.Forms custom control to Uno Platform with practical examples.
A custom control in Xamarin.Forms consists of two files:
The class for your control inherits from ContentView and is marked as partial because some functionality is added by compiler-generated code. The standard empty class contains a constructor with a call to InitializeComponent, which loads the associated XAML file and defines any named members (marked with x:Name attributes) so you can refer to them in your code.
We'll walk through migrating the CardView demo from the Xamarin.Forms samples. The CardView is a sample control based on Xamarin.Forms' ContentView, designed to display an image, header, and body text in a card format (often used in a list).
In Xamarin.Forms, bindable properties are created as static instances of BindableProperty:
public static readonly BindableProperty CardTitleProperty =
BindableProperty.Create(
nameof(CardTitle),
typeof(string),
typeof(CardView),
string.Empty);
With this defined, you can set these properties from XAML when you create instances of your custom control, including data-binding them to your view model.
In Uno and WinUI, the equivalent class is DependencyProperty, which has a static Register method with a very similar signature. You pass the default value in the constructor for PropertyMetadata:
public static readonly DependencyProperty CardTitleProperty =
DependencyProperty.Register(
nameof(CardTitle),
typeof(string),
typeof(CardView),
new PropertyMetadata(string.Empty));
To easily use these properties from code, define instance properties on the class that wrap and strongly type these binding properties:
Xamarin.Forms and Uno Platform (same code):
public string CardTitle
{
get => (string)GetValue(CardView.CardTitleProperty);
set => SetValue(CardView.CardTitleProperty, value);
}
The Uno/WinUI DependencyObject contains the same GetValue/SetValue methods, so this code doesn't need to change.
Xamarin.Forms:
ContentViewxmlns="http://xamarin.com/schemas/2014/forms"Uno Platform:
ContentControlxmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"Several common components exist with different names between these two flavors of XAML:
| Xamarin.Forms | Uno Platform / WinUI |
|---|---|
Label | TextBlock |
BoxView | Rectangle |
Frame | Border |
Entry | TextBox |
Switch | ToggleSwitch |
StackLayout | StackPanel |
ContentView | ContentControl |
Xamarin.Forms:
HeightRequest and WidthRequest to set preferred sizeUno Platform:
Height and Width propertiesXamarin.Forms:
Color properties for controlsUno Platform:
Brush properties, which can be solid colors, gradients, and moreColor as a property, and it's converted to a SolidColorBrush under the hoodColors typeAccent Color:
Color.Accent{ThemeResource SystemAccentColor} resourceXamarin.Forms:
HorizontalOptions and VerticalOptions use the same enumerationStart, End, Fill, CenterUno Platform:
Alignment rather than OptionsExample:
VerticalOptions="Center" becomes VerticalAlignment="Center"HorizontalOptions="Start" becomes HorizontalAlignment="Left"Xamarin.Forms:
FontAttributes for bold or italic stylingUno Platform:
FontWeight for setting bold (e.g., FontWeight="Bold")FontStyle for setting italic (e.g., FontStyle="Italic")<ContentView xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="CardViewDemo.Controls.CardView"
BindingContext="{x:Reference this}">
<Frame CornerRadius="5"
HasShadow="True"
BorderColor="Gray"
BackgroundColor="White">
<StackLayout>
<Label Text="{Binding CardTitle}"
FontAttributes="Bold"
FontSize="Large"
HorizontalOptions="Center"
VerticalOptions="Center" />
<BoxView Color="Gray"
HeightRequest="1"
HorizontalOptions="Fill" />
<Label Text="{Binding CardDescription}"
VerticalOptions="FillAndExpand"
HorizontalOptions="Fill" />
</StackLayout>
</Frame>
</ContentView>
<ContentControl xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="CardViewDemo.Controls.CardView"
DataContext="{Binding RelativeSource={RelativeSource Self}}">
<Border CornerRadius="5"
BorderBrush="Gray"
BorderThickness="1"
Background="White">
<Border.Shadow>
<ThemeShadow />
</Border.Shadow>
<StackPanel>
<TextBlock Text="{Binding CardTitle}"
FontWeight="Bold"
FontSize="24"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
<Rectangle Fill="Gray"
Height="1"
HorizontalAlignment="Stretch" />
<TextBlock Text="{Binding CardDescription}"
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch"
TextWrapping="Wrap" />
</StackPanel>
</Border>
</ContentControl>
ContentView → ContentControlFrame → BorderHasShadow="True" → <Border.Shadow><ThemeShadow /></Border.Shadow>StackLayout → StackPanelLabel → TextBlockBoxView → RectangleFontAttributes="Bold" → FontWeight="Bold"HorizontalOptions → HorizontalAlignmentBindingContext="{x:Reference this}" → DataContext="{Binding RelativeSource={RelativeSource Self}}"using Xamarin.Forms;
namespace CardViewDemo.Controls
{
public partial class CardView : ContentView
{
public static readonly BindableProperty CardTitleProperty =
BindableProperty.Create(nameof(CardTitle), typeof(string), typeof(CardView), string.Empty);
public static readonly BindableProperty CardDescriptionProperty =
BindableProperty.Create(nameof(CardDescription), typeof(string), typeof(CardView), string.Empty);
public string CardTitle
{
get => (string)GetValue(CardTitleProperty);
set => SetValue(CardTitleProperty, value);
}
public string CardDescription
{
get => (string)GetValue(CardDescriptionProperty);
set => SetValue(CardDescriptionProperty, value);
}
public CardView()
{
InitializeComponent();
}
}
}
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace CardViewDemo.Controls
{
public partial class CardView : ContentControl
{
public static readonly DependencyProperty CardTitleProperty =
DependencyProperty.Register(
nameof(CardTitle),
typeof(string),
typeof(CardView),
new PropertyMetadata(string.Empty));
public static readonly DependencyProperty CardDescriptionProperty =
DependencyProperty.Register(
nameof(CardDescription),
typeof(string),
typeof(CardView),
new PropertyMetadata(string.Empty));
public string CardTitle
{
get => (string)GetValue(CardTitleProperty);
set => SetValue(CardTitleProperty, value);
}
public string CardDescription
{
get => (string)GetValue(CardDescriptionProperty);
set => SetValue(CardDescriptionProperty, value);
}
public CardView()
{
InitializeComponent();
}
}
}
Xamarin.Forms → Microsoft.UI.Xaml and Microsoft.UI.Xaml.ControlsContentView → ContentControlBindableProperty → DependencyPropertyCreate() → Register() with PropertyMetadataIn the CardView sample, one of the views shows how the appearance of the custom control can be overridden by applying a new control template. This is possible with any ContentControl-derived control.
<controls:CardView.ControlTemplate>
<ControlTemplate>
<Frame BackgroundColor="DarkGray" CornerRadius="10">
<StackLayout>
<Label Text="{TemplateBinding CardTitle}"
TextColor="White"
FontSize="Large" />
<Label Text="{TemplateBinding CardDescription}"
TextColor="White" />
</StackLayout>
</Frame>
</ControlTemplate>
</controls:CardView.ControlTemplate>
<controls:CardView.Template>
<ControlTemplate TargetType="controls:CardView">
<Border Background="DarkGray" CornerRadius="10">
<StackPanel>
<TextBlock Text="{TemplateBinding CardTitle}"
Foreground="White"
FontSize="24" />
<TextBlock Text="{TemplateBinding CardDescription}"
Foreground="White" />
</StackPanel>
</Border>
</ControlTemplate>
</controls:CardView.Template>
ControlTemplate → TemplateFrame → Border, etc.)<Frame HasShadow="True">
<!-- Content -->
</Frame>
<Border>
<Border.Shadow>
<ThemeShadow />
</Border.Shadow>
<!-- Content -->
</Border>
In code-behind, you must also set a translation on the Border to raise it in the Z-axis:
#if !HAS_UNO_WINUI
CardBorder.Translation += new System.Numerics.Vector3(0, 0, 32);
#endif
[!NOTE] The Z-translation for shadows is not currently supported in Uno on all platforms. The conditional compilation ensures the code only runs where supported.
If you need platform-specific behavior, use conditional compilation:
#if __ANDROID__
// Android-specific code
#elif __IOS__
// iOS-specific code
#elif __WASM__
// WebAssembly-specific code
#elif WINDOWS
// Windows-specific code
#endif
Once migrated, use your custom control the same way in XAML:
<Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:CardViewDemo.Controls">
<StackPanel Spacing="10" Padding="20">
<controls:CardView CardTitle="First Card"
CardDescription="This is the first card"/>
<controls:CardView CardTitle="Second Card"
CardDescription="This is the second card"/>
</StackPanel>
</Page>
When migrating custom controls from Xamarin.Forms to Uno Platform:
ContentView to ContentControlBindableProperty.Create() to DependencyProperty.Register()Label → TextBlock, Frame → Border, etc.)HeightRequest/WidthRequest to Height/WidthColor properties to Brush properties (often automatic)HorizontalOptions/VerticalOptions to HorizontalAlignment/VerticalAlignmentFontAttributes to FontWeight and FontStyleHasShadow to <Border.Shadow><ThemeShadow /></Border.Shadow>TargetTypeStart/End in Xamarin.Forms → Left/Right in Uno PlatformMigrating custom controls from Xamarin.Forms to Uno Platform involves:
ContentView to ContentControlBindableProperty to DependencyPropertyThe structure remains very similar, and much of the logic can be reused with minimal changes. The XAML flavor is similar enough that migration is straightforward once you understand the key differences.
The complete sample project showing how to migrate a Xamarin.Forms custom control to Uno Platform is available in the Uno.Samples repository.