Подтвердить что ты не робот

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

В настоящее время я пишу свою первую программу на С#, и я чрезвычайно новичок в языке (используется только для работы с 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);
        }
    }
}

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

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

4b9b3361

Ответ 1

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

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

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

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

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

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

Ответ 2

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

Ответ 3

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

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 помогает с такими типами. Он позволяет регистрировать изменения, как процент, и отправляет их автоматически из потока Background в поток ui. Для большинства задач thread < > update ui BackgroundWorker - отличный инструмент.

Ответ 4

Вы правы, что вы должны использовать 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)));
        }
    }
}

Ответ 5

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

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

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

Ответ 6

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

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

Ответ 7

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

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

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

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

Ответ 8

Почувствовал необходимость добавить этот лучший ответ, так как ничего, кроме 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