Si tratta di un esempio in risposta ad una richiesta sul forum Microsoft, Questo è il link al thread in cui un principiante ha dapprima postato un problema e una volta risolto il problema contingente, ha spiegato che vorrebbe realizzare un programma per fare quiz in cui inserire i quiz su dei file di testo e da questi creare al volo i controlli sulla form per fare le domande.
La mia risposta è stata che non si tratta della soluzione migliore, quella di creare tutti i controlli al volo, e in questo articolo vediamo un esempio di come si fa e perché non la considero la soluzione più usabile per creare una applicazione.
In seguito posterò un esempio di come risolverei la cosa in modo meno complicato.
L’esempio
L’esempio che ho creato è un applicazione Windows Forms (perché è quello che ha usato chi ha posto la domanda), contiene una form con 2 opzioni di menu, ognuna di queste genera una serie di controlli che corrispondono ad una domanda a risposta singola ed una domanda a risposte multiple.
Il programma risultante ha il seguente aspetto:
La form base con le due opzioni di menu.
I controlli generati per una domanda con singola opzione.
I controlli generati per una domanda con più opzioni.
Il bottone Show, in entrambi i casi mostra la risposta o le risposte selezionate.
Vediamo ora il codice dietro alla form:
private string mSelectedAnswer;
private List<string> mSelectedAnswers = new List<string>();
Innanzi tutto abbiamo definito una stringa ove memorizzare la risposta singola ed una lista ove memorizzare le risposte multiple.
private void firstToolStripMenuItem_Click(object sender, EventArgs e)
{
this.pnlControlContainer.Controls.Clear();
Panel p = new Panel();
p.Name = "contentPanel";
this.pnlControlContainer.Controls.Add(p);
p.Dock = DockStyle.Fill;
p.BringToFront();
Label l = new Label();
l.Name = "question";
l.Text = "What is the matrix?";
p.Controls.Add(l);
l.Location = new Point(10, 30);
GroupBox gb = new GroupBox();
gb.Name = "grbOptions";
gb.Text = "Answers:";
p.Controls.Add(gb);
gb.Width = 300;
gb.Height = 120;
gb.Location = new Point(10, 60);
RadioButton opt = new RadioButton();
opt.Text = "The matrix is a series of numbers used to map data.";
opt.Name = "Option1";
opt.Checked = false;
gb.Controls.Add(opt);
opt.Dock = DockStyle.Top;
opt.CheckedChanged += Opt_CheckedChanged;
opt = new RadioButton();
opt.Text = "The matrix is how a Warehouse can be mapped for automatic management.";
opt.Name = "Option2";
opt.Checked = false;
gb.Controls.Add(opt);
opt.Dock = DockStyle.Top;
opt.CheckedChanged += Opt_CheckedChanged;
opt = new RadioButton();
opt.Text = "The matrix is the reality as we see it around us.";
opt.Name = "Option3";
opt.Checked = false;
gb.Controls.Add(opt);
opt.Dock = DockStyle.Top;
opt.CheckedChanged += Opt_CheckedChanged;
Button but = new Button();
but.Name = "Uno";
but.Click += Uno_Click;
but.Text = "Show Answer";
p.Controls.Add(but);
but.Location = new Point(200, 190);
}
Il codice che genera i controlli dell’ opzione a risposta singola.
Vediamolo controllo per controllo, innanzi tutto, nella form1, oltre al menuStrip1 che contiene le 2 opzioni, abbiamo inserito un panel, pnlControlContainer, che ha solo la funzione di fare da contenitore ai controlli generati da codice permettendoci di cancellarli quando cambiamo opzione passando da un tipo di quiz all’altro. Se non avessimo inserito questo pannello ed avessimo usato l’istruzione Controls.Clear che vedrete applichiamo prima di generare i controlli, avremmo cancellato anche il menuStrip rendendo la form non utilizzabile.
this.pnlControlContainer.Controls.Clear();
Panel p = new Panel();
p.Name = "contentPanel";
this.pnlControlContainer.Controls.Add(p);
p.Dock = DockStyle.Fill;
p.BringToFront();
La prima cosa che facciamo è cancellare il contenuto del pannello principale, dopodiché creiamo un altro pannello in cui inseriremo i nostri controlli. Ci serve esclusivamente per posizionare i controlli generati, pertanto lo inseriamo nel pannello principale e modifichiamo la sua property Dock in modo che riempia tutto lo spazio disponibile.
Label l = new Label();
l.Name = "question";
l.Text = "What is the matrix?";
p.Controls.Add(l);
l.Location = new Point(10, 30);
Generiamo ora una label dove inseriremo il testo della domanda, la label verrà aggiunta ai controlli del pannello generato in precedenza. Fate attenzione che modifichiamo la sua Location dopo averla inserita nel pannello, perché Location che è la posizione dell’angolo superiore sinistro della label, è relativa al suo contenitore quindi il pannello, è opportuno aggiornarla dopo averla inserita in tale contenitore.
GroupBox gb = new GroupBox();
gb.Name = "grbOptions";
gb.Text = "Answers:";
p.Controls.Add(gb);
gb.Width = 300;
gb.Height = 120;
gb.Location = new Point(10, 60);
Adesso generiamo una groupbox in cui inseriremo le risposte opzionali. Perché creare una groupbox? Per dare un etichetta al gruppo di opzioni, e soprattutto perché i RadioButton che utilizzeremo per le risposte a opzione singola, sono fatti in modo da reagire automaticamente come un gruppo se sono inseriti in un contenitore (come la groupbox) pertanto quando selezioneremo un opzione, se un altra era selezionata in precedenza verrà deselezionata Vedremo come utilizzeremo questo meccanismo per sapere sempre la risposta selezionata.
RadioButton opt = new RadioButton();
opt.Text = "The matrix is a series of numbers used to map data.";
opt.Name = "Option1";
opt.Checked = false;
gb.Controls.Add(opt);
opt.Dock = DockStyle.Top;
opt.CheckedChanged += Opt_CheckedChanged;
Generiamo il primo dei RadioButton delle risposte opzionali. Essendo un quiz, inizialmente tutte le risposte saranno false. Per non doverci preoccupare di posizionare ognuno dei controlli RadioButton, utilizziamo la property Dock = Top in modo che i controlli si allarghino come ad occupare una “riga” nel loro contenitore. Vedremo come il posizionamento sia fra le cose più complicate di questo tipo di codice. Il RadioButton verrà inserito nei Controls della Groupbox.
Potete notare che ho creato un event handler per l’evento CheckedChanged del RadioButton, in questo evento gestiremo l’aggiornamento della risposta selezionata.
opt = new RadioButton();
opt.Text = "The matrix is how a Warehouse can be mapped for automatic management.";
opt.Name = "Option2";
opt.Checked = false;
gb.Controls.Add(opt);
opt.Dock = DockStyle.Top;
opt.CheckedChanged += Opt_CheckedChanged;
opt = new RadioButton();
opt.Text = "The matrix is the reality as we see it around us.";
opt.Name = "Option3";
opt.Checked = false;
gb.Controls.Add(opt);
opt.Dock = DockStyle.Top;
opt.CheckedChanged += Opt_CheckedChanged;
Il secondo e terzo RadioButton sono identici al precedente se non per la risposta e il nome.
Button but = new Button();
but.Name = "Uno";
but.Click += Uno_Click;
but.Text = "Show Answer";
p.Controls.Add(but);
but.Location = new Point(200, 190);
Come ultima cosa aggiungiamo un Button per verificare la risposta selezionata. Il suo posizionamento e la sua dimensione che non ho controllato fanno in modo che non tutto il testo si veda nella form, e questo è un altro dei motivi per cui generare i controlli da codice non è sempre la cosa più semplice del mondo.
private void Opt_CheckedChanged(object sender, EventArgs e)
{
RadioButton btn = sender as RadioButton;
if (btn != null)
{
if (btn.Checked)
{
mSelectedAnswer = btn.Text;
}
}
}
Questo è l’ event handler che gestisce la modifica della risposta al cambio di opzione sui radio button. In modo che il Button che mostra la risposta possa mostrare sempre quella corretta. Da notare per i principianti che tutti gli Event Handler hanno come primo parametro un object che per convenzione viene chiamato sender. Questo object è sempre l’oggetto che ha sollevato l’evento, quindi nel caso dei RadioButton è il RadioButton la cui property Checked è stata modificata. Pertanto l’operazione di Cast che facciamo del Sender in RadioButton ci permette di leggere il Text del RadioButton e fare quel che ci serve.
Perché controllare il null? per disciplina, in questo caso sappiamo di averlo usato solo sui RadioButton, ma in una applicazione del mondo reale, quando c’è molto codice da scrivere, e Copia ed Incolla può accadere sia usato (spesso) potrebbe portarci a introdurre un errore perché abbiamo incollato il codice di un RadioButton in qualcos’altro, perciò sempre meglio controllare, anche perché se poi il dato non ci fosse, in debug ce ne accorgeremo subito.
private void Uno_Click(object sender, EventArgs e)
{
MessageBox.Show("The Selected answer is:" + mSelectedAnswer);
}
Il click del button, semplicemente mostra la variabile che l’event handler precedente gestisce.
private void secondToolStripMenuItem_Click(object sender, EventArgs e)
{
this.pnlControlContainer.Controls.Clear();
Panel p = new Panel();
p.Name = "contentPanel";
this.pnlControlContainer.Controls.Add(p);
p.Dock = DockStyle.Fill;
p.BringToFront();
Label l = new Label();
l.Name = "question";
l.Text = "Which of these have been singers of The Marillion?";
p.Controls.Add(l);
l.Location = new Point(10, 30);
GroupBox gb = new GroupBox();
gb.Name = "grbOptions";
gb.Text = "Answers:";
p.Controls.Add(gb);
gb.Width = 300;
gb.Height = 120;
gb.Location = new Point(10, 60);
CheckBox chk = new CheckBox();
chk.Text = "Fish";
chk.Name = "Option1";
chk.Checked = false;
gb.Controls.Add(chk);
chk.Dock = DockStyle.Top;
chk.CheckedChanged += Chk_CheckedChanged;
chk = new CheckBox();
chk.Text = "David Lee Roth";
chk.Name = "Option2";
chk.Checked = false;
gb.Controls.Add(chk);
chk.Dock = DockStyle.Top;
chk.CheckedChanged += Chk_CheckedChanged;
chk = new CheckBox();
chk.Text = "Steve Hogarth";
chk.Name = "Option3";
chk.Checked = false;
gb.Controls.Add(chk);
chk.Dock = DockStyle.Top;
chk.CheckedChanged += Chk_CheckedChanged;
Button but = new Button();
but.Name = "Due";
but.Text = "Show Answers";
but.Click += Due_Click;
p.Controls.Add(but);
but.Location = new Point(200, 190);
}
Il codice del generatore del quiz con risposte multiple, cosa cambia? solamente che i RadioButton sono sostituiti dalle Checkbox.
private void Chk_CheckedChanged(object sender, EventArgs e)
{
CheckBox chk = sender as CheckBox;
if (chk != null)
{
if (chk.Checked)
{
mSelectedAnswers.Add(chk.Text);
}
else
{
mSelectedAnswers.Remove(chk.Text);
}
}
}
In questo caso, cambia l’event handler per il Checked Changed delle checkbox, infatti, visto che si tratta di risposte multiple, dobbiamo gestire una lista di risposte, e lo facciamo semplicemente controllando il valore assunto dalla property Checked della CheckBox in modo da aggiungere o togliere la stringa del testo della risposta dalla lista.
Perché creare al volo i controlli in questo modo non è un pattern fra i più consigliabili per una applicazione?
Per prima cosa perché dobbiamo scrivere una notevole quantità di codice anche per qualcosa di semplice come quello che abbiamo fatto in questo esempio. E soprattutto perché controllare l’aspetto, la dimensione ed il posizionamento dei controlli è piuttosto complicato. Pertanto, se ci sono delle motivazioni quali ad esempio una enorme varietà di possibili diverse combinazioni dei controlli, o magari il fatto che per quanto diversi ce ne siano pochi, allora potete prendere in considerazione questo tipo di codice.
Altrimenti, il mio consiglio è quello di creare una serie di User Control, uno per ogni tipo di domanda o comunque configurazione, ed eventualmente modificare solo le stringhe domande e risposte al loro interno.
Per qualsiasi domanda, approfondimento, commento, o per segnalare un errore potete usare il link al modulo di contatto in cima alla pagina.