GUI'yi başka bir iş parçacığından nasıl güncelleyebilirim?


1392

Birini Labeldiğerinden güncellemenin en basit yolu Threadhangisidir?

  • Ben üzerinde Formçalışan thread1ve bundan başka bir iş parçacığı ( thread2) başlıyorum .

  • İken thread2bazı dosyaları işliyor Ben güncellemek istiyorum Labelüzerine Formmevcut durumu ile thread2bireyin çalışması.

Bunu nasıl yapabilirim?


25
.Net 2.0+ sadece bunun için BackgroundWorker sınıfına sahip değildir. UI iş parçacığı farkında. 1. Bir BackgroundWorker oluşturun 2. İki delege ekleyin (biri işlemek için, diğeri tamamlanmak için)
Sangha'yı


4
.NET 4.5 ve C # 5.0 için cevaba bakın: stackoverflow.com/a/18033198/2042090
Ryszard Dżegan

5
Bu soru Gtk # GUI için geçerli değildir. Gtk # için şu ve bu cevaba bakınız .
hlovdal

Dikkat: Bu soruya verilen cevaplar artık OT'nin karmaşık bir karmaşası ("WPF uygulamam için yaptığım şey") ve geçmiş .NET 2.0 yapıları.
Marc L.

Yanıtlar:


768

.NET 2.0 için, tam olarak ne istediğinizi yapan ve herhangi bir mülk için çalışan güzel bir kod parçası 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 });
  }
}

Buna şöyle deyin:

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

.NET 3.0 veya üstünü kullanıyorsanız, yukarıdaki yöntemi Controlsınıfın bir uzantı yöntemi olarak yeniden yazabilirsiniz;

myLabel.SetPropertyThreadSafe("Text", status);

GÜNCELLEME 05/10/2010:

.NET 3.0 için bu kodu kullanmalısınız:

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

daha temiz, daha basit ve daha güvenli bir sözdizimi sağlamak için LINQ ve lambda ifadelerini kullanan:

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

Özellik adı artık derleme zamanında denetlenmiyor, aynı zamanda mülkün türü de, bu nedenle (örneğin) bir boolean özelliğine bir dize değeri atamak ve dolayısıyla bir çalışma zamanı istisnasına neden olmak imkansız.

Ne yazık ki bu kimsenin bir başkasının Controlmülkünü ve değerini geçirmek gibi aptalca şeyler yapmasını engellemez , bu nedenle aşağıdakiler mutlu bir şekilde derlenir:

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

Bu nedenle, pass-in özelliğinin gerçekten ait olduğundan emin olmak için çalışma zamanı denetimlerini ekledim Control yöntemin çağrıldığına . Mükemmel değil, ama yine de .NET 2.0 sürümünden çok daha iyi.

Herhangi birinin derleme zamanı güvenliği için bu kodu nasıl geliştireceği hakkında başka önerileri varsa, lütfen yorum yapın!


3
This.GetType () öğesinin propertyInfo.ReflectedType ile aynı şekilde değerlendirildiği durumlar vardır (örneğin, WinForms'daki LinkLabel). Büyük bir C # deneyimim yok, ancak özel durumun olması gerektiğini düşünüyorum: if (propertyInfo == null || (!@this.GetType (). IsSubclassOf (propertyInfo.ReflectedType) && @ this.GetType ( )! = propertyInfo.ReflectedType) || @ this.GetType (). GetProperty (propertyInfo.Name, propertyInfo.PropertyType) == null)
Corvin

9
@lan bu SetControlPropertyThreadSafe(myLabel, "Text", status)başka bir modül veya sınıf veya formdan çağrılabilir
Smith

71
Sağlanan çözelti gereksiz yere karmaşıktır. Sadeliğe değer veriyorsanız, Marc Gravell'in çözümüne veya Zaid Masud'un çözümüne bakın.
Frank Hileman

8
Her Invoke çok fazla kaynağa mal olduğu için birden fazla özelliği güncellerseniz bu çözüm tonlarca kaynağı boşa harcar. İş parçacığı güvenliği özelliğinin bu şekilde tasarlandığını sanmıyorum. Kullanıcı arayüzü güncelleme işlemlerinizi Kapsülleyin ve ONCE (her mülk için değil)
Konsol

4
Neden bu kodu BackgroundWorker bileşeni üzerinde kullanasınız?
Andy

1080

En basit yol anonim bir yöntemdir 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

InvokeTamamlanıncaya kadar yürütmeyi engellediğine dikkat edin - bu senkronize koddur. Soru eşzamansız kod sormuyor, ancak Stack Overflow'da bu kodu öğrenmek istediğinizde eşzamansız kod yazma hakkında çok fazla içerik var.


8
OP olarak görülmesi , form dışında herhangi bir sınıftan / örnekten bahsetmedi , bu kötü bir varsayılan değil ...
Marc Gravell

39
"This" anahtar kelimesinin "Control" sınıfına atıfta bulunduğunu unutmayın.
AZ.

8
@codecompleting her iki durumda da güvenlidir ve zaten bir çalışan olduğumuzu biliyoruz, neden bildiğimiz bir şeyi kontrol ediyoruz?
Marc Gravell

4
@Dragouf gerçekten değil - bu yöntemi kullanmanın önemli noktalarından biri, çalışanın hangi parçaların ve UI iş parçacığında hangi parçaların çalıştığını zaten bilmenizdir. Kontrol etmeye gerek yok.
Marc Gravell

3
@ Joan.bdm Bu konuda yorum yapmak için yeterince yakın bir yer yok
Marc Gravell

401

Uzun süreli çalışma

Yana .NET 4.5 ve C # 5.0 kullanmanız gereken Görev tabanlı zaman uyumsuz Desen (TAP) ile birlikte zaman uyumsuz - bekliyoruz anahtar kelimeleri tüm alanlarda (GUI dahil):

TAP, yeni geliştirme için önerilen eşzamansız tasarım modelidir

yerine Asenkron Modeli (APM) Programlama ve Olay tabanlı uyumsuz Desen (DAP) (ikinci içerir BackgroundWorker Class ).

Ardından, yeni geliştirme için önerilen çözüm:

  1. Bir olay işleyicinin eşzamansız uygulanması (Evet, hepsi bu):

    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. UI iş parçacığını bildiren ikinci iş parçacığının uygulanması:

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

Aşağıdakilere dikkat edin:

  1. Kısa ve temiz kod, geri aramalar ve açık evreler olmadan sıralı olarak yazılır.
  2. Görev yerine Konuya .
  3. zaman uyumsuz anahtar kelime, kullanmasına izin vermesinden bekliyoruz sırayla bitmiş görev kadar tamamlama durumunu ulaşmasını ve bu arada olay işleyicisi önlemek hangi UI iş parçacığı engellemez.
  4. Endişelerin Ayrılması (SoC) tasarım ilkesini destekleyen ve açık dağıtım programı ve çağırma gerektirmeyen ilerleme sınıfı (bkz. IProgress Arayüzü ) . Oluşturma yerinden (burada UI iş parçacığı) geçerli SynchronizationContext kullanır .
  5. TaskCreationOptions.LongRunning , görevi ThreadPool'da kuyruğa almamaya ilişkin ipuçları verir .

Daha ayrıntılı örnekler için bakınız: C #'ın Geleceği: Joseph Albahari tarafından 'bekleyen' iyi şeyler gelir .

Ayrıca bkz . UI Diş Açma Modeli kavramı.

İstisnaları işleme

Aşağıdaki snippet, Enabledarka plan yürütme sırasında birden çok tıklamayı önlemek için istisnaların nasıl ele alınacağına ve düğmenin özelliğinin nasıl değiştirileceğine bir örnektir .

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
Bir SecondThreadConcern.LongWork()istisna atarsa, UI iş parçacığı tarafından yakalanabilir mi? Bu mükemmel bir gönderi, btw.
kdbanman

2
Gereksinimlerinizi karşılamak için cevaba ek bir bölüm ekledim. Saygılarımızla.
Ryszard Dżegan

3
ExceptionDispatchInfo sınıf async-bekliyoruz desende UI iş parçacığı üzerinde arka plan istisna yeniden atma o mucize sorumludur.
Ryszard Dżegan

1
Bunu yapmanın sadece Çağır / Başla'yı çağırmaktan çok daha ayrıntılı olduğunu düşünen sadece ben miyim ?!
MeTitus

2
Task.Delay(500).Wait()? Mevcut iş parçacığını engellemek için bir Görev oluşturmanın anlamı nedir? Bir iş parçacığı havuzu iş parçacığını asla engellememelisiniz!
Yarik

236

Marc Gravell'in .NET 4 için en basit çözümünün varyasyonu :

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

Veya bunun yerine Eylem temsilcisini kullanın:

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

İkisinin karşılaştırması için buraya bakın: MethodInvoker vs Action for Control.


1
bu örnekte 'kontrol' nedir? Kullanıcı arayüzü kontrolüm? Bunu WPF'de bir etiket kontrolünde uygulamaya çalışıyorum ve Invoke etiketimin bir üyesi değil.
Dbloom

@Styxriver stackoverflow.com/a/3588137/206730 gibi uzatma yöntemi nedir ?
Kiquenet

'Eylem y;' sınıf veya yöntem içinde text özelliğini değiştirme ve metni bu kod parçasıyla güncelleyin 'yourcontrol.Invoke (y = () => yourcontrol.Text = "new text");'
Antonio Leite

4
@Dbloom üye değil çünkü sadece WinForms için. WPF için Dispatcher.Invoke
sLw

1
Bu çözümü izliyordum, ancak bazen kullanıcı arayüzüm güncellenmiyordu. Ben this.refresh()geçersiz kılmak ve GUI yeniden boyamak zorunda olduğunu buldum .. yararlı ise ..
Rakibul Haq

137

.NET 3.5+ için ateşleme ve unutma uzantısı yöntemi

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

Bu, aşağıdaki kod satırı kullanılarak çağrılabilir:

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

5
@Bu kullanımın anlamı nedir? "Kontrol" eşdeğer olmaz mı? @ This'in herhangi bir faydası var mı?
argyle

14
@jeromeyers - Bu @thissadece değişken adıdır, bu durumda uzantıyı çağıran geçerli kontrole referanstır. Bunu kaynağınızla veya teknenizde yüzen her şeyi yeniden adlandırabilirsiniz. Ben kullanıyorum @this, çünkü bu uzantıyı çağıran ve normalde (uzatma olmayan) kodda 'this' anahtar kelimesini kullanarak tutarlı olan (en azından kafamda) bu Control'a atıfta bulunuyor.
StyxRiver

1
Bu harika, kolay ve benim için en iyi çözüm. Ui iş parçacığında yapmanız gereken tüm işleri dahil edebilirsiniz. Örnek: this.UIThread (() => {txtMessage.Text = message; listBox1.Items.Add (message);});
otomatik

1
Bu çözümü gerçekten çok seviyorum. Küçük nit: Ben bu yöntemi adlandırmak OnUIThreadyerine UIThread.
ToolmakerSteve

2
Bu yüzden bu uzantıyı adlandırdım RunOnUiThread. Ama bu sadece kişisel zevk.
Grisgram

66

Bunu yapmanın klasik yolu budur:

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

Çalışan iş parçacığınızda bir etkinlik var. UI iş parçacığınız işi yapmak için başka bir iş parçacığını başlatır ve çalışan iş parçacığının durumunu görüntüleyebilmeniz için o işçi olayını bağlar.

Daha sonra kullanıcı arayüzünde, gerçek kontrolü değiştirmek için bir etiket veya ilerleme çubuğu gibi konuları kesmeniz gerekir.


62

Basit çözüm kullanmaktır Control.Invoke.

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

void updateGUI() {
    // update gui here
}

basitlik için aferin! sadece basit değil, aynı zamanda iyi çalışır! Microsoft'un neden olması gerektiği kadar basitleştiremediğini gerçekten anlamadım! ana iş parçacığında 1 satır çağırmak için, birkaç işlev yazmalıyız!
MBH

1
@MBH Katılıyorum. BTW, yukarıdaki bir uzantı yöntemini tanımlayan stackoverflow.com/a/3588137/199364 yanıtını fark ettiniz mi? Bunu özel bir yardımcı program sınıfında bir kez yapın, o zaman Microsoft'un bizim için yapmadığını daha fazla önemsemek zorunda kalmazsınız :)
ToolmakerSteve

@ToolmakerSteve Tam olarak ne demek oluyor! haklısın bir yol bulabiliriz, ama demek istediğim, DRY (kendinizi tekrar etmeyin) bakış açısıyla, ortak çözümü olan sorun, Microsoft tarafından minimum çaba ile onlar tarafından çözülebilir, bu da çok zaman kazandıracaktır. programcılar :)
MBH

47

Diş açma kodu genellikle hatalıdır ve test edilmesi her zaman zordur. Kullanıcı arabirimini bir arka plan görevinden güncellemek için iş parçacığı kodu yazmanıza gerek yoktur. Görevi çalıştırmak için BackgroundWorker sınıfını ve kullanıcı arabirimini güncelleştirmek için ReportProgress yöntemini kullanmanız yeterlidir . Genellikle, tamamlanmış bir yüzdeyi rapor edersiniz, ancak durum nesnesini içeren başka bir aşırı yükleme vardır. Aşağıda, yalnızca bir dize nesnesini bildiren bir örnek verilmiştir:

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

Her zaman aynı alanı güncellemek istiyorsanız sorun değil. Daha karmaşık güncellemeleriniz varsa, kullanıcı arabirimi durumunu temsil edecek bir sınıf tanımlayabilir ve ReportProgress yöntemine iletebilirsiniz.

Son bir şey, WorkerReportsProgressbayrağı ayarladığınızdan emin olun , aksi takdirde ReportProgressyöntem tamamen yok sayılır.


2
İşlemin sonunda, kullanıcı arayüzünü üzerinden güncellemek de mümkündür backgroundWorker1_RunWorkerCompleted.
DavidRR

41

Cevapların büyük çoğunluğu gerçekleşmeyi bekleyenControl.Invoke bir yarış şartıdır . Örneğin, kabul edilen cevabı düşünün:

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

Kullanıcı hemen önce formu kapatır this.Invoke(hatırlamak, denir thisolan Formnesne), bir ObjectDisposedExceptionolasılıkla tetiklenir.

Çözüm SynchronizationContext, özellikle hamilton.danielb'in önerdiği SynchronizationContext.Currentgibi kullanmaktır (diğer cevaplar tamamen gereksiz olan belirli uygulamalara dayanır ). Biraz yerine yerine kullanmak için kodunu değiştirmek istiyorum (genellikle işçi iş parçacığı beklemek için gerek yok gibi):SynchronizationContextSynchronizationContext.PostSynchronizationContext.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 ve sonraki sürümlerde, zaman uyumsuz işlemler için gerçekten görevler kullanmanız gerektiğini unutmayın. Eşdeğer göreve dayalı yaklaşım (n) kullanarak n-san'ın cevabına bakınız TaskScheduler.FromCurrentSynchronizationContext.

Son olarak, .NET 4.5 ve sonraki sürümlerde , uzun süren işlemin hala çalışırken UI kodunu çalıştırması gereken durumlar için Ryszard Dżegan's tarafından gösterildiği gibi Progress<T>(temel SynchronizationContext.Currentolarak yaratıldığında yakalar) kullanabilirsiniz .


37

Güncellemenin doğru iş parçacığında gerçekleştiğinden emin olmanız gerekir; UI iş parçacığı.

Bunu yapmak için, olay işleyicisini doğrudan çağırmak yerine çağırmanız gerekir.

Bunu, etkinliğinizi şu şekilde yükselterek yapabilirsiniz:

(Kod buraya kafamdan yazılmıştır, bu yüzden doğru sözdizimi vb. İçin kontrol etmedim, ancak gitmenizi sağlamalı.)

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 denetimleri ISynchronizeInvokearabirimi uygulamadığından, yukarıdaki kodun WPF projelerinde çalışmayacağını unutmayın .

Yukarıdaki kodun Windows Forms ve WPF ve diğer tüm platformlarla çalıştığından emin olmak için AsyncOperation, AsyncOperationManagerve SynchronizationContextsınıflarına göz atabilirsiniz .

Olayları bu şekilde kolayca yükseltmek için, yalnızca arayarak bir etkinliği artırmayı basitleştirmemi sağlayan bir uzantı yöntemi oluşturdum:

MyEvent.Raise(this, EventArgs.Empty);

Tabii ki, bu konuyu sizin için soyutlayacak BackGroundWorker sınıfını da kullanabilirsiniz.


Gerçekten, ama bu konuda GUI kodumu 'dağınık' sevmiyorum. GUI'm çağırması gerekip gerekmediği umurumda değil. Başka bir deyişle: bağlam-swithc gerçekleştirmek GUI'nin sorumluluğu olduğunu sanmıyorum.
Frederik Gheysels

1
Temsilcinin ayrılması vb. Aşırıya kaçmış gibi görünüyor - neden sadece: SynchronizationContext.Current.Send (delegate {MyEvent (...);}, null);
Marc Gravell

SynchronizationContext'e her zaman erişiminiz var mı? Sınıfınız bir sınıf lib'inde olsa bile?
Frederik Gheysels

29

GUI iş parçacığında yöntemi çağırmanız gerekir. Bunu Control.Invoke'u çağırarak yapabilirsiniz.

Örneğin:

delegate void UpdateLabelDelegate (string message);

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

    MyLabelControl.Text = message;
}

1
Çağırma satırı bana bir derleyici hatası veriyor. 'System.Windows.Forms.Control.Invoke (System.Delegate, object [])' için en çok yüklenmiş yöntem eşleşmesi bazı geçersiz argümanlara sahip
CruelIO

28

Senaryo önemsizliği nedeniyle aslında durum için UI iş parçacığı anketi olurdu. Bence oldukça zarif olabileceğini göreceksin.

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

Yaklaşım, ISynchronizeInvoke.Invokeve ISynchronizeInvoke.BeginInvokeyöntemlerini kullanırken gereken marşalizasyon işleminden kaçınır . Marshaling tekniğini kullanmanın yanlış bir yanı yok, ancak bilmeniz gereken birkaç uyarı var.

  • BeginInvokeÇok sık aramadığınızdan emin olun, aksi takdirde mesaj pompasını aşabilir.
  • Arayan Invokeiş parçacığı üzerinde bir engelleme çağrısıdır. Bu iş parçacığında yapılan işi geçici olarak durduracaktır.

Bu cevapta önerdiğim strateji, konuların iletişim rollerini tersine çeviriyor. İş parçacığı veri itmek yerine UI iş parçacığı bunun için yoklar. Bu, birçok senaryoda kullanılan ortak bir örüntüdür. Tüm yapmak istediğiniz işçi iş parçacığı ilerleme bilgilerini görüntülemek olduğundan o zaman bu çözüm marshaling çözüm için harika bir alternatif olduğunu düşünüyorum bulacaksınız. Aşağıdaki avantajlara sahiptir.

  • Aksine, kullanıcı arayüzü ve alt ipler gevşek bağlı kalır Control.Invokeya da Control.BeginInvokebu sıkıca çiftler yaklaşım.
  • UI iş parçacığı, çalışan iş parçacığının ilerlemesini engellemez.
  • Çalışan iş parçacığı, UI iş parçacığının güncelleştirmeyi geçirdiği zamana hakim olamaz.
  • Kullanıcı arabiriminin ve çalışan iş parçacıklarının işlem gerçekleştirme aralıkları bağımsız kalabilir.
  • Çalışan iş parçacığı, UI iş parçacığının ileti pompasını aşamaz.
  • UI iş parçacığı, UI'nin ne zaman ve ne sıklıkta güncellendiğini belirler.

3
İyi bir fikir. Bahsetmediğiniz tek şey, WorkerThread bittiğinde zamanlayıcıyı nasıl doğru bir şekilde attığınızdır. Bunun, uygulama sona erdiğinde soruna neden olabileceğini unutmayın (örn. Kullanıcı uygulamayı kapatır). Bunu nasıl çözeceğiniz hakkında bir fikriniz var mı?
Matt

Bunun yerine bir anonim işleyicisi kullanmanın @Matt Elapsedzamanlayıcıyı kaldırmak böylece formu tanzim edildiğinde olay, üye yöntemi kullanmak ...
Phil1970

@ Phil1970 - İyi bir nokta. Bunu istedim System.Timers.ElapsedEventHandler handler = (s, a) => { MyProgressLabel.Text = m_Text; };ve atamak istediniz m_Timer.Elapsed += handler;, daha sonra atma bağlamında m_Timer.Elapsed -= handler;doğru muyum? Ve burada tartışılan tavsiyelere uyarak imha / kapanma için .
Matt

27

Önceki yanıtlardaki hiçbir Çağrıyı başlat gerekli değildir.

WindowsFormsSynchronizationContext'e bakmanız gerekir:

// 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 yönteminin başlık altında ne kullandığını düşünüyorsunuz? :)
inanılmaz

23

Bu, .NET Framework 3.0 kullanan yukarıdaki çözüme benzer, ancak derleme zamanı güvenlik desteği sorununu çözdü .

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

Kullanmak:

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

Kullanıcı yanlış veri türünü geçerse derleyici başarısız olur.

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

23

Salvete! Bu soruyu araştırdıktan sonra, FrankG ve Oregon Ghost'un cevaplarını benim için en kolay olanı buldum. Şimdi, Visual Basic kod ve bu snippet'i bir dönüştürücü aracılığıyla koştu; bu yüzden nasıl sonuçlandığından emin değilim.

Günlük görüntüleme türü olarak kullanıyorum form_Diagnostics,denilen bir richtext kutusu olan adlı bir iletişim formu var updateDiagWindow,. Metnini tüm evrelerden güncelleyebilmem gerekiyordu. Ek satırlar, pencerenin otomatik olarak en yeni satırlara kaymasına izin verir.

Ve böylece, şimdi tüm programın herhangi bir yerinden, herhangi bir iplik geçirmeden çalışacağını düşündüğünüz şekilde ekranı bir satırla güncelleyebilirim:

  form_Diagnostics.updateDiagWindow(whatmessage);

Ana Kod (bunu formunuzun sınıf kodunun içine koyun):

#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

Birçok amaç için bu kadar basit:

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

"serviceGUI ()", formda (bu) istediğiniz kadar denetimi değiştirebilen bir GUI düzeyi yöntemidir. Diğer evreden "updateGUI ()" çağırın. Geçiş değerlerine parametreler eklenebilir veya (büyük olasılıkla daha hızlı), sınıf erişim değişkenlerini gerektiğinde üzerlerine kilitli olarak kullanabilir, eğer bunlara erişen dişler arasında kararsızlığa neden olabilecek bir çakışma olasılığı varsa. GUI olmayan iş parçacığı zaman açısından kritik ise Invoke yerine BeginInvoke kullanın (Brian Gideon'un uyarısını aklınızda bulundurun).


21

Bu Ian Kemp çözümünün C # 3.0 varyasyonunda:

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

Buna şöyle diyorsunuz:

myButton.SetPropertyInGuiThread(b => b.Text, "Click Me!")
  1. "MemberExpression olarak" sonucuna boş denetim ekler.
  2. Statik tip güvenliğini artırır.

Aksi takdirde, orijinal çok güzel bir çözümdür.


21
Label lblText; //initialized elsewhere

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

   lblText.Text = text;           
}

Bunun BeginInvoke(), Invoke()kilitlenmelere neden olma olasılığının daha düşük olması nedeniyle tercih edildiğine dikkat edin (ancak, yalnızca bir etikete metin atarken bu bir sorun değildir):

Kullanırken Invoke()yöntemin geri dönmesini bekliyorsunuz. Şimdi, çağrılan kodda iş parçacığını beklemek zorunda kalacak bir şey yapıyor olabilirsiniz, bu, aradığınız bazı işlevlere gömüldüğünde hemen belli olmayacak, bu da olay işleyicileri aracılığıyla dolaylı olarak gerçekleşebilir. Yani iş parçacığını beklerdiniz, iş parçacığı sizi bekler ve kilitlenirsiniz.

Bu aslında yayınlanan yazılımımızın bazılarının askıda kalmasına neden oldu. Bu değiştirerek düzeltme kolay yeterliydi Invoke()ile BeginInvoke(). Bir dönüş değerine ihtiyacınız varsa, eşzamanlı çalışmaya ihtiyacınız yoksa kullanın BeginInvoke().


20

Aynı sorunla karşılaştığımda Google'dan yardım aldım, ancak bana basit bir çözüm vermek yerine, MethodInvoker ve blah blah blah . Bu yüzden kendi başıma çözmeye karar verdim. İşte benim çözümüm:

Bunun gibi bir temsilci yapın:

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
}

Bu işlevi, bunun gibi yeni bir iş parçacığında çağırabilirsiniz

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

Kafanız karışmasın Thread(() => .....). Bir iş parçacığı üzerinde çalışırken anonim bir işlev veya lambda ifade kullanın. Kod satırlarını azaltmak için ThreadStart(..)burada açıklamamam gereken yöntemi de kullanabilirsiniz .


17

Bunun gibi bir şey kullanın:

 this.Invoke((MethodInvoker)delegate
            {
                progressBar1.Value = e.ProgressPercentage; // runs on UI thread
            });

Varsa e.ProgressPercentage, bunu çağırdığınız yöntemden zaten UI iş parçacığında değil misiniz?
LarsTech

ProgressChanged olayı UI iş parçacığında çalışır. BackgroundWorker'ı kullanmanın kolaylıklarından biri de budur. Tamamlandı olayı GUI üzerinde de çalışır. UI olmayan iş parçacığında çalışan tek şey DoWork yöntemidir.
LarsTech

15

Zaten var olan temsilciyi kullanabilirsiniz Action:

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

14

Benim sürümüm bir satır yinelemeli "mantra" eklemek için :

Bağımsız değişken yok:

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

        // Your code!
    }

Bağımsız değişkenleri olan bir işlev için:

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

BU BT .


Bazı argümanlar : Genellikle kod okunabilirliğinin {}if () bir satırdaki ifadeden . Ama bu durumda rutin hepsi aynı "mantra" dır. Bu yöntem proje üzerinde tutarlıysa kod okunabilirliğini bozmaz. Ve kodunuzu çöpten kurtarır (beş yerine bir kod satırı).

Gördüğünüz gibi if(InvokeRequired) {something long}sadece "bu fonksiyon başka bir evreden aramak güvenlidir".


13

Bunu kullanarak etiketi yenilemeyi deneyin

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

    public static void Refresh(this UIElement uiElement)
    {
        uiElement.Dispatcher.Invoke(DispatcherPriority.Render, EmptyDelegate);
    }
}

Onun için mi Windows Forms ?
Kiquenet

13

Bir sınıf değişkeni oluşturun:

SynchronizationContext _context;

Kullanıcı arayüzünüzü oluşturan yapıcıda ayarlayın:

var _context = SynchronizationContext.Current;

Etiketi güncellemek istediğinizde:

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

12

İnvoke ve delegate kullanmalısınız

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

12

Diğer cevapların çoğu benim için biraz karmaşık (C # için yeniyim), bu yüzden benim yazıyorum:

Bir WPF başvurum var ve bir çalışanı aşağıdaki gibi tanımladım:

Konu:

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

Çözüm:

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

Yukarıdaki çizginin ne anlama geldiğini henüz bilmiyorum, ama işe yarıyor.

İçin WinForms :

Çözüm:

txtLog.Invoke((MethodInvoker)delegate
{
    txtLog.Text = "my message";
});

Soru WPF ile değil Winforms hakkındaydı.
Marc L.

Teşekkürler. Yukarıdaki WinForms çözümü eklendi.
Manohar Reddy Poreddy

... bu aynı soruya verilmiş olan diğer cevapların sadece bir kopyası, ama tamam. Neden çözümün bir parçası olmasın ve sadece cevabınızı silin?
Marc L.

hmm, sen doğru, eğer sadece cevabımı dikkatle okuyorsun, başlangıç ​​kısmı (cevabı yazmamın nedeni) ve umarım biraz daha dikkatle, aynı problemi olan ve bugün için onaylanan biri olduğunu görüyorsun benim basit cevabım ve tüm bu neden oldu hakkında gerçek bir hikaye öngörülebilir eğer daha da attn ile, ben wpf için arama bile google beni buraya gönderir. Elbette bu az ya da çok belirgin 3 nedeni kaçırdığınızdan, neden aşağı oyunuzu kaldırmayacağınızı anlayabiliyorum. Tamam olanı temizlemek yerine, çok daha zor olan yeni bir şey yaratın.
Manohar Reddy Poreddy

8

Bence en kolay yol:

   void Update()
   {
       BeginInvoke((Action)delegate()
       {
           //do your update
       });
   }

8

Örneğin, geçerli iş parçacığı dışındaki bir denetime erişin:

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

Orada lblThresholdbir Etiket ve Speed_Thresholdglobal bir değişkendir.


8

UI iş parçacığında olduğunuzda, senkronizasyon bağlamı görev zamanlayıcısını isteyebilirsiniz. Size UI iş parçacığında her şeyi zamanlayan bir TaskScheduler verir .

Ardından, sonuç hazır olduğunda başka bir görev (UI iş parçacığında zamanlanan) seçip bir etikete ataması için görevlerinizi zincirleyebilirsiniz.

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

Bu, şimdi eşzamanlı kod yazmanın tercih edilen yolu olan görevler (iş parçacıkları değil) için geçerlidir .


1
Arama Task.Startgenellikle iyi bir uygulama değildir blogs.msdn.com/b/pfxteam/archive/2012/01/14/10256832.aspx
Ohad Schneider

8

Sadece cevapları okudum ve bu çok sıcak bir konu gibi görünüyor. Şu anda .NET 3.5 SP1 ve Windows Forms kullanıyorum.

InvokeRequired özelliğini kullanan önceki yanıtlarda büyük ölçüde açıklanan iyi bilinen formül , vakaların çoğunu kapsar, ancak tüm havuzu kapsamaz .

Tanıtıcı henüz oluşturulmamışsa ne olur ?

InvokeRequired mülkiyet, tarif edildiği gibi burada (MSDN Control.InvokeRequired Mülkiyet referansı) çağrı çağrı GUI iplikten yapılmış ya eğer yanlış GUI iplik olmayan bir iplikten yapılmış true döndürürse ya da Sap oldu henüz oluşturulmadı.

Başka bir iş parçacığı tarafından gösterilen ve güncellenen kalıcı bir form istiyorsanız, bir istisna ile karşılaşabilirsiniz. Bu formun kalıcı olarak gösterilmesini istediğiniz için aşağıdakileri yapabilirsiniz:

private MyForm _gui;

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

Delege GUI'de bir Etiketi güncelleyebilir:

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

Bu , etiket güncellemesinden önceki işlemler GUI iş parçacığının Form oluşturması için gereken süreden "daha az zaman alır" (okuma ve basitleştirme olarak yorumlama) durumunda InvalidOperationException özelliğine neden olabilir 'ın Kulp . Bu, ShowDialog () yönteminde olur.

Ayrıca, Tutamağı şu şekilde :

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

Tanıtıcı henüz oluşturulmamışsa işlemi gerçekleştirebilirsiniz : GUI güncellemesini (yukarıdaki kodda gösterildiği gibi) yoksayabilirsiniz veya bekleyebilirsiniz (daha riskli). Bu soruya cevap vermelidir.

İsteğe bağlı şeyler: Şahsen ben aşağıdakileri kodlama geldi:

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

Bu ThreadSafeGuiCommand örneği ile başka bir iş parçacığı tarafından güncellenen formlarımı beslemek ve GUI ( ) bu gibi güncelleştiren yöntemleri şöyle tanımlayın:

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

Bu şekilde, isteğe bağlı olarak iyi tanımlanmış bir süre (zaman aşımı) için bekleyen, ne iş parçacığı arama yapacak GUI güncellenmiş olacak eminim.


1
IsHandleCreated'ı da kontrol ettiğim için bunu bulmak için buraya geldim. Kontrol edilecek diğer bir özellik IsDisposed. Formunuz atılırsa, üzerinde Invoke () öğesini çağıramazsınız. Kullanıcı, arka plan iş parçacığınız tamamlanmadan formu kapattıysa, form atandığında kullanıcı arayüzüne geri dönmeye çalışmasını istemezsiniz.
Jon

Ben başlamak için kötü bir fikir olduğunu söyleyebilirim ... Normalde, alt formu hemen göstermek ve arka plan işleme yaparken bir ilerleme çubuğu veya başka bir geri bildirim olurdu. Veya önce tüm işlemleri yaparsınız ve ardından sonucu oluşturma sırasında yeni forma geçirirsiniz. Her ikisini aynı anda yapmak genel olarak marjinal faydalara sahip olacak, ancak daha az bakım yapılabilir bir koda sahip olacaktır.
Phil1970

Açıklanan senaryoda, arka plan iş parçacığının bir ilerleme görünümü olarak kullanılan kalıcı bir form dikkate alınır . Kalıcı olması gerektiği için Form.ShowDialog () yöntemi çağrılarak gösterilmelidir . Bunu yaparak, çağrıyı izleyen kodunuzun form kapatılana kadar yürütülmesini engellersiniz. Bu nedenle, arka plan iş parçacığını verilen örnekten farklı bir şekilde başlatamazsanız (ve elbette bunu yapabilirsiniz), arka plan iş parçacığı başladıktan sonra bu formun kalıcı olarak gösterilmesi gerekir. Bu durumda, Tanıtıcı'nın oluşturulup oluşturulmadığını kontrol etmeniz gerekir. Kalıcı bir forma ihtiyacınız yoksa, bu başka bir hikaye.
Sume
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.