using System.ComponentModel; using Microsoft.AspNetCore.Components; using OpenCvSharp; using VisionBuilder.UI.Blazor.Helpers; using VisionBuilder.UI.Common.ViewModel; namespace VisionBuilder.UI.Blazor.Components; public partial class PreviewWindow : ComponentBase, IDisposable { [Parameter] public PreviewVM? ViewModel { get; set; } private string _dataUri = string.Empty; private bool _isError; private DateTime _lastRenderTime = DateTime.MinValue; private Timer? _pendingTimer; private PreviewVM? _subscribedVm; private bool _disposed; protected override void OnParametersSet() { if (_subscribedVm != ViewModel) { Unsubscribe(); Subscribe(); } } private void Subscribe() { _subscribedVm = ViewModel; if (ViewModel != null) ViewModel.PropertyChanged += OnPropertyChanged; } private void Unsubscribe() { if (_subscribedVm != null) _subscribedVm.PropertyChanged -= OnPropertyChanged; _subscribedVm = null; } private async void OnPropertyChanged(object? sender, PropertyChangedEventArgs e) { if (e.PropertyName != nameof(PreviewVM.ImagePreview) && e.PropertyName != nameof(PreviewVM.IsError)) return; var now = DateTime.UtcNow; var elapsed = (now - _lastRenderTime).TotalMilliseconds; if (elapsed < 100) { // Schedule a render for when the throttle window expires, // so we always show the latest frame. _pendingTimer?.Dispose(); _pendingTimer = new Timer(_ => RenderLatest(), null, (int)(100 - elapsed) + 1, Timeout.Infinite); return; } await RenderCurrentFrame(); } private async void RenderLatest() { _pendingTimer?.Dispose(); _pendingTimer = null; await RenderCurrentFrame(); } private async Task RenderCurrentFrame() { if (_disposed) return; _lastRenderTime = DateTime.UtcNow; var mat = ViewModel?.ImagePreview; var isError = ViewModel?.IsError ?? false; string dataUri = string.Empty; if (mat != null && !mat.Empty()) { dataUri = mat.ToBase64DataUri(); } _dataUri = dataUri; _isError = isError; await InvokeAsync(StateHasChanged); } public void Dispose() { _disposed = true; _pendingTimer?.Dispose(); Unsubscribe(); } }