Pertanyaan Bagaimana cara memperbarui GUI dari utas lainnya?


Apa cara paling sederhana untuk memperbarui Label dari utas lainnya?

Saya punya Form di thread1, dan dari situ saya memulai utas yang lain (thread2). Sementara thread2 sedang memproses beberapa file yang ingin saya perbarui Label pada Form dengan status saat ini thread2kerja.

Bagaimana saya bisa melakukannya?


1144
2018-03-19 09:37


asal


Jawaban:


Untuk .NET 2.0, inilah sedikit kode yang saya tulis yang benar-benar sesuai dengan yang Anda inginkan, dan berfungsi untuk properti apa pun di a 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 });
  }
}

Sebut saja seperti ini:

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

Jika Anda menggunakan .NET 3.0 atau di atasnya, Anda bisa menulis ulang metode di atas sebagai metode ekstensi dari Control kelas, yang kemudian akan menyederhanakan panggilan ke:

myLabel.SetPropertyThreadSafe("Text", status);

PERBARUI 05/10/2010:

Untuk .NET 3.0 Anda harus menggunakan kode ini:

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

yang menggunakan ekspresi LINQ dan lambda untuk memungkinkan sintaks yang lebih bersih, lebih sederhana, dan lebih aman:

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

Tidak hanya nama properti yang sekarang diperiksa pada waktu kompilasi, jenis properti juga, jadi tidak mungkin untuk (misalnya) menetapkan nilai string ke properti boolean, dan karenanya menyebabkan pengecualian runtime.

Sayangnya ini tidak menghentikan siapa pun dari melakukan hal-hal bodoh seperti lewat yang lain Controlproperti dan nilai, sehingga berikut ini akan dengan senang hati menyusun:

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

Oleh karena itu saya menambahkan pemeriksaan runtime untuk memastikan bahwa properti yang diteruskan benar-benar milik Control bahwa metode yang dipanggil. Tidak sempurna, tetapi masih jauh lebih baik daripada versi .NET 2.0.

Jika ada yang punya saran lebih lanjut tentang cara meningkatkan kode ini untuk keamanan waktu kompilasi, silakan komentar!


688
2018-03-19 10:37



Itu paling sederhana cara adalah metode anonim diteruskan ke 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

Perhatikan itu Invoke blokir eksekusi hingga selesai - ini adalah kode sinkron. Pertanyaannya tidak menanyakan kode asynchronous, tetapi ada banyak konten di Stack Overflow tentang menulis kode asinkron ketika Anda ingin mempelajarinya.


938
2018-03-19 10:17



Menangani kerja panjang

Sejak .NET 4.5 dan C # 5.0 kamu harus menggunakan Pola Asynchronous berbasis Tugas (TAP) bersama async-menunggu kata kunci di semua area (termasuk GUI):

TAP adalah pola desain asinkron yang direkomendasikan untuk pengembangan baru

dari pada Asynchronous Programming Model (APM) dan Pola Asinkron Berbasis Peristiwa (EAP) (yang terakhir termasuk BackgroundWorker Class).

Kemudian, solusi yang disarankan untuk pengembangan baru adalah:

  1. Pelaksanaan asynchronous dari event handler (Ya, itu saja):

    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. Implementasi utas kedua yang memberi tahu untaian UI:

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

Perhatikan hal-hal berikut:

  1. Kode pendek dan bersih ditulis secara berurutan tanpa callback dan untaian eksplisit.
  2. Tugas dari pada Benang.
  3. async kata kunci, yang memungkinkan untuk digunakan menunggu yang pada gilirannya mencegah pengendali event mencapai status penyelesaian sampai tugas selesai dan sementara itu tidak memblokir utas UI.
  4. Kelas kemajuan (lihat Antarmuka IProgress) yang mendukung Pemisahan Kekhawatiran (SoC) prinsip desain dan tidak memerlukan dispatcher eksplisit dan memohon. Ini menggunakan arus SinkronisasiKonteks dari tempat pembuatannya (di sini untaian UI).
  5. TaskCreationOptions.LongRunning petunjuk itu untuk tidak mengantrekan tugas ThreadPool.

Untuk contoh yang lebih verbose, lihat: Masa Depan C #: Hal-hal baik datang kepada mereka yang 'menunggu' oleh Joseph Albahari.

Lihat juga tentang Model UI Threading konsep.

Menangani pengecualian

Cuplikan di bawah ini adalah contoh bagaimana menangani pengecualian dan tombol toggle Enabled properti untuk mencegah beberapa klik selama eksekusi latar belakang.

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

333
2017-08-03 13:09



Variasi dari Marc Gravell's paling sederhana larutan untuk .NET 4:

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

Atau gunakan delegasi Aksi sebagai gantinya:

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

Lihat di sini untuk perbandingan keduanya: MethodInvoker vs Action untuk Kontrol.BeginInvoke


195
2018-05-29 18:51



Metode ekstensi api dan lupa untuk .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();
        }
    }
}

Ini bisa disebut menggunakan baris kode berikut:

this.UIThread(() => this.myLabel.Text = "Text Goes Here");

115
2017-08-27 21:10



Ini adalah cara klasik Anda harus melakukan ini:

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

Untaian pekerja Anda memiliki acara. Untaian UI Anda memulai utas lain untuk melakukan pekerjaan dan menghubungkan acara pekerja tersebut sehingga Anda dapat menampilkan status utas pekerja.

Kemudian di UI, Anda perlu melakukan lintas untaian untuk mengubah kontrol yang sebenarnya ... seperti label atau bilah kemajuan.


57
2018-03-19 10:31



Solusi sederhana adalah menggunakan Control.Invoke.

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

void updateGUI() {
    // update gui here
}

50
2018-03-19 09:46



Kode Threading sering buggy dan selalu sulit untuk diuji. Anda tidak perlu menulis kode threading untuk memperbarui antarmuka pengguna dari tugas latar belakang. Cukup gunakan BackgroundWorker kelas untuk menjalankan tugas dan tugasnya Laporan Kemajuan metode untuk memperbarui antarmuka pengguna. Biasanya, Anda hanya melaporkan persentase lengkap, tetapi ada kelebihan lainnya yang mencakup objek status. Berikut ini contoh yang hanya melaporkan objek string:

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

Tidak apa-apa jika Anda selalu ingin memperbarui bidang yang sama. Jika Anda memiliki pembaruan yang lebih rumit untuk dibuat, Anda dapat menentukan kelas untuk mewakili status UI dan menyebarkannya ke metode ReportProgress.

Satu hal terakhir, pastikan untuk mengatur WorkerReportsProgress bendera, atau ReportProgress metode akan sepenuhnya diabaikan.


41
2017-08-27 20:42



Sebagian besar jawaban digunakan Control.Invoke yang mana kondisi lomba menunggu untuk terjadi. Misalnya, pertimbangkan jawaban yang diterima:

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

Jika pengguna menutup formulir sebelumnya this.Invoke disebut (ingat, this adalah Form objek), sebuah ObjectDisposedException kemungkinan akan dipecat.

Solusinya adalah menggunakan SynchronizationContext, secara khusus SynchronizationContext.Current sebagai hamilton.danielb menyarankan (jawaban lain bergantung pada spesifik SynchronizationContext implementasi yang benar-benar tidak perlu). Saya akan sedikit memodifikasi kodenya untuk digunakan SynchronizationContext.Post daripada SynchronizationContext.Send meskipun (biasanya tidak diperlukan thread pekerja untuk menunggu):

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

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

Perhatikan bahwa pada .NET 4.0 dan ke atas Anda harus benar-benar menggunakan tugas untuk operasi async. Lihat n-san jawaban untuk pendekatan berbasis tugas yang setara (menggunakan TaskScheduler.FromCurrentSynchronizationContext).

Akhirnya, pada .NET 4.5 dan yang bisa Anda gunakan juga Progress<T> (yang pada dasarnya menangkap SynchronizationContext.Current setelah pembentukannya) seperti yang ditunjukkan oleh Ryszard Dżegan's untuk kasus di mana operasi yang berjalan lama perlu menjalankan kode UI saat masih berfungsi.


31
2018-05-24 08:45



Anda harus memastikan bahwa pembaruan terjadi pada utas yang benar; utas UI.

Untuk melakukan ini, Anda harus Meminta event-handler alih-alih memanggilnya secara langsung.

Anda dapat melakukan ini dengan meningkatkan acara Anda seperti ini:

(Kode diketik di sini di luar kepala saya, jadi saya belum memeriksa sintaks yang benar, dll., Tetapi itu harus membuat Anda pergi.)

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

Perhatikan bahwa kode di atas tidak akan berfungsi pada proyek WPF, karena kontrol WPF tidak mengimplementasikan ISynchronizeInvoke antarmuka.

Untuk memastikan bahwa kode di atas berfungsi dengan Formulir Windows dan WPF, dan semua platform lainnya, Anda dapat melihat AsyncOperation, AsyncOperationManager dan SynchronizationContext kelas.

Agar mudah meningkatkan acara dengan cara ini, saya telah membuat metode ekstensi, yang memungkinkan saya menyederhanakan peningkatan acara hanya dengan menelepon:

MyEvent.Raise(this, EventArgs.Empty);

Tentu saja, Anda juga dapat menggunakan kelas BackGroundWorker, yang akan menguraikan masalah ini untuk Anda.


30
2018-03-19 09:45