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

.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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.14 02:23:09