Click here to Skip to main content
14,364,399 members

Xamarin SKIASharp: Guide to MVVM

Rate this:
4.00 (1 vote)
Please Sign up or sign in to vote.
4.00 (1 vote)
17 Oct 2019CPOL
A guide to using the canvas control from SKIASharp in the MVVM way

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:

Hello

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?

  1. 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.
  2. 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();
        // and so on.

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.

  1. One button to colour the circle in red
  2. One button to colour the circle in green
  3. 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.

  1. Add a property description for bindable property.
  2. 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.
// 1. Add a property description for bindable property.
public static readonly BindableProperty RendererProperty = BindableProperty.Create(
    nameof(Renderer),
    typeof(Renderers.CircleRenderer),
    typeof(SKRenderView),
    null,

// 2. Change the Renderer property
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:

  1. Change the Renderer property declaration and add the propertyChanged parameter.
  2. Implement the RendererChanged function.
  3. Finally, implement the RefreshRequested event handler.
// 1. Change the Renderer property declaration and add the propertyChanged parameter.
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);
    });

// 2. Implement the RendererChanged function.
void RendererChanged(Renderers.CircleRenderer currentRenderer, 
                    Renderers.CircleRenderer newRenderer)
{
    if (currentRenderer != newRenderer)
    {
        // detach the event from old renderer
        if (currentRenderer != null)
            currentRenderer.RefreshRequested -= Renderer_RefreshRequested;

        // attach the event to new renderer
        if (newRenderer != null)
            newRenderer.RefreshRequested += Renderer_RefreshRequested;

        // refresh the contrl
        InvalidateSurface();
    }
}

// 3. Finally, implement the RefreshRequested event handler.
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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Laurent Bouffioux
Software Developer (Senior)
Belgium Belgium
No Biography provided

Comments and Discussions

 
QuestionFile download missing? Pin
Member 1167000011-Oct-19 0:49
memberMember 1167000011-Oct-19 0:49 
AnswerRe: File download missing? Pin
Laurent Bouffioux17-Oct-19 10:42
memberLaurent Bouffioux17-Oct-19 10:42 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Article
Posted 7 Oct 2019

Tagged as

Stats

3.5K views
44 downloads
3 bookmarked