Search code examples
c#xamlmaui

In .NET MAUI, how do I pass variables to a GraphicsView.Drawable in a ContentView


I have the following collectionView in ProceduresPage.xaml (my questions is related to the view:ScoreGaugeView part and how to pass the score to my IDrawable in order to draw a needle gauge).

<?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"
             x:Class="MediSkillApp.View.ProceduresPage"
             Title="Alle mine indgreb"
             xmlns:viewmodel="clr-namespace:MediSkillApp.ViewModel"
             x:DataType="viewmodel:ProceduresViewModel"
             xmlns:model="clr-namespace:MediSkillApp.Model"
             xmlns:view="clr-namespace:MediSkillApp.View">

    <Grid ColumnDefinitions="*"
          ColumnSpacing="5"
          RowSpacing="0">

        <CollectionView
            BackgroundColor="Transparent"
            ItemsSource="{Binding Procedures}"  
            RemainingItemsThresholdReachedCommand="{Binding GetProceduresCommand}"
            RemainingItemsThreshold="5">
            <CollectionView.ItemTemplate>
                <DataTemplate x:DataType="model:Procedure">
                    <VerticalStackLayout>
                        <Frame Margin="5">
                            <Grid ColumnDefinitions="64,*, 64">
                                <Image Source="{Binding Icon}"
                                   HeightRequest="48"
                                   WidthRequest="48"
                                   HorizontalOptions="Start"/>
                                <VerticalStackLayout
                                    Grid.Column="1">
                                    <Label Text="{Binding ProcedureTypeString}"
                                           Style="{StaticResource Heading}"/>
                                    <Label Text="{Binding OpRoleString}"
                                           Style="{StaticResource NormalLabel}"/>
                                    <Label Text="{Binding Date, StringFormat='{0:dd/MM-yyyy}'}"
                                           Style="{StaticResource NormalLabel}"/>
                                </VerticalStackLayout>
                                <view:ScoreGaugeView
                                    Grid.Column="2"
                                    ScoreValue="{Binding GetAvgScore}"
                                    IsVisible="{Binding IsScored}"/>
                            </Grid>
                        </Frame>
                    </VerticalStackLayout>

                </DataTemplate>
            </CollectionView.ItemTemplate>
        </CollectionView>

        <ActivityIndicator IsVisible="{Binding IsBusy}"
                           IsRunning="{Binding IsBusy}"
                           HorizontalOptions="FillAndExpand"
                           VerticalOptions="CenterAndExpand"/>
    </Grid>
       
</ContentPage>

It produces frames like these (an icon, some text, and a score if it has one):

enter image description here

Now, I want to add a scoregauge needle to show the score.

I also have the ScoreGaugeView (ScoreGaugeView.xaml)

<?xml version="1.0" encoding="utf-8" ?>
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:drawables="clr-namespace:MediSkillApp.Drawables"
             x:Class="MediSkillApp.View.ScoreGaugeView">

    <ContentView.Resources>
        <drawables:ScoreGaugeDrawable x:Key="scoreGaugeDrawable"/>
    </ContentView.Resources>

    <ContentView.ControlTemplate>

        <ControlTemplate>
            <VerticalStackLayout>
                <Label Text="{TemplateBinding ScoreValue, StringFormat='{0:F2}'}"
                       HorizontalOptions="Center"/>
                <Image Source="scoremeter.png"/>

                <GraphicsView>
                    <GraphicsView.Drawable>
                        <drawables:ScoreGaugeDrawable/>
                    </GraphicsView.Drawable>
                </GraphicsView>

            </VerticalStackLayout>
        </ControlTemplate>
    </ContentView.ControlTemplate>
</ContentView>

And my ScoreGaugeDrawable (ScoreGaugeDrawable.cs)

namespace MediSkillApp.Drawables;

public class ScoreGaugeDrawable : IDrawable
{    public void Draw(ICanvas canvas, RectF dirtyRect) {
        var centerPoint = new PointF(32, 0);
        var circleRadius = 5;

        canvas.FillColor = Colors.Black;
        canvas.FillCircle(centerPoint, circleRadius);
    }
}

So my question is how to pass the score to my ScoreGaugeDrawable, so I know where to point the needle?

EDIT: I updated my ScoreGaugeView.xaml to the following

<?xml version="1.0" encoding="utf-8" ?>
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:drawables="clr-namespace:MediSkillApp.Drawables"
             x:Class="MediSkillApp.View.ScoreGaugeView">

    <VerticalStackLayout>
        <Label Text="{Binding GetAvgScore, StringFormat='{0:F2}'}"
                HorizontalOptions="Center"/>
        <Image Source="scoremeter.png"/>

        <GraphicsView>
            <GraphicsView.Drawable>
                <drawables:ScoreGaugeDrawable 
                    Score="{Binding AvgScore}"/>
            </GraphicsView.Drawable>
        </GraphicsView>

    </VerticalStackLayout>

</ContentView>

And the ScoreGaugeDrawable.cs to:

namespace MediSkillApp.Drawables;

public class ScoreGaugeDrawable : BindableObject, IDrawable
{
    private string score = "";
    public static readonly BindableProperty ScoreProperty = BindableProperty.Create(nameof(Score),
        typeof(double),
        typeof(ScoreGaugeDrawable));

    public double Score
    {
        get => (double)GetValue(ScoreProperty);
        set => SetValue(ScoreProperty, value);
    }

    public void Draw(ICanvas canvas, RectF dirtyRect)
    {
        var centerPoint = new PointF(32, 0);
        var circleRadius = 5;

        canvas.FillColor = Colors.Black;
        canvas.FillCircle(centerPoint, circleRadius);

        canvas.StrokeColor = Colors.Black;
        canvas.DrawLine(centerPoint, new Point(0, Score));
    }
}

This will print the number in the label but not pass the value to the drawable. In my Procedure class I have:

    public double AvgScore { get; set; }

    public string GetAvgScore {
        get {
            if (AvgScore == 0) return "";
            return AvgScore.ToString();
        }
    }

Solution

  • You should be able to do this with a BindableProperty. You can add that to your ScoreGaugeDrawable class and then pass the value to it from the XAML, e.g. using a Binding expression.

    Update your ScoreGaugeDrawable like this:

    using Microsoft.Maui.Controls;
    
    public class ScoreGaugeDrawable : BindableObject, IDrawable
    {
        public static readonly BindableProperty ScoreProperty = BindableProperty.Create(nameof(Score), typeof(double), typeof(ScoreGaugeDrawable));
    
        public double Score
        {
            get => (double)GetValue(ScoreProperty);
            set => SetValue(ScoreProperty, value);
        }
    
        public void Draw(ICanvas canvas, RectF dirtyRect) 
        {
            var centerPoint = new PointF(32, 0);
            var circleRadius = 5;
    
            canvas.FillColor = Colors.Black;
            canvas.FillCircle(centerPoint, circleRadius);
    
            //Add your drawing logic that uses the Score property 
            //...
        }
    }
    

    Then you should be able to pass a value to the ScoreGaugeDrawable like this, e.g. by Binding to another property that holds the score value:

    <GraphicsView>
        <GraphicsView.Drawable>
            <drawables:ScoreGaugeDrawable Score="{Binding SomeScore}"/>
        </GraphicsView.Drawable>
    </GraphicsView>
    

    Let me know if this works, because I've seen issues with Bindings and Drawables before.

    By the way, you can safely remove this from your XAML, since that's not used anywhere:

    <ContentView.Resources>
        <drawables:ScoreGaugeDrawable x:Key="scoreGaugeDrawable"/>
    </ContentView.Resources>
    

    Something that I also noticed is that you're using a ControlTemplate, but for no apparent reason. It should be possible to remove that and place the VerticalStackLayout directly into the ContentView.