Introduction
When we start learning some programming technology like .NET Framework on Xamarin, we begin with a well-known « Hello World » followed by a form that seems like:
Obviously, it's good to start but it's not very fun. We quickly want to make something fun to show, like displaying and manipulating an image, drawing geometric shapes, and so on.
We find the way very quickly: SKIASharp
, it is a graphic library that implements a Canvas
control that allows all 2D manipulation operations: image, geometric shapes, transformation, paths, effects, and so on.
In this article, we will see how to transform a basic use of the control to an implementation that fulfills the needs of MVVM architecture.
Note
The SKIASharp
documentation is very clear and complete, this article will not repeat it.
The article also assumes that the reader has a minimal experience in Xamarin .NET programming.
It is also important to understand the steps to be familiar with the MVVM architecture, although we will mostly concentrate on the ViewModel
.
First Step
Let’s start by creating a project named SKIAToMVVM
and our first page named CirclePage
.
Then, we add the SKIA Canvas
control which is named SKCanvasView.
We need to declare a namespace in the XAML:
<ContentPage … xmlns:skia="clr-namespace:SkiaSharp.Views.Forms;
assembly=SkiaSharp.Views.Forms"…>
Let's add the control in our page, we notice that we respond to the PaintSurface
event by calling the Canvas_PaintSurface
function which will be declared in the code-behind.
<skia:SKCanvasView x:Name="Canvas" PaintSurface="Canvas_PaintSurface"/>
Finally, in the code-behind, let’s implement the Canvas_PaintSurface
function.
private void Canvas_PaintSurface(object sender, SKPaintSurfaceEventArgs e)
{
SKCanvas canvas = e.Surface.Canvas;
canvas.Clear();
SKPaint fillPaint = new SKPaint
{
Style = SKPaintStyle.Fill,
Color = new SKColor(128, 128, 240)
};
canvas.DrawCircle(e.Info.Width / 2, e.Info.Height / 2, 100, fillPaint);
}
Problems
At this point, what problems can we see?
- Personally, when I use XAML to describe user interfaces, I am a follower of the zero-code-behind, that is to say that no code must be written in the code-behind.
- More important, the drawing rendering is bound to the page. If we want to draw the same drawing in another page, we have to duplicate the code.
Solutions, Start Refactoring
Let’s start to transform the code to be reusable.
The first reflex is to out the code contained in the Canvas_PaintSurface
event handler into a separated function, for example, into a CircleRenderer
class, that has the PaintSurface
method.
class CircleRenderer
{
void PaintSurface(SKSurface surface, SKImageInfo info)
{
SKCanvas canvas = surface.Canvas;
canvas.Clear();
The event handler becomes:
private void Canvas_PaintSurface(object sender, SKPaintSurfaceEventArgs e)
{
new CircleRenderer().PaintSurface(e.Surface, e.Info);
}
To reach the zero-code-behind, we have to remove the event handler itself.
To do it, we create a custom control. Let’s declare a class, for example SKRenderView
which inherits from the control SKCanvasView
.
Now, in SKRenderView
, we have access to the protected virtual
method OnPaintSurface
that we will override.
class SKRenderView : SKCanvasView
{
protected override void OnPaintSurface(SKPaintSurfaceEventArgs e)
{
new CircleRenderer().PaintSurface(e.Surface, e.Info);
}
}
Like this, we may remove the event handler in the code-behind. It is now emptied.
However, we have to modify the XAML to use the new control.
Let’s remove the xmlns:skia
namespace declared earlier and let’s replace it by the namespace that contains the new custom control. (It depends on your project).
xmlns:ctrl="clr-namespace:SKIAToMVVM.Controls"
Also let's change the description of the canvas control.
<ctrl:SKRenderView x:Name="Canvas"/>
New Problems
Unfortunately, this solution is not totally satisfactory. Indeed, the drawing rendering is now bound to the control. Doing like this, each different drawing requires to create a new custom control.
The ideal solution towards which we will be heading is to integrate to the ViewModel
all information the control needs so that it can render the drawing whatever it is.
Let’s change the XAML file to define the way we want to provide information to the control.
The control must have a Renderer
property that will receive the renderer object we want (In our example, it is CircleRenderer
).
We get after this step, the following code:
<ctrl:SKRenderView x:Name="Canvas" Renderer="{StaticResource CircleRenderer}"/>
And we have to define the resource (and the needed namespace, depending on your project):
xmlns:renderer="clr-namespace:SKIAToMVVM.Renderers"
<ContentPage.Resources>
<ResourceDictionary>
<renderer:CircleRenderer x:Key="CircleRenderer" />
</ResourceDictionary>
</ContentPage.Resources>
The ViewModel
At this point, so we have:
- a presentation XAML file that contains a
Canvas
Control - an empty code-behind
- a
CircleRenderer
class in charge of rendering the drawing - an instance of
CircleRenderer
as a resource - an error in the XAML file, indeed, we haven’t define the
Renderer
property in the control yet.
In order to introduce the ViewModel
, we will add 3 buttons in the View
.
- One button to colour the circle in red
- One button to colour the circle in green
- One button to colour the circle in blue
The 3 buttons are bound to the same command, the colour is passed in the command argument.
<Button Text="Rouge" Command="{Binding ColorCommand}"
CommandParameter="#ff0000" Grid.Column="0" />
<Button Text="Vert" Command="{Binding ColorCommand}"
CommandParameter="#00ff00" Grid.Column="1" />
<Button Text="Bleu" Command="{Binding ColorCommand}"
CommandParameter="#0000ff" Grid.Column="2" />
Let’s create the ViewModel
class CirclePageModel
.
It will contain:
- One
ColorCommand
property of type Command
and the execute
function ExecuteColorCommand
. The CanExecute
function in this case is optional. - One
Renderer
property of type CircleRenderer
:
ColorCommand = new Command(ExecuteColorCommand);
The ExecuteColorCommand
function receives the colour as a characters string
, we create an SKColor
object from this value to assign to the Renderer
.
void ExecuteColorCommand(object colorArgument)
{
SKColor color = SKColor.Parse((string)colorArgument);
Renderer.FillColor = color;
}
We still have to create the FillColor
property in the CircleRenderer
class and use it.
public SKColor FillColor { get; set; } = new SKColor(160, 160, 160);
public void PaintSurface(SKSurface surface, SKImageInfo info)
{
…
SKPaint fillPaint = …
Color = FillColor
…
}
Let’s bind the CirclePageModel
to the page.
xmlns:model="clr-namespace:SKIAToMVVM.ViewModels"
<ContentPage.BindingContext>
<model:CirclePageModel />
</ContentPage.BindingContext>
To Complete the Control
There still an error in XAML file to correct. We wrote:
Renderer="{StaticResource CircleRenderer}"
But the Renderer
property does not exist in the CircleRenderer
class, we just need to create it:
public Renderers.CircleRenderer Renderer { get; set; }
We can execute the application and we can see a grey circle (default value of FillColor
).
When we click on the buttons, nothing happens.
We notice when we place some breakpoints that the buttons are working.
So, what happened?
The reason is simple: in the XAML file, we bound the Renderer control’s property to a resource, thus we actually have 2 instances of CircleRenderer
, one known by the control and another one known by the ViewModel
.
Then what to do?
We have to modify the Renderer
property of the SKCanvasView
control so that is bindable.
For this, we just have to add the requested code.
- Add a property description for bindable property.
- Change the
Renderer
property, currently we have an auto-property, we transform it into a full-property to be able to call the methods GetValue
and SetValue
from BindableObject
base class.
public static readonly BindableProperty RendererProperty = BindableProperty.Create(
nameof(Renderer),
typeof(Renderers.CircleRenderer),
typeof(SKRenderView),
null,
public Renderers.CircleRenderer Renderer
{
get { return (Renderers.CircleRenderer)GetValue(RendererProperty); }
set { SetValue(RendererProperty, value); }
}
And in the XAML file, we replace:
Renderer="{StaticResource CircleRenderer}"
With:
Renderer="{Binding Renderer}"
And we remove the resource that is now unused.
<renderer:CircleRenderer x:Key="CircleRenderer" />
Let’s execute and push on the colour change buttons. We notice once again that nothing happened.
That’s normal, we did not notify the control to refresh itself.
We would like to say to the control to refresh itself when we change the colour in the renderer.
How?
To refresh the control, we call InvalidateSurface
method from the control.
However, we do not have access to the control neither in the ViewModel
nor in the Renderer
, and that is normal. The control must remain unknown outside the View
.
The solution is simple: how an object notify a change ? Obviously using an event.
Let’s create the event in the CircleRenderer
class.
public event EventHandler RefreshRequested;
We have to raise the event in the FillColor
property, let’s transform the auto-property to full-property.
SKColor _fillColor = new SKColor(160, 160, 160);
public SKColor FillColor
{
get => _fillColor;
set
{
if (_fillColor != value)
{
_fillColor = value;
RefreshRequested?.Invoke(this, EventArgs.Empty);
}
}
}
The control has to attach the event, so change the SKRenderView
control:
- Change the
Renderer
property declaration and add the propertyChanged
parameter. - Implement the
RendererChanged
function. - Finally, implement the
RefreshRequested
event handler.
public static readonly BindableProperty RendererProperty = BindableProperty.Create(
nameof(Renderer),
typeof(Renderers.CircleRenderer),
typeof(SKRenderView),
null,
defaultBindingMode: BindingMode.TwoWay,
propertyChanged: (bindable, oldValue, newValue) =>
{
((SKRenderView)bindable).RendererChanged(
(Renderers.CircleRenderer)oldValue, (Renderers.CircleRenderer)newValue);
});
void RendererChanged(Renderers.CircleRenderer currentRenderer,
Renderers.CircleRenderer newRenderer)
{
if (currentRenderer != newRenderer)
{
if (currentRenderer != null)
currentRenderer.RefreshRequested -= Renderer_RefreshRequested;
if (newRenderer != null)
newRenderer.RefreshRequested += Renderer_RefreshRequested;
InvalidateSurface();
}
}
void Renderer_RefreshRequested(object sender, EventArgs e)
{
InvalidateSurface();
}
Finalize
There still one thing less satisfying to modify. Indeed, in the control, the type of our Renderer
property is CircleRenderer
. This is absolutely not generic: if we want to draw an image or a rectangle, having a renderer called CircleRenderer
is not appropriate.
To resolve the problem, we can
- Either use a base class for our renderers
- Or use an interface
We can also combine the two methods and have a base class that implements an interface.
We will jus use an interface and call it IRenderer
. We extract the interface from the CircleRenderer
class and get:
interface IRenderer
{
void PaintSurface(SKSurface surface, SKImageInfo info);
event EventHandler RefreshRequested;
}
In the SKRenderView
control , we replace all references to CircleRenderer
by IRenderer
.
There is still in the ViewModel
, the Renderer
property of type CircleRenderer
that we can replace with IRenderer
. It is the programmer’s responsibility to know what he needs in the ViewModel
.
Conclusion
We have just seen how to transform a rigid and non-reusable code structure to a structure that respects the MVVM model.
We gain multiple advantages:
- We do not have code-behind in the page anymore
- We have reusable code
- The code structure respects the MVVM model
- The responsibilities of each object are correctly assigned:
- The page shows the controls.
- The SKIA control is a support for drawing.
- The renderer draws the drawing.
- The
ViewModel
knows what must be drawn.
Thanks for reading.
History
- 8th October, 2019: Initial version
- 17th October, 2019: Repost source code zip file, link broken