Reputation: 169
I have problems understanding how binding works in .NET MAUI. My goal is to have two ways of displaying a score inside a CollectionView, a label with the number and a graphical representation. The binding of the score to the label works just fine, but the binding to the Drawable does not. If I write a number instead of using a binding, it is passed just fine.
What am I doing wrong?
ProceduresPage.xaml
<?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:model="clr-namespace:MediSkillApp.Model"
xmlns:drawables="clr-namespace:MediSkillApp.Drawables"
xmlns:viewmodel="clr-namespace:MediSkillApp.ViewModel"
x:Class="MediSkillApp.View.ProceduresPage"
x:DataType="viewmodel:ProceduresViewModel"
Title="Alle mine indgreb">
<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
Grid.Column="0"
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>
<VerticalStackLayout
Grid.Column="2"
IsVisible="{Binding IsScored}">
<!-- this binding works -->
<Label Text="{Binding AvgScore, StringFormat='{0:F2}'}"
HorizontalOptions="Center"/>
<Image Source="scoremeter.png"/>
<GraphicsView>
<GraphicsView.Drawable>
<!-- this binding does not -->
<drawables:ScoreGaugeDrawable
Score="{Binding AvgScore}"/>
</GraphicsView.Drawable>
</GraphicsView>
</VerticalStackLayout>
</Grid>
</Frame>
</VerticalStackLayout>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
<ActivityIndicator IsVisible="{Binding IsBusy}"
IsRunning="{Binding IsBusy}"
HorizontalOptions="FillAndExpand"
VerticalOptions="CenterAndExpand"/>
</Grid>
</ContentPage>
ScoreGaugeDrawable.cs
namespace MediSkillApp.Drawables;
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);
canvas.StrokeColor = Colors.Black;
canvas.DrawLine(centerPoint, new Point(0, Score * 10)); //Just draw something for testing
}
}
Procedure.cs
namespace MediSkillApp.Model;
public class Procedure
{
public string Identifier { get; set; }
public DateTime Date { get; set; }
public string GetDate {
get => Date.ToString("d/M-yyyy");
}
public int ProcedureType { get; set; }
public string ProcedureTypeString { get; set; }
public double AvgScore { get; set; }
public string GetAvgScore {
get {
if (AvgScore == 0) return "";
return AvgScore.ToString();
}
}
public int OpRole { get; set; }
public string OpRoleString { get; set; }
public string Icon {
get {
switch (ProcedureType) {
case 7:
return Icons.IconBleed;
case 8:
return Icons.IconTEA;
case 18:
return Icons.IconTEA;
default:
return Icons.IconSurgery;
}
}
}
public bool IsScored => AvgScore > 0;
}
ProceduresViewModel.cs
using MediSkillApp.Model;
using MediSkillApp.Services;
namespace MediSkillApp.ViewModel;
public partial class ProceduresViewModel : BaseViewModel
{
public ObservableCollection<Procedure> Procedures { get; } = new();
private APIService APIservice;
public ProceduresViewModel(APIService aPIservice) {
APIservice = aPIservice;
}
[RelayCommand]
public async Task GetProceduresAsync() {
if(IsBusy) return;
try {
IsBusy = true;
var procedures = await APIservice.GetProceduresAsync("8", "dawda", Procedures.Count, 15);
foreach (Procedure procedure in procedures) {
Procedures.Add(procedure);
}
} catch(Exception ex) {
Debug.WriteLine(ex);
} finally {
IsBusy = false;
}
}
public void ClearProcedures() {
Procedures.Clear();
}
}
Upvotes: 5
Views: 1753
Reputation: 8914
I was able to reproduce the problem. It seems that Drawables cannot be used with BindableProperty
, at least it doesn't have any effect, the value of the property doesn't get updated.
I managed to find a workaround for this issue, however. Instead of adding the Score
property to the ScoreGaugeDrawable
, you can add it to the GraphicsView
by extending it via inheritance.
You can remove the BindableObject
base class as well as the bindable ScoreProperty
from the ScoreGaugeDrawable
and turn the Score
property into a regular property with default getter and setter:
namespace MediSkillApp.Drawables;
public class ScoreGaugeDrawable : IDrawable
{
public double Score { get; set; }
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 * 10)); //Just draw something for testing
}
}
Then, create a ScoreGraphicsView
that inherits from GraphicsView
and add the bindable ScoreProperty
to it:
namespace MediSkillApp.Drawables;
public class ScoreGraphicsView : GraphicsView
{
public double Score
{
get => (double)GetValue(ScoreProperty);
set => SetValue(ScoreProperty, value);
}
public static readonly BindableProperty ScoreProperty = BindableProperty.Create(nameof(Score), typeof(double), typeof(ScoreGraphicsView), propertyChanged: ScorePropertyChanged);
private static void ScorePropertyChanged(BindableObject bindable, object oldValue, object newValue)
{
if (bindable is not ScoreGraphicsView { Drawable: ScoreGaugeDrawable drawable } view)
{
return;
}
drawable.Score = (double)newValue;
view.Invalidate();
}
}
This way, the score needs to be passed to the GraphicsView, which (unfortunately) must now know about the ScoreGaugeDrawable
. What this code does is, it receives any updates to the bindable ScoreProperty
and relays the value to the ScoreGaugeDrawable
. If the value has changed and the Drawable
is of type ScoreGaugeDrawable
, the new value is set and then the view gets invalidated, which triggers a redraw.
You can use the ScoreGraphicsView
and ScoreGaugeDrawable
like this in your XAML then:
<drawables:ScoreGraphicsView
Score="{Binding AvgScore}">
<drawables:ScoreGraphicsView.Drawable>
<drawables:ScoreGaugeDrawable/>
</drawables:ScoreGraphicsView.Drawable>
</drawables:ScoreGraphicsView>
This is not ideal, but should solve your problem for the time being. I've tested this myself in my MAUI Samples repository and it works quite well.
Upvotes: 7