wpf 如何从在另一个类中运行的另一个线程更新UI

fae0ux8s  于 2022-11-18  发布在  其他
关注(0)|答案(8)|浏览(390)

我目前正在用C#写我的第一个程序,我对这种语言非常陌生(到目前为止只使用C)。我做了很多研究,但所有的答案都太笼统了,我根本无法让它工作。
所以这里我的(非常常见的)问题:我有一个WPF应用程序,它从用户填写的几个文本框中获取输入,然后用它来做大量的计算。它们应该需要大约2-3分钟,所以我想更新一个进度条和一个文本块,告诉我当前的状态是什么。而且我需要存储用户的UI输入,并将它们提供给线程,所以我有第三个类,我用它来创建一个对象,并想把这个对象传递给后台线程。显然,我会在另一个线程中运行计算,这样UI就不会冻结,但我不知道如何更新UI,因为所有的计算方法都是另一个类的一部分。经过大量的研究,我认为最好的方法是使用调度员和第三方物流,而不是后台工作人员,但老实说,我不确定他们如何工作,经过大约20个小时的试错与其他答案,我决定自己问一个问题。
下面是我的一个结构非常简单的程序:

public partial class MainWindow : Window
{
    public MainWindow()
    {
        Initialize Component();
    }

    private void startCalc(object sender, RoutedEventArgs e)
    {
        inputValues input = new inputValues();

        calcClass calculations = new calcClass();

        try
        {
             input.pota = Convert.ToDouble(aVar.Text);
             input.potb = Convert.ToDouble(bVar.Text);
             input.potc = Convert.ToDouble(cVar.Text);
             input.potd = Convert.ToDouble(dVar.Text);
             input.potf = Convert.ToDouble(fVar.Text);
             input.potA = Convert.ToDouble(AVar.Text);
             input.potB = Convert.ToDouble(BVar.Text);
             input.initStart = Convert.ToDouble(initStart.Text);
             input.initEnd = Convert.ToDouble(initEnd.Text);
             input.inita = Convert.ToDouble(inita.Text);
             input.initb = Convert.ToDouble(initb.Text);
             input.initc = Convert.ToDouble(initb.Text);
         }
         catch
         {
             MessageBox.Show("Some input values are not of the expected Type.", "Wrong Input", MessageBoxButton.OK, MessageBoxImage.Error);
         }
         Thread calcthread = new Thread(new ParameterizedThreadStart(calculations.testMethod);
         calcthread.Start(input);
    }

public class inputValues
{
    public double pota, potb, potc, potd, potf, potA, potB;
    public double initStart, initEnd, inita, initb, initc;
}

public class calcClass
{
    public void testmethod(inputValues input)
    {
        Thread.CurrentThread.Priority = ThreadPriority.Lowest;
        int i;
        //the input object will be used somehow, but that doesn't matter for my problem
        for (i = 0; i < 1000; i++)
        {
            Thread.Sleep(10);
        }
    }
}

如果有人能简单地解释一下如何从testmethod内部更新UI,我将非常感激。由于我是C#和面向对象编程的新手,太复杂的答案我很可能不理解,但我会尽我所能。
此外,如果有人有一个更好的想法一般(也许使用backgroundworker或其他任何东西),我是开放的看到它。

6tqwzwtp

6tqwzwtp1#

首先,您需要使用Dispatcher.Invoke从另一个线程更改UI,要从另一个类更改UI,您可以使用事件。
然后,您可以在主类中注册到该事件,并将更改分派到UI,在计算类中,您可以在希望通知UI时引发该事件:

class MainWindow : Window
{
    private void startCalc()
    {
        //your code
        CalcClass calc = new CalcClass();
        calc.ProgressUpdate += (s, e) => {
            Dispatcher.Invoke((Action)delegate() { /* update UI */ });
        };
        Thread calcthread = new Thread(new ParameterizedThreadStart(calc.testMethod));
        calcthread.Start(input);
    }
}

class CalcClass
{
    public event EventHandler ProgressUpdate;

    public void testMethod(object input)
    {
        //part 1
        if(ProgressUpdate != null)
            ProgressUpdate(this, new YourEventArgs(status));
        //part 2
    }
}

更新日期:

这似乎仍然是一个经常访问的问题和答案,我想更新这个答案,说明我现在会如何做(使用.NET 4.5)-这会稍微长一点,因为我将展示一些不同的可能性:

class MainWindow : Window
{
    Task calcTask = null;

    void buttonStartCalc_Clicked(object sender, EventArgs e) { StartCalc(); } // #1
    async void buttonDoCalc_Clicked(object sender, EventArgs e) // #2
    {
        await CalcAsync(); // #2
    }

    void StartCalc()
    {
        var calc = PrepareCalc();
        calcTask = Task.Run(() => calc.TestMethod(input)); // #3
    }
    Task CalcAsync()
    {
        var calc = PrepareCalc();
        return Task.Run(() => calc.TestMethod(input)); // #4
    }
    CalcClass PrepareCalc()
    {
        //your code
        var calc = new CalcClass();
        calc.ProgressUpdate += (s, e) => Dispatcher.Invoke((Action)delegate()
            {
                // update UI
            });
        return calc;
    }
}

class CalcClass
{
    public event EventHandler<EventArgs<YourStatus>> ProgressUpdate; // #5

    public TestMethod(InputValues input)
    {
        //part 1
        ProgressUpdate.Raise(this, status); // #6 - status is of type YourStatus
        // alternative version to the extension for C# 6+:
        ProgressUpdate?.Invoke(this, new EventArgs<YourStatus>(status));
        //part 2
    }
}

static class EventExtensions
{
    public static void Raise<T>(this EventHandler<EventArgs<T>> theEvent,
                                object sender, T args)
    {
        if (theEvent != null)
            theEvent(sender, new EventArgs<T>(args));
    }
}

@1)如何启动“同步”计算并在后台运行
@2)如何“异步”启动与“等待”:此处,计算在方法返回之前执行并完成,但由于async/await,UI未被阻止(*BTW:此类事件处理程序是async void的唯一有效用法,因为事件处理程序必须返回void-在所有其他情况下使用async Task *)
@3)我们现在使用Task来代替新的Thread。为了以后能够检查它的(成功)完成,我们将它保存在全局calcTask成员中。在后台,这也会启动一个新线程并在那里运行操作,但它更容易处理,并且有一些其他的好处。
@4)这里我们也开始了这个动作,但是这次我们返回了任务,所以“异步事件处理程序”可以“等待它”。我们也可以创建async Task CalcAsync(),然后创建await Task.Run(() => calc.TestMethod(input)).ConfigureAwait(false);(FYI:ConfigureAwait(false)是为了避免死锁,如果您使用async/await,您应该仔细阅读这方面的内容,因为在这里解释太多了),这将导致相同的工作流,但是由于Task.Run是唯一的“可等待操作”,并且是最后一个操作,我们可以简单地返回任务并保存一次上下文切换,这节省了一些执行时间。
@5)这里我现在使用一个“强类型泛型事件”,这样我们就可以轻松地传递和接收“状态对象
@6)这里我使用下面定义的扩展名,它(除了易用性之外)解决旧示例中可能出现的争用情况。在那里,如果事件处理程序在另一个线程中被删除,则可能会在if检查之后、调用之前发生事件获取null的情况。在这里不会发生这种情况,因为扩展获得了事件委托的“副本,”并且在相同的情况下,处理程序仍然在Raise方法内部注册。

sdnqo3pr

sdnqo3pr2#

我要在这里抛给你一个曲线球。如果我说过一次,我已经说过一百次了。像InvokeBeginInvoke这样的封送处理操作并不总是用辅助线程进度更新UI的最佳方法。
在这种情况下,让工作线程将其进度信息发布到一个共享数据结构中,然后UI线程定期轮询该数据结构,通常效果更好。

  • 它打破了Invoke强加的UI和工作线程之间的紧密耦合。
  • UI线程决定了UI控件何时更新......无论如何,当您真正考虑它时,它应该是这样的。
  • 如果从工作线程使用BeginInvoke,则不会出现UI消息队列溢出的风险。
  • 工作线程不必像Invoke那样等待UI线程的响应。
  • 您可以在UI和工作线程上获得更大的吞吐量。
  • InvokeBeginInvoke是开销很大的操作。

因此,在calcClass中创建一个保存进度信息的数据结构。

public class calcClass
{
  private double percentComplete = 0;

  public double PercentComplete
  {
    get 
    { 
      // Do a thread-safe read here.
      return Interlocked.CompareExchange(ref percentComplete, 0, 0);
    }
  }

  public testMethod(object input)
  {
    int count = 1000;
    for (int i = 0; i < count; i++)
    {
      Thread.Sleep(10);
      double newvalue = ((double)i + 1) / (double)count;
      Interlocked.Exchange(ref percentComplete, newvalue);
    }
  }
}

然后在MainWindow类中使用DispatcherTimer来定期轮询进度信息。配置DispatcherTimer以在最适合您的情况的时间间隔引发Tick事件。

public partial class MainWindow : Window
{
  public void YourDispatcherTimer_Tick(object sender, EventArgs args)
  {
    YourProgressBar.Value = calculation.PercentComplete;
  }
}
lrl1mhuk

lrl1mhuk3#

您认为应该使用Dispatcher更新UI线程上的控件是正确的,长时间运行的进程不应该在UI线程上运行也是正确的。即使您在UI线程上异步运行长时间运行的进程,它仍然会导致性能问题。
需要注意的是,Dispatcher.CurrentDispatcher将返回当前线程的调度程序,而不一定是UI线程。我认为您可以使用Application.Current.Dispatcher来获取对UI线程调度程序的引用(如果可用),但如果不可用,则必须将UI调度程序传递到后台线程。
通常我使用Task Parallel Library来进行线程操作,而不是BackgroundWorker。我只是觉得它更容易使用。
例如,

Task.Factory.StartNew(() => 
    SomeObject.RunLongProcess(someDataObject));

其中

void RunLongProcess(SomeViewModel someDataObject)
{
    for (int i = 0; i <= 1000; i++)
    {
        Thread.Sleep(10);

        // Update every 10 executions
        if (i % 10 == 0)
        {
            // Send message to UI thread
            Application.Current.Dispatcher.BeginInvoke(
                DispatcherPriority.Normal,
                (Action)(() => someDataObject.ProgressValue = (i / 1000)));
        }
    }
}
polkgigr

polkgigr4#

与UI交互的所有内容都必须在UI线程中调用(除非它是一个冻结对象)。为此,可以使用调度程序。

var disp = /* Get the UI dispatcher, each WPF object has a dispatcher which you can query*/
disp.BeginInvoke(DispatcherPriority.Normal,
        (Action)(() => /*Do your UI Stuff here*/));

我在这里使用了BeginInvoke,通常后台工作者不需要等待UI更新。如果你想等待,你可以使用Invoke。但是你应该注意不要频繁地调用BeginInvoke,这会让你非常讨厌。
顺便说一下,BackgroundWorker类可以帮助完成这类任务。它允许报告更改,比如百分比,并自动将其从后台线程调度到ui线程。对于大多数线程〈〉更新ui任务,BackgroundWorker是一个很好的工具。

m3eecexj

m3eecexj5#

如果这是一个很长的计算,那么我会去后台工作。它有进度支持。它也有取消支持。

http://msdn.microsoft.com/en-us/library/cc221403(v=VS.95).aspx

这里我有一个绑定到内容的TextBox。

private void backgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
        Debug.Write("backgroundWorker_RunWorkerCompleted");
        if (e.Cancelled)
        {
            contents = "Cancelled get contents.";
            NotifyPropertyChanged("Contents");
        }
        else if (e.Error != null)
        {
            contents = "An Error Occured in get contents";
            NotifyPropertyChanged("Contents");
        }
        else
        {
            contents = (string)e.Result;
            if (contentTabSelectd) NotifyPropertyChanged("Contents");
        }
    }
xxb16uws

xxb16uws6#

你必须回到你的主线程(也叫UI thread)来更新你的UI。任何其他试图更新你的UI的线程都会导致exceptions被抛出。
因此,因为您在WPF中,所以可以使用Dispatcher,更具体地说,可以在dispatcher上使用beginInvoke。这将允许您在UI线程中执行需要完成的操作(通常是更新UI)。
您可能还希望通过维护对控件/窗体的引用,在您的business中“注册”UI,以便可以使用它的dispatcher

ee7vknir

ee7vknir7#

感谢上帝,微软在WPF中解决了这个问题:)
每个Control,比如进度条、按钮、窗体等等,上面都有一个Dispatcher。你可以给予Dispatcher一个需要执行的Action,它会在正确的线程上自动调用它(Action就像一个函数委托)。
您可以找到here示例。
当然,您必须让控件可以从其他类访问,例如,将其设置为public,并将对Window的引用传递给其他类,或者只将引用传递给进度条。

o75abkj4

o75abkj48#

我觉得有必要添加这个更好的答案,因为除了BackgroundWorker之外,似乎没有什么能帮到我,而且到目前为止,处理这个问题的答案非常不完整。下面是更新名为MainWindow的XAML页面的方法,该页面具有如下所示的Image标记:

<Image Name="imgNtwkInd" Source="Images/network_on.jpg" Width="50" />

使用BackgroundWorker进程显示您是否已连接到网络:

using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;

public partial class MainWindow : Window
{
    private BackgroundWorker bw = new BackgroundWorker();

    public MainWindow()
    {
        InitializeComponent();

        // Set up background worker to allow progress reporting and cancellation
        bw.WorkerReportsProgress = true;
        bw.WorkerSupportsCancellation = true;

        // This is your main work process that records progress
        bw.DoWork += new DoWorkEventHandler(SomeClass.DoWork);

        // This will update your page based on that progress
        bw.ProgressChanged += new ProgressChangedEventHandler(bw_ProgressChanged);

        // This starts your background worker and "DoWork()"
        bw.RunWorkerAsync();

        // When this page closes, this will run and cancel your background worker
        this.Closing += new CancelEventHandler(Page_Unload);
    }

    private void bw_ProgressChanged(object sender, ProgressChangedEventArgs e)
    {
        BitmapImage bImg = new BitmapImage();
        bool connected = false;
        string response = e.ProgressPercentage.ToString(); // will either be 1 or 0 for true/false -- this is the result recorded in DoWork()

        if (response == "1")
            connected = true;

        // Do something with the result we got
        if (!connected)
        {
            bImg.BeginInit();
            bImg.UriSource = new Uri("Images/network_off.jpg", UriKind.Relative);
            bImg.EndInit();
            imgNtwkInd.Source = bImg;
        }
        else
        {
            bImg.BeginInit();
            bImg.UriSource = new Uri("Images/network_on.jpg", UriKind.Relative);
            bImg.EndInit();
            imgNtwkInd.Source = bImg;
        }
    }

    private void Page_Unload(object sender, CancelEventArgs e)
    {
        bw.CancelAsync();  // stops the background worker when unloading the page
    }
}

public class SomeClass
{
    public static bool connected = false;

    public void DoWork(object sender, DoWorkEventArgs e)
    {
        BackgroundWorker bw = sender as BackgroundWorker;

        int i = 0;
        do 
        {
            connected = CheckConn();  // do some task and get the result

            if (bw.CancellationPending == true)
            {
                e.Cancel = true;
                break;
            }
            else
            {
                Thread.Sleep(1000);
                // Record your result here
                if (connected)
                    bw.ReportProgress(1);
                else
                    bw.ReportProgress(0);
            }
        }
        while (i == 0);
    }

    private static bool CheckConn()
    {
        bool conn = false;
        Ping png = new Ping();
        string host = "SomeComputerNameHere";

        try
        {
            PingReply pngReply = png.Send(host);
            if (pngReply.Status == IPStatus.Success)
                conn = true;
        }
        catch (PingException ex)
        {
            // write exception to log
        }
        return conn;
    }
}

如欲了解更多信息,请访问:https://msdn.microsoft.com/en-us/library/cc221403(v=VS.95).aspx

相关问题