Drawing X-mas controls in .NET MAUI
Tags: dotnetmaui, mobile, dotnet, xmas
Published: 2023-07-23 04:46:36

> This article is part of the [#MAUIUIJuly](https://goforgoldman.com/posts/maui-ui-july-23/) initiative by [Matt Goldman](https://twitter.com/mattgoldman). 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](https://learn.microsoft.com/en-us/dotnet/maui/user-interface/controls/shapes/).

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rmwmt4rrxwtlvzh6g25s.png)


Adding a `ContentView` element to the project:

![Adding a ContentView element to the project](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/va2taowubd30zdjc55ct.png)


**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` 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);
*/
        }
    }

}
```

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 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>
```

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


![A .NET MAUI app showing a Christmas Tree!](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z87d46r05wsy94jhiakj.png)




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 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
        });
    }
}
```


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>
```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hjz6j1vy7ae0ot06j8bb.png)

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

![Placing some spheres on the tree!](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hdlqj9w4msxodple38re.png)

*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 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
        });
    }
}
```


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:

![A .NET MAUI app with Christmas tree, star and sphere controls](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/beodj08ddek0uv8cemkk.png)


Drag the star to the top of the tree!

![Dragging star and spheres on the tree!](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/eqpxogiguhssht22rqb3.png)




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](https://github.com/icebeam7/XmasControlsNetMaui).

I hope that this post was interesting and useful for you. I invite you to visit [my blog](https://www.luisbeltran.mx/) 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](https://goforgoldman.com/posts/maui-ui-july-23/) publications!

See you next time,
Luis