Accesso ai controlli dell’interfaccia utente in Task.Run con async / attendere su WinForms

Ho il seguente codice in un’applicazione WinForms con un pulsante e un’etichetta:

using System; using System.IO; using System.Threading.Tasks; using System.Windows.Forms; namespace WindowsFormsApplication1 { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private async void button1_Click(object sender, EventArgs e) { await Run(); } private async Task Run() { await Task.Run(async () => { await File.AppendText("temp.dat").WriteAsync("a"); label1.Text = "test"; }); } } } 

Questa è una versione semplificata dell’applicazione reale su cui sto lavorando. Avevo l’impressione che usando async / await nel mio Task.Run potevo impostare la proprietà label1.Text . Tuttavia, quando si esegue questo codice viene visualizzato l’errore che non si trova sul thread dell’interfaccia utente e non riesco ad accedere al controllo.

Perché non riesco ad accedere al controllo dell’etichetta?

Quando si utilizza Task.Run() , si dice che non si desidera che il codice venga eseguito nel contesto corrente, quindi è esattamente ciò che accade.

Ma non è necessario utilizzare Task.Run() nel codice. I metodi async scritti non bloccano il thread corrente, quindi puoi utilizzarli direttamente dal thread dell’interfaccia utente. Se lo fai, await si assicurerà che il metodo ritorni sul thread dell’interfaccia utente.

Ciò significa che se scrivi il tuo codice in questo modo, funzionerà:

 private async void button1_Click(object sender, EventArgs e) { await Run(); } private async Task Run() { await File.AppendText("temp.dat").WriteAsync("a"); label1.Text = "test"; } 

Prova questo

 private async Task Run() { await Task.Run(async () => { await File.AppendText("temp.dat").WriteAsync("a"); }); label1.Text = "test"; } 

O

 private async Task Run() { await File.AppendText("temp.dat").WriteAsync("a"); label1.Text = "test"; } 

O

 private async Task Run() { var task = Task.Run(async () => { await File.AppendText("temp.dat").WriteAsync("a"); }); var continuation = task.ContinueWith(antecedent=> label1.Text = "test",TaskScheduler.FromCurrentSynchronizationContext()); await task;//I think await here is redundant } 

async / await non garantisce che verrà eseguito nel thread dell’interfaccia utente. await attiverà l’attuale SynchronizationContext e continuerà l’esecuzione con il contesto acquisito una volta completata l’attività.

Quindi nel tuo caso hai un Task.Run annidato che si trova dentro Task.Run quindi la seconda await catturerà il contesto che non sarà UiSynchronizationContext perché viene eseguito da WorkerThread da ThreadPool .

Questo risponde alla tua domanda?

Prova questo:

sostituire

 label1.Text = "test"; 

con

 SetLabel1Text("test"); 

e aggiungi quanto segue alla tua class:

 private void SetLabel1Text(string text) { if (InvokeRequired) { Invoke((Action)SetLabel1Text, text); return; } label1.Text = text; } 

InvokeRequired restituisce true se NON si è sul thread dell’interfaccia utente. Il metodo Invoke () accetta il delegato e i parametri, passa al thread dell’interfaccia utente e quindi chiama il metodo in modo ricorsivo. Si ritorna dopo la chiamata Invoke () perché il metodo è già stato chiamato in modo ricorsivo prima del ritorno di Invoke (). Se ti capita di essere sul thread dell’interfaccia utente quando viene chiamato il metodo, l’InvokeRequired è falso e l’assegnazione viene eseguita direttamente.

Perché usi Task.Run? che avvia un nuovo thread di lavoro (legato alla cpu) e causa il tuo problema.

probabilmente dovresti semplicemente farlo:

  private async Task Run() { await File.AppendText("temp.dat").WriteAsync("a"); label1.Text = "test"; } 

attendi assicurati di continuare sullo stesso contesto, tranne se usi .ConfigureAwait (false);

Perché è su un thread diverso e le chiamate cross-thread non sono consentite.

Dovrai passare il “contesto” al thread che stai iniziando. Guarda un esempio qui: http://reedcopsey.com/2009/11/17/synchronizing-net-4-tasks-with-the-ui-thread/

Sto per darti la mia ultima risposta che ho dato per la comprensione asincrona.

La soluzione è come sapete che quando si chiama il metodo asincrono è necessario eseguire come attività.

Ecco un codice app per console veloce che puoi usare come riferimento, facilitando la comprensione del concetto.

 using System; using System.Threading; using System.Threading.Tasks; public class Program { public static void Main() { Console.WriteLine("Starting Send Mail Async Task"); Task task = new Task(SendMessage); task.Start(); Console.WriteLine("Update Database"); UpdateDatabase(); while (true) { // dummy wait for background send mail. if (task.Status == TaskStatus.RanToCompletion) { break; } } } public static async void SendMessage() { // Calls to TaskOfTResult_MethodAsync Task returnedTaskTResult = MailSenderAsync(); bool result = await returnedTaskTResult; if (result) { UpdateDatabase(); } Console.WriteLine("Mail Sent!"); } private static void UpdateDatabase() { for (var i = 1; i < 1000; i++) ; Console.WriteLine("Database Updated!"); } private static async Task MailSenderAsync() { Console.WriteLine("Send Mail Start."); for (var i = 1; i < 1000000000; i++) ; return true; } } 

Qui sto cercando di avviare un'attività chiamata send mail. Interim Voglio aggiornare il database, mentre lo sfondo sta eseguendo l'attività di invio della posta.

Una volta che l'aggiornamento del database è avvenuto, è in attesa del completamento dell'attività di invio della posta. Tuttavia, con questo approccio è abbastanza chiaro che posso eseguire attività in background e continuare comunque con il thread (principale) originale.