DEV Community

Luis Beltran
Luis Beltran

Posted on

Drawing X-mas controls in .NET MAUI

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 main ContentPage.
  • All ContentViews are added inside a Controls folder.

Some images are added for reference:

The .NET MAUI project structure:

The .NET MAUI project structure

Adding a ContentView element to the project:

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>
Enter fullscreen mode Exit fullscreen mode

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 and Path classes.
  • There are two bindable properties: LeavesBrushProperty and TrunkBrushProperty backed by the properties LeavesBrush and TrunkBrush, 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 the DropGestureRecognizer 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 the AbsoluteLayout 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);
*/
        }
    }

}
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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 the LeavesBrush property. You don't have to if you don't want to, as the LeavesBrush member accepts any Brush, such as a simple SolidColorBrush definition (example: LeavesBrush="#0f0")
  • The controls alias is used to find the XmasTreeControl 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>
Enter fullscreen mode Exit fullscreen mode

Let's see it in action! Run the app and take a look.

A .NET MAUI app showing a Christmas Tree!

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>
Enter fullscreen mode Exit fullscreen mode

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 a BindableProperty backed to SphereBrush of type Brush, allowing the user to define a solid or gradient fill color for the sphere. When its value changes, the OnSphereBrushChanged 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
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

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 the VerticalStackLayout 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>
Enter fullscreen mode Exit fullscreen mode

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!

A .NET MAUI app showing Christmas tree and spheres

Don't forget you can actually interact with the spheres by placing them on the tree!

Placing some spheres 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>

Enter fullscreen mode Exit fullscreen mode

Now you know what's next regarding the C# code. Here's a summary first:

  • StarStrokeProperty is the BindableProperty backed to StarStroke of type Brush, allowing the user to define a solid or gradient color for the star. When its value changes, the OnStarStrokeChanged 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
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

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" />
Enter fullscreen mode Exit fullscreen mode

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:

A .NET MAUI app with Christmas tree, star and sphere controls

Drag the star to the top of the tree!

Dragging star and spheres on 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

Top comments (0)