Bagaimana cara saya memperbarui GUI dari utas lain?

1393

Mana cara paling sederhana untuk memperbarui Labeldari yang lain Thread?

  • Saya memiliki Formrunning thread1, dan dari situ saya memulai utas lainnya ( thread2).

  • Sementara thread2sedang memproses beberapa file saya ingin memperbarui Labeldi Formdengan status thread2pekerjaan 's.

Bagaimana saya bisa melakukan itu?

Kekejaman
sumber
25
Tidak .net 2.0+ memiliki kelas BackgroundWorker hanya untuk ini. Itu UI thread sadar. 1. Buat BackgroundWorker 2. Tambahkan dua delegasi (satu untuk diproses, dan satu untuk penyelesaian)
Preet Sangha
13
mungkin sedikit terlambat: codeproject.com/KB/cs/Threadsafe_formupdating.aspx
MichaelD
4
Lihat jawaban untuk .NET 4.5 dan C # 5.0: stackoverflow.com/a/18033198/2042090
Ryszard Dżegan
5
Pertanyaan ini tidak berlaku untuk Gtk # GUI. Untuk Gtk # lihat ini dan jawaban ini .
hlovdal
Hati-hati: jawaban atas pertanyaan ini sekarang berantakan berantakan dari OT ("inilah yang saya lakukan untuk aplikasi WPF saya") dan artefak .NET 2.0 historis.
Marc L.

Jawaban:

768

Untuk .NET 2.0, berikut ini sedikit kode yang saya tulis yang melakukan persis seperti yang Anda inginkan, dan berfungsi untuk semua properti di 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 Controlkelas, yang kemudian akan menyederhanakan panggilan ke:

myLabel.SetPropertyThreadSafe("Text", status);

PEMBARUAN 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 sekarang diperiksa pada waktu kompilasi, tipe 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 melewati Controlproperti dan nilai orang lain, jadi yang berikut akan dengan senang hati dikompilasi:

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

Oleh karena itu saya menambahkan cek runtime untuk memastikan bahwa properti yang lewat benar-benar milik Controlmetode 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 berkomentar!

Ian Kemp
sumber
3
Ada kasus-kasus ketika this.GetType () mengevaluasi sama dengan propertyInfo.ReflectedType (misalnya LinkLabel di WinForms). Saya tidak punya pengalaman C # yang besar, tapi saya pikir syarat pengecualiannya adalah: if (propertyInfo == null || ([email protected] (). IsSubclassOf (propertyInfo.ReflectedType) && @ this.GetType ( )! = propertyInfo.ReflectedType) || @ this.GetType (). GetProperty (propertyInfo.Name, propertyInfo.PropertyType) == null)
Corvin
9
@lan dapat ini SetControlPropertyThreadSafe(myLabel, "Text", status)dipanggil dari modul atau kelas atau bentuk lain
Smith
71
Solusi yang diberikan tidak perlu rumit. Lihat solusi Marc Gravell, atau solusi Zaid Masud, jika Anda menghargai kesederhanaan.
Frank Hileman
8
Solusi ini tidak memboroskan banyak sumber daya jika Anda memperbarui banyak properti karena setiap Invoke menghabiskan banyak sumber daya. Saya rasa ini bukan fitur Thread Safety yang dimaksudkan. Jangan enkapsulasi tindakan pembaruan UI Anda dan Aktifkan SEKALI (dan bukan per properti)
Konsol
4
Mengapa Anda menggunakan kode ini di atas komponen BackgroundWorker?
Andy
1080

Cara paling sederhana adalah metode anonim yang 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 bahwa Invokeblok eksekusi sampai selesai - ini adalah kode sinkron. Pertanyaannya tidak menanyakan tentang kode asinkron, tetapi ada banyak konten di Stack Overflow tentang penulisan kode asinkron ketika Anda ingin mempelajarinya.

Marc Gravell
sumber
8
Melihat sebagai OP belum disebutkan setiap kelas / contoh kecuali bentuk, yang tidak default buruk ...
Marc Gravell
39
Jangan lupa kata kunci "ini" mereferensikan kelas "Kontrol".
AZ.
8
@codecompleting itu aman juga, dan kita sudah tahu kita sedang pekerja, jadi mengapa memeriksa sesuatu yang kita tahu
Marc Gravell
4
@ Dragouf tidak benar-benar - salah satu titik menggunakan metode ini adalah bahwa Anda sudah tahu bagian mana yang dijalankan pada pekerja, dan yang berjalan pada utas UI. Tidak perlu diperiksa.
Marc Gravell
3
@ Joan.bdm tidak ada konteks yang cukup dekat bagi saya untuk mengomentari itu
Marc Gravell
401

Menangani pekerjaan panjang

Sejak .NET 4.5 dan C # 5.0 Anda harus menggunakan Pola Asinkron (TAP) berbasis Tugas bersama dengan async - tunggu kata kunci di semua area (termasuk GUI):

TAP adalah pola desain asinkron yang direkomendasikan untuk pengembangan baru

alih-alih Asynchronous Programming Model (APM) dan Asynchronous Pattern (EAP) berbasis peristiwa (yang terakhir termasuk Kelas BackgroundWorker ).

Kemudian, solusi yang disarankan untuk pengembangan baru adalah:

  1. Implementasi asinkron dari pengendali event (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 utas 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 yang berikut ini:

  1. Kode pendek dan bersih ditulis secara berurutan tanpa panggilan balik dan utas eksplisit.
  2. Tugas alih-alih Utas .
  3. async kata kunci, yang memungkinkan untuk menggunakan menunggu yang pada gilirannya mencegah event handler dari mencapai status penyelesaian sampai tugas selesai dan sementara itu tidak memblokir utas UI.
  4. Kelas Progress (lihat IProgress Interface ) yang mendukung prinsip desain Separation of Concerns (SoC) dan tidak memerlukan operator dan pemohon yang eksplisit. Ia menggunakan SynchronizationContext saat ini dari tempat pembuatannya (di sini utas UI).
  5. TaskCreationOptions.LongRunning yang mengisyaratkan untuk tidak mengantri tugas ke ThreadPool .

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

Lihat juga tentang konsep Model Threading UI .

Menangani pengecualian

Cuplikan di bawah ini adalah contoh cara menangani pengecualian dan beralih Enabledproperti tombol 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...");
    }
}
Ryszard Dżegan
sumber
2
Jika SecondThreadConcern.LongWork()melempar pengecualian, dapatkah itu ditangkap oleh utas UI? Ini adalah pos yang bagus, btw.
kdbanman
2
Saya telah menambahkan bagian tambahan pada jawaban untuk memenuhi persyaratan Anda. Salam.
Ryszard Dżegan
3
Kelas ExceptionDispatchInfo bertanggung jawab atas mukjizat rethrowing exception latar belakang pada utas UI dalam pola async-waiting.
Ryszard Dżegan
1
Apakah hanya saya yang berpikir bahwa cara melakukan ini lebih verbose daripada hanya memohon Invoke / Begin ?!
MeTitus
2
Task.Delay(500).Wait()? Apa gunanya membuat Tugas untuk hanya memblokir utas saat ini? Anda seharusnya tidak pernah memblokir utas utas!
Yarik
236

Variasi solusi paling sederhana dari Marc Gravell untuk .NET 4:

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

Atau gunakan delegasi Tindakan sebagai gantinya:

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

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

Zaid Masud
sumber
1
apa 'kontrol' dalam contoh ini? Kontrol UI saya? Mencoba menerapkan ini di WPF pada kontrol label, dan Invoke bukan anggota label saya.
Dbloom
Bagaimana dengan metode ekstensi seperti @styxriver stackoverflow.com/a/3588137/206730 ?
Kiquenet
menyatakan 'Aksi y;' di dalam kelas atau metode mengubah properti teks dan memperbarui teks dengan potongan kode 'yourcontrol.Invoke (y = () => yourcontrol.Text = "teks baru");'
Antonio Leite
4
@Dbloom itu bukan anggota karena hanya untuk WinForms. Untuk WPF Anda menggunakan Dispatcher.Invoke
sLw
1
Saya mengikuti solusi ini tetapi kadang-kadang UI saya tidak diperbarui. Saya menemukan bahwa saya perlu this.refresh()memaksa membatalkan dan mengecat ulang GUI .. jika itu membantu ..
Rakibul Haq
137

Api dan lupakan metode ekstensi 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 dapat dipanggil menggunakan baris kode berikut:

this.UIThread(() => this.myLabel.Text = "Text Goes Here");
StyxRiver
sumber
5
Apa gunanya penggunaan @ ini? Bukankah "kontrol" setara? Apakah ada manfaat untuk @ini?
argyle
14
@ jeromeyers - Ini @thishanyalah nama variabel, dalam hal ini referensi ke kontrol saat ini memanggil ekstensi. Anda bisa mengubahnya menjadi sumber, atau apa pun yang mengapung perahu Anda. Saya menggunakan @this, karena mengacu pada 'Kontrol ini' yang memanggil ekstensi dan konsisten (di kepala saya, setidaknya) dengan menggunakan kata kunci 'ini' dalam kode normal (non-ekstensi).
StyxRiver
1
Ini hebat, mudah dan bagi saya solusi terbaik. Anda dapat memasukkan semua pekerjaan yang harus Anda lakukan di utas ui. Contoh: this.UIThread (() => {txtMessage.Text = message; listBox1.Items.Add (message);});
Otomatis
1
Saya sangat suka solusi ini. Kecil nit: Saya akan nama metode ini OnUIThreadbukan UIThread.
ToolmakerSteve
2
Itu sebabnya saya menamai ekstensi ini RunOnUiThread. Tapi itu hanya selera pribadi.
Grisgram
66

Ini adalah cara klasik yang harus Anda lakukan 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;
        }
    }
}

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

Kemudian di UI Anda harus memotong utas untuk mengubah kontrol yang sebenarnya ... seperti label atau bilah kemajuan.

Telah
sumber
62

Solusi sederhana adalah menggunakan Control.Invoke.

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

void updateGUI() {
    // update gui here
}
OregonGhost
sumber
dilakukan dengan baik untuk kesederhanaan! tidak hanya sederhana, tetapi juga berfungsi dengan baik! Saya benar-benar tidak mengerti mengapa microsoft tidak dapat membuatnya lebih sederhana seperti yang seharusnya! untuk memanggil 1 baris di utas utama, kita harus menulis beberapa fungsi!
MBH
1
@MBH Setuju. BTW, apakah Anda melihat jawaban stackoverflow.com/a/3588137/199364 di atas, yang menentukan metode ekstensi? Lakukan itu sekali dalam kelas utilitas kustom, maka tidak perlu peduli lagi bahwa Microsoft tidak melakukannya untuk kita :)
ToolmakerSteve
@ToolmakerSteve Thats persis seperti apa itu! Anda benar kami dapat menemukan cara, tetapi maksud saya dari sudut KERING (jangan ulangi sendiri), masalah yang memiliki solusi umum, dapat diselesaikan oleh mereka dengan upaya minimal oleh Microsoft yang akan menghemat banyak waktu untuk programmer :)
MBH
47

Kode Threading sering bermasalah dan selalu sulit untuk diuji. Anda tidak perlu menulis kode threading untuk memperbarui antarmuka pengguna dari tugas latar belakang. Cukup gunakan kelas BackgroundWorker untuk menjalankan tugas dan metode ReportProgress untuk memperbarui antarmuka pengguna. Biasanya, Anda hanya melaporkan persentase selesai, tetapi ada kelebihan lain yang mencakup objek negara. Berikut ini contoh yang baru saja 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 masalah jika Anda selalu ingin memperbarui bidang yang sama. Jika Anda memiliki pembaruan yang lebih rumit untuk dibuat, Anda bisa mendefinisikan kelas untuk mewakili negara UI dan meneruskannya ke metode ReportProgress.

Satu hal terakhir, pastikan untuk mengatur WorkerReportsProgressbendera, atau ReportProgressmetode ini akan diabaikan sepenuhnya.

Don Kirkby
sumber
2
Pada akhir pemrosesan, dimungkinkan juga untuk memperbarui antarmuka pengguna melalui backgroundWorker1_RunWorkerCompleted.
DavidRR
41

Sebagian besar jawaban menggunakan Control.Invokeyang merupakan kondisi lomba yang 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 sebelum this.Invokedipanggil (ingat, thisadalah Formobjek), ObjectDisposedExceptionkemungkinan akan dipecat.

Solusinya adalah menggunakan SynchronizationContext, khususnya SynchronizationContext.Currentseperti yang disarankan hamilton.danielb (jawaban lain bergantung pada SynchronizationContextimplementasi spesifik yang sama sekali tidak perlu). Saya akan sedikit memodifikasi kodenya untuk digunakan SynchronizationContext.Postdaripada SynchronizationContext.Sendmeskipun (karena biasanya tidak perlu utas 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 yang lebih tinggi, Anda harus benar-benar menggunakan tugas untuk operasi async. Lihat jawaban n-san untuk pendekatan berbasis tugas yang setara (menggunakan TaskScheduler.FromCurrentSynchronizationContext).

Akhirnya, pada .NET 4.5 dan yang lebih tinggi Anda juga dapat menggunakan Progress<T>(yang pada dasarnya menangkap SynchronizationContext.Currentsaat pembuatannya) seperti yang ditunjukkan oleh Ryszard Dżegan untuk kasus-kasus di mana operasi jangka panjang perlu menjalankan kode UI saat masih bekerja.

Ohad Schneider
sumber
37

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

Untuk melakukan ini, Anda harus Meminta pengatur acara alih-alih memanggilnya secara langsung.

Anda dapat melakukan ini dengan meningkatkan acara Anda seperti ini:

(Kode ini diketikkan di sini dari kepala saya, jadi saya belum memeriksa sintaks yang benar, dll., Tetapi itu akan 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 ISynchronizeInvokeantarmuka.

Untuk memastikan bahwa kode di atas berfungsi dengan Windows Forms dan WPF, dan semua platform lainnya, Anda dapat melihat pada AsyncOperation, AsyncOperationManagerdan SynchronizationContextkelas.

Untuk meningkatkan acara dengan mudah seperti ini, saya telah membuat metode ekstensi, yang memungkinkan saya untuk menyederhanakan acara dengan hanya menelepon:

MyEvent.Raise(this, EventArgs.Empty);

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

Frederik Gheysels
sumber
Memang, tapi saya tidak suka 'mengacaukan' kode GUI saya dengan masalah ini. GUI saya seharusnya tidak peduli apakah perlu untuk memohon atau tidak. Dengan kata lain: saya tidak berpikir bahwa itu adalah responsability dari GUI untuk melakukan konteks-swithc.
Frederik Gheysels
1
Memecah delegasi, dll, tampaknya berlebihan - mengapa tidak hanya: SynchronizationContext.Current.Send (delegate {MyEvent (...);}, null);
Marc Gravell
Apakah Anda selalu memiliki akses ke SynchronizationContext? Bahkan jika kelas Anda berada di kelas lib?
Frederik Gheysels
29

Anda harus mengaktifkan metode pada utas GUI. Anda dapat melakukannya dengan memanggil Control.Invoke.

Sebagai contoh:

delegate void UpdateLabelDelegate (string message);

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

    MyLabelControl.Text = message;
}
Kieron
sumber
1
Baris invoke memberi saya kesalahan kompiler. Metode overload terbaik cocok untuk 'System.Windows.Forms.Control.Invoke (System.Delegate, object [])' memiliki beberapa argumen yang tidak valid
CruelIO
28

Karena sepele dari skenario saya benar-benar akan memiliki polling UI untuk statusnya. Saya pikir Anda akan menemukan bahwa itu bisa sangat elegan.

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

Pendekatan ini menghindari operasi marshaling yang diperlukan saat menggunakan ISynchronizeInvoke.Invokedan ISynchronizeInvoke.BeginInvokemetode. Tidak ada yang salah dengan menggunakan teknik marshaling, tetapi ada beberapa peringatan yang perlu Anda waspadai.

  • Pastikan Anda tidak menelepon BeginInvoketerlalu sering atau itu bisa membanjiri pompa pesan.
  • Memanggil Invokeutas pekerja adalah panggilan pemblokiran. Ini akan menghentikan sementara pekerjaan yang dilakukan di utas itu.

Strategi yang saya usulkan dalam jawaban ini membalikkan peran komunikasi utas. Alih-alih pekerja thread mendorong data jajak pendapat UI untuk itu. Ini adalah pola umum yang digunakan dalam banyak skenario. Karena semua yang ingin Anda lakukan adalah menampilkan informasi kemajuan dari utas pekerja maka saya pikir Anda akan menemukan bahwa solusi ini adalah alternatif yang bagus untuk solusi marshaling. Ini memiliki keuntungan sebagai berikut.

  • UI dan utas pekerja tetap digabungkan secara longgar sebagai lawan dari Control.Invokeatau Control.BeginInvokependekatan yang secara ketat memasangkannya.
  • Utas UI tidak akan menghalangi kemajuan utas pekerja.
  • Utas pekerja tidak dapat mendominasi waktu utas UI menghabiskan pembaruan.
  • Interval di mana UI dan utas pekerja melakukan operasi dapat tetap independen.
  • Utas pekerja tidak dapat membanjiri pompa pesan utas UI.
  • Utas UI dapat menentukan kapan dan seberapa sering UI diperbarui.
Brian Gideon
sumber
3
Ide bagus. Satu-satunya hal yang tidak Anda sebutkan adalah bagaimana Anda membuang timer dengan benar begitu WorkerThread selesai. Catatan ini dapat menyebabkan masalah ketika aplikasi berakhir (yaitu pengguna menutup aplikasi). Apakah Anda punya ide bagaimana menyelesaikannya?
Matt
@Mat Alih-alih menggunakan penangan anonim untuk Elapsedacara, Anda menggunakan metode anggota sehingga Anda dapat menghapus timer ketika formulir dibuang ...
Phil1970
@ Phil1970 - Poin bagus. Maksudmu seperti System.Timers.ElapsedEventHandler handler = (s, a) => { MyProgressLabel.Text = m_Text; };dan menugaskannya m_Timer.Elapsed += handler;, kemudian dalam konteks buang melakukan apa m_Timer.Elapsed -= handler;aku benar? Dan untuk pembuangan / penutupan mengikuti saran seperti yang dibahas di sini .
Matt
27

Tidak ada satu pun barang Invoke dalam jawaban sebelumnya yang diperlukan.

Anda perlu melihat 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
}
Jon H
sumber
4
Menurut Anda apa yang digunakan metode Post di bawah tenda? :)
increddibelly
23

Yang ini mirip dengan solusi di atas menggunakan .NET Framework 3.0, tetapi itu memecahkan masalah dukungan keamanan waktu kompilasi .

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

Menggunakan:

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

Kompiler akan gagal jika pengguna melewati tipe data yang salah.

this.lblTimeDisplay.SetPropertyValue(a => a.Visible, "sometext");
Francis
sumber
23

Selamatkan! Setelah mencari pertanyaan ini, saya menemukan jawaban oleh FrankG dan Oregon Ghost sebagai yang termudah yang paling berguna bagi saya. Sekarang, saya kode dalam Visual Basic dan menjalankan cuplikan ini melalui konverter; jadi saya tidak yakin bagaimana hasilnya.

Saya memiliki bentuk dialog yang disebut form_Diagnostics,yang memiliki kotak richtext, yang disebut updateDiagWindow,yang saya gunakan sebagai semacam tampilan logging. Saya harus dapat memperbarui teksnya dari semua utas. Baris tambahan memungkinkan jendela untuk secara otomatis menggulir ke baris terbaru.

Jadi, saya sekarang dapat memperbarui tampilan dengan satu baris, dari mana saja di seluruh program dengan cara yang menurut Anda akan berfungsi tanpa threading:

  form_Diagnostics.updateDiagWindow(whatmessage);

Kode Utama (letakkan ini di dalam kode kelas formulir Anda):

#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
sumber
21

Untuk banyak tujuan, sesederhana ini:

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

"serviceGUI ()" adalah metode level GUI dalam formulir (ini) yang dapat mengubah kontrol sebanyak yang Anda inginkan. Panggil "updateGUI ()" dari utas lainnya. Parameter dapat ditambahkan untuk memberikan nilai, atau (mungkin lebih cepat) menggunakan variabel lingkup kelas dengan kunci yang diperlukan jika ada kemungkinan bentrokan antara thread mengaksesnya yang dapat menyebabkan ketidakstabilan. Gunakan BeginInvoke sebagai ganti Invoke jika utas non-GUI sangat menentukan waktu (ingatlah peringatan Brian Gideon).

Frankg
sumber
21

Ini dalam variasi C # 3.0 dari solusi Ian Kemp saya:

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

Anda menyebutnya seperti ini:

myButton.SetPropertyInGuiThread(b => b.Text, "Click Me!")
  1. Itu menambahkan nol-memeriksa hasil "sebagai MemberExpression".
  2. Ini meningkatkan keamanan tipe statis.

Kalau tidak, yang asli adalah solusi yang sangat bagus.

Rotaerk
sumber
21
Label lblText; //initialized elsewhere

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

   lblText.Text = text;           
}

Catatan yang BeginInvoke()lebih disukai daripada Invoke()karena cenderung menyebabkan kebuntuan (namun, ini bukan masalah di sini ketika hanya menetapkan teks ke label):

Saat menggunakan Invoke()Anda sedang menunggu metode untuk kembali. Sekarang, mungkin Anda melakukan sesuatu dalam kode yang dipanggil yang perlu menunggu utasnya, yang mungkin tidak segera jelas jika itu terkubur dalam beberapa fungsi yang Anda panggil, yang itu sendiri dapat terjadi secara tidak langsung melalui event handler. Jadi Anda akan menunggu utasnya, utas itu akan menunggu Anda dan Anda menemui jalan buntu.

Ini sebenarnya menyebabkan beberapa perangkat lunak kami dirilis untuk menggantung. Itu cukup mudah untuk memperbaiki dengan mengganti Invoke()dengan BeginInvoke(). Kecuali Anda memiliki kebutuhan untuk operasi sinkron, yang mungkin terjadi jika Anda membutuhkan nilai balik, gunakan BeginInvoke().

ILoveFortran
sumber
20

Ketika saya menemukan masalah yang sama, saya mencari bantuan dari Google, tetapi alih-alih memberi saya solusi sederhana, itu lebih membingungkan saya dengan memberikan contoh MethodInvokerdan bla bla bla. Jadi saya memutuskan untuk menyelesaikannya sendiri. Inilah solusi saya:

Buat delegasi seperti ini:

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
}

Anda dapat memanggil fungsi ini di utas baru seperti ini

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

Jangan bingung dengan Thread(() => .....). Saya menggunakan fungsi anonim atau ekspresi lambda ketika saya mengerjakan utas. Untuk mengurangi baris kode Anda dapat menggunakan ThreadStart(..)metode yang juga tidak seharusnya saya jelaskan di sini.

ahmar
sumber
17

Cukup gunakan sesuatu seperti ini:

 this.Invoke((MethodInvoker)delegate
            {
                progressBar1.Value = e.ProgressPercentage; // runs on UI thread
            });
Hassan Shouman
sumber
Jika sudah e.ProgressPercentage, bukankah Anda sudah berada di utas UI dari metode yang Anda panggil ini?
LarsTech
Acara ProgressChanged berjalan di utas UI. Itulah salah satu kemudahan menggunakan BackgroundWorker. Acara Selesai berjalan di gui juga. Satu-satunya hal yang berjalan di utas non-UI adalah metode DoWork.
LarsTech
15

Anda dapat menggunakan delegasi yang sudah ada Action:

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

Versi saya adalah memasukkan satu baris "mantra" rekursif:

Tanpa argumen:

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

        // Your code!
    }

Untuk fungsi yang memiliki argumen:

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

ITULAH ITU .


Beberapa argumentasi : Biasanya buruk untuk keterbacaan kode untuk menempatkan {} setelah suatuif () pernyataan dalam satu baris. Tetapi dalam hal ini "mantra" semua-sama-rutin. Itu tidak merusak pembacaan kode jika metode ini konsisten terhadap proyek. Dan itu menyimpan kode Anda dari sampah sembarangan (satu baris kode, bukan lima).

Seperti yang Anda lihat, if(InvokeRequired) {something long}Anda tahu "fungsi ini aman untuk dipanggil dari utas lainnya".

MajesticRa
sumber
13

Coba segarkan label menggunakan ini

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

    public static void Refresh(this UIElement uiElement)
    {
        uiElement.Dispatcher.Invoke(DispatcherPriority.Render, EmptyDelegate);
    }
}
Ivaylo Slavov
sumber
Apakah itu untuk Formulir Windows ?
Kiquenet
13

Buat variabel kelas:

SynchronizationContext _context;

Atur di konstruktor yang membuat UI Anda:

var _context = SynchronizationContext.Current;

Saat Anda ingin memperbarui label:

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

Anda harus menggunakan invoke dan delegate

private delegate void MyLabelDelegate();
label1.Invoke( new MyLabelDelegate(){ label1.Text += 1; });
A. Zalonis
sumber
12

Sebagian besar jawaban lain sedikit rumit bagi saya untuk pertanyaan ini (saya baru di C #), jadi saya menulis milik saya:

Saya memiliki aplikasi WPF dan telah mendefinisikan pekerja seperti di bawah ini:

Isu:

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

Larutan:

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

Saya belum mencari tahu apa arti kalimat di atas, tetapi berhasil.

Untuk WinForms :

Larutan:

txtLog.Invoke((MethodInvoker)delegate
{
    txtLog.Text = "my message";
});
Manohar Reddy Poreddy
sumber
Pertanyaannya adalah tentang WinForms, bukan WPF.
Marc L.
Terima kasih. Menambahkan solusi WinForms di atas.
Manohar Reddy Poreddy
... yang hanya merupakan salinan dari banyak jawaban lain untuk pertanyaan yang sama, tapi oke. Mengapa tidak menjadi bagian dari solusi dan hapus saja jawaban Anda?
Marc L.
hmm, betullah Anda, jika saja, Anda membaca jawaban saya dengan perhatian, bagian awal (alasan mengapa saya menulis jawaban), dan semoga dengan sedikit perhatian Anda melihat ada seseorang yang memiliki masalah yang sama persis & terangkat hari ini untuk jawaban sederhana saya, dan dengan lebih banyak perhatian jika Anda dapat melihat kisah nyata tentang mengapa semua ini terjadi, google mengirim saya ke sini bahkan ketika saya mencari wpf. Tentu karena Anda melewatkan 3 alasan ini lebih atau kurang jelas, saya bisa mengerti mengapa Anda tidak akan menghapus downvote Anda. Alih-alih membersihkan yang oke, buat sesuatu yang baru yang jauh lebih sulit.
Manohar Reddy Poreddy
8

Cara termudah yang saya pikirkan:

   void Update()
   {
       BeginInvoke((Action)delegate()
       {
           //do your update
       });
   }
Dengan mudah Semenov
sumber
8

Misalnya, akses kontrol selain dari utas saat ini:

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

Ada lblThresholdLabel dan Speed_Thresholdvariabel global.

Da Xiong
sumber
8

Saat Anda berada di utas UI, Anda dapat meminta penjadwalan tugas konteks sinkronisasi. Ini akan memberi Anda TaskScheduler yang menjadwalkan semuanya di utas UI.

Kemudian, Anda dapat mengaitkan tugas Anda sehingga saat hasilnya siap maka tugas lain (yang dijadwalkan pada utas UI) mengambilnya dan menetapkannya ke label.

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

Ini berfungsi untuk tugas (bukan utas) yang merupakan cara yang disukai untuk menulis kode bersamaan sekarang .

nosalan
sumber
1
Memanggil Task.Startbiasanya bukan praktik yang baik blogs.msdn.com/b/pfxteam/archive/2012/01/14/10256832.aspx
Ohad Schneider
8

Saya baru saja membaca jawabannya dan ini tampaknya menjadi topik yang sangat panas. Saya saat ini menggunakan .NET 3.5 SP1 dan Windows Forms.

Rumus terkenal sangat dijelaskan dalam jawaban sebelumnya yang menggunakan InvokeRequired properti mencakup sebagian besar kasus, tetapi tidak seluruh kumpulan.

Bagaimana jika Handel belum dibuat?

The InvokeRequired properti, seperti yang dijelaskan di sini (referensi Control.InvokeRequired Properti untuk MSDN) mengembalikan nilai true jika panggilan itu dibuat dari benang yang tidak thread GUI, palsu baik jika panggilan itu dibuat dari benang GUI, atau jika Handle adalah belum dibuat.

Anda dapat menemukan pengecualian jika Anda ingin memiliki formulir modal ditampilkan dan diperbarui oleh utas lainnya. Karena Anda ingin formulir itu ditampilkan secara kecil, Anda dapat melakukan hal berikut:

private MyForm _gui;

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

Dan delegasi dapat memperbarui Label pada GUI:

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

Hal ini dapat menyebabkan InvalidOperationException jika operasi sebelum update label "mengambil sedikit waktu" (membacanya dan menafsirkannya sebagai penyederhanaan) dari waktu yang dibutuhkan untuk thread GUI untuk membuat Form 's Handle . Ini terjadi dalam metode ShowDialog () .

Anda juga harus memeriksa untuk Handle seperti ini:

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

Anda dapat menangani operasi untuk melakukan jika Menangani belum dibuat: Anda bisa mengabaikan pembaruan GUI (seperti yang ditunjukkan pada kode di atas) atau Anda bisa menunggu (lebih berisiko). Ini harus menjawab pertanyaan.

Hal-hal opsional: Secara pribadi saya membuat kode berikut:

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

Saya memberi makan formulir saya yang diperbarui oleh utas lain dengan turunan dari ThreadSafeGuiCommand ini , dan saya mendefinisikan metode yang memperbarui GUI (dalam Formulir saya) seperti ini:

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

Dengan cara ini saya cukup yakin bahwa saya akan memperbarui GUI saya, utas apa pun yang akan melakukan panggilan, secara opsional menunggu jumlah waktu yang ditentukan dengan baik (batas waktu).

Sume
sumber
1
Datang ke sini untuk menemukan ini, karena saya juga memeriksa IsHandleCreated. Satu properti lain untuk diperiksa adalah IsDisposed. Jika formulir Anda dibuang, Anda tidak dapat memanggil Invoke () di atasnya. Jika pengguna menutup formulir sebelum utas latar belakang Anda bisa menyelesaikan, Anda tidak ingin mencoba memanggil kembali ke UI ketika formulir dibuang.
Jon
Saya akan mengatakan bahwa itu adalah ide yang buruk untuk memulai dengan ... Biasanya, Anda akan segera menunjukkan formulir anak dan memiliki progress bar atau umpan balik lainnya saat melakukan pemrosesan latar belakang. Atau Anda akan melakukan semua pemrosesan terlebih dahulu dan kemudian meneruskan hasilnya ke bentuk baru saat pembuatan. Melakukan keduanya pada saat yang sama umumnya akan memiliki manfaat marjinal tetapi kode lebih sedikit dapat dipertahankan.
Phil1970
Skenario yang dijelaskan memperhitungkan formulir modal yang digunakan sebagai tampilan kemajuan pekerjaan latar belakang. Karena itu harus modal, itu harus ditunjukkan dengan memanggil metode Form.ShowDialog () . Dengan melakukan ini, Anda mencegah kode Anda yang mengikuti panggilan dieksekusi sampai formulir ditutup. Jadi, kecuali Anda dapat memulai utas latar belakang secara berbeda dari contoh yang diberikan (dan, tentu saja, Anda bisa) formulir ini harus ditampilkan secara moderat setelah utas latar dimulai. Dalam hal ini, Anda perlu memeriksa untuk Handle yang akan dibuat. Jika Anda tidak memerlukan formulir modal, maka itu cerita lain.
Sume