使用计时器和线程更新 Winforms 标签,股票应用程序

Updating Winforms Label with Timer and Thread, stock app

以前可能有人问过它的要点,但我完全迷路了,所以我正在寻找一些个人指导。一直在尝试使用 WinForms 和 Yahoo API 制作一个有趣的股票跟踪器应用程序。尝试获取它,以便您可以输入一个跟踪器符号,它将创建一个新的标签,该标签将不时地自我更新。但是,它一直给我关于“跨线程操作无效”的错误消息。我试过用谷歌搜索,但是,是的,完全迷路了。以上就是大部分代码,希望大家看得懂。

using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using YahooFinanceApi;

namespace stockpoging4
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            System.Globalization.CultureInfo.DefaultThreadCurrentUICulture = System.Globalization.CultureInfo.GetCultureInfo("en-US");
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            using (Prompt prompt = new Prompt("Enter the ticker symbol", "Add ticker"))
            {
                string result = prompt.Result;
                result = result.ToUpper();
                if (!string.IsNullOrEmpty(result))
                {
                    do_Things(result);
                }
            }
        }
        public async Task<string> getStockPrices(string symbol)
        {
            try
            {
                var securities = await Yahoo.Symbols(symbol).Fields(Field.RegularMarketPrice).QueryAsync();
                var aapl = securities[symbol];
                var price = aapl[Field.RegularMarketPrice];
                return symbol + " $" + price;

            }
            catch
            {
                return "404";
            }
        }


        public async void do_Things(string result)
        {
            string price;
            Label label = null;

            if (label == null)
            {
                price = await getStockPrices(result);
                label = new Label() { Name = result, Text = result + " $" + price };
                flowLayoutPanel2.Controls.Add(label);
            }
            else
            {
                Thread testThread = new Thread(async delegate ()
                {
                    uiLockingTask();
                    price = await getStockPrices(result);
                    label.Text = result + " $" + price;
                    label.Update();
                });
            }
            System.Timers.Timer timer = new System.Timers.Timer(10000);
            timer.Start();
            timer.Elapsed += do_Things(results);
        }

        private void uiLockingTask() {
            Thread.Sleep(5000);
        }
    }
}

最近使用异步更简单。

这是一个 class,它会在每个时间间隔触发一个 Action

public class UITimer : IDisposable
{
    private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();

    // use a private function which returns a task
    private async Task Innerloop(TimeSpan interval, Action<UITimer> action)
    {
        try
        {
            while (!_cancellationTokenSource.IsCancellationRequested)
            {
                await Task.Delay(interval, _cancellationTokenSource.Token);
                action(this);
            }
        }
        catch (OperationCanceledException) { }
    }

    // the constructor calls the private StartTimer, (the first part will run synchroniously, until the away delay) 
    public UITimer(TimeSpan interval, Action<UITimer> action) =>
        _ = Innerloop(interval, action);

    // make sure the while loop will stop.
    public void Dispose() =>
        _cancellationTokenSource?.Cancel();
}

如果您使用 dotnet 3.0 或更高版本,则可以使用 IAsyncDisposable。有了这个你就可以等待 DisposeAsync 方法,所以你可以等待 _timerTask 完成。

然后我创建了一个新表单,并将此作为代码隐藏:

public partial class Form1 : Form
{
    private readonly UITimer _uiTimer;
    private int _counter;

    public Form1()
    {
        InitializeComponent();

        // setup the time and pass the callback action
        _uiTimer = new UITimer(TimeSpan.FromSeconds(1), Update);
    }

    // the orgin timer is passed as parameter.
    private void Update(UITimer timer)
    {
        // do your thing on the UI thread.
        _counter++;
        label1.Text= _counter.ToString();
    }

    private void Form1_FormClosed(object sender, FormClosedEventArgs e)
    {
        // make sure the time (whileloop) is stopped.
        _uiTimer.Dispose();
    }
}

优点是,回调在 UI 线程上运行但不会阻塞它。 await Task.Delay(..) 在后台使用计时器,但在 UI 线程 上发布 method/statemachine 的其余部分(因为 UI 线程有一个 SynchronizaionContext )

简单但有效 ;-)

让我指出您实施中的几件事。

  1. 您在 timer.Start 之后订阅了 timer.Elapsed,在短时间间隔的情况下可能无效
  2. 事件处理程序在后台调用,这就是您不断收到“跨线程操作无效”的原因。 UI 组件应该从后台线程正确调度,例如,通过调用 flowLayoutPanel2.BeginInvoke(new Action(() => flowLayoutPanel2.Controls.Add(label)));和 label.BeginInvoke(新操作(label.Update))。此更改已经可以修复您的异常。
  3. 尽管我会以不同的方式实现此功能,但在这里我 post 稍微更改了代码,通过一些调整就可以完全满足您的需求。
    public partial class Form1 : Form 
    {
        Task _runningTask;
        CancellationTokenSource _cancellationToken;
    
        public Form1()
        {
            System.Globalization.CultureInfo.DefaultThreadCurrentUICulture = System.Globalization.CultureInfo.GetCultureInfo("en-US");
            InitializeComponent();
        }
    
        private void button1_Click(object sender, EventArgs e)
        {
            using (Prompt prompt = new Prompt("Enter the ticker symbol", "Add ticker"))
            {
                string result = prompt.Result;
                result = result.ToUpper();
                if (!string.IsNullOrEmpty(result))
                {
                    do_Things(result);
                    _cancellationToken = new CancellationTokenSource();
                    _runningTask = StartTimer(() => do_Things(result), _cancellationToken);
                }
            }
        }
    
        private void onCancelClick()
        {
            _cancellationToken.Cancel();
        }
    
    
        public async Task<string> getStockPrices(string symbol)
        {
            try
            {
                var securities = await Yahoo.Symbols(symbol).Fields(Field.RegularMarketPrice).QueryAsync();
                var aapl = securities[symbol];
                var price = aapl[Field.RegularMarketPrice];
                return symbol + " $" + price;
    
            }
            catch
            {
                return "404";
            }
        }
    
        private async Task StartTimer(Action action, CancellationTokenSource cancellationTokenSource)
        {
            try
            {
                while (!cancellationTokenSource.IsCancellationRequested)
                {
                    await Task.Delay(1000, cancellationTokenSource.Token);
                    action();
                }
            }
            catch (OperationCanceledException) { }
        }
    
    
        public async void do_Things(string result)
        {
            var price = await getStockPrices(result);
            var label = new Label() { Name = result, Text = result + " $" + price };
            flowLayoutPanel2.BeginInvoke(new Action(() => flowLayoutPanel2.Controls.Add(label)));
        }
    }