Jak zaktualizować GUI z innego wątku?


1392

Jaki jest najprostszy sposób na aktualizację Labelz innego Thread?

  • Mam Formbieg thread1i od tego zaczynam inny wątek ( thread2).

  • Natomiast thread2przetwarza niektóre pliki Chciałbym aktualizuje Labelna Formz aktualnego stanu thread2„s pracy.

Jak mogłem to zrobić?


25
Czy .NET 2.0+ nie ma klasy BackgroundWorker tylko do tego. Jest świadomy wątku interfejsu użytkownika. 1. Utwórz BackgroundWorker 2. Dodaj dwóch delegatów (jednego do przetworzenia, a drugiego do ukończenia)
Preet Sangha


4
Zobacz odpowiedź na .NET 4.5 i C # 5.0: stackoverflow.com/a/18033198/2042090
Ryszard Dżegan

5
To pytanie nie dotyczy interfejsu Gtk # GUI. Dla Gtk # zobacz to i odpowiedź.
hlovdal

Uwaga: odpowiedzi na to pytanie są teraz zagracone bałaganem OT („oto, co zrobiłem dla mojej aplikacji WPF”) i historycznymi artefaktami .NET 2.0.
Marc L.,

Odpowiedzi:


768

Dla .NET 2.0, oto kawałek kodu, który napisałem, który robi dokładnie to, co chcesz i działa dla dowolnej właściwości w 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 });
  }
}

Nazwij to tak:

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

Jeśli używasz platformy .NET 3.0 lub nowszej, możesz przepisać powyższą metodę jako metodę rozszerzenia Controlklasy, co uprościłoby wywołanie:

myLabel.SetPropertyThreadSafe("Text", status);

AKTUALIZACJA 05/10/2010:

W przypadku .NET 3.0 należy użyć tego kodu:

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

który wykorzystuje wyrażenia LINQ i lambda, aby umożliwić znacznie czystszą, prostszą i bezpieczniejszą składnię:

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

Teraz nie tylko nazwa właściwości jest sprawdzana w czasie kompilacji, ale także typ właściwości, więc niemożliwe jest (na przykład) przypisanie wartości ciągu do właściwości boolowskiej, a tym samym spowodowanie wyjątku czasu wykonywania.

Niestety nie powstrzymuje to nikogo przed robieniem głupich rzeczy, takich jak przekazywanie cudzej Controlwłasności i wartości, więc następujące elementy z przyjemnością się skompilują:

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

Dlatego dodałem kontrole środowiska wykonawczego, aby upewnić się, że przekazana właściwość faktycznie należy do Controlmetody, która jest wywoływana. Nie idealny, ale wciąż o wiele lepszy niż wersja .NET 2.0.

Jeśli ktoś ma jakieś dodatkowe sugestie dotyczące ulepszenia tego kodu w celu zapewnienia bezpieczeństwa podczas kompilacji, prosimy o komentarz!


3
Zdarzają się przypadki, gdy this.GetType () zwraca to samo co propertyInfo.ReflectedType (np. LinkLabel na WinForms). Nie mam dużego doświadczenia w C #, ale myślę, że warunkiem wyjątku powinno być: if (propertyInfo == null || (!@this.GetType (). IsSubclassOf (propertyInfo.ReflectedType) && @ this.GetType ( )! = propertyInfo.ReflectedType) || @ this.GetType (). GetProperty (propertyInfo.Name, propertyInfo.PropertyType) == null)
Corvin

9
@lan można to SetControlPropertyThreadSafe(myLabel, "Text", status)nazwać z innego modułu, klasy lub formularza
Smith

71
Dostarczone rozwiązanie jest niepotrzebnie złożone. Zobacz rozwiązanie Marc Gravell lub rozwiązanie Zaid Masud, jeśli cenisz sobie prostotę.
Frank Hileman

8
To rozwiązanie marnuje mnóstwo zasobów, jeśli aktualizujesz wiele właściwości, ponieważ każde Wywołanie kosztuje dużo zasobów. Nie sądzę, żeby tak właśnie działała funkcja Thread Thread. Wykonaj enkapsultację akcji aktualizacji interfejsu użytkownika i wywołaj ją RAZ (a nie według właściwości)
Konsola

4
Dlaczego, u licha, miałbyś używać tego kodu zamiast składnika BackgroundWorker?
Andy,

1079

Najprostszym sposobem jest metoda anonimowe przeszedł do 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

Zauważ, że Invokeblokuje wykonywanie, dopóki się nie zakończy - jest to kod synchroniczny. Pytanie nie dotyczy kodu asynchronicznego, ale w przepełnieniu stosu jest dużo treści na temat pisania kodu asynchronicznego, gdy chcesz się o nim dowiedzieć.


8
Widząc, że OP nie wspomniał o żadnej klasie / instancji poza formą, to nie jest złe ustawienie domyślne ...
Marc Gravell

39
Nie zapomnij, że słowo kluczowe „this” odnosi się do klasy „Control”.
AZ.

8
@kompletowanie jest bezpieczne w obu przypadkach i już wiemy, że pracujemy, więc po co sprawdzać coś, co wiemy?
Marc Gravell

4
@Dragouf nie jest tak naprawdę - jednym z punktów korzystania z tej metody jest to, że już wiesz, które części działają na procesie roboczym, a które na wątku interfejsu użytkownika. Nie trzeba sprawdzać.
Marc Gravell

3
@ Joan.bdm nie ma wystarczającego kontekstu, aby móc to skomentować
Marc Gravell

400

Obsługa długiej pracy

Od wersji .NET 4.5 i C # 5.0 należy używać wzorców asynchronicznych opartych na zadaniach (TAP) wraz z asynchronicznym - czekaj na słowa kluczowe we wszystkich obszarach (w tym GUI):

TAP jest zalecanym wzorcem projektowania asynchronicznego dla nowego rozwoju

zamiast modelu asynchronicznego programowania (APM) i asynchronicznego wzorca opartego na zdarzeniu (EAP) (ten ostatni obejmuje klasę BackgroundWorker ).

Następnie zalecane rozwiązanie dla nowego rozwoju to:

  1. Asynchroniczna implementacja modułu obsługi zdarzeń (Tak, to wszystko):

    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. Implementacja drugiego wątku, który powiadamia wątek interfejsu użytkownika:

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

Zwróć uwagę na następujące:

  1. Krótki i przejrzysty kod pisany sekwencyjnie, bez wywołań zwrotnych i jawnych wątków.
  2. Zadanie zamiast wątku .
  3. Słowo kluczowe async , które pozwala na użycie funkcji „ czekaj”, która z kolei uniemożliwia modułowi obsługi zdarzeń osiągnięcie stanu ukończenia aż do zakończenia zadania, a tymczasem nie blokuje wątku interfejsu użytkownika.
  4. Klasa postępu (patrz Interfejs IProgress ), który obsługuje zasadę projektowania Separation of Concerns (SoC) i nie wymaga jawnego wysyłania i wywoływania. Wykorzystuje bieżący kontekst synchronizacji z miejsca jego utworzenia (tutaj wątku interfejsu użytkownika).
  5. TaskCreationOptions.LongRunning to wskazówka, aby nie umieszczać zadania w kolejce w ThreadPool .

Dla bardziej gadatliwy przykłady patrz: The Future of C #: Dobre rzeczy przychodzą do tych, którzy „Oczekujcie” przez Josepha Albahari .

Zobacz także koncepcję modelu gwintowania interfejsu użytkownika .

Obsługa wyjątków

Poniższy fragment kodu jest przykładem sposobu obsługi wyjątków i przełączania Enabledwłaściwości przycisku, aby zapobiec wielokrotnym kliknięciom podczas wykonywania w tle.

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
Jeśli SecondThreadConcern.LongWork()zgłasza wyjątek, czy może zostać przechwycony przez wątek interfejsu użytkownika? To jest świetny post, btw.
kdbanman,

2
Dodałem dodatkową sekcję do odpowiedzi, aby spełnić twoje wymagania. Pozdrowienia.
Ryszard Dżegan

3
Klasa ExceptionDispatchInfo jest odpowiedzialny za ten cud Ponowne generowanie wyjątku tła na wątków UI w asynchronicznym-Oczekujcie wzorca.
Ryszard Dżegan

1
Czy to tylko ja myślę, że ten sposób robienia tego jest bardziej szczegółowy niż tylko przywołanie Invoke / Begin ?!
MeTitus,

2
Task.Delay(500).Wait()? Po co tworzyć zadanie, aby po prostu zablokować bieżący wątek? Nigdy nie należy blokować wątku puli wątków!
Yarik,

236

Odmiana Marc Gravell jest najprostszym rozwiązanie dla .NET 4:

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

Lub użyj zamiast tego akcji Delegata:

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

Zobacz tutaj porównanie dwóch: MethodInvoker vs Action for Control.BeginInvoke


1
czym jest „kontrola” w tym przykładzie? Moja kontrola interfejsu użytkownika? Próbuję zaimplementować to w WPF na formancie etykiety, a Invoke nie jest członkiem mojej etykiety.
Dbloom

Co z metodą rozszerzenia, taką jak @styxriver stackoverflow.com/a/3588137/206730 ?
Kiquenet,

zadeklaruj „Action y;” wewnątrz klasy lub metody zmieniającej właściwość text i zaktualizuj tekst za pomocą tego fragmentu kodu „yourcontrol.Invoke (y = () => yourcontrol.Text =„ new text ”);”
Antonio Leite,

4
@Dbloom nie jest członkiem, ponieważ dotyczy tylko WinForm. W przypadku WPF korzystasz z narzędzia Dispatcher.Invoke
sLw

1
Postępowałem zgodnie z tym rozwiązaniem, ale czasami mój interfejs nie był aktualizowany. Odkryłem, że muszę this.refresh()wymusić unieważnienie i odmalowanie GUI .. jeśli jest to pomocne ..
Rakibul Haq 5'18

137

Uruchom i zapomnij metodę rozszerzenia dla .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();
        }
    }
}

Można to wywołać za pomocą następującego wiersza kodu:

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

5
Jaki jest sens tego użycia? Czy „kontrola” nie byłaby równoważna? Czy są jakieś korzyści dla @this?
argyle

14
@jeromeyers - Jest @thisto po prostu nazwa zmiennej, w tym przypadku odniesienie do bieżącej kontrolki wywołującej rozszerzenie. Możesz zmienić nazwę na source lub cokolwiek płynie łodzią. Używam @this, ponieważ odnosi się do „tego formantu”, który wywołuje rozszerzenie i jest spójny (przynajmniej w mojej głowie) z użyciem słowa kluczowego „to” w normalnym kodzie (bez rozszerzenia).
StyxRiver

1
To jest świetne, łatwe i dla mnie najlepsze rozwiązanie. Możesz dołączyć całą pracę, którą musisz wykonać w wątku interfejsu użytkownika. Przykład: this.UIThread (() => {txtMessage.Text = message; listBox1.Items.Add (message);});
Auto

1
Naprawdę podoba mi się to rozwiązanie. Minor nit: OnUIThreadZamiast tego nazwałbym tę metodę UIThread.
ToolmakerSteve

2
Dlatego nazwałem to rozszerzenie RunOnUiThread. Ale to tylko osobisty gust.
Grisgram

66

Jest to klasyczny sposób, w jaki powinieneś to zrobić:

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

Wątek roboczy zawiera zdarzenie. Wątek interfejsu użytkownika uruchamia inny wątek w celu wykonania pracy i łączy to zdarzenie robocze, aby można było wyświetlić stan wątku roboczego.

Następnie w interfejsie użytkownika musisz krzyżować wątki, aby zmienić rzeczywistą kontrolę ... jak etykieta lub pasek postępu.


62

Prostym rozwiązaniem jest użycie Control.Invoke.

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

void updateGUI() {
    // update gui here
}

dobrze zrobione dla prostoty! nie tylko proste, ale także działa dobrze! Naprawdę nie rozumiałem, dlaczego Microsoft nie może uprościć tego, jak ma być! do wywołania 1 linii w głównym wątku powinniśmy napisać kilka funkcji!
MBH,

1
@MBH Zgadzam się. BTW, czy zauważyłeś odpowiedź stackoverflow.com/a/3588137/199364 powyżej, która definiuje metodę rozszerzenia? Zrób to raz w niestandardowej klasie narzędzi, a potem nie musisz się już przejmować, że Microsoft nam tego nie zrobił :)
ToolmakerSteve

@ToolmakerSteve To dokładnie to, co to miało być! masz rację, możemy znaleźć sposób, ale mam na myśli DRY (nie powtarzaj się) z punktu widzenia problemu, który ma wspólne rozwiązanie, można go rozwiązać przy minimalnym wysiłku firmy Microsoft, co pozwoli zaoszczędzić dużo czasu na programistów :)
MBH

47

Kodowanie wątków jest często błędne i zawsze trudne do przetestowania. Nie trzeba pisać kodu wątków, aby zaktualizować interfejs użytkownika z zadania w tle. Wystarczy użyć klasy BackgroundWorker, aby uruchomić zadanie, i jego metody ReportProgress , aby zaktualizować interfejs użytkownika. Zwykle po prostu zgłaszasz procent ukończenia, ale występuje inne przeciążenie, które obejmuje obiekt stanu. Oto przykład, który po prostu zgłasza obiekt typu 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();
    }

W porządku, jeśli zawsze chcesz zaktualizować to samo pole. Jeśli masz do wykonania bardziej skomplikowane aktualizacje, możesz zdefiniować klasę reprezentującą stan interfejsu użytkownika i przekazać ją do metody ReportProgress.

I ostatnia rzecz, pamiętaj o ustawieniu WorkerReportsProgressflagi, inaczej ReportProgressmetoda zostanie całkowicie zignorowana.


2
Pod koniec przetwarzania można również zaktualizować interfejs użytkownika za pośrednictwem backgroundWorker1_RunWorkerCompleted.
DavidRR

41

Zdecydowana większość odpowiedzi używa, Control.Invokeco jest warunkiem wyścigu . Rozważmy na przykład zaakceptowaną odpowiedź:

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

Jeśli użytkownik zamknie formularz tuż przed this.Invokewywołaniem (pamiętaj, że thisjest to Formobiekt), anObjectDisposedException prawdopodobnie zostanie zwolniony.

Rozwiązaniem jest użycie SynchronizationContext, szczególnie SynchronizationContext.Currentjak sugeruje hamilton.danielb (inne odpowiedzi opierają się na konkretnych SynchronizationContextimplementacjach, co jest całkowicie niepotrzebne). Chciałbym nieco zmodyfikować swój kod używać SynchronizationContext.Postzamiast SynchronizationContext.Sendchociaż (jak zwykle nie ma potrzeby wątku pracownika czekać):

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

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

Pamiętaj, że w .NET 4.0 i nowszych wersjach naprawdę powinieneś używać zadań do operacji asynchronicznych. Zobacz odpowiedź n-san na równoważne podejście oparte na zadaniu (używanie TaskScheduler.FromCurrentSynchronizationContext).

Wreszcie, w .NET 4.5 i nowszych można również użyć Progress<T>(który w zasadzie rejestruje SynchronizationContext.Currentpo jego utworzeniu), jak wykazał Ryszard Dżegan w przypadkach, gdy długo działająca operacja musi uruchamiać kod interfejsu użytkownika podczas pracy.


37

Musisz upewnić się, że aktualizacja nastąpi we właściwym wątku; wątek interfejsu użytkownika.

Aby to zrobić, musisz wywołać moduł obsługi zdarzeń zamiast wywoływać go bezpośrednio.

Możesz to zrobić, podnosząc swoje wydarzenie w ten sposób:

(Kod jest wpisany tutaj z mojej głowy, więc nie sprawdziłem poprawnej składni itp., Ale powinienem zacząć.)

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

Pamiętaj, że powyższy kod nie będzie działał w projektach WPF, ponieważ formanty WPF nie implementują ISynchronizeInvokeinterfejsu.

W celu upewnienia się, że powyższy kod prac z Windows Forms i WPF i wszystkich innych platformach, można mieć okiem na AsyncOperation, AsyncOperationManageriSynchronizationContext klas.

Aby łatwo wywoływać zdarzenia w ten sposób, stworzyłem metodę rozszerzenia, która pozwala mi uprościć wywoływanie zdarzenia, po prostu wywołując:

MyEvent.Raise(this, EventArgs.Empty);

Oczywiście możesz również skorzystać z klasy BackGroundWorker, która wyodrębni tę sprawę za Ciebie.


Rzeczywiście, ale nie lubię zaśmiecać mojego kodu GUI tą sprawą. Moje GUI nie powinno obchodzić, czy musi się wywoływać, czy nie. Innymi słowy: nie sądzę, aby wykonywanie graficznego przełączania kontekstu było odpowiedzialnością GUI.
Frederik Gheysels

1
Rozbicie delegata na części itp. Wydaje się przesadą - dlaczego nie tylko: SynchronizationContext.Current.Send (delegate {MyEvent (...);}, null);
Marc Gravell

Czy zawsze masz dostęp do SynchronizationContext? Nawet jeśli twoja klasa jest w klasie lib?
Frederik Gheysels

29

Musisz wywołać metodę w wątku GUI. Możesz to zrobić, wywołując Control.Invoke.

Na przykład:

delegate void UpdateLabelDelegate (string message);

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

    MyLabelControl.Text = message;
}

1
W linii invoke pojawia się błąd kompilatora. Najlepsze przeciążone dopasowanie metody dla „System.Windows.Forms.Control.Invoke (System.Delegate, object [])” zawiera kilka nieprawidłowych argumentów
CruelIO

28

Z powodu trywialności scenariusza miałbym sondę wątku interfejsu użytkownika dotyczącą statusu. Myślę, że przekonasz się, że może być dość elegancki.

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

Podejście to pozwala uniknąć operacji zestawiania wymaganej podczas korzystania z ISynchronizeInvoke.InvokeiISynchronizeInvoke.BeginInvoke metod . Nie ma nic złego w stosowaniu techniki marszrutowania, ale należy pamiętać o kilku zastrzeżeniach.

  • Upewnij się, że nie dzwonisz BeginInvoke zbyt często, ponieważ może to spowodować przepełnienie pompy komunikatów.
  • Wywoływanie Invokewątku roboczego jest połączeniem blokującym. Tymczasowo zatrzyma pracę wykonywaną w tym wątku.

Strategia, którą proponuję w tej odpowiedzi, odwraca role komunikacyjne wątków. Zamiast wypychania danych przez wątek roboczy, wątek interfejsu użytkownika sprawdza go. Jest to powszechny wzorzec stosowany w wielu scenariuszach. Ponieważ wszystko, co chcesz zrobić, to wyświetlać informacje o postępie z wątku roboczego, więc myślę, że przekonasz się, że to rozwiązanie jest świetną alternatywą dla rozwiązania zestawiania. Ma następujące zalety.

  • Interfejs użytkownika i wątki robocze pozostają luźno powiązane, w przeciwieństwie do podejścia Control.Invokelub Control.BeginInvokepodejścia, które ściśle je łączy.
  • Wątek interfejsu użytkownika nie będzie hamował postępu wątku roboczego.
  • Wątek roboczy nie może zdominować czasu, w którym wątek interfejsu użytkownika spędza aktualizację.
  • Interwały, w których interfejs użytkownika i wątki robocze wykonują operacje, mogą pozostać niezależne.
  • Wątek roboczy nie może przekroczyć pompy komunikatów wątku interfejsu użytkownika.
  • Wątek interfejsu użytkownika określa, kiedy i jak często interfejs użytkownika jest aktualizowany.

3
Dobry pomysł. Jedyne, o czym nie wspomniałeś, to to, jak właściwie pozbywasz się timera po zakończeniu WorkerThread. Uwaga: może to powodować problemy po zakończeniu aplikacji (tj. Użytkownik zamyka aplikację). Czy masz pomysł, jak to rozwiązać?
Matt

@Matt Zamiast korzystać z anonimowego modułu obsługi Elapsedzdarzeń, używasz metody członka, dzięki czemu możesz usunąć licznik czasu, gdy formularz jest usuwany ...
Phil1970 20.04.2018

@ Phil1970 - Dobra uwaga. Miałeś na myśli System.Timers.ElapsedEventHandler handler = (s, a) => { MyProgressLabel.Text = m_Text; };polubienie i przypisanie go m_Timer.Elapsed += handler;później, w kontekście rozporządzania, czy m_Timer.Elapsed -= handler;mam rację? I do pozbywania się / zamykania zgodnie z radą omówioną tutaj .
Matt

27

Żadne z elementów Invoke z poprzednich odpowiedzi nie jest konieczne.

Musisz spojrzeć na WindowsFormsSynchronizationContext:

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

...

// In some non-UI Thread

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

...

void UpdateGUI(object userData)
{
    // Update your GUI controls here
}

4
jak myślisz, czego używa metoda Post pod maską? :)
niesamowicie

23

To jest podobne do powyższego rozwiązania używającego .NET Framework 3.0, ale rozwiązało problem obsługi bezpieczeństwa podczas kompilacji .

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

Używać:

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

Kompilator zawiedzie, jeśli użytkownik przekaże nieprawidłowy typ danych.

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

23

Salvete! Po wyszukaniu tego pytania znalazłem odpowiedzi FrankG i Oregon Ghost, które są dla mnie najłatwiejsze i najbardziej przydatne. Teraz piszę w języku Visual Basic i przeprowadziłem ten fragment kodu przez konwerter; więc nie jestem pewien, jak to się potoczy.

Mam okno dialogowe o nazwie o nazwie form_Diagnostics,RichtextupdateDiagWindow, którego używam jako rodzaj wyświetlania dziennika. Musiałem być w stanie zaktualizować jego tekst ze wszystkich wątków. Dodatkowe linie umożliwiają automatyczne przewijanie okna do najnowszych linii.

Mogę teraz zaktualizować ekran o jedną linię z dowolnego miejsca w całym programie w sposób, który według ciebie działałby bez wątków:

  form_Diagnostics.updateDiagWindow(whatmessage);

Kod główny (wstaw to do kodu klasy formularza):

#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

W wielu celach jest to tak proste:

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

„serviceGUI ()” to metoda na poziomie GUI w formularzu (this), która może zmieniać dowolną liczbę kontrolek. Wywołaj „updateGUI ()” z drugiego wątku. Parametry można dodawać w celu przekazania wartości lub (prawdopodobnie szybciej) używać zmiennych zasięgu klasy z blokadami na nich zgodnie z wymaganiami, jeśli istnieje możliwość zderzenia między dostępnymi do nich wątkami, co mogłoby spowodować niestabilność. Użyj BeginInvoke zamiast Invoke, jeśli wątek inny niż GUI ma krytyczne znaczenie dla czasu (pamiętając o ostrzeżeniu Briana Gideona).


21

To w mojej odmianie Cana 3.0 rozwiązania Iana Kempa:

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

Nazywasz to tak:

myButton.SetPropertyInGuiThread(b => b.Text, "Click Me!")
  1. Dodaje sprawdzanie wartości zerowej do wyniku „as MemberExpression”.
  2. Poprawia statyczne bezpieczeństwo typu.

W przeciwnym razie oryginał jest bardzo fajnym rozwiązaniem.


21
Label lblText; //initialized elsewhere

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

   lblText.Text = text;           
}

Zauważ, że BeginInvoke()jest to lepsze niżInvoke() ponieważ jest mniej prawdopodobne, że spowoduje zakleszczenie (jednak nie jest to problemem przy przypisywaniu tekstu do etykiety):

Podczas używania Invoke() czekasz na powrót metody. Może się zdarzyć, że zrobisz coś w wywoływanym kodzie, który będzie musiał czekać na wątek, co może nie być od razu oczywiste, jeśli jest zakopane w niektórych wywoływanych funkcjach, co samo może nastąpić pośrednio za pośrednictwem procedur obsługi zdarzeń. Będziesz więc czekał na wątek, wątek będzie na ciebie czekał, a ty będziesz zablokowany.

To faktycznie spowodowało zawieszenie się części naszego wydanego oprogramowania. Łatwo było to naprawić, zastępując Invoke()go BeginInvoke(). Jeśli nie potrzebujesz operacji synchronicznej, co może mieć miejsce, jeśli potrzebujesz wartości zwracanej, użyj BeginInvoke().


20

Kiedy napotkałem ten sam problem, szukałem pomocy od Google, ale zamiast dać mi proste rozwiązanie, pomyliłem się bardziej, podając przykłady MethodInvokeri bla bla bla. Postanowiłem więc rozwiązać to samodzielnie. Oto moje rozwiązanie:

Utwórz delegata w ten sposób:

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
}

Możesz wywołać tę funkcję w nowym wątku takim jak ten

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

Nie myl się Thread(() => .....). Podczas pracy na wątku używam anonimowej funkcji lub wyrażenia lambda. Aby zmniejszyć liczbę wierszy kodu, możesz również użyć ThreadStart(..)metody, której nie powinienem tutaj wyjaśniać.


17

Po prostu użyj czegoś takiego:

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

Jeśli tak e.ProgressPercentage, czy nie jesteś już w wątku interfejsu użytkownika z metody, którą wywołujesz?
LarsTech

Zdarzenie ProgressChanged działa w wątku interfejsu użytkownika. To jedna z udogodnień korzystania z BackgroundWorker. Zakończone zdarzenie działa również w interfejsie GUI. Jedyną rzeczą działającą w wątku innym niż interfejs użytkownika jest metoda DoWork.
LarsTech

15

Możesz użyć już istniejącego delegata Action:

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

14

Moja wersja polega na wstawieniu jednej linii rekurencyjnej „mantry”:

Bez argumentów:

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

        // Your code!
    }

Dla funkcji, która ma argumenty:

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

TO JEST .


Trochę argumentacji : zwykle źle jest umieścić czytelność kodu po wstawieniu {} poif () wyrażeniu instrukcji w jednym wierszu. Ale w tym przypadku jest to rutynowa „ta sama mantra”. Nie psuje to czytelności kodu, jeśli metoda ta jest spójna w całym projekcie. I to oszczędza twój kod przed śmieceniem (jeden wiersz kodu zamiast pięciu).

Jak widać if(InvokeRequired) {something long}, po prostu wiesz, że „można bezpiecznie wywoływać tę funkcję z innego wątku”.


13

Spróbuj odświeżyć etykietę przy użyciu tego

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

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

Czy to dla Windows Forms ?
Kiquenet,

13

Utwórz zmienną klasową:

SynchronizationContext _context;

Ustaw go w konstruktorze, który tworzy interfejs użytkownika:

var _context = SynchronizationContext.Current;

Kiedy chcesz zaktualizować etykietę:

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

12

Musisz użyć invoke i delegować

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

12

Większość innych odpowiedzi na to pytanie jest dla mnie trochę skomplikowana (jestem nowy w C #), więc piszę:

Mam aplikację WPF i zdefiniowałem pracownika, jak poniżej:

Kwestia:

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

Rozwiązanie:

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

Nie wiem jeszcze, co oznacza powyższa linia, ale działa.

W przypadku WinForms :

Rozwiązanie:

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

Pytanie dotyczyło Winforms, a nie WPF.
Marc L.,

Dzięki. Dodano powyżej rozwiązanie WinForms.
Manohar Reddy Poreddy

... który jest tylko kopią wielu innych odpowiedzi na to samo pytanie, ale dobrze. Dlaczego nie dołączyć do rozwiązania i po prostu usunąć swoją odpowiedź?
Marc L.,

hmm, poprawne, jesteście, jeśli tylko, uważnie przeczytacie moją odpowiedź, część początkową (powód, dla którego napisałem odpowiedź), i mam nadzieję, że przy odrobinie większej uwagi zauważycie, że jest ktoś, kto miał dokładnie ten sam problem i został dziś oceniony za moja prosta odpowiedź i jeszcze więcej uwagi, jeśli można przewidzieć prawdziwą historię, dlaczego to wszystko się stało, to Google wysyła mnie tutaj, nawet gdy szukam wpf. Jasne, skoro przeoczyłeś te mniej lub bardziej oczywiste 3 powody, rozumiem, dlaczego nie usuniesz swojej opinii. Zamiast sprzątać w porządku, stwórz coś nowego, co jest znacznie trudniejsze.
Manohar Reddy Poreddy

8

Myślę, że najłatwiejszy sposób:

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

8

Na przykład uzyskaj dostęp do kontrolki innej niż w bieżącym wątku:

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

Tam lblThresholdjest Label i Speed_Thresholdzmienna globalna.


8

Gdy jesteś w wątku interfejsu użytkownika, możesz zapytać go o harmonogram zadań kontekstu synchronizacji. Dałoby ci TaskScheduler, który planuje wszystko w wątku interfejsu użytkownika.

Następnie możesz połączyć swoje zadania, aby gdy wynik był gotowy, inne zadanie (zaplanowane w wątku interfejsu użytkownika) wybiera je i przypisuje do etykiety.

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

Działa to w przypadku zadań (nie wątków), które są obecnie preferowanym sposobem pisania współbieżnego kodu .


1
Dzwonienie Task.Startzazwyczaj nie jest dobrą praktyką blogs.msdn.com/b/pfxteam/archive/2012/01/14/10256832.aspx
Ohad Schneider

8

Właśnie przeczytałem odpowiedzi i wydaje się to bardzo gorący temat. Obecnie używam .NET 3.5 SP1 i Windows Forms.

Dobrze znana formuła doskonale opisana w poprzednich odpowiedziach, która korzysta z właściwości InvokeRequired, obejmuje większość przypadków, ale nie całą pulę.

Co jeśli uchwyt nie został jeszcze utworzony?

Właściwość InvokeRequired , jak opisano tutaj (odwołanie Control.InvokeRequired Właściwość do MSDN) zwraca wartość true, jeśli wywołanie zostało wykonane z wątku, który nie jest wątkiem GUI, false, jeśli wywołanie zostało wykonane z wątku GUI, lub jeśli uchwyt był jeszcze nie utworzone.

Możesz spotkać wyjątek, jeśli chcesz, aby formularz modalny był pokazywany i aktualizowany przez inny wątek. Ponieważ chcesz, aby ten formularz był wyświetlany modalnie, możesz wykonać następujące czynności:

private MyForm _gui;

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

Delegat może zaktualizować etykietę w 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!";
}

Może to spowodować InvalidOperationException jeśli operacje przed aktualizacją etykiety „zajmuje mniej czasu” (czytać i interpretować go jako uproszczenie) niż czas potrzebny do wątku GUI aby utworzyć formularz „s uchwyt . Dzieje się tak w ramach metody ShowDialog () .

Powinieneś również sprawdzić uchwyt w ten sposób:

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

Możesz obsłużyć operację, aby wykonać, jeśli uchwyt nie został jeszcze utworzony: możesz po prostu zignorować aktualizację GUI (jak pokazano w powyższym kodzie) lub możesz poczekać (bardziej ryzykowne). To powinno odpowiedzieć na pytanie.

Opcjonalne rzeczy: Osobiście wymyśliłem kodowanie:

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

Karmię moje formularze, które są aktualizowane przez inny wątek, instancją tego ThreadSafeGuiCommand i definiuję metody, które aktualizują GUI (w moim formularzu) w następujący sposób:

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

W ten sposób jestem całkiem pewien, że będę aktualizować GUI niezależnie od tego, który wątek wykona połączenie, opcjonalnie czekając na ściśle określony czas (limit czasu).


1
Przybyłem tutaj, aby to znaleźć, ponieważ sprawdzam również IsHandleCreated. Inną właściwością do sprawdzenia jest IsDisposed. Jeśli twój formularz zostanie usunięty, nie możesz wywołać na nim Invoke (). Jeśli użytkownik zamknął formularz przed ukończeniem wątku w tle, nie chcesz, aby próbował oddzwonić do interfejsu użytkownika, gdy formularz jest usuwany.
Jon

Powiedziałbym, że to zły pomysł, aby zacząć od ... Zwykle od razu pokazywałbyś formularz potomny i miałeś pasek postępu lub inne informacje podczas przetwarzania w tle. Albo najpierw wykonasz całe przetwarzanie, a następnie przekażesz wynik do nowego formularza podczas tworzenia. Wykonanie obu w tym samym czasie generalnie miałoby marginalne korzyści, ale znacznie mniejszy w utrzymaniu kod.
Phil1970,

Opisany scenariusz uwzględnia formę modalną używaną jako widok postępu zadania wątku w tle. Ponieważ musi być modalny, należy go pokazać, wywołując metodę Form.ShowDialog () . W ten sposób zapobiegniesz wykonywaniu kodu następującego po wywołaniu, dopóki formularz nie zostanie zamknięty. Tak więc, chyba że możesz uruchomić wątek w tle inaczej niż w podanym przykładzie (i oczywiście możesz), ten formularz musi zostać pokazany modalnie po uruchomieniu wątku w tle. W takim przypadku musisz sprawdzić, czy uchwyt ma zostać utworzony. Jeśli nie potrzebujesz formy modalnej, to inna historia.
Sume
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.