Press "Enter" to skip to content

Come passare un dato da una form ad un altra e aggiornare un valore a video nella form ricevente

In risposta ad una domanda fatta sul Form Microsoft C# (dove invito chiunque visiti il mio sito a porre quesiti per avere risposte) posto un piccolo esempio che mostra come creare una form da una esistente e aggiornare un valore in modo temporizzato.

Il thread originale sul forum non riguarda direttamente l’aggiornamento dei dati fra form, ma la prima risposta a cui questo esempio fa seguito, riguarda la correzione degli errori primari nel codice, poi, parleremo di multithreading e magari posterò un esempio in quel senso.

Il problema

Il programma in questione ha una form principale che permette una serie di attività, ed ha una form secondaria che deve visualizzare in modo temporizzato i valori derivati da una lettura sulla porta seriale. Il codice originale scritto da chi ha posto la domanda aveva un errore fondamentale, ovvero creava una form nuova ogni volta che doveva aggiornare il valore letto dalla seriale.

Vi invito a non fare smorfie dicendo “Ma come, è una cosa banale dai!” perché tutti da principianti siamo passati da questo problema, e chiunque neghi è un bugiardo patentato, anche io ho avuto questo problema, nel lontano 2003 quando ho scritto la mia prima applicazione con le form e sono la prima ad alzare la mano per indicare che sfortunatamente, le cose banali sono quelle che ti bloccano come muri, diventano davvero banali solo dopo che qualcuno ti ha spiegato come fare.

L’esempio

In questo esempio ho creato un progetto con 2 Form, siccome non mi piace lasciare i nomi originali alle classi (Form1, Form2) ho dato loro dei nomi utili a chi legge per comprendere meglio anche quando guarderanno il codice il prossimo anno.

MainForm01

La Form Principale MainForm  ha una Label, una TextBox (txtManual) e tre Button, (buttonPass, buttonStart, buttonStop)

DisplayForm01

La Form secondaria DisplayForm ha 2 Label, due TextBox (txtManualData, txtTimedData)

Il codice di MainForm.cs

private System.Timers.Timer mTemporizedShow;
 
private DisplayForm mDisplay;
 
private Random mRnd;

Prima di tutto definiamo 3 variabili a livello di classe che ci serviranno per quello che vogliamo fare:

  • mTemporizedShow: è un timer e se qualcuno chiede perché non hai usato un System.Windows.Forms.Timer invece di questo? Semplicemente perché chi ha fatto la domanda ha usato questo (facendosi del male senza saperlo) ma ci permette di parlare dell’uso della UI in caso di MultiThreading.
  • mDisplay: è la variabile che conterrà il nostro form secondario, permettendoci di aggiornarlo dopo averlo creato.
  • mRnd: è un generatore di numeri casuali che utilizzerò per simulare la lettura temporizzata di un dato da una fonte esterna.
public MainForm()
{
	InitializeComponent();
	StartPosition = FormStartPosition.CenterScreen;
	mRnd = new Random(DateTime.Now.Millisecond);
}

Nel costruttore della form facciamo apparire la finestra in centro allo schermo e inizializziamo il generatore di numeri casuali con un valore casuale (il millisecondo del momento).

private void buttonPass_Click(object sender, EventArgs e)
{
	CheckCreateDisplayForm();
	mDisplay.ManualData = txtManual.Text;
}

Il codice che risponde al click di buttonPass, il bottone che passa il dato manualmente inserito alla form secondaria, cosa fa questo codice:

  • Controlla se esiste la form secondaria e se non c’è la crea e la mostra (il metodo lo vediamo fra poco)
  • Aggiorna il valore del dato manuale
private void buttonStart_Click(object sender, EventArgs e)
{
	if (mTemporizedShow == null)
	{
		mTemporizedShow = new System.Timers.Timer();
		mTemporizedShow.Elapsed += MTemporizedShow_Elapsed;
		mTemporizedShow.Interval = 1000;
	}
	mTemporizedShow.Start();
}

Il codice che risponde al click di buttonStart, il bottone che fa partire il timer, cosa fa questo codice?

  • Se il timer non è ancora mai stato inizializzato lo genera e lo predispone.
  • Poi lo fa partire.

E cos’è MTemporizedShow_Elapsed? fra poco lo vediamo, è il codice che aggiorna i dati quando il timer ha contato fino a 1000 millisecondi, ovvero 1 secondo.

private void buttonStop_Click(object sender, EventArgs e)
{
	if (mTemporizedShow != null)
	{
		mTemporizedShow.Stop();
	}
}

Il codice che risponde al click di buttonStop, il bottone che ferma il timer, cosa fa questo codice?

  • Controlla se il timer esiste (perché ovviamente i vostri utenti possono schiacciare i bottoni nell’ordine che preferiscono quindi ricordatevi i controlli per evitare eccezioni)
  • Se esiste, ferma il timer.
private void CheckCreateDisplayForm()
{
	if (mDisplay == null)
	{
		mDisplay = new DisplayForm();
		mDisplay.Owner = this;
		mDisplay.Closed += MDisplay_Closed;
		mDisplay.Show();
	}
}

Il codice che genera la form di visualizzazione dei dati, cosa fa questo codice?

  • Se non è stata creata la form, genera la nuova form
  • Indica alla form che MainForm è il suo Owner, così che se viene chiuso MainForm si chiuda anche DisplayForm.
  • Aggiunge un gestore evento all’evento Closed della form (perché lo vediamo fra poco)
  • Mostra la form.
private void MTemporizedShow_Elapsed(object sender, ElapsedEventArgs e)
{
	CheckCreateDisplayForm();
	//Create some random data and show them
	mDisplay.TimedData = mRnd.Next(0, 100).ToString();
 
}

Questo è il codice eseguito quando il timer scatta, ovvero:

  • Controlla e genera la form se non esiste
  • Aggiorna il valore della textbox dei dati temporizzati
private void MDisplay_Closed(object sender, EventArgs e)
{
	//Reset the form variable if the user closes it
	mDisplay = null;
}

Questo è il codice eseguito quando l’utente chiude la form di visualizzazione dati, che cosa fa questo codice?

  • Azzera la variabile che contiene il riferimento alla form chiusa, in modo che possa essere aperta nuovamente dal timer o dal bottone di invio dati manuali. Se non facciamo così potete verificare che otterrete una bellissima eccezione.

Il codice di DisplayForm.cs

public DisplayForm()
{
	InitializeComponent();
	StartPosition = FormStartPosition.CenterScreen;
}

Il costruttore della form, semplicemente posiziona la form in mezzo allo schermo (in Windows Forms manca il posizionamento CenterOwner, che in questo caso sarebbe più utile).

public string ManualData
{
	get
	{
		return txtManualData.Text;
	}
	set
	{
		txtManualData.Text = value;
	}
}

La prima property, questo codice espone in modo pubblico la property Text della prima textbox, txtManualData così che sia visibile dalla form chiamante.

public string TimedData
{
	get
	{
		return txtTimedData.Text;
	}
	set
	{
		txtTimedData.Text = value;
	}
}

La seconda property, questo codice espone in modo pubblico la property Text della seconda textbox, txtTimedData così che sia visibile alla form chiamante.

Il Test

Se facciamo partire il programma otteniamo questo:

MainForm02

la nostra Form principale, adesso proviamo a scrivere qualcosa nella textbox e premere il tasto Pass Data:

MainForm03

La form secondaria appare in mezzo alla principale, il risultato è comunque questo, la form secondaria si apre e mostra la stringa passata.

Se scriviamo qualcos’altro e premiamo di nuovo pass data:

MainForm04

La form secondaria sarà aggiornata.

Adesso premiamo invece Start Timer:

xtread_exception

Sfortunatamente, invece di ottenere il dato temporizzato, otteniamo un eccezione, Cross-thread operation not valid. A cosa è dovuta? Semplicemente al fatto che la classe System.Timers.Timer che è stata utilizzata per produrre quanto necessario a effettuare la lettura temporizzata, lavora su un thread diverso da quello della User Interface dove le Form risiedono, e il suo event handler viene chiamato da questo thread, non è possibile aggiornare direttamente la User Interface da un thread esterno.

Come si risolve tutto questo? ci sono 2 soluzioni:

Soluzione 1 Utilizzare un Timer disegnato per la User Interface e quindi Thread Safe

Sostituendo il System.Timers.Timer con un System.Windows.Forms.Timer

private System.Windows.Forms.Timer mTemporizedShow;

E modificando il codice di buttonStart:

private void buttonStart_Click(object sender, EventArgs e)
{
	if (mTemporizedShow == null)
	{
		//mTemporizedShow = new System.Timers.Timer();
		//mTemporizedShow.Elapsed += MTemporizedShow_Elapsed;
		mTemporizedShow = new System.Windows.Forms.Timer();
		mTemporizedShow.Tick += MTemporizedShow_Tick;
		mTemporizedShow.Interval = 1000;
	}
	mTemporizedShow.Start();
}

Creando l’event handler corretto:

private void MTemporizedShow_Tick(object sender, EventArgs e)
{
	CheckCreateDisplayForm();
	//Create some random data and show them
	mDisplay.TimedData = mRnd.Next(0, 100).ToString();
}

E riprovando

MainForm05

Vedrete che funziona.

Soluzione 2 Conservare il Timer originale e inserire il codice per la gestione del cross threading

Se per qualsiasi motivo non vi fosse possibile utilizzare il Timer delle Windows Forms, e doveste per forza utilizzare il timer definito ecco come risolvere il problema:

private void MTemporizedShow_Elapsed(object sender, ElapsedEventArgs e)
{
	try
	{
		if (this.InvokeRequired)
		{
			this.BeginInvoke(new MethodInvoker(delegate ()
			{
				MTemporizedShow_Elapsed(sender, e);
			}));
		}
		else
		{
			CheckCreateDisplayForm();
			//Create some random data and show them
			mDisplay.TimedData = mRnd.Next(0, 100).ToString();
		}
	}
	catch (Exception ex)
	{
		MessageBox.Show(ex.Message);
	}
}

In questo caso è necessario modificare il metodo che gestisce l’evento Elapsed del timer e testare la property InvokeRequired della Form. Tale property viene posta a True se la chiamata all’event handler è effettuata su un diverso thread rispetto a quello della User Interface, testando questa variabile, possiamo utilizzare il metodo BeginInvoke a cui passeremo un delegate creato al volo per l’event handler stesso, e questo effettuerà una chiamata al nostro event handler dal thread in cui si trova la form eseguendo correttamente l’aggiornamento della User Interface.

A voi l’onere e l’onore di utilizzare quello che più si confà alle esigenze della vostra applicazione.

Potete scaricare il progetto esempio dal link qui indicato:

Per qualsiasi domanda, approfondimento, commento, o per segnalare un errore potete usare il link al modulo di contatto in cima alla pagina.