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

1393

Какой самый простой способ обновить Labelиз другого Thread?

  • У меня Formработает thread1, и с этого я запускаю другой поток ( thread2).

  • В то время thread2как обрабатывает некоторые файлы, я хотел бы обновить Labelна Formтекущий статус thread2работы.

Как я мог это сделать?

CruelIO
источник
25
Разве .net 2.0+ не имеет класса BackgroundWorker только для этого. Это пользовательский интерфейс потока в курсе. 1. Создайте BackgroundWorker 2. Добавьте двух делегатов (один для обработки и один для завершения)
Preet Sangha
13
возможно, немного поздно: codeproject.com/KB/cs/Threadsafe_formupdating.aspx
MichaelD
4
См. Ответ для .NET 4.5 и C # 5.0: stackoverflow.com/a/18033198/2042090
Рышард Деган
5
Этот вопрос не относится к Gtk # GUI. Для Gtk # см. Этот и этот ответ.
hlovdal
Осторожно: ответы на этот вопрос теперь являются беспорядочным беспорядком OT («вот что я сделал для своего приложения WPF») и исторических артефактов .NET 2.0.
Марк Л.

Ответы:

768

Для .NET 2.0, вот хороший фрагмент кода, который я написал, который делает именно то, что вы хотите, и работает для любого свойства в Control:

private delegate void SetControlPropertyThreadSafeDelegate(
    Control control, 
    string propertyName, 
    object propertyValue);

public static void SetControlPropertyThreadSafe(
    Control control, 
    string propertyName, 
    object propertyValue)
{
  if (control.InvokeRequired)
  {
    control.Invoke(new SetControlPropertyThreadSafeDelegate               
    (SetControlPropertyThreadSafe), 
    new object[] { control, propertyName, propertyValue });
  }
  else
  {
    control.GetType().InvokeMember(
        propertyName, 
        BindingFlags.SetProperty, 
        null, 
        control, 
        new object[] { propertyValue });
  }
}

Назовите это так:

// thread-safe equivalent of
// myLabel.Text = status;
SetControlPropertyThreadSafe(myLabel, "Text", status);

Если вы используете .NET 3.0 или выше, вы можете переписать указанный выше метод как метод расширения Controlкласса, что упростит вызов:

myLabel.SetPropertyThreadSafe("Text", status);

ОБНОВЛЕНИЕ 05/10/2010:

Для .NET 3.0 вы должны использовать этот код:

private delegate void SetPropertyThreadSafeDelegate<TResult>(
    Control @this, 
    Expression<Func<TResult>> property, 
    TResult value);

public static void SetPropertyThreadSafe<TResult>(
    this Control @this, 
    Expression<Func<TResult>> property, 
    TResult value)
{
  var propertyInfo = (property.Body as MemberExpression).Member 
      as PropertyInfo;

  if (propertyInfo == null ||
      !@this.GetType().IsSubclassOf(propertyInfo.ReflectedType) ||
      @this.GetType().GetProperty(
          propertyInfo.Name, 
          propertyInfo.PropertyType) == null)
  {
    throw new ArgumentException("The lambda expression 'property' must reference a valid property on this Control.");
  }

  if (@this.InvokeRequired)
  {
      @this.Invoke(new SetPropertyThreadSafeDelegate<TResult> 
      (SetPropertyThreadSafe), 
      new object[] { @this, property, value });
  }
  else
  {
      @this.GetType().InvokeMember(
          propertyInfo.Name, 
          BindingFlags.SetProperty, 
          null, 
          @this, 
          new object[] { value });
  }
}

который использует LINQ и лямбда-выражения, чтобы обеспечить более чистый, простой и безопасный синтаксис:

myLabel.SetPropertyThreadSafe(() => myLabel.Text, status); // status has to be a string or this will fail to compile

Теперь не только имя свойства проверяется во время компиляции, но и тип свойства, поэтому невозможно (например) присвоить строковое значение логическому свойству и, следовательно, вызвать исключение времени выполнения.

К сожалению, это не мешает никому делать глупости, такие как передача чужого Controlсвойства и значения, поэтому с радостью скомпилируем следующее:

myLabel.SetPropertyThreadSafe(() => aForm.ShowIcon, false);

Поэтому я добавил проверки во время выполнения, чтобы убедиться, что переданное свойство действительно принадлежит тому, Controlкоторый вызывается методом. Не идеально, но все же намного лучше, чем версия .NET 2.0.

Если у кого-то есть какие-либо дальнейшие предложения о том, как улучшить этот код для безопасности во время компиляции, пожалуйста, прокомментируйте!

Ян Кемп
источник
3
Есть случаи, когда this.GetType () оценивается так же, как propertyInfo.ReflectedType (например, LinkLabel на WinForms). У меня нет большого опыта C #, но я думаю, что условие для исключения должно быть: if (propertyInfo == null || (!@this.GetType (). IsSubclassOf (propertyInfo.ReflectedType) && @ this.GetType ( )! = propertyInfo.ReflectedType) || @ this.GetType (). GetProperty (propertyInfo.Name, propertyInfo.PropertyType) == null)
Корвин
9
@lan это SetControlPropertyThreadSafe(myLabel, "Text", status)можно вызвать из другого модуля, класса или формы
Смит
71
Предоставленное решение является излишне сложным. Посмотрите решение Марка Гравелла или решение Зейда Масуда, если вы цените простоту.
Фрэнк Хайлеман,
8
Это решение тратит впустую массу ресурсов, если вы обновляете несколько свойств, так как каждый Invoke стоит больших ресурсов. Я не думаю, что так или иначе была задумана особенность Thread Safety. Инкапсулируйте свои действия по обновлению пользовательского интерфейса и вызывайте его ОДИН РАЗ (а не для каждого свойства)
Консоль
4
С какой стати вы используете этот код поверх компонента BackgroundWorker?
Энди
1080

Самый простой способ - это анонимный метод, переданный в Label.Invoke:

// Running on the worker thread
string newText = "abc";
form.Label.Invoke((MethodInvoker)delegate {
    // Running on the UI thread
    form.Label.Text = newText;
});
// Back on the worker thread

Обратите внимание, что Invokeблокирует выполнение до его завершения - это синхронный код. Вопрос не касается асинхронного кода, но в Stack Overflow есть много информации о написании асинхронного кода, когда вы хотите узнать о нем.

Марк Гравелл
источник
8
Учитывая, что ОП не упомянул ни один класс / экземпляр, кроме формы, это неплохое значение по умолчанию ...
Марк Гравелл
39
Не забывайте, что ключевое слово «this» ссылается на класс «Control».
А-Я.
8
@ codecompleting это в любом случае безопасно, и мы уже знаем, что работаем, так зачем проверять то, что мы знаем?
Марк Гравелл
4
@ Dragouf не совсем - одна из целей использования этого метода заключается в том, что вы уже знаете, какие части выполняются на рабочем, а какие - на потоке пользовательского интерфейса. Не нужно проверять.
Марк Гравелл
3
@ Joan.bdm, мне не хватает контекста, чтобы прокомментировать это
Марк Гравелл
401

Обработка долгой работы

Начиная с .NET 4.5 и C # 5.0, вы должны использовать основанный на задачах асинхронный шаблон (TAP) вместе с асинхронными - ожидать ключевые слова во всех областях (включая GUI):

TAP - рекомендуемый шаблон асинхронного проектирования для новой разработки

вместо модели асинхронного программирования (APM) и асинхронного шаблона на основе событий (EAP) (последний включает класс BackgroundWorker ).

Тогда рекомендуемое решение для новой разработки:

  1. Асинхронная реализация обработчика событий (да, вот и все):

    private async void Button_Clicked(object sender, EventArgs e)
    {
        var progress = new Progress<string>(s => label.Text = s);
        await Task.Factory.StartNew(() => SecondThreadConcern.LongWork(progress),
                                    TaskCreationOptions.LongRunning);
        label.Text = "completed";
    }
  2. Реализация второго потока, который уведомляет поток пользовательского интерфейса:

    class SecondThreadConcern
    {
        public static void LongWork(IProgress<string> progress)
        {
            // Perform a long running work...
            for (var i = 0; i < 10; i++)
            {
                Task.Delay(500).Wait();
                progress.Report(i.ToString());
            }
        }
    }

Обратите внимание на следующее:

  1. Короткий и чистый код, написанный последовательным образом без обратных вызовов и явных потоков.
  2. Задача вместо темы .
  3. Ключевое слово async , которое позволяет использовать await, что, в свою очередь, препятствует достижению обработчиком события состояния завершения до завершения задачи и в то же время не блокирует поток пользовательского интерфейса.
  4. Класс Progress (см. Интерфейс IProgress ), поддерживающий принцип разработки Разделение проблем (SoC) и не требующий явного диспетчера и вызова. Он использует текущий SynchronizationContext из своего места создания (здесь поток пользовательского интерфейса).
  5. TaskCreationOptions.LongRunning, который намекает не помещать задачу в ThreadPool .

Для более подробного примера смотрите: Будущее C #: Хорошие вещи приходят к тем , кто «Await» от Джозефа Albahari .

Смотрите также о концепции UI Threading Model .

Обработка исключений

Приведенный ниже фрагмент кода является примером того, как обрабатывать исключения и Enabledсвойство кнопки-переключателя, чтобы предотвратить многократные щелчки во время фонового выполнения.

private async void Button_Click(object sender, EventArgs e)
{
    button.Enabled = false;

    try
    {
        var progress = new Progress<string>(s => button.Text = s);
        await Task.Run(() => SecondThreadConcern.FailingWork(progress));
        button.Text = "Completed";
    }
    catch(Exception exception)
    {
        button.Text = "Failed: " + exception.Message;
    }

    button.Enabled = true;
}

class SecondThreadConcern
{
    public static void FailingWork(IProgress<string> progress)
    {
        progress.Report("I will fail in...");
        Task.Delay(500).Wait();

        for (var i = 0; i < 3; i++)
        {
            progress.Report((3 - i).ToString());
            Task.Delay(500).Wait();
        }

        throw new Exception("Oops...");
    }
}
Рышард Деган
источник
2
Если SecondThreadConcern.LongWork()выдается исключение, может ли оно быть перехвачено потоком пользовательского интерфейса? Это отличный пост, кстати.
kdbanman
2
Я добавил дополнительный раздел к ответу, чтобы выполнить ваши требования. С уважением.
Рышард Деган
3
Класс ExceptionDispatchInfo отвечает за это чудо повторного выброса фонового исключения в потоке пользовательского интерфейса в шаблоне async-await.
Рышард Деган
1
Неужели я просто думаю, что этот способ сделать более многословным, чем просто вызывать Invoke / Begin ?!
MeTitus
2
Task.Delay(500).Wait()? Какой смысл создавать Задачу, чтобы просто заблокировать текущий поток? Вы никогда не должны блокировать поток пула потоков!
Ярик
236

Вариация простейшего решения Марка Гравелла для .NET 4:

control.Invoke((MethodInvoker) (() => control.Text = "new text"));

Или используйте вместо этого делегат Action:

control.Invoke(new Action(() => control.Text = "new text"));

Смотрите здесь для сравнения двух: MethodInvoker vs Action for Control.BeginInvoke

Зайд Масуд
источник
1
что такое «контроль» в этом примере? Мой интерфейс управления? Попытка реализовать это в WPF на элементе управления меткой, и Invoke не является членом моей метки.
Dbloom
Как насчет метода расширения, такого как @styxriver stackoverflow.com/a/3588137/206730 ?
Kiquenet
объявить «Действие у;» внутри класса или метода измените свойство text и обновите текст с помощью этого фрагмента кода 'yourcontrol.Invoke (y = () => yourcontrol.Text = "new text");'
Антонио Лейте
4
@Dbloom это не участник, потому что это только для WinForms. Для WPF вы используете Dispatcher.Invoke
sLw
1
Я следовал этому решению, но иногда мой пользовательский интерфейс не обновлялся. Я обнаружил, что мне нужно this.refresh()принудительно сделать недействительным и перекрасить GUI .. если это будет полезно ..
Rakibul Haq
137

Запустите и забудьте метод расширения для .NET 3.5+

using System;
using System.Windows.Forms;

public static class ControlExtensions
{
    /// <summary>
    /// Executes the Action asynchronously on the UI thread, does not block execution on the calling thread.
    /// </summary>
    /// <param name="control"></param>
    /// <param name="code"></param>
    public static void UIThread(this Control @this, Action code)
    {
        if (@this.InvokeRequired)
        {
            @this.BeginInvoke(code);
        }
        else
        {
            code.Invoke();
        }
    }
}

Это можно вызвать с помощью следующей строки кода:

this.UIThread(() => this.myLabel.Text = "Text Goes Here");
StyxRiver
источник
5
Какой смысл использования @this? Разве «контроль» не будет эквивалентен? Есть ли какие-то преимущества у @this?
Аргайл
14
@jeromeyers - @thisпросто имя переменной, в данном случае ссылка на текущий элемент управления, вызывающий расширение. Вы можете переименовать его в источник, или что-то еще, что плавает на вашей лодке. Я использую @this, потому что он ссылается на «этот элемент управления», который вызывает расширение и соответствует (по крайней мере, в моей голове) использованию ключевого слова «это» в обычном (не являющемся расширением) коде.
StyxRiver,
1
Это здорово, легко и для меня лучшее решение. Вы можете включить всю работу, которую вы должны сделать, в поток пользовательского интерфейса. Пример: this.UIThread (() => {txtMessage.Text = message; listBox1.Items.Add (message);});
Авто
1
Мне очень нравится это решение. Незначительная гнида: я бы назвал этот метод, OnUIThreadа не UIThread.
ToolmakerSteve
2
Вот почему я назвал это расширение RunOnUiThread. Но это только личный вкус.
Grisgram
66

Это классический способ сделать это:

using System;
using System.Windows.Forms;
using System.Threading;

namespace Test
{
    public partial class UIThread : Form
    {
        Worker worker;

        Thread workerThread;

        public UIThread()
        {
            InitializeComponent();

            worker = new Worker();
            worker.ProgressChanged += new EventHandler<ProgressChangedArgs>(OnWorkerProgressChanged);
            workerThread = new Thread(new ThreadStart(worker.StartWork));
            workerThread.Start();
        }

        private void OnWorkerProgressChanged(object sender, ProgressChangedArgs e)
        {
            // Cross thread - so you don't get the cross-threading exception
            if (this.InvokeRequired)
            {
                this.BeginInvoke((MethodInvoker)delegate
                {
                    OnWorkerProgressChanged(sender, e);
                });
                return;
            }

            // Change control
            this.label1.Text = e.Progress;
        }
    }

    public class Worker
    {
        public event EventHandler<ProgressChangedArgs> ProgressChanged;

        protected void OnProgressChanged(ProgressChangedArgs e)
        {
            if(ProgressChanged!=null)
            {
                ProgressChanged(this,e);
            }
        }

        public void StartWork()
        {
            Thread.Sleep(100);
            OnProgressChanged(new ProgressChangedArgs("Progress Changed"));
            Thread.Sleep(100);
        }
    }


    public class ProgressChangedArgs : EventArgs
    {
        public string Progress {get;private set;}
        public ProgressChangedArgs(string progress)
        {
            Progress = progress;
        }
    }
}

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

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

Спасли
источник
62

Простое решение заключается в использовании Control.Invoke.

void DoSomething()
{
    if (InvokeRequired) {
        Invoke(new MethodInvoker(updateGUI));
    } else {
        // Do Something
        updateGUI();
    }
}

void updateGUI() {
    // update gui here
}
OregonGhost
источник
хорошо сделано для простоты! не только просто, но и хорошо работает! Я действительно не понимаю, почему Microsoft не может сделать это проще, чем должно быть! для вызова 1 строки в главном потоке мы должны написать пару функций!
MBH
1
@MBH Согласен. Кстати, вы заметили ответ выше stackoverflow.com/a/3588137/199364 , который определяет метод расширения? Сделайте это один раз в классе пользовательских утилит, и вам больше не нужно заботиться о том, чтобы Microsoft не делала этого за нас :)
ToolmakerSteve
@ToolmakerSteve Это именно то, что он хотел! Вы правы, мы можем найти способ, но я имею в виду, с точки зрения СУХОЙ (не повторяйся), проблема, которая имеет общее решение, может быть решена ими с минимальными усилиями Microsoft, что сэкономит много времени для программисты :)
MBH
47

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

    private void button1_Click(object sender, EventArgs e)
    {
        backgroundWorker1.WorkerReportsProgress = true;
        backgroundWorker1.RunWorkerAsync();
    }

    private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
    {
        Thread.Sleep(5000);
        backgroundWorker1.ReportProgress(0, "A");
        Thread.Sleep(5000);
        backgroundWorker1.ReportProgress(0, "B");
        Thread.Sleep(5000);
        backgroundWorker1.ReportProgress(0, "C");
    }

    private void backgroundWorker1_ProgressChanged(
        object sender, 
        ProgressChangedEventArgs e)
    {
        label1.Text = e.UserState.ToString();
    }

Это хорошо, если вы всегда хотите обновить одно и то же поле. Если вам нужно сделать более сложные обновления, вы можете определить класс для представления состояния пользовательского интерфейса и передать его в метод ReportProgress.

И последнее: обязательно установите WorkerReportsProgressфлаг, иначе ReportProgressметод будет полностью проигнорирован.

Дон Киркби
источник
2
В конце обработки также возможно обновить пользовательский интерфейс через backgroundWorker1_RunWorkerCompleted.
DavidRR
41

Подавляющее большинство ответов используют условия гонки, ожидающиеControl.Invoke своего появления . Например, рассмотрим принятый ответ:

string newText = "abc"; // running on worker thread
this.Invoke((MethodInvoker)delegate { 
    someLabel.Text = newText; // runs on UI thread
});

Если пользователь закрывает форму непосредственно перед this.Invokeвызовом (помните, thisявляется ли Formобъект), ObjectDisposedExceptionскорее всего, будет запущен.

Решение заключается в использовании SynchronizationContext, в частности, SynchronizationContext.Currentкак предлагает hamilton.danielb (другие ответы зависят от конкретных SynchronizationContextреализаций, что совершенно не нужно). Я бы немного изменил его код для использования, SynchronizationContext.Postа не для этого SynchronizationContext.Send(поскольку обычно рабочему потоку не нужно ждать):

public partial class MyForm : Form
{
    private readonly SynchronizationContext _context;
    public MyForm()
    {
        _context = SynchronizationContext.Current
        ...
    }

    private MethodOnOtherThread()
    {
         ...
         _context.Post(status => someLabel.Text = newText,null);
    }
}

Обратите внимание, что в .NET 4.0 и выше вы действительно должны использовать задачи для асинхронных операций. Смотрите ответ n-san для эквивалентного подхода на основе задач (используя TaskScheduler.FromCurrentSynchronizationContext).

Наконец, в .NET 4.5 и более поздних версиях вы также можете использовать Progress<T>(что в основном фиксируется SynchronizationContext.Currentпри его создании), как продемонстрировал Рышард Деган (Ryszard Dżegan), для случаев, когда при длительной операции необходимо запускать код пользовательского интерфейса во время работы.

Охад Шнайдер
источник
37

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

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

Вы можете сделать это, подняв ваше событие следующим образом:

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

if( MyEvent != null )
{
   Delegate[] eventHandlers = MyEvent.GetInvocationList();

   foreach( Delegate d in eventHandlers )
   {
      // Check whether the target of the delegate implements 
      // ISynchronizeInvoke (Winforms controls do), and see
      // if a context-switch is required.
      ISynchronizeInvoke target = d.Target as ISynchronizeInvoke;

      if( target != null && target.InvokeRequired )
      {
         target.Invoke (d, ... );
      }
      else
      {
          d.DynamicInvoke ( ... );
      }
   }
}

Обратите внимание, что приведенный выше код не будет работать в проектах WPF, поскольку элементы управления WPF не реализуют ISynchronizeInvokeинтерфейс.

Для того , чтобы убедиться , что код выше работы с Windows Forms и WPF, а также всех других платформ, вы можете взглянуть на AsyncOperation, AsyncOperationManagerи SynchronizationContextклассы.

Чтобы легко вызывать события таким образом, я создал метод расширения, который позволяет мне упростить вызов события, просто вызвав:

MyEvent.Raise(this, EventArgs.Empty);

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

Фредерик Гейселс
источник
Действительно, но я не люблю «загромождать» мой код GUI этим вопросом. Мой GUI не должен заботиться о том, нужно ли ему вызывать или нет. Другими словами: я не думаю, что именно GUI отвечает за контекстное переключение.
Фредерик Гейселс
1
Разбиение делегата и т. Д. Кажется излишним - почему бы не просто: SynchronizationContext.Current.Send (делегат {MyEvent (...);}, ноль);
Марк Гравелл
У вас всегда есть доступ к SynchronizationContext? Даже если ваш класс находится в классе lib?
Фредерик Гейселс
29

Вам нужно будет вызвать метод в потоке GUI. Вы можете сделать это, вызвав Control.Invoke.

Например:

delegate void UpdateLabelDelegate (string message);

void UpdateLabel (string message)
{
    if (InvokeRequired)
    {
         Invoke (new UpdateLabelDelegate (UpdateLabel), message);
         return;
    }

    MyLabelControl.Text = message;
}
Кирон
источник
1
Строка invoke дает мне ошибку компилятора. Наилучшее совпадение перегруженного метода для 'System.Windows.Forms.Control.Invoke (System.Delegate, object [])' содержит недопустимые аргументы
CruelIO
28

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

public class MyForm : Form
{
  private volatile string m_Text = "";
  private System.Timers.Timer m_Timer;

  private MyForm()
  {
    m_Timer = new System.Timers.Timer();
    m_Timer.SynchronizingObject = this;
    m_Timer.Interval = 1000;
    m_Timer.Elapsed += (s, a) => { MyProgressLabel.Text = m_Text; };
    m_Timer.Start();
    var thread = new Thread(WorkerThread);
    thread.Start();
  }

  private void WorkerThread()
  {
    while (...)
    {
      // Periodically publish progress information.
      m_Text = "Still working...";
    }
  }
}

Такой подход позволяет избежать операции маршалинга , необходимые при использовании ISynchronizeInvoke.Invokeи ISynchronizeInvoke.BeginInvokeметодов. Нет ничего плохого в использовании техники маршалинга, но есть несколько предостережений, о которых нужно знать.

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

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

  • Пользовательский интерфейс и рабочие потоки остаются слабо связанными, а не Control.Invokeили Control.BeginInvokeподход, который тесно связывает их.
  • Поток пользовательского интерфейса не будет препятствовать продвижению рабочего потока.
  • Рабочий поток не может доминировать во время, которое поток пользовательского интерфейса тратит на обновление.
  • Интервалы, с которыми пользовательский интерфейс и рабочие потоки выполняют операции, могут оставаться независимыми.
  • Рабочий поток не может переполнить насос сообщений потока пользовательского интерфейса.
  • Поток пользовательского интерфейса определяет, когда и как часто пользовательский интерфейс обновляется.
Brian Gideon
источник
3
Хорошая идея. Единственное, что вы не упомянули, это то, как вы правильно располагаете таймер после завершения WorkerThread. Обратите внимание, что это может вызвать проблемы, когда приложение заканчивается (т. Е. Пользователь закрывает приложение). У вас есть идея, как это решить?
Мэтт
@Matt Вместо того, чтобы использовать анонимный обработчик для Elapsedсобытия, вы используете метод member, чтобы вы могли удалить таймер при удалении формы ...
Phil1970
@ Phil1970 - Хороший вопрос. Вы имели в виду, как System.Timers.ElapsedEventHandler handler = (s, a) => { MyProgressLabel.Text = m_Text; };и назначить его через m_Timer.Elapsed += handler;, позже в контексте утилизации, делаю m_Timer.Elapsed -= handler;я правильно? И для утилизации / закрытия, следуя советам, которые обсуждались здесь .
Мэтт
27

В предыдущих ответах не требуется ничего из Invoke.

Вам нужно взглянуть на WindowsFormsSynchronizationContext:

// In the main thread
WindowsFormsSynchronizationContext mUiContext = new WindowsFormsSynchronizationContext();

...

// In some non-UI Thread

// Causes an update in the GUI thread.
mUiContext.Post(UpdateGUI, userData);

...

void UpdateGUI(object userData)
{
    // Update your GUI controls here
}
Джон Х
источник
4
Как вы думаете, что метод Post использует под капотом? :)
невероятно
23

Это похоже на решение выше с использованием .NET Framework 3.0, но оно решило проблему обеспечения безопасности во время компиляции .

public  static class ControlExtension
{
    delegate void SetPropertyValueHandler<TResult>(Control souce, Expression<Func<Control, TResult>> selector, TResult value);

    public static void SetPropertyValue<TResult>(this Control source, Expression<Func<Control, TResult>> selector, TResult value)
    {
        if (source.InvokeRequired)
        {
            var del = new SetPropertyValueHandler<TResult>(SetPropertyValue);
            source.Invoke(del, new object[]{ source, selector, value});
        }
        else
        {
            var propInfo = ((MemberExpression)selector.Body).Member as PropertyInfo;
            propInfo.SetValue(source, value, null);
        }
    }
}

Использовать:

this.lblTimeDisplay.SetPropertyValue(a => a.Text, "some string");
this.lblTimeDisplay.SetPropertyValue(a => a.Visible, false);

Компилятор потерпит неудачу, если пользователь передаст неверный тип данных.

this.lblTimeDisplay.SetPropertyValue(a => a.Visible, "sometext");
Фрэнсис
источник
23

Salvete! Поискав этот вопрос, я обнаружил, что ответы FrankG и Oregon Ghost были самыми простыми и полезными для меня. Теперь я кодирую в Visual Basic и запускаю этот фрагмент через конвертер; так что я не совсем уверен, как это получается.

У меня есть диалоговая форма, form_Diagnostics,которая называется richtext box, updateDiagWindow,который я использую для отображения журнала. Мне нужно было иметь возможность обновить его текст из всех тем. Дополнительные строки позволяют окну автоматически прокручиваться до самых новых строк.

Итак, теперь я могу обновить отображение одной строкой из любой точки всей программы так, как вы думаете, она будет работать без каких-либо потоков:

  form_Diagnostics.updateDiagWindow(whatmessage);

Основной код (поместите его в код класса вашей формы):

#region "---------Update Diag Window Text------------------------------------"
// This sub allows the diag window to be updated by all threads
public void updateDiagWindow(string whatmessage)
{
    var _with1 = diagwindow;
    if (_with1.InvokeRequired) {
        _with1.Invoke(new UpdateDiagDelegate(UpdateDiag), whatmessage);
    } else {
        UpdateDiag(whatmessage);
    }
}
// This next line makes the private UpdateDiagWindow available to all threads
private delegate void UpdateDiagDelegate(string whatmessage);
private void UpdateDiag(string whatmessage)
{
    var _with2 = diagwindow;
    _with2.appendtext(whatmessage);
    _with2.SelectionStart = _with2.Text.Length;
    _with2.ScrollToCaret();
}
#endregion
bgmCoder
источник
21

Для многих целей это так просто:

public delegate void serviceGUIDelegate();
private void updateGUI()
{
  this.Invoke(new serviceGUIDelegate(serviceGUI));
}

«serviceGUI ()» - это метод уровня GUI в форме (this), который может изменять столько элементов управления, сколько вы хотите. Вызовите updateGUI () из другого потока. Параметры могут быть добавлены для передачи значений или (возможно, быстрее) использовать переменные области видимости класса с блокировками для них по мере необходимости, если есть какая-либо вероятность столкновения между потоками, обращающимися к ним, что может вызвать нестабильность. Используйте BeginInvoke вместо Invoke, если не-GUI-поток критичен ко времени (учитывая предупреждение Брайана Гидеона).

Frankg
источник
21

Это в моем C # 3.0 варианте решения Яна Кемпа:

public static void SetPropertyInGuiThread<C,V>(this C control, Expression<Func<C, V>> property, V value) where C : Control
{
    var memberExpression = property.Body as MemberExpression;
    if (memberExpression == null)
        throw new ArgumentException("The 'property' expression must specify a property on the control.");

    var propertyInfo = memberExpression.Member as PropertyInfo;
    if (propertyInfo == null)
        throw new ArgumentException("The 'property' expression must specify a property on the control.");

    if (control.InvokeRequired)
        control.Invoke(
            (Action<C, Expression<Func<C, V>>, V>)SetPropertyInGuiThread,
            new object[] { control, property, value }
        );
    else
        propertyInfo.SetValue(control, value, null);
}

Вы называете это так:

myButton.SetPropertyInGuiThread(b => b.Text, "Click Me!")
  1. Он добавляет нулевую проверку к результату «as MemberExpression».
  2. Это улучшает статическую безопасность типов.

В противном случае оригинал - очень хорошее решение.

Rotaerk
источник
21
Label lblText; //initialized elsewhere

void AssignLabel(string text)
{
   if (InvokeRequired)
   {
      BeginInvoke((Action<string>)AssignLabel, text);
      return;
   }

   lblText.Text = text;           
}

Обратите внимание, что BeginInvoke()это предпочтительнее, Invoke()потому что это менее вероятно, чтобы вызвать взаимоблокировки (однако, это не проблема здесь, просто назначая текст метке):

При использовании Invoke()вы ждете возврата метода. Теперь может случиться так, что вы делаете что-то в вызываемом коде, который должен будет ожидать поток, что может быть неочевидно, если он скрыт в некоторых вызываемых вами функциях, что само по себе может происходить косвенно через обработчики событий. Таким образом, вы будете ждать нити, нить будет ждать вас, и вы зашли в тупик.

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

ILoveFortran
источник
20

Когда я столкнулся с той же проблемой, я обратился за помощью к Google, но вместо того, чтобы дать мне простое решение, он смутил меня больше, приводя примеры MethodInvokerи бла-бла-бла. Поэтому я решил решить это самостоятельно. Вот мое решение:

Сделайте делегата следующим образом:

Public delegate void LabelDelegate(string s);

void Updatelabel(string text)
{
   if (label.InvokeRequired)
   {
       LabelDelegate LDEL = new LabelDelegate(Updatelabel);
       label.Invoke(LDEL, text);
   }
   else
       label.Text = text
}

Вы можете вызвать эту функцию в новом потоке, как это

Thread th = new Thread(() => Updatelabel("Hello World"));
th.start();

Не путайся с Thread(() => .....). Я использую анонимную функцию или лямбда-выражение, когда я работаю над потоком. Чтобы уменьшить количество строк кода, вы также можете использовать ThreadStart(..)метод, который я не должен здесь объяснять.

Ахмар
источник
17

Просто используйте что-то вроде этого:

 this.Invoke((MethodInvoker)delegate
            {
                progressBar1.Value = e.ProgressPercentage; // runs on UI thread
            });
Хасан Шуман
источник
Если у вас есть e.ProgressPercentage, не вы уже в потоке пользовательского интерфейса от метода, который вы вызываете это?
LarsTech
Событие ProgressChanged запускается в потоке пользовательского интерфейса. Это одно из удобных условий использования BackgroundWorker. Событие Completed работает и на графическом интерфейсе. Единственное, что выполняется в потоке, не связанном с пользовательским интерфейсом, - это метод DoWork.
LarsTech
15

Вы можете использовать уже существующий делегат Action:

private void UpdateMethod()
{
    if (InvokeRequired)
    {
        Invoke(new Action(UpdateMethod));
    }
}
Embedd_Khurja
источник
14

Моя версия - вставить одну строку рекурсивной «мантры»:

Без аргументов:

    void Aaaaaaa()
    {
        if (InvokeRequired) { Invoke(new Action(Aaaaaaa)); return; } //1 line of mantra

        // Your code!
    }

Для функции, которая имеет аргументы:

    void Bbb(int x, string text)
    {
        if (InvokeRequired) { Invoke(new Action<int, string>(Bbb), new[] { x, text }); return; }
        // Your code!
    }

ЭТО ЭТО .


Некоторые аргументы . Обычно читаемость кода плохо ставить {} после if ()оператора в одну строку. Но в данном случае это обычная все та же «мантра». Это не нарушает читабельность кода, если этот метод согласован в проекте. И это спасает ваш код от мусора (одна строка кода вместо пяти).

Как видите, if(InvokeRequired) {something long}вы просто знаете, что «эту функцию безопасно вызывать из другого потока».

MajesticRa
источник
13

Попробуйте обновить этикетку, используя это

public static class ExtensionMethods
{
    private static Action EmptyDelegate = delegate() { };

    public static void Refresh(this UIElement uiElement)
    {
        uiElement.Dispatcher.Invoke(DispatcherPriority.Render, EmptyDelegate);
    }
}
Ивайло Славов
источник
Это для Windows Forms ?
Kiquenet
13

Создайте переменную класса:

SynchronizationContext _context;

Установите его в конструкторе, который создает ваш пользовательский интерфейс:

var _context = SynchronizationContext.Current;

Когда вы хотите обновить ярлык:

_context.Send(status =>{
    // UPDATE LABEL
}, null);
blackmind
источник
12

Вы должны использовать invoke и делегировать

private delegate void MyLabelDelegate();
label1.Invoke( new MyLabelDelegate(){ label1.Text += 1; });
А. Залонис
источник
12

Большинство других ответов немного сложны для меня в этом вопросе (я новичок в C #), поэтому я пишу свои:

У меня есть приложение WPF, и я определил работника, как показано ниже:

Выпуск:

BackgroundWorker workerAllocator;
workerAllocator.DoWork += delegate (object sender1, DoWorkEventArgs e1) {
    // This is my DoWork function.
    // It is given as an anonymous function, instead of a separate DoWork function

    // I need to update a message to textbox (txtLog) from this thread function

    // Want to write below line, to update UI
    txt.Text = "my message"

    // But it fails with:
    //  'System.InvalidOperationException':
    //  "The calling thread cannot access this object because a different thread owns it"
}

Решение:

workerAllocator.DoWork += delegate (object sender1, DoWorkEventArgs e1)
{
    // The below single line works
    txtLog.Dispatcher.BeginInvoke((Action)(() => txtLog.Text = "my message"));
}

Мне еще предстоит выяснить, что означает приведенная выше строка, но она работает.

Для WinForms :

Решение:

txtLog.Invoke((MethodInvoker)delegate
{
    txtLog.Text = "my message";
});
Манохар Редди Поредди
источник
Вопрос был о Winforms, а не о WPF.
Марк Л.
Спасибо. Добавлено решение WinForms выше.
Манохар Редди Поредди
... который является просто копией многих других ответов на этот же вопрос, но все в порядке. Почему бы не стать частью решения и просто удалить свой ответ?
Марк Л.
хм, правильно, если вы только что прочитали мой ответ с вниманием, начальную часть (причину, по которой я написал ответ), и, надеюсь, с чуть большим вниманием вы увидите, что есть кто-то, у кого была точно такая же проблема и за которую проголосовали сегодня мой простой ответ, и с еще большим вниманием, если бы вы могли предвидеть реальную историю о том, почему все это произошло, Google отправляет меня сюда, даже когда я ищу wpf. Конечно, поскольку вы пропустили эти более или менее очевидные 3 причины, я могу понять, почему вы не уберете свое отрицательное мнение. Вместо того, чтобы чистить нормальный, создайте что-то новое, что намного сложнее.
Манохар Редди Поредди
8

Самый простой способ, я думаю:

   void Update()
   {
       BeginInvoke((Action)delegate()
       {
           //do your update
       });
   }
Василий Семенов
источник
8

Например, получить доступ к элементу управления, кроме текущего потока:

Speed_Threshold = 30;
textOutput.Invoke(new EventHandler(delegate
{
    lblThreshold.Text = Speed_Threshold.ToString();
}));

Это lblThresholdметка и Speed_Thresholdглобальная переменная.

Да сюн
источник
8

Когда вы находитесь в потоке пользовательского интерфейса, вы можете запросить у него планировщик задач контекста синхронизации. Это даст вам TaskScheduler, который планирует все в потоке пользовательского интерфейса.

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

public partial class MyForm : Form
{
  private readonly TaskScheduler _uiTaskScheduler;
  public MyForm()
  {
    InitializeComponent();
    _uiTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
  }

  private void buttonRunAsyncOperation_Click(object sender, EventArgs e)
  {
    RunAsyncOperation();
  }

  private void RunAsyncOperation()
  {
    var task = new Task<string>(LengthyComputation);
    task.ContinueWith(antecedent =>
                         UpdateResultLabel(antecedent.Result), _uiTaskScheduler);
    task.Start();
  }

  private string LengthyComputation()
  {
    Thread.Sleep(3000);
    return "47";
  }

  private void UpdateResultLabel(string text)
  {
    labelResult.Text = text;
  }
}

Это работает для задач (не потоков), которые сейчас являются предпочтительным способом написания параллельного кода .

nosalan
источник
1
Вызов , Task.Startкак правило , не является хорошей практикой blogs.msdn.com/b/pfxteam/archive/2012/01/14/10256832.aspx
Ohad Schneider
8

Я только что прочитал ответы, и это, кажется, очень горячая тема. В настоящее время я использую .NET 3.5 SP1 и Windows Forms.

Хорошо известная формула, в значительной степени описанная в предыдущих ответах, которая использует свойство InvokeRequired, охватывает большинство случаев, но не весь пул.

Что, если дескриптор еще не создан?

Свойство InvokeRequired , как описано здесь (ссылка на свойство Control.InvokeRequired для MSDN), возвращает true, если вызов был сделан из потока, который не является потоком GUI, или false, если вызов был сделан из потока GUI, или если Handle был еще не создано.

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

private MyForm _gui;

public void StartToDoThings()
{
    _gui = new MyForm();
    Thread thread = new Thread(SomeDelegate);
    thread.Start();
    _gui.ShowDialog();
}

И делегат может обновить метку в графическом интерфейсе:

private void SomeDelegate()
{
    // Operations that can take a variable amount of time, even no time
    //... then you update the GUI
    if(_gui.InvokeRequired)
        _gui.Invoke((Action)delegate { _gui.Label1.Text = "Done!"; });
    else
        _gui.Label1.Text = "Done!";
}

Это может привести к InvalidOperationException , если операции перед обновлением лейбла «занимает меньше времени» (читать и интерпретировать его как упрощением) , чем время, необходимое для GUI потока , чтобы создать форму «s Handle . Это происходит внутри метода ShowDialog () .

Вы также должны проверить дескриптор следующим образом:

private void SomeDelegate()
{
    // Operations that can take a variable amount of time, even no time
    //... then you update the GUI
    if(_gui.IsHandleCreated)  //  <---- ADDED
        if(_gui.InvokeRequired)
            _gui.Invoke((Action)delegate { _gui.Label1.Text = "Done!"; });
        else
            _gui.Label1.Text = "Done!";
}

Вы можете обрабатывать выполняемую операцию , если ручка не была создана еще: Вы можете просто игнорировать обновление GUI (как показано в коде выше) , или вы можете ждать (более рискованными). Это должно ответить на вопрос.

Необязательный материал: лично я придумал кодировать следующее:

public class ThreadSafeGuiCommand
{
  private const int SLEEPING_STEP = 100;
  private readonly int _totalTimeout;
  private int _timeout;

  public ThreadSafeGuiCommand(int totalTimeout)
  {
    _totalTimeout = totalTimeout;
  }

  public void Execute(Form form, Action guiCommand)
  {
    _timeout = _totalTimeout;
    while (!form.IsHandleCreated)
    {
      if (_timeout <= 0) return;

      Thread.Sleep(SLEEPING_STEP);
      _timeout -= SLEEPING_STEP;
    }

    if (form.InvokeRequired)
      form.Invoke(guiCommand);
    else
      guiCommand();
  }
}

Я передаю свои формы, которые обновляются другим потоком, экземпляром этого ThreadSafeGuiCommand , и я определяю методы, которые обновляют графический интерфейс (в моей форме) следующим образом:

public void SetLabeTextTo(string value)
{
  _threadSafeGuiCommand.Execute(this, delegate { Label1.Text = value; });
}

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

Sume
источник
1
Пришел сюда, чтобы найти это, так как я также проверяю IsHandleCreated. Еще одно свойство для проверки - IsDisposed. Если ваша форма удалена, вы не можете вызвать Invoke () для нее. Если пользователь закрыл форму до того, как ваш фоновый поток мог завершиться, вы не захотите, чтобы он пытался перезвонить в пользовательский интерфейс при удалении формы.
Джон
Я бы сказал, что начинать с этого - плохая идея. Обычно, вы бы сразу показывали дочернюю форму и имели индикатор выполнения или какую-то другую обратную связь при выполнении фоновой обработки. Или вы сначала выполните всю обработку, а затем передадите результат в новую форму при создании. Выполнение того и другого в одно и то же время, как правило, будет иметь незначительные преимущества, но при этом код будет гораздо менее обслуживаемым.
Phil1970
Описанный сценарий учитывает модальную форму, используемую в качестве представления прогресса фонового задания. Поскольку он должен быть модальным, его нужно показать, вызвав метод Form.ShowDialog () . Этим вы предотвращаете выполнение кода, следующего за вызовом, до тех пор, пока форма не будет закрыта. Таким образом, если вы не можете запустить фоновый поток иначе, чем в данном примере (и, конечно, вы можете), эта форма должна быть модально показана после запуска фонового потока. В этом случае вам необходимо проверить, будет ли создан дескриптор. Если вам не нужна модальная форма, тогда это другая история.
Sume