Как обновить пользовательский интерфейс из другого потока, работающего в другом классе

В настоящее время я пишу свою первую программу на C#, и я очень плохо знаком с языком (до сих пор работал только с C). Я провел много исследований, но все ответы были слишком общими, и я просто не мог заставить его работать.

Итак, вот моя (очень распространенная) проблема: у меня есть приложение WPF, которое принимает входные данные из нескольких текстовых полей, заполненных пользователем, а затем использует их для выполнения с ними большого количества вычислений. Они должны занять около 2-3 минут, поэтому я хотел бы обновить индикатор выполнения и текстовый блок, сообщающий мне, каков текущий статус. Также мне нужно сохранить входные данные пользовательского интерфейса и передать их потоку, поэтому у меня есть третий класс, который я использую для создания объекта и хотел бы передать этот объект в фоновый поток. Очевидно, что я выполняю вычисления в другом потоке, чтобы пользовательский интерфейс не зависал, но я не знаю, как обновить пользовательский интерфейс, поскольку все методы вычисления являются частью другого класса. После многих исследований я думаю, что лучшим способом будет использовать диспетчеров и TPL, а не фонового работника, но, честно говоря, я не уверен, как они работают, и после примерно 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);
        }
    }
}

Я был бы очень признателен, если бы у кого-то было простое объяснение, как обновить пользовательский интерфейс внутри метода тестирования. Поскольку я новичок в C# и объектно-ориентированном программировании, слишком сложные ответы я, скорее всего, не пойму, хотя я сделаю все возможное.

Также, если у кого-то есть лучшая идея в целом (может быть, с помощью backgroundworker или что-то еще), я открыт, чтобы увидеть это.

8 ответов

Решение

Сначала вам нужно использовать Dispatcher.Invoke чтобы изменить пользовательский интерфейс из другого потока и сделать это из другого класса, вы можете использовать события.
Затем вы можете зарегистрироваться на это событие (события) в основном классе и отправить изменения в пользовательский интерфейс, а в классе вычислений вы выбрасываете событие, когда хотите уведомить пользовательский интерфейс:

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
        //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 пользовательский интерфейс не заблокирован (кстати: такие обработчики событий являются единственными допустимыми async void так как обработчик события должен вернуть void - использовать async Task во всех остальных случаях)

@ 3) вместо нового Thread теперь мы используем Task, Чтобы потом иметь возможность проверить его (успешное) завершение, мы сохраняем его в глобальном calcTask член. В фоновом режиме это также запускает новый поток и выполняет действие там, но это намного проще в обработке и имеет некоторые другие преимущества.

@ 4) Здесь мы также запускаем действие, но на этот раз мы возвращаем задачу, поэтому "обработчик асинхронных событий" может "ожидать его". Мы могли бы также создать async Task CalcAsync() а потом await Task.Run(() => calc.TestMethod(input)).ConfigureAwait(false); (К вашему сведению: ConfigureAwait(false) чтобы избежать тупиков, вы должны прочитать об этом, если вы используете async / await как было бы много, чтобы объяснить здесь), что приведет к тому же рабочему процессу, но как Task.Run является единственной "ожидаемой операцией" и является последней, которую мы можем просто вернуть в задачу и сохранить один переключатель контекста, что экономит некоторое время выполнения.

@ 5) Здесь я теперь использую "строго типизированное родовое событие", чтобы мы могли легко передавать и получать наш "объект статуса"

@6) Здесь я использую расширение, определенное ниже, которое (помимо простоты использования) решает возможные условия гонки в старом примере. Там могло случиться, что событие получило null после if -check, но перед вызовом, если обработчик событий был удален в другом потоке именно в этот момент. Это не может произойти здесь, так как расширения получают "копию" делегата события, и в той же ситуации обработчик все еще зарегистрирован внутри Raise метод.

Я собираюсь бросить вам кривой мяч здесь. Если бы я сказал это однажды, я бы сказал это сто раз. Маршалинг операции, такие как Invoke или же BeginInvoke не всегда лучшие методы для обновления пользовательского интерфейса с прогрессом рабочего потока.

В этом случае обычно лучше, если рабочий поток публикует свою информацию о прогрессе в общей структуре данных, которую поток пользовательского интерфейса затем опрашивает через регулярные промежутки времени. Это имеет несколько преимуществ.

  • Это нарушает тесную связь между пользовательским интерфейсом и рабочим потоком, что Invoke навязывает.
  • Поток пользовательского интерфейса должен диктовать, когда элементы управления пользовательского интерфейса обновляются... так, как это должно быть в любом случае, когда вы действительно думаете об этом.
  • Нет риска переполнения очереди сообщений пользовательского интерфейса, как в случае, если BeginInvoke были использованы из рабочего потока.
  • Рабочий поток не должен ждать ответа от потока пользовательского интерфейса, как в случае с Invoke,
  • Вы получаете большую пропускную способность как в пользовательском интерфейсе, так и в рабочих потоках.
  • Invoke а также BeginInvoke дорогие операции.

Так в вашем 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;
  }
}

Вы правы, что вы должны использовать Dispatcher обновить элементы управления в потоке пользовательского интерфейса, а также правильно, что долго выполняющиеся процессы не должны выполняться в потоке пользовательского интерфейса. Даже если вы выполняете длительный процесс асинхронно в потоке пользовательского интерфейса, это может вызвать проблемы с производительностью.

Следует отметить, что Dispatcher.CurrentDispatcher вернет диспетчер для текущего потока, не обязательно для потока пользовательского интерфейса. Я думаю, что вы можете использовать Application.Current.Dispatcher чтобы получить ссылку на диспетчер потока пользовательского интерфейса, если он вам доступен, но если нет, вам придется передать диспетчер пользовательского интерфейса в фоновый поток.

Обычно я использую параллельную библиотеку задач для потоковых операций вместо 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)));
        }
    }
}

Все, что взаимодействует с пользовательским интерфейсом, должно вызываться в потоке пользовательского интерфейса (если это не замороженный объект). Для этого вы можете воспользоваться диспетчером.

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 здесь, обычно фоновщик не должен ждать обновления пользовательского интерфейса. Если вы хотите подождать, вы можете использовать Invoke, Но вы должны быть осторожны, чтобы не вызывать BeginInvoke слишком часто, это может стать очень неприятным.

Кстати, класс BackgroundWorker помогает с подобными задачами. Он позволяет сообщать об изменениях, например, в процентах, и автоматически отправляет их из фонового потока в поток пользовательского интерфейса. Для большинства задач по обновлению пользовательского интерфейса BackgroundWorker - отличный инструмент.

Вам придется вернуться к своей основной теме (также называемой UI thread) чтобы update пользовательский интерфейс. Любой другой поток, пытающийся обновить ваш интерфейс, просто вызовет exceptions быть брошенным повсюду.

Так как вы находитесь в WPF, вы можете использовать Dispatcher а конкретнее beginInvoke на этом dispatcher, Это позволит вам выполнить то, что нужно сделать (как правило, обновить пользовательский интерфейс) в потоке пользовательского интерфейса.

Вы также можете "зарегистрировать" UI в вашем business, поддерживая ссылку на элемент управления / форму, чтобы вы могли использовать его dispatcher,

Если это долгий расчет, то я бы пошел фоновый рабочий. Имеет поддержку прогресса. Также есть поддержка отмены.

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");
        }
    }

Слава Богу, Microsoft получила это в WPF:)

каждый Control, как индикатор выполнения, кнопка, форма и т. д. имеет Dispatcher в теме. Вы можете дать Dispatcher Action это должно быть выполнено, и он автоматически вызовет его в правильном потоке (Action это как делегат функции).

Вы можете найти пример здесь.

Конечно, вы должны иметь доступ к элементу управления из других классов, например, сделав его public и передать ссылку на Window к вашему другому классу, или, возможно, передавая ссылку только на индикатор выполнения.

Почувствовал необходимость добавить этот лучший ответ, так как ничего кроме BackgroundWorker казалось, помог мне, и ответ, касающийся этого до сих пор, был ужасно неполным. Вот так вы бы обновили страницу XAML под названием MainWindow который имеет тег изображения, как это:

<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

Другие вопросы по тегам