by Shaun Lawrence
This post is part of the MAUI UI July community series of blog posts and videos, hosted by Matt Goldman. Be sure to check out the other posts in this series!
The plan for this post is to show how we can build a clone of the drawing game “Draw Something”.
We have a bit of groundwork to do before we can build those 3 items so let’s go ahead and do that first.
First things first we need to create a .NET MAUI project - I am going to assume you know how to do this but if not then the Microsoft Docs page on Getting Started should have you covered.
I have chosen the project name of DrawSomething
.
I have chosen the name GamePageViewModel
for this and I have located it under a folder called ViewModels
in case we eventually decide to add more.
namespace DrawSomething.ViewModels;
public class GamePageViewModel
{
public string Subject { get; } = "GLOBE";
public IList<Color> AvailableColors { get; } = new List<Color>
{
Colors.Black,
Colors.Red,
Colors.Orange,
Colors.Yellow,
Colors.Green,
Colors.Blue,
Colors.Indigo,
Colors.Violet,
Colors.White
};
}
You may be noticing that our view model doesn’t implement INotifyPropertyChanged
and therefore doesn’t update the UI when values change. OK I’ll admit I have cheated a little here and decided not to update the UI, the bindings will work fine on their first use but won’t receive updates if they are changed in the view model. Perhaps this could be a little extra assignment for you ;).
I ripped out MainPage
and created a new page called GamePage
located under a folder called Pages
.
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:viewmodels="clr-namespace:DrawSomething.ViewModels"
xmlns:controls="clr-namespace:DrawSomething.Controls"
x:Class="DrawSomething.Pages.GamePage"
x:DataType="viewmodels:GamePageViewModel"
BackgroundColor="LightGray"
Title="{Binding Subject, StringFormat='You are drawing {0}'}">
<Grid RowDefinitions="*,10*,*,*">
</Grid>
</ContentPage>
We will need to update our AppShell.xaml
file to point to this new GamePage
rather than the old MainPage
that we just deleted, it should now look like:
<?xml version="1.0" encoding="UTF-8" ?>
<Shell
x:Class="DrawSomething.AppShell"
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:pages="clr-namespace:DrawSomething.Pages"
Shell.FlyoutBehavior="Disabled">
<ShellContent
Title="Home"
ContentTemplate="{DataTemplate pages:GamePage}" />
</Shell>
With the key changes being:
xmlns:pages="clr-namespace:DrawSomething.Pages"
namespaceContentTemplate
from local:MainPage
to pages:GamePage
.We also need to set the BindingContext
of our GamePage
, I have opted for following best practices so we have two steps to follow:
public GamePage(GamePageViewModel gamePageViewModel)
{
InitializeComponent();
BindingContext = gamePageViewModel;
}
Shell
can create our page and its dependencies. Open up MauiProgram.cs
and add into the CreateMauiApp
method:builder.Services.AddTransient<GamePage>();
builder.Services.AddTransient<GamePageViewModel>();
Our page will be built with a Grid
split into 4 rows - the first 3 covering the items on our list and the final one to just providing some padding without having to set fixed sizing options for the button icons.
Let’s proceed to building each of those numbered components.
We will be using a CollectionView
to build our color selection bar as it provides an easy way to layout the items as well as handle the selection for us.
In our GamePage
we will want to add:
<CollectionView
x:Name="ColorList"
ItemsSource="{Binding AvailableColors}"
SelectionMode="Single"
ItemsLayout="HorizontalList">
<CollectionView.ItemTemplate>
<DataTemplate>
<Grid WidthRequest="48">
<Frame
BackgroundColor="{Binding}"
Margin="0,0,0,10" />
</Grid>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
We leave a little Margin
on the bottom of the Frame
to make the selection more visible to the user.
This is the most involved part! First we need to create a control that encapsulates the drawing logic for us.
We will be making use of the GraphicsView
which is provided as part of .NET MAUI, this view allows us to render 2D graphics onto a drawable canvas. This is performed by the Drawable
property which must implement the IDrawable
interface - this provides the implementor with access to the canvas and dimensions to know how and where to draw. In order for us to inform the GraphicsView
to redraw the contents of the canvas and ultimately call IDrawable.Draw
we need to call Invalidate
on the GraphicsView
- if this isn’t completely clear don’t worry this should become clearer as we work through the example.
For this approach I have opted for a purely C# implementation as I found that XAML just added noise.
using System.Windows.Input;
namespace DrawSomething.Controls;
public class DrawingSurface : GraphicsView, IDrawable
{
public DrawingSurface()
{
Drawable = this;
}
public void Draw(ICanvas canvas, RectF dirtyRect)
{
// Do our drawing here.
}
}
That’s simple right? If only - now that we have a surface, we need to add an implementation to handle the drawing of lines. We need to provide:
For this we need to create a BindableProperty
so that when we add the DrawingSurface
to our page we can bind to the selection in our color selection bar.
public static readonly BindableProperty DrawingColorProperty =
BindableProperty.Create(
nameof(DrawingColor),
typeof(Color),
typeof(DrawingSurface),
Colors.Black);
public Color DrawingColor
{
get => (Color)GetValue(DrawingColorProperty);
set => SetValue(DrawingColorProperty, value);
}
For this we need to create a BindableProperty
so that when we add the DrawingSurface
to our page we can bind to a way of setting the line thickness. Perhaps a Slider
?
public static readonly BindableProperty LineThicknessProperty =
BindableProperty.Create(
nameof(LineThickness),
typeof(float),
typeof(DrawingSurface),
10f);
public float LineThickness
{
get => (float)GetValue(LineThicknessProperty);
set => SetValue(LineThicknessProperty, value);
}
The GraphicsView
provides us with the ability to monitor the user’s interaction with the control based on events with Interaction in the name (e.g. StartInteraction, DragInteraction, EndInteraction).
In our DrawingSurface
constructor we want to add:
DragInteraction += DrawingSurface_DragInteraction;
StartInteraction += DrawingSurface_StartInteraction;
EndInteraction += DrawingSurface_EndInteraction;
And then the methods that will be called:
private void DrawingSurface_DragInteraction(object sender, TouchEventArgs e)
{
}
private void DrawingSurface_EndInteraction(object sender, TouchEventArgs e)
{
}
private void DrawingSurface_StartInteraction(object sender, TouchEventArgs e)
{
}
These event handlers provide us with the ability to keep track of the users interaction.
We are going to treat each time the user starts interacting (puts the finger on the screen or clicks down on the mouse) through to the time they end interacting (lifts their finger off the screen or mouse) with the surface as a single line or path.
.NET MAUI Graphics provides us with the PathF
struct that represents a collection of points which we can draw onto the canvas
in the Draw
method but we also need to know which color and how thick this path will be. So we are going to create a class to encapsulate the details of each path:
namespace DrawSomething;
public class DrawingPath
{
public DrawingPath(Color color, float thickness)
{
Color = color;
Thickness = thickness;
Path = new PathF();
}
public Color Color { get; }
public PathF Path { get; }
public float Thickness { get; }
public void Add(PointF point) => Path.LineTo(point);
}
Then in our DrawingSurface
class we need to add a few key bits:
Fields to store the current interaction as well as the previous interactions:
private DrawingPath currentPath;
private readonly IList<DrawingPath> paths = new List<DrawingPath>();
Next we need to record the starting interaction, so let’s fill our the body of the DrawingSurface_StartInteraction
method:
private void DrawingSurface_StartInteraction(object sender, TouchEventArgs e)
{
currentPath = new DrawingPath(DrawingColor, LineThickness);
currentPath.Add(e.Touches.First());
paths.Add(currentPath);
Invalidate();
}
Here we record the currently selected color and line thickness, grab the first touch that is available from the interaction TouchEventArgs
. Note this touch interaction supports multi-touch should you require - perhaps to allow for gestures such as zooming/panning. Finally we add the current path to our collection of paths
purely to reduce the complexity of our drawing code.
Now let’s handle the user moving their finger or mouse around through the DrawingSurface_DragInteraction
method:
private void DrawingSurface_DragInteraction(object sender, TouchEventArgs e)
{
currentPath.Add(e.Touches.First());
Invalidate();
}
And finally we need to handle the scenario when the user has finished interacting with the surface in the DrawingSurface_EndInteraction
method:
private void DrawingSurface_EndInteraction(object sender, TouchEventArgs e)
{
currentPath.Add(e.Touches.First());
Invalidate();
}
It is worth highlighting the use of the Invalidate
method in this and each of the above event handlers. As mentioned earlier on the call to Invalidate
will cause our IDrawable.Draw
method to be called and ultimately allow us to draw the content.
Our final step in section Creating the DrawingSurface control is to perform the drawing. Inside our Draw
method we want to add the following:
public void Draw(ICanvas canvas, RectF dirtyRect)
{
foreach (var path in paths)
{
canvas.StrokeColor = path.Color;
canvas.StrokeSize = path.Thickness;
canvas.StrokeLineCap = LineCap.Round;
canvas.DrawPath(path.Path);
}
}
This allows us to loop through all of the interactions that we have recorded by the user and display the result.
Now that we have a control we need to actually use it! This is the second row in the Grid
in our GamePage
so you can guess where our next destination is…
Let’s open up the GamePage.xaml
file and add the following inside the Grid
and below our color selection bar:
<controls:DrawingSurface
Grid.Row="1"
x:Name="DrawingSurface"
BackgroundColor="White"
DrawingColor="{Binding SelectedItem, Source={x:Reference ColorList}}"/>
Due to the power of bindings we don’t need to create a property on our view model for both the color selection and the drawing surface to bind to and keep in sync - we can simply bind the DrawingColor
property of our DrawingSurface
control to the SelectedItem
property on the control called ColorList
. This really helps to keep the view related information out of the view model and reduce the code that we write.
The final part of our build is to add in the functionality to clear the surface or undo the last action.
For this we first need to add the ability to perform these functions on the DrawingSurface
and then link to them in the page.
Let’s open up our DrawingSurface.cs
file and do that. We need to add:
public static readonly BindableProperty ClearCommandProperty =
BindableProperty.CreateReadOnly(
nameof(ClearCommand),
typeof(ICommand),
typeof(DrawingSurface),
default,
BindingMode.OneWayToSource,
defaultValueCreator: CreateClearCommand).BindableProperty;
public ICommand ClearCommand => (ICommand)GetValue(ClearCommandProperty);
static object CreateClearCommand(BindableObject bindable)
=> new Command(() => ((DrawingSurface)bindable).Clear());
private void Clear()
{
paths.Clear();
Invalidate();
}
This looks a bit different to the other bindable properties we created so let’s see what is going on:
We have created a ClearCommand
property that when execute will call the Clear
method on our DrawingSurface
. This is the opposite of the common interactions that we see with commands, usually the flow is from the source (view model) to the target (view), for example the Button.Command
will call code in our view model.
Our approach is achieved by:
BindingMode.OneWayToSource
- further reading.get
for our ClearCommand
property.BindableProperty.CreateReadOnly
to create a read-only property.For the icons on our buttons I used the following from Google Fonts:
Note that I did not just drop these into the \Resources\Images
folder as they would come out with the pre-supplied color, I managed to make use of the magical TintColor
settings to change the color of the images without having to open an image editor. For more information on what can be achieved check out the Microsoft Docs.
To achieve this we need to:
Resources
folderDrawSomething.csproj
file<ItemGroup>
<MauiImage Include="Resources\clear.svg" TintColor="DarkSlateGray" />
<MauiImage Include="Resources\undo.svg" TintColor="DarkSlateGray" />
</ItemGroup>
You can add these into any ItemGroup
already in the project file and I opted to include with the image related ones.
Now that we have our images and the functionality in the DrawingSurface
control we can add the Button
s to work the magic. Inside our GamePage.xaml
file we want to add the third rows contents as:
<HorizontalStackLayout Grid.Row="2">
<Button
ImageSource="undo.png"
BackgroundColor="LightGray"
Command="{Binding UndoCommand, Source={x:Reference DrawingSurface}}"/>
<Button
ImageSource="clear.png"
BackgroundColor="LightGray"
Command="{Binding ClearCommand, Source={x:Reference DrawingSurface}}"/>
</HorizontalStackLayout>
Note that despite providing files like undo.svg
we actually refer to them as undo.png
this is because the tooling converts the svg file over to multiple different png files to suit the various platforms that our app will deploy to.
There we have it! The ability for me to create some terrible drawings.
Please feel free to send me your drawings ;) or to simply reach out, I am on twitter Bijington.
I would like to point out that the .NET MAUI Community Toolkit does actually offer a DrawingView control that will provide the functionality that we have implemented in our DrawingSurface
control.
My motivation for not using that control in this post was to provide an overview of using .NET MAUI Graphics. If you have enjoyed this then I thoroughly recommend checking out all of the possibilities that the Graphics offers because we have only just touched the surface. The Microsoft Docs pages provide a good insight. Or failing that come and check out the fun I am having trying to use .NET MAUI Graphics in building a lightweight 2D Game Engine over on GitHub.
The source for this creation can be found at:
tags: C# - maui - graphics - MauiUiJuly