.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
相关产品推荐
相关产品推荐

