.NET MAUI 绑定到 GraphicsView

3
我在理解.NET MAUI中绑定工作方式方面遇到了问题。我的目标是在CollectionView内有两种显示分数的方式,一种是带数字的标签,另一种是图形化表示。分数与标签的绑定正常工作,但是与Drawable的绑定却不起作用。如果我写一个数字而不是使用绑定,它可以正常传递。

我做错了什么?

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();
    }
}

AvgScore 属性定义在哪里?我猜它在 ProceduresViewModel 中。你应该展示那个 ViewModel 的相关部分。 - Julian
你在 Draw() 方法中设置了断点以查看它被调用的频率和 Score 的值吗? - Julian
我已经更新了ViewModel和Procedure模型。当我在Draw中设置断点时,它只会在我期望的时候调用,即当IsScored为true(在Procedure.cs中定义),但值为0。 - KnightofniDK
1个回答

5

我已经成功复现了这个问题,似乎可绘制对象不能与BindableProperty一起使用,至少它没有任何作用,属性的值不会得到更新。

然而,我成功地找到了一个解决方法。你可以通过继承将Score属性添加到GraphicsView中,而不是将其添加到ScoreGaugeDrawable中。

你可以从ScoreGaugeDrawable中删除BindableObject基类以及可绑定的ScoreProperty,并将Score属性转换为具有默认getter和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
    }
}

然后,创建一个继承自GraphicsViewScoreGraphicsView,并将可绑定的ScoreProperty添加到其中:

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();
    }
}

这种方式需要将分数传递给GraphicsView,但是(不幸的是)现在必须知道ScoreGaugeDrawable。这段代码的作用是接收可绑定的ScoreProperty的任何更新,并将值中继到ScoreGaugeDrawable。如果值已更改且Drawable的类型为ScoreGaugeDrawable,则设置新值,然后视图无效,从而触发重绘。

您可以在XAML中像这样使用ScoreGraphicsViewScoreGaugeDrawable

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

这不是最理想的解决方案,但暂时可以解决你的问题。我已经在我的MAUI Samples存储库中进行了测试,效果非常好。


ewerspej,你是个天才!非常感谢,我快要疯了,你让我的一天变得美好。顺便说一下,在你的回答中有几个打字错误('set'中RadiusProperty应该是ScoreProperty,而ScoreDrawable应该是ScoreGaugeDrawable以符合原始命名)。 - KnightofniDK
谢谢,我已经修复了。那是因为我先尝试了解决方案,然后在调整名称之前将其复制到答案中。 - Julian
这被标记为答案,但是对我来说,这个链接https://dev59.com/PdiKpIgBRmDukGFEjKpm#76022807才有效。 - MartyIX
@MartyIX 你在评论中链接到了这个答案。这是有意为之吗?我猜你可能是想链接到另一个问题的答案吧? - Julian
嘿,谢谢你告诉我。你说得对。我删除了那条评论以消除混淆。 - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接