Як оновити графічний інтерфейс з іншого потоку?


1392

Який найпростіший спосіб оновити файл Labelз іншого Thread?

  • У мене Formпрацює thread1, і з цього я починаю ще одну нитку ( thread2).

  • Хоча я thread2обробляю деякі файли, я хотів би оновити Labelна Formпоточний стан thread2роботи.

Як я міг це зробити?


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
Ryszard Dżegan

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

ОНОВЛЕННЯ 10.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 () оцінюється так само, як властивістьInfo.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 коштує багато ресурсів. Я не думаю, що таким чином була призначена функція безпеки різьби. Зробіть Encapsulte ваші дії з оновлення користувальницького інтерфейсу та запросіть його, колись (а не на власність)
консоль

4
Чому б на землі ви використовували цей код над компонентом BackgroundWorker?
Енді

1079

Найпростіший шлях анонімний метод пройшов в 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
Не забувайте, що ключове слово "це" посилається на клас "Control".
AZ.

8
@codecompleting це безпечно в будь-якому випадку, і ми вже знаємо, що ми працюємо, тож чому перевіряти щось, що ми знаємо?
Марк Гравелл

4
@Dragouf насправді - один із моментів використання цього методу полягає в тому, що ви вже знаєте, які частини працюють на робочому, а які - на потоці інтерфейсу. Не потрібно перевіряти.
Марк Гравелл

3
@ Joan.bdm ніде немає достатнього контексту, щоб я коментував це
Марк Гравелл

400

Довга робота

З .NET 4.5 і C # 5.0 ви повинні використовувати Task на основі асинхронного (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 , що дозволяє використовувати функцію очікування, яка в свою чергу перешкоджає обробнику події досягти стану завершення до завершення завдання, а тим часом не блокує потік інтерфейсу користувача.
  4. Клас прогресу (див. Інтерфейс 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()викидає виняток, чи може його спіймати нитка інтерфейсу? Це чудовий пост, btw.
kdbanman

2
Я додав додатковий розділ до відповіді, щоб виконати ваші вимоги. З повагою
Рішард Джеган

3
Клас ExceptionDispatchInfo несе відповідальність за чудо від витоку фонового винятку на потоці інтерфейсу користувача у шаблоні async-wait.
Ryszard Dżegan

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 не є членом мого лейбла.
Дблом

Що про методі розширення як @styxriver stackoverflow.com/a/3588137/206730 ?
Кікенет

оголосити "Дія у;" всередині класу чи методу, що змінюють властивість тексту та оновлюють текст за допомогою цього фрагмента коду 'yourcontrol.Invoke (y = () => yourcontrol.Text = "новий текст");'
Антоніо Лейте

4
@Dbloom не є членом, тому що це лише для WinForms. Для WPF ви використовуєте Dispatcher.Invoke
sLw

1
Я дотримувався цього рішення, але іноді мій інтерфейс не оновлювався. Я виявив, що мені потрібно this.refresh()змусити визнати недійсним і перефарбувати графічний інтерфейс .. якщо це корисно ..
Ракібул Хак

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

5
У чому сенс використання @ цього? Чи не було б "контроль" еквівалентним? Чи є якісь переваги для @this?
argyle

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
}

молодець для простоти! не тільки простий, але і добре працює! Я дійсно не розумів, чому мікрософт не може зробити його простішим, як це має бути! для виклику 1 рядка в основному потоці, ми повинні написати пару функцій!
MBH

1
@MBH Погоджуюся. До речі, ви помітили відповідь stackoverflow.com/a/3588137/199364 вище, яка визначає метод розширення? Зробіть це один раз у користувальницькому класі комунальних служб, тоді не потрібно більше піклуватися про те, щоб Microsoft не зробив це для нас :)
ToolmakerSteve

@ToolmakerSteve Ось саме це означало бути! Ви маєте рацію, ми можемо знайти спосіб, але я маю на увазі з точки зору DRY (не повторюйте себе) проблему, яка має спільне рішення, вирішити їх можна мінімальними зусиллями 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's для тих випадків, коли тривала операція потребує запуску коду інтерфейсу під час роботи.


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 виконувати контекст-swithc.
Фредерік Гейселс

1
Розбиття делегата на частини тощо видається непосильним - чому б не просто: SynchronizationContext.Current.Send (делегат {MyEvent (...);}, null);
Marc Gravell

Чи завжди у вас є доступ до SynchronizationContext? Навіть якщо ваш клас знаходиться в класі
Фредерік Гейсельс

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
Рядок виклику дає мені помилку компілятора. Найкращий збіг перевантаженого методу для '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підхід , який щільно пари їх.
  • Потік інтерфейсу не перешкоджає прогресу робочої нитки.
  • Робоча нитка не може домінувати над тим, як поток інтерфейсу витрачається на оновлення.
  • Інтервали, через які користувальницький інтерфейс та робочі потоки виконують операції, можуть залишатися незалежними.
  • Робоча нитка не може перекрити насос повідомлення потоку інтерфейсу.
  • Потік користувальницького інтерфейсу отримує диктувати, коли і як часто користувальницький інтерфейс оновлюється.

3
Гарна ідея. Єдине, що ви не згадали, це те, як ви правильно розпоряджаєтесь таймером після завершення роботи WorkerThread. Зверніть увагу, що це може спричинити проблеми, коли програма закінчується (тобто користувач закриває програму). У вас є ідея, як це вирішити?
Мт

@Matt Замість того, щоб використовувати анонімний обробник для Elapsedподії, ви використовуєте метод учасника, щоб ви могли видалити таймер, коли форма розміщена ...
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 під кришкою? :)
increddibelly

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

Сальвет! Шукаючи це питання, я знайшов відповіді ФранкГ та Орегон Привид найлегшими для мене найкориснішими. Тепер я кодував у Visual Basic і проніс цей фрагмент через конвертор; тож я не впевнений, як саме виходить.

У мене є діалогова форма, form_Diagnostics,яка має вікно багатотексту, яке називається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

21

Для багатьох цілей це так просто:

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

"serviceGUI ()" - це метод рівня GUI у формі (це), який може змінити стільки елементів управління, скільки вам потрібно. Викличте "updateGUI ()" з іншого потоку. Параметри можуть бути додані для передачі значень або (можливо, швидше) використовувати змінні області класу з блокуванням на них за необхідності, якщо є можливість зіткнення між потоками, що звертаються до них, що може спричинити нестабільність. Використовуйте BeginInvoke замість Invoke, якщо потік, що не є графічним інтерфейсом, має критичний час (пам'ятаючи про попередження Брайана Гедеона).


21

Це в моєму варіанті рішення C # 3.0 рішення Ian Kemp:

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. Він додає нульову перевірку результату "як MemberExpression".
  2. Це покращує безпеку статичного типу.

Інакше оригінал - це дуже приємне рішення.


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().


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. Завершена подія також працює на гуї. Єдине, що працює в потоці, що не використовується UI, це метод DoWork.
LarsTech

15

Ви можете використовувати вже наявного делегата Action:

private void UpdateMethod()
{
    if (InvokeRequired)
    {
        Invoke(new Action(UpdateMethod));
    }
}

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!
    }

ТО - це .


Деякі аргументації : Зазвичай читати код погано, щоб ставити {} після anif () оператора в одному рядку. Але в цьому випадку це звичайна все та сама "мантра". Він не порушує читабельність коду, якщо цей метод відповідає проекту. І це рятує ваш код від засмічення (один рядок коду замість п'яти).

Як ви бачите, if(InvokeRequired) {something long}ви просто знаєте, "цю функцію безпечно телефонувати з іншої теми".


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 ?
Кікенет

13

Створіть змінну класу:

SynchronizationContext _context;

Встановіть його в конструкторі, який створює ваш інтерфейс:

var _context = SynchronizationContext.Current;

Коли ви хочете оновити етикетку:

_context.Send(status =>{
    // UPDATE LABEL
}, null);

12

Ви повинні використовувати виклик та делегувати

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

Наприклад, отримати доступ до іншого керування, ніж у поточному потоці:

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

Там lblThresholdє Label і 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;
  }
}

Це працює для завдань (не потоків), які є кращим способом написання одночасного коду .



8

Я просто читаю відповіді, і це здається дуже гарячою темою. Зараз я використовую .NET 3.5 SP1 та Windows Forms.

Добре відома формула, чудово описана в попередніх відповідях, яка використовує властивість InvokeRequired, охоплює більшість випадків, але не весь пул.

Що робити, якщо ручка ще не створена?

The InvokeRequired властивість, як описано тут (посилання Control.InvokeRequired нерухомості в MSDN) повертає істину , якщо виклик був зроблений з потоку , який не є GUI потік, або хибним , якщо виклик був зроблений з GUI потоку, або якщо ручка була ще не створено.

Ви можете натрапити на виняток, якщо хочете, щоб модальна форма була показана та оновлена ​​іншим потоком. Оскільки ви хочете, щоб ця форма відображалася модально, ви можете зробити наступне:

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

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

Необов’язкові речі: Особисто я придумав, кодуючи таке:

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

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


1
Сюди приїхав, щоб знайти це, оскільки я також перевіряю IsHandleCreate. Ще одна властивість перевірити - IsDisposed. Якщо ваша форма розміщена, ви не можете викликати Invoke (). Якщо користувач закрив форму до того, як ваш фоновий потік зможе заповнити, ви не хочете, щоб він намагався передзвонити до інтерфейсу користувача, коли форма розміщена.
Jon

Я б сказав, що це погана ідея починати з ... Зазвичай, ви б показували дочірню форму негайно і мали панель прогресу або інший зворотній зв'язок під час обробки фону. Або ви спершу зробите всю обробку, а потім перейдете до нової форми при створенні. Якщо робити одночасно і те, і інше, це, як правило, матиме граничні переваги, але набагато менш рентабельний код.
Phil1970

Описаний сценарій враховує модальну форму, яка використовується як подання ходу завдання фонового потоку. Оскільки він повинен бути модальним, він повинен бути показаний за допомогою виклику методу Form.ShowDialog () . Тим самим ви запобігаєте виконанню коду, який слід за викликом, поки форма не закриється. Отже, якщо ви не можете запустити фонову нитку в інший спосіб із наведеного прикладу (і, звичайно, можна), ця форма повинна бути модально показана після запуску фонової нитки. У цьому випадку потрібно перевірити, чи буде створена ручка. Якщо вам не потрібна модальна форма, то це вже інша історія.
Сума
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.