.NET MAUI Android:实现Label动态更新显示下一次振动时间
.NET MAUI振动应用:动态显示下一次振动时间解决方案
问题描述
我正在开发一个基于.NET MAUI的振动控制应用,可根据用户输入的模式运行振动。目前需要在界面的Label中动态显示下一次振动的触发时间,经过两天调研未找到相关解决方案。
已创建VibExecutor模型类,其Start方法可运行选定的振动模式,并通过DateTime类型的公共字段NextRun记录下一次触发时间。
现有代码
VibExecutor类
public partial class VibExecutor : IDisposable { private static System.Timers.Timer Timer { get; set; } public VibProg CurrentVibProg { get; set; } public DateTime? NextRun { get; set; } public void Dispose() { Timer?.Dispose(); } public void Start(VibProg vibProg) { if (vibProg == null) throw new ArgumentException("vibProg can not be a null"); CurrentVibProg = vibProg; vibProg.Init(); NextRun = vibProg.NextRun(); if (NextRun.HasValue) { Timer = new() { AutoReset = false }; Timer.Elapsed += TimerCallback; InitTimerForNextRun(NextRun); } } private void InitTimerForNextRun(DateTime? when) { Timer.Stop(); if (when.HasValue) { Timer.Interval = Math.Max(1, (when - DateTime.Now).Value.TotalMilliseconds); Timer.Start(); } } private void TimerCallback(object source, ElapsedEventArgs e) { CurrentVibProg.MakeNoise(); InitTimerForNextRun(CurrentVibProg. NextRun()); } public void Stop() { InitTimerForNextRun(null); } public bool IsRunning { get { return Timer.Enabled; } } }
MainPage类
public partial class MainPage : ContentPage { public IList<VibProg> VibProgList { get; set; } public VibProg SelectedProgram { get; set; } public static VibExecutor VibExecutor { get; set; } public MainPage() { InitializeComponent(); BindPicker(); periodEntry.TextChanged += ParameterChanged; durationEntry.TextChanged += ParameterChanged; programPicker.SelectedIndexChanged += ProgramPickerChangedValue; startButton.Clicked += Start; cancelButton.Clicked += Cancel; } public void Start(object sender, EventArgs e) { HideOptions(sender, e); VibExecutor = new(); VibExecutor.Start(SelectedProgram); LockStartButton(sender, e); UnlockCancelButton(sender, e); } public void Cancel(object sender, EventArgs e) { VibExecutor.Stop(); VibExecutor.Dispose(); UnlockStartButton(sender, e); ShowOptions(sender, e); LockCancelButton(sender, e); } #region Picker functionalities public void BindPicker() { VibProgList = VibProgRegister.All; // Set the binding context to the current page BindingContext = this; // Set the binding properties for the Picker programPicker.SetBinding(Picker.ItemsSourceProperty, new Binding("VibProgList")); programPicker.ItemDisplayBinding = new Binding("Name"); } public void ProgramPickerChangedValue(object sender, EventArgs e) { SelectedProgram = (VibProg)programPicker.SelectedItem; ShowOptions(sender, e); int periodS = SelectedProgram.PeriodMs / 1000; int durationS = SelectedProgram.DurationMs / 1000; periodLabel.Text = SelectedProgram.PeriodDescription; periodEntry.Text = periodS.ToString(); durationLabel.Text = SelectedProgram.DurationDescription; durationEntry.Text = durationS.ToString(); } #endregion #region Parameters handling public void ParameterChanged(object sender, TextChangedEventArgs e) { var period = periodEntry.Text; var duration = durationEntry.Text; _ = int.TryParse(period, out int _period); _ = int.TryParse(duration, out int _duration); bool periodRes = ValidateParameter(_period); bool durationRes = ValidateParameter(_duration); if (periodRes && durationRes) { SelectedProgram.PeriodMs = _period * 1000; SelectedProgram.DurationMs = _duration * 1000; UnlockStartButton(sender, e); } else { LockStartButton(sender, e); } } private bool ValidateParameter(int param) { int min = 1; int max = 20; if (param > max || param < min) return false; return true; } #endregion #region Locking and unlocking buttons functions private void UnlockCancelButton(object sender, EventArgs e) { cancelButton.IsEnabled = true; cancelButton.BackgroundColor = Color.Parse("Red"); } private void LockStartButton(object sender, EventArgs e) { startButton.IsEnabled = false; startButton.BackgroundColor = Color.Parse("DarkGrey"); } private void UnlockStartButton(object sender, EventArgs e) { startButton.IsEnabled = true; startButton.BackgroundColor = Color.Parse("Green"); } private void LockCancelButton(object sender, EventArgs e) { cancelButton.IsEnabled = false; cancelButton.BackgroundColor = Color.Parse("DarkGrey"); } #endregion #region Showing and hiding programs options private void ShowOptions(object sender, EventArgs e) { options.IsVisible = true; } private void HideOptions(object sender, EventArgs e) { options.IsVisible = false; } #endregion }
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" x:Class="FoxNet.GardenDefender.MainPage"> <ScrollView> <VerticalStackLayout Spacing="25" Padding="30,0" VerticalOptions="Center"> <Label Text="Select program:" /> <Picker x:Name="programPicker" Title="Program type"/> <StackLayout x:Name="options" IsVisible="False"> <Label x:Name="periodLabel" /> <Entry x:Name="periodEntry" MaxLength="2" Keyboard="Numeric" /> <Label x:Name="durationLabel" /> <Entry x:Name="durationEntry" MaxLength="2" Keyboard="Numeric" /> </StackLayout> <Label x:Name="errorLabel" Text="Enter values from given ranges!" TextColor="Red" FontAttributes="Bold" IsVisible="False"/> <Button x:Name="startButton" Text="Start" IsEnabled="False" Clicked="Start" BackgroundColor="DarkGray" /> <Button x:Name="cancelButton" IsEnabled="False" Text="Cancel" Clicked="Cancel" BackgroundColor="DarkGray" /> </VerticalStackLayout> </ScrollView> </ContentPage>
解决方案
要实现NextRun变化时实时更新UI,核心是让VibExecutor支持属性变更通知,同时处理线程切换(因为定时器回调在非UI线程)。
步骤1:修改VibExecutor实现INotifyPropertyChanged
将NextRun改为属性并触发变更通知,确保通知在UI线程执行:
public partial class VibExecutor : IDisposable, INotifyPropertyChanged { private static System.Timers.Timer Timer { get; set; } public VibProg CurrentVibProg { get; set; } private DateTime? _nextRun; public DateTime? NextRun { get => _nextRun; set { if (_nextRun != value) { _nextRun = value; OnPropertyChanged(nameof(NextRun)); } } } public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged(string propertyName) { // 强制在UI线程触发变更通知,避免跨线程更新UI报错 MainThread.BeginInvokeOnMainThread(() => { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); }); } // 保留原有方法,仅修改TimerCallback中的NextRun赋值 private void TimerCallback(object source, ElapsedEventArgs e) { CurrentVibProg.MakeNoise(); // 直接赋值属性,触发变更通知 NextRun = CurrentVibProg.NextRun(); InitTimerForNextRun(NextRun); } // 其他原有代码不变... }
步骤2:在XAML中添加显示Label
在界面中加入用于显示下一次振动时间的Label,例如放在errorLabel和startButton之间:
<Label x:Name="errorLabel" Text="请输入指定范围内的值!" TextColor="Red" FontAttributes="Bold" IsVisible="False"/> <Label x:Name="nextRunLabel" Text="下一次振动时间:"/> <Button x:Name="startButton" Text="启动" IsEnabled="False" Clicked="Start" BackgroundColor="DarkGray" />
步骤3:绑定属性或订阅变更事件
方式1:使用数据绑定(推荐)
在MainPage的Start方法中,将nextRunLabel绑定到VibExecutor.NextRun:
public void Start(object sender, EventArgs e) { HideOptions(sender, e); VibExecutor = new(); VibExecutor.Start(SelectedProgram); // 绑定并格式化时间显示 nextRunLabel.SetBinding(Label.TextProperty, new Binding(nameof(VibExecutor.NextRun), source: VibExecutor, stringFormat: "下一次振动时间:{0:yyyy-MM-dd HH:mm:ss}", fallbackValue: "无下一次振动计划")); LockStartButton(sender, e); UnlockCancelButton(sender, e); }
方式2:手动订阅变更事件
如果不想使用数据绑定,可手动订阅PropertyChanged事件并更新Label:
public void Start(object sender, EventArgs e) { HideOptions(sender, e); VibExecutor = new(); VibExecutor.PropertyChanged += VibExecutor_PropertyChanged; VibExecutor.Start(SelectedProgram); LockStartButton(sender, e); UnlockCancelButton(sender, e); } private void VibExecutor_PropertyChanged(object sender, PropertyChangedEventArgs e) { if (e.PropertyName == nameof(VibExecutor.NextRun)) { if (VibExecutor.NextRun.HasValue) { nextRunLabel.Text = $"下一次振动时间:{VibExecutor.NextRun.Value:yyyy-MM-dd HH:mm:ss}"; } else { nextRunLabel.Text = "无下一次振动计划"; } } } // 在Cancel方法中取消订阅,避免内存泄漏 public void Cancel(object sender, EventArgs e) { VibExecutor.PropertyChanged -= VibExecutor_PropertyChanged; VibExecutor.Stop(); VibExecutor.Dispose(); UnlockStartButton(sender, e); ShowOptions(sender, e); LockCancelButton(sender, e); nextRunLabel.Text = ""; }
关键注意点
- 定时器回调在非UI线程执行,必须通过
MainThread.BeginInvokeOnMainThread确保UI更新在UI线程完成,否则会抛出跨线程异常。 - 使用数据绑定更符合.NET MAUI的MVVM设计模式,也能减少手动事件订阅的代码量。
- 若使用事件订阅,记得在取消振动时移除事件绑定,避免内存泄漏。
内容的提问来源于stack exchange,提问作者leesek
相关产品推荐
相关产品推荐

