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

Я сейчас пишу свою первую программу на 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 # и объектно-ориентированном программировании, слишком сложные ответы я, скорее всего, не пойму, хотя я приложу все усилия.

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

Ответы [ 8 ]

55 голосов
/ 07 марта 2012

Сначала вам нужно использовать 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
    }
}

UPDATE:
Поскольку это все еще часто посещаемый вопрос и ответ, я хочу обновить этот ответ, указав, как бы я это делал сейчас (с .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, но перед вызовом, если бы обработчик события был удален в другом потоке именно в этот момент. Этого не может быть, поскольку расширения получают «копию» делегата события, и в той же ситуации обработчик все еще регистрируется в методе Raise.

28 голосов
/ 08 марта 2012

Я собираюсь бросить вам кривой мяч здесь.Если бы я сказал это однажды, я бы сказал это сто раз.Операции маршалинга, такие как 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;
  }
}
8 голосов
/ 07 марта 2012

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

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

Обычно я использую 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)));
        }
    }
}
4 голосов
/ 07 марта 2012

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

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 - отличный инструмент.

1 голос
/ 07 марта 2012

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

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");
        }
    }
1 голос
/ 07 марта 2012

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

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

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

0 голосов
/ 18 августа 2016

Чувствовал необходимость добавить этот лучший ответ, так как ничто, кроме BackgroundWorker, казалось, не помогло мне, и ответ, касающийся этого вопроса, до сих пор был ужасно неполным. Вот как вы должны обновить страницу XAML с именем MainWindow, которая имеет тег 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

0 голосов
/ 07 марта 2012

Слава Богу, Microsoft получила , которую вычислило в WPF:)

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

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...