This article is part of the #MAUIUIJuly initiative by Matt Goldman. You'll find other helpful articles and tutorials published daily by community members and experts there, so make sure to check it out every day.
Welcome! In this post, I'm going to explain how you can draw X-mas controls in .NET MAUI.
There are more than 40 controls (views) that you can use in your .NET MAUI apps, and one of them is Shapes. Shape
is a type of View
that enables you to draw a geometry, such as an ellipse, a polygon, a polyline, or even a custom one. Some properties include Stroke
(the shape outline Brush), StrokeThickness
(the Stroke width), and Fill
(the Brush used to paint its interior), among others. More information about Shapes can be found in the official documentation.
Let's demonstrate what we can do with Shapes by creating a few Christmas-themed controls. Before we start, some considerations:
- Each example is incorporated into a
ContentView
reusable control (with XAML and C# code), which is then instantiated in a mainContentPage
. - All ContentViews are added inside a Controls folder.
Some images are added for reference:
The .NET MAUI project structure:
Adding a ContentView
element to the project:
Example #1: A X-mas Tree control
Let's start with the UI (XAML code) of a Christmas tree that consists of 4 polygons: the first three represent the leaves, while the last one constitutes the trunk. In the ensuing code you can observe that each Polygon
shape defines a Points
member which specifies a set of coordinates for the vertex points of the polygon (the AbsoluteLayout
is very handy in this case, as it allows all coordinates to be in the same context). Three coordinates pairs are used for the leaves polygons, meaning that a triangle is being drawn in each one; similarly, the trunk polygon contains four XY pairs to represent a rectangle.
Take a look at the Content
of the XmasTreeControl.xaml
ContentView
:
<ContentView.Content>
<AbsoluteLayout Margin="5">
<Polygon Points="100,0 0,100 200,100" />
<Polygon Points="100,50 0,150 200,150"/>
<Polygon Points="100,100 0,200 200,200"/>
<Polygon Points="80,200 120,200 120,300 80,300"/>
<AbsoluteLayout.GestureRecognizers>
<DropGestureRecognizer AllowDrop="True" Drop="OnDrop" />
</AbsoluteLayout.GestureRecognizers>
</AbsoluteLayout>
</ContentView.Content>
You can see that the above XAML also includes a DropGestureRecognizer
member. More on that in the C# code.
Now the fun part. When this control is instantiated as part of the UI, we would like to define the color of both the leaves and the trunk. We can do that thanks to bindable properties that we are defining in the C# class associated to the XmasTreeControl ContentView
file. Let's understand what we are adding first, then take a look at the code:
- Required namespaces are included for
Shapes
andPath
classes. - There are two bindable properties:
LeavesBrushProperty
andTrunkBrushProperty
backed by the propertiesLeavesBrush
andTrunkBrush
, respectively. - Both properties are of type
Brush
, which means that a solid color or a gradient can be used for them. - When the value of the binding property changes, the corresponding callback method (that fills the polygon content with the corresponding new Brush value) is invoked.
- The
OnDrop
method is added for interaction purposes. Previously referenced in theDropGestureRecognizer
XAML code, it detects when the user drops another View element into the tree. Later we are adding star and sphere controls, and this code detects which type of object the user tried to "place" on the tree, incorporating the dragged control on specific coordinates in theAbsoluteLayout
defined as part of the tree. - There are two comment blocks inside
OnDrop
method because we haven't created the other custom controls yet. We'll enable these sections later.
This is the code for XmasTreeControl.xaml.cs
class file:
using Microsoft.Maui.Controls.Shapes;
using Path = Microsoft.Maui.Controls.Shapes.Path;
namespace XmasControlsNetMaui.Controls;
public partial class XmasTreeControl : ContentView
{
public XmasTreeControl()
{
InitializeComponent();
}
public static readonly BindableProperty LeavesBrushProperty =
BindableProperty.Create(nameof(LeavesBrush),
typeof(Brush),
typeof(XmasTreeControl),
Brush.Transparent,
BindingMode.TwoWay,
propertyChanged: OnLeavesBrushChanged);
public Brush LeavesBrush
{
get => (Brush)GetValue(LeavesBrushProperty);
set { SetValue(LeavesBrushProperty, value); }
}
public static readonly BindableProperty TrunkBrushProperty =
BindableProperty.Create(nameof(TrunkBrush),
typeof(Brush),
typeof(XmasTreeControl),
Brush.Transparent,
BindingMode.TwoWay,
propertyChanged: OnTrunkBrushChanged);
public Brush TrunkBrush
{
get => (Brush)GetValue(TrunkBrushProperty);
set { SetValue(TrunkBrushProperty, value); }
}
private static void OnLeavesBrushChanged(BindableObject bindable, object oldValue, object newValue)
{
var xmasTreeControl = bindable as XmasTreeControl;
xmasTreeControl.FillBrush(xmasTreeControl.Content as AbsoluteLayout, newValue as Brush, 3);
}
private static void OnTrunkBrushChanged(BindableObject bindable, object oldValue, object newValue)
{
var xmasTreeControl = bindable as XmasTreeControl;
xmasTreeControl.FillBrush(xmasTreeControl.Content as AbsoluteLayout, newValue as Brush, 4);
}
void FillBrush(AbsoluteLayout layout, Brush brush, int pointsCount)
{
foreach (var item in layout.Children)
{
var control = item as Polygon;
if (control != null)
{
if (control.Points.Count == pointsCount)
{
control.Fill = brush;
control.Stroke = brush;
}
}
}
}
List<Tuple<int, int, bool>> positions = new List<Tuple<int, int, bool>>()
{
new Tuple<int, int, bool>(0, 75, false),
new Tuple<int, int, bool>(75, 25, false),
new Tuple<int, int, bool>(100, 100, false),
new Tuple<int, int, bool>(0, 175, false),
new Tuple<int, int, bool>(150, 175, false),
};
private void OnDrop(object sender, DropEventArgs e)
{
var properties = e.Data.Properties;
if (properties.ContainsKey("Sphere"))
{
/*
var sphere = (Ellipse)properties["Sphere"];
var xmasSphere = new XmasSphereControl()
{
SphereBrush = sphere.Fill
};
var layout = this.Content as AbsoluteLayout;
var position = positions.FirstOrDefault(x => !x.Item3);
if (position != null)
{
layout.SetLayoutBounds((IView)xmasSphere,
new Rect(new Point(position.Item1, position.Item2), new Size(xmasSphere.Width, xmasSphere.Height)));
layout.Children.Add(xmasSphere);
positions[positions.IndexOf(position)] = new Tuple<int, int, bool>(position.Item1, position.Item2, true);
}
*/
}
else if (properties.ContainsKey("Star"))
{
/*
var star = (Path)properties["Star"];
var xmasStar = new XmasStarControl()
{
StarStroke = star.Stroke
};
var layout = this.Content as AbsoluteLayout;
layout.SetLayoutBounds((IView)xmasStar,
new Rect(new Point(90, -5), new Size(xmasStar.Width, xmasStar.Height)));
layout.Children.Add(xmasStar);
*/
}
}
}
Now, let's say we want to display this tree in our MainPage
for instance. Since the ContentView
was created inside a folder, we need to reference its namespace, so include the following line as part of the ContentPage
element in your MainPage.xaml
file:
xmlns:controls="clr-namespace:XmasControlsNetMaui.Controls"
By the way, the controls
prefix is an alias that simplifies the way of including members of that namespace (in this case, the custom controls we are creating) in the XAML code.
Now, let's add the tree. Before I show you the code, let's understand that:
- The XmasTreeControl instance is a child of a main
VerticalStackLayout
element (because we are adding other controls later). - For demonstration purposes, a
LinearGradientBrush
is used for theLeavesBrush
property. You don't have to if you don't want to, as theLeavesBrush
member accepts any Brush, such as a simpleSolidColorBrush
definition (example:LeavesBrush="#0f0"
) - The
controls
alias is used to find theXmasTreeControl
class.
Now, add the following element:
<VerticalStackLayout Margin="20" Spacing="20">
<controls:XmasTreeControl>
<controls:XmasTreeControl.LeavesBrush>
<LinearGradientBrush EndPoint="1,0">
<GradientStop Offset="0.4" Color="LightGreen" />
<GradientStop Offset="1.0" Color="ForestGreen" />
</LinearGradientBrush>
</controls:XmasTreeControl.LeavesBrush>
<controls:XmasTreeControl.TrunkBrush>
<LinearGradientBrush EndPoint="1,1">
<GradientStop Offset="0.2" Color="#cdaa7d" />
<GradientStop Offset="1.0" Color="#654321" />
</LinearGradientBrush>
</controls:XmasTreeControl.TrunkBrush>
</controls:XmasTreeControl>
</VerticalStackLayout>
Let's see it in action! Run the app and take a look.
Neat! But we are not done yet, let's move on to the next example.
Example #2: A X-mas Sphere control
The second example is a Christmas sphere control. In this case, the main element is an Ellipse
shape (despite the name, we are actually drawing a circle because the width and height are the same). Moreover, we are including a small polygon that represents an ornament cap for the sphere. You can see in the code below that there is a DragGestureRecognizer
definition, which will be referenced later in the C# ContentView class.
Here is the Content
of the XmasSphereControl.xaml
ContentView file:
<ContentView.Content>
<StackLayout Margin="5" Spacing="0">
<Polygon Fill="Gold"
Points="22,0 28,0 30,5 20,5"
HorizontalOptions="Center"/>
<Ellipse
HeightRequest="50"
HorizontalOptions="Start"
VerticalOptions="Start"
WidthRequest="50" />
<StackLayout.GestureRecognizers>
<DragGestureRecognizer CanDrag="True" DragStarting="OnDrag" />
</StackLayout.GestureRecognizers>
</StackLayout>
</ContentView.Content>
Similarly to what we did earlier to define the color of the tree leaves & trunk, we are now adding a way to specify the sphere color. So we need to add C# code. But first, let's explain it:
-
SphereBrushProperty
is aBindableProperty
backed toSphereBrush
of typeBrush
, allowing the user to define a solid or gradient fill color for the sphere. When its value changes, theOnSphereBrushChanged
method is automatically invoked in order to update the sphere fill brush. - The
OnDrag
method brings the data for the Sphere control when the user drags it. This is important, since we are adding several spheres later into our UI, and it is important to know which one was chosen.
XmasSphereControl.xaml.cs
:
using Microsoft.Maui.Controls.Shapes;
namespace XmasControlsNetMaui.Controls;
public partial class XmasSphereControl : ContentView
{
public XmasSphereControl()
{
InitializeComponent();
}
public static readonly BindableProperty SphereBrushProperty =
BindableProperty.Create(nameof(SphereBrush),
typeof(Brush),
typeof(XmasTreeControl),
Brush.Transparent,
BindingMode.TwoWay,
propertyChanged: OnSphereBrushChanged);
public Brush SphereBrush
{
get => (Brush)GetValue(SphereBrushProperty);
set { SetValue(SphereBrushProperty, value); }
}
private static void OnSphereBrushChanged(BindableObject bindable, object oldValue, object newValue)
{
var xmasSphereControl = bindable as XmasSphereControl;
xmasSphereControl.FillBrush(xmasSphereControl.Content as StackLayout, newValue as Brush);
}
void FillBrush(StackLayout layout, Brush brush)
{
foreach (var item in layout.Children)
{
var control = item as Ellipse;
if (control != null)
{
control.Fill = brush;
control.Stroke = brush;
}
}
}
private void OnDrag(object sender, DragStartingEventArgs e)
{
var stack = (sender as Element).Parent as StackLayout;
var sphere = stack.Children[1] as Ellipse;
e.Data.Properties.Add("Sphere", new Ellipse()
{
WidthRequest = sphere.Width,
HeightRequest = sphere.Height,
Fill = sphere.Fill
});
}
}
And now, it is time to draw some spheres in our app! Go back to the MainPage.xaml
file and below the XmasTreeControl
element that you added earlier, include the code that is presented afterwards. Some explanation about it first:
- The
StackLayout
member is the second child of theVerticalStackLayout
element that was created before - For each sphere, a different brush is used. For demonstration purposes, a RadialGradientBrush is implemented with various GradientStops (which in turns simulates a kind of light source, cool, I think)
- Again, there's no need for that if you don't want, it can be as simple as
SphereBrush="Red"
for a simple SolidColorBrush definition. - All spheres are arranged horizontally.
Now, the code:
<StackLayout Orientation="Horizontal">
<controls:XmasSphereControl>
<controls:XmasSphereControl.SphereBrush>
<RadialGradientBrush>
<GradientStop Offset="0.3" Color="LightBlue" />
<GradientStop Offset="1.0" Color="Blue" />
</RadialGradientBrush>
</controls:XmasSphereControl.SphereBrush>
</controls:XmasSphereControl>
<controls:XmasSphereControl>
<controls:XmasSphereControl.SphereBrush>
<RadialGradientBrush Center="0.3,0.3">
<GradientStop Offset="0.1" Color="LightCyan" />
<GradientStop Offset="1.0" Color="Orange" />
</RadialGradientBrush>
</controls:XmasSphereControl.SphereBrush>
</controls:XmasSphereControl>
<controls:XmasSphereControl>
<controls:XmasSphereControl.SphereBrush>
<RadialGradientBrush Center="0.7,0.7">
<GradientStop Offset="0.2" Color="LightGray" />
<GradientStop Offset="1.0" Color="DarkGray" />
</RadialGradientBrush>
</controls:XmasSphereControl.SphereBrush>
</controls:XmasSphereControl>
<controls:XmasSphereControl>
<controls:XmasSphereControl.SphereBrush>
<RadialGradientBrush>
<GradientStop Offset="0.5" Color="Pink" />
<GradientStop Offset="1.0" Color="Red" />
</RadialGradientBrush>
</controls:XmasSphereControl.SphereBrush>
</controls:XmasSphereControl>
<controls:XmasSphereControl>
<controls:XmasSphereControl.SphereBrush>
<RadialGradientBrush Center="0.3,0.3">
<GradientStop Offset="0.1" Color="#b9f9e5" />
<GradientStop Offset="1.0" Color="#3ec69b" />
</RadialGradientBrush>
</controls:XmasSphereControl.SphereBrush>
</controls:XmasSphereControl>
</StackLayout>
Before we test the app, we must go back to XmasTreeControl.xaml.cs
and remove the first comment block (the one that looks for the "Sphere" key) inside the OnDrop
method.
Once that code is enabled, we can run the app to see the new content!
Don't forget you can actually interact with the spheres by placing them on the tree!
For some reason, in the Android emulator I had to double-click on the sphere for the drag gesture to be recognized... weird.
Example #3: A X-mas Star control
Our third and final example is a Christmas star. To create this control we are using a Path
that was obtained from the Internet and draws a star. Same as before, a DragGestureRecognizer
is included so the user can drag this element (in order to place it later on the tree).
This is the Content
for the XmasStarControl.xaml
ContentView
:
<ContentView.Content>
<StackLayout>
<Path Data="M12 .587l3.668 7.568 8.332 1.151-6.064 5.828 1.48 8.279-7.416-3.967-7.417 3.967 1.481-8.279-6.064-5.828 8.332-1.151z" StrokeThickness="3" />
<StackLayout.GestureRecognizers>
<DragGestureRecognizer CanDrag="True" DragStarting="OnDrag" />
</StackLayout.GestureRecognizers>
</StackLayout>
</ContentView.Content>
Now you know what's next regarding the C# code. Here's a summary first:
-
StarStrokeProperty
is theBindableProperty
backed toStarStroke
of typeBrush
, allowing the user to define a solid or gradient color for the star. When its value changes, theOnStarStrokeChanged
method is automatically invoked in order to update the star (Path) color. - The
OnDrag
method brings the data for the Star control when the user drags it.
This is the corresponding code for the XmasStarControl.xaml.cs
class file:
using Microsoft.Maui.Controls.Shapes;
using Path = Microsoft.Maui.Controls.Shapes.Path;
namespace XmasControlsNetMaui.Controls;
public partial class XmasStarControl : ContentView
{
public XmasStarControl()
{
InitializeComponent();
}
public static readonly BindableProperty StarStrokeProperty =
BindableProperty.Create(nameof(StarStroke),
typeof(Brush),
typeof(XmasTreeControl),
Brush.Transparent,
BindingMode.TwoWay,
propertyChanged: OnStarStrokeChanged);
public Brush StarStroke
{
get => (Brush)GetValue(StarStrokeProperty);
set { SetValue(StarStrokeProperty, value); }
}
private static void OnStarStrokeChanged(BindableObject bindable, object oldValue, object newValue)
{
var xmasStarControl = bindable as XmasStarControl;
xmasStarControl.FillStroke(xmasStarControl.Content as StackLayout, newValue as Brush);
}
void FillStroke(StackLayout layout, Brush brush)
{
foreach (var item in layout.Children)
{
var control = item as Path;
if (control != null)
control.Stroke = brush;
}
}
private void OnDrag(object sender, DragStartingEventArgs e)
{
var stack = (sender as Element).Parent as StackLayout;
var star = stack.Children[0] as Path;
e.Data.Properties.Add("Star", new Path()
{
Data = star.Data,
Stroke = star.Stroke
});
}
}
Finally, let's include our star in MainPage.xaml, and this time, we'll keep it simple by using a SolidColorBrush
for the StarStroke
property (of course, you can use any other Brush
you want including gradients). Just add the following line below the previously defined StackLayout
that contains the spheres (this means that the star control is the third child of the main VerticalStackLayout
element):
<controls:XmasStarControl StarStroke="Goldenrod" />
Oh, by the way! We must uncomment the second comment block in XmasTreeControl.xaml
, the one that checks for the "Star" key.
After that, let's run the app for the third time:
Drag the star to the top of the tree!
In this post, you have learned how to create your very own set of controls thanks to the .NET MAUI ContentView
element. You can include any Views you want inside the custom control, for this example we used Shapes.
By the way, the project is available on GitHub.
I hope that this post was interesting and useful for you. I invite you to visit my blog for more technical posts about Xamarin, .NET MAUI, Azure, and much more in Spanish language =)
Thanks for your time, and enjoy the rest of the #MAUIUIJuly publications!
See you next time,
Luis