You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

.NET Maui自定义SkiaSharp控件绑定属性行为异常

.NET Maui自定义SkiaSharp进度条阈值逻辑绑定失效问题

问题描述

参考《使用SkiaSharp编写.NET Maui自定义控件》教程,用C# Markup替代XAML实现进度条控件,除实例化需使用ProgressBar.Maui.ProgressBar完整命名空间外,其余功能正常。

为减少控件重绘次数,添加了进度变化阈值逻辑(阈值设为0.5f),仅当进度变化量达到阈值时才更新绑定属性并重绘。但出现不一致行为:

  • 通过按钮绑定命令每次增加0.01f进度(小于阈值)时,控件每次点击都会更新;
  • 在页面构造函数中手动设置progressBar.Progress = 0.01f时,控件符合预期不会频繁重绘。

原因分析

核心问题在于数据绑定绕过了CLR属性的setter逻辑:

  • 手动设置Progress属性时,会执行自定义的CLR属性setter,阈值判断逻辑生效,只有变化量达标才会调用SetValue更新BindableProperty,进而触发重绘;
  • 通过MVVM绑定更新时,框架会直接调用SetValue修改BindableProperty的值,跳过CLR属性的setter,导致阈值判断完全未执行。只要ViewModel的Progress属性变更,就会触发OnBindablePropertyChanged回调,直接调用InvalidateSurface重绘控件。

解决方案

以下提供三种可行方案,可根据场景选择:

方案1:将阈值逻辑移到BindableProperty回调中

为ProgressProperty单独设置自定义回调,统一处理手动设置和绑定更新的逻辑:

修改后的ProgressBar.cs关键代码

using SkiaSharp;
using SkiaSharp.Views.Maui;
using SkiaSharp.Views.Maui.Controls;
using System.Diagnostics;

namespace ProgressBar.Maui;

public class ProgressBar : SKCanvasView
{
    public static readonly BindableProperty ProgressProperty =
        BindableProperty.Create(nameof(Progress), typeof(float), typeof(ProgressBar), 0f, 
            propertyChanged: OnProgressPropertyChanged);

    public static readonly BindableProperty ProgressColorProperty =
        BindableProperty.Create(nameof(ProgressColor), typeof(Color), typeof(ProgressBar), Colors.CornflowerBlue, propertyChanged: OnBindablePropertyChanged);

    public static readonly BindableProperty BaseColorProperty =
        BindableProperty.Create(nameof(BaseColor), typeof(Color), typeof(ProgressBar), Colors.LightGray, propertyChanged: OnBindablePropertyChanged);

    public float Progress
    {
        get => (float)GetValue(ProgressProperty);
        set => SetValue(ProgressProperty, value);
    }

    public Color ProgressColor
    {
        get => (Color)GetValue(ProgressColorProperty);
        set => SetValue(ProgressColorProperty, value);
    }

    public Color BaseColor
    {
        get => (Color)GetValue(BaseColorProperty);
        set => SetValue(BaseColorProperty, value);
    }

    private SKCanvas _canvas;
    private SKRect _drawRect;
    private SKImageInfo _info;

    private const float _RedrawThreshold = 0.5f;
    private float _previousProgress = 0f;

    private SKPaint _basePaint = new()
    {
        Style = SKPaintStyle.Fill,
        IsAntialias = true,
    };

    private SKPaint _progressPaint = new()
    {
        Style = SKPaintStyle.Fill,
        IsAntialias = true,
    };

    protected override void OnPaintSurface(SKPaintSurfaceEventArgs e)
    {
        base.OnPaintSurface(e);
        _canvas = e.Surface.Canvas;
        _canvas.Clear();
        _info = e.Info;
        _drawRect = new SKRect(0, 0, _info.Width, _info.Height);
        DrawBase();
        DrawProgress();
    }

    private void DrawBase()
    {
        using SKPath basePath = new();
        basePath.AddRect(_drawRect);
        _basePaint.Color = BaseColor.ToSKColor();
        _canvas.DrawPath(basePath, _basePaint);
    }

    private void DrawProgress()
    {
        using SKPath progressPath = new();
        var progressRect = new SKRect(0, 0, _info.Width * Progress, _info.Height);
        progressPath.AddRect(progressRect);
        _progressPaint.Color = ProgressColor.ToSKColor();
        _canvas.DrawPath(progressPath, _progressPaint);
    }

    // 单独处理Progress属性变化
    private static void OnProgressPropertyChanged(BindableObject bindable, object oldValue, object newValue)
    {
        var progressBar = (ProgressBar)bindable;
        float oldProgress = (float)oldValue;
        float newProgress = (float)newValue;

        if (Math.Abs(newProgress - progressBar._previousProgress) >= _RedrawThreshold)
        {
            progressBar._previousProgress = newProgress;
            progressBar.InvalidateSurface();
            Debug.WriteLine("Progress updated and surface invalidated.");
        }
        else
        {
            // 回滚属性值,保证内部状态与BindableProperty一致
            progressBar.SetValue(ProgressProperty, progressBar._previousProgress);
            Debug.WriteLine("Progress change below threshold. No redraw.");
        }
    }

    private static void OnBindablePropertyChanged(BindableObject bindable, object oldValue, object newValue)
    {
        if (oldValue != newValue)
            ((ProgressBar)bindable).InvalidateSurface();
    }
}

方案2:在ViewModel层控制阈值

将阈值判断逻辑移到ViewModel中,只有当进度变化达标时才触发属性变更通知:

修改后的SampleViewModel.cs

namespace ProgressBarSample.ViewModels;

public partial class SampleViewModel : BaseViewModel
{
    private float _progress = 0.01f;
    private const float _RedrawThreshold = 0.5f;
    private float _previousProgress = 0.01f;

    public float Progress
    {
        get => _progress;
        set
        {
            if (Math.Abs(value - _previousProgress) >= _RedrawThreshold)
            {
                _progress = value;
                _previousProgress = value;
                OnPropertyChanged();
                Debug.WriteLine("ViewModel Progress updated.");
            }
            else
            {
                Debug.WriteLine("ViewModel Progress change below threshold. No notify.");
            }
        }
    }

    [RelayCommand]
    private void IncrementProgress()
    {
        Debug.WriteLine("IncrementProgress()");
        Progress += 0.01f;
    }
}

方案3:重写BindableProperty的CoerceValue回调

利用CoerceValue回调拦截属性值变更,仅允许符合阈值条件的更新:

public static readonly BindableProperty ProgressProperty =
    BindableProperty.Create(nameof(Progress), typeof(float), typeof(ProgressBar), 0f, 
        coerceValue: CoerceProgressValue,
        propertyChanged: OnBindablePropertyChanged);

private static object CoerceProgressValue(BindableObject bindable, object value)
{
    var progressBar = (ProgressBar)bindable;
    float newProgress = (float)value;
    if (Math.Abs(newProgress - progressBar._previousProgress) < _RedrawThreshold)
    {
        return progressBar._previousProgress;
    }
    progressBar._previousProgress = newProgress;
    return value;
}

内容的提问来源于stack exchange,提问作者master_ruko

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.11 19:54:53