Microsoft® Visual C#® 2010 Step by Step

TITOLO ORIGINALE
Microsoft® Visual C#® 2010 Step by Step
Copyright © 2010 by John Sharp.
All rights reserved. Published by arrangement with the original publisher, Microsoft Corporation, Redmond,
Washington, U.S.A.
ITALIAN language edition published by SPERLING & KUPFER EDITORI S.p.A
All rights reserved. No part of this book may be reproduced or transmitted in any form or by means, electronic
or mechanical, including photocopying, recording or by any information storage retrieval system, without
permission of the publisher.
Nessuna parte del testo può essere riprodotta senza autorizzazione scritta di
Sperling & Kupfer Editori S.p.A.
Copertina per l’edizione italiana: Sperling & Kupfer Editori S.p.A.
Traduzione, revisione tecnica e realizzazione editoriale: IT-Wire srl
Ogni cura è stata posta nella raccolta e nella verifica della documentazione contenuta in questo libro.
Tuttavia né gli autori, né Microsoft Press, né Sperling & Kupfer Editori possono assumersi alcuna
responsabilità derivante dall’utilizzo della stessa. Lo stesso dicasi per ogni persona o società coinvolta nella
creazione, nella produzione e nella distribuzione di questo libro.
Microsoft, Microsoft Press, Excel, IntelliSense, Internet Explorer, Jscript, MS, MSDN, SQL Server, Visual
Basic, Visual C#, Visual C++, Visual Studio, Win32, Windows, e Windows Vista sono marchi o marchi
registrati di Microsoft Corporation negli Stati Uniti e negli altri Paesi. Altri nomi di prodotti e di aziende
citati in queste pagine possono essere marchi dei rispettivi proprietari.
© 2010 Sperling & Kupfer Editori S.p.A.
Mondadori Informatica® è un marchio registrato da Arnoldo Mondadori Editore S.p.A.
Prima edizione Miti informatica: ottobre 2012
ISBN: 978-88-6114-349-4
Edizioni:
1 2 3 4 5 6 7 8 9 10
2012 2013 2014 2015 2016
Finito di stampare nel mese di ottobre 2012
presso Mondadori Printing S.p.A.
Stabilimento di N.S.M. di Cles (TN)
Printed in Italy
Sommario breve parte I-III
Parte I
1
2
3
4
5
6
Parte II
7
8
9
10
11
12
13
14
Parte III
15
16
17
18
19
20
21
Introduzione a Microsoft Visual C#
e Microsoft Visual Studio 2010
Introduzione a C# . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
Utilizzo di variabili, operatori ed espressioni . . . . . . . . . . . . . . . . . . . . . 27
Scrittura di metodi e applicazione di ambiti . . . . . . . . . . . . . . . . . . . . . 47
Utilizzo delle istruzioni decisionali . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
Utilizzo degli operatori di assegnazione composti
e delle istruzioni di iterazione . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
Gestione di errori ed eccezioni . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109
Analisi del linguaggio C#
Creazione e gestione di classi e oggetti . . . . . . . . . . . . . . . . . . . . . . . . . .129
Introduzione a valori e riferimenti . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .151
Creazione di tipi valore con enumerazioni e strutture . . . . . . . . . .173
Utilizzo di matrici e insiemi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .191
Introduzione alle matrici di parametri . . . . . . . . . . . . . . . . . . . . . . . . . . . .219
Utilizzo dell’ereditarietà . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .231
Creazione di interfacce e definizione di classi astratte . . . . . . . . . .253
Utilizzo di Garbage collection e gestione delle risorse . . . . . . . . . .279
Creazione dei componenti
Implementazione delle proprietà per accedere ai campi . . . .
Utilizzo degli indicizzatori . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Interruzione del programma e gestione degli eventi . . . . . . .
Introduzione ai generics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Enumerazione degli insiemi . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Interrogazione dei dati in memoria mediante
espressioni di query. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Overload degli operatori . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
295
315
329
353
381
395
419
iii
Sommario parte I-III
Ringraziamenti . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xxi
Introduzione . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xxiii
Parte I
1
Introduzione a Microsoft Visual C# e Microsoft Visual Studio 2010
Introduzione a C# . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
Primo approccio all’ambiente di programmazione di Visual Studio 2010 . . . . . . . . . . . . . 3
Scrittura del primo programma . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
Utilizzo dello spazio dei nomi. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
Creazione di un’applicazione grafica . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
Riferimenti rapidi del capitolo 1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
2
Utilizzo di variabili, operatori ed espressioni . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .27
Nozioni sulle istruzioni . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
Utilizzo degli identificatori . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
Identificazione delle parole chiave . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
Utilizzo delle variabili . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
Assegnazione del nome alle variabili . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
Dichiarazione delle variabili . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
Utilizzo dei tipi di dati primitivi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
Variabili locali non assegnate . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
Visualizzazione dei valori dei tipi di dati primitivi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
Utilizzo degli operatori aritmetici . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
Operatori e tipi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
Analisi degli operatori aritmetici . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
Controllo della precedenza . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
Utilizzo dell’associatività per valutare le espressioni . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
Associatività e operatore di assegnazione . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
Aumento e diminuzione del valore delle variabili . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
Prefissi e suffissi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
Dichiarazione di variabili locali tipizzate in modo implicito . . . . . . . . . . . . . . . . . . . . . . . . 44
Riferimenti rapidi del capitolo 2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46
v
vi
Sommario parte I-III
3
Scrittura di metodi e applicazione di ambiti . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
Creazione di metodi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
Dichiarazione di un metodo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
Restituzione di dati da un metodo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
Chiamata di metodi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
Definizione della sintassi di una chiamata di metodo . . . . . . . . . . . . . . . . . . . . . . . . . . 51
Applicazione di ambito . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
Definizione dell’ambito locale . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54
Definizione dell’ambito della classe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54
Overload dei metodi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
Scrittura di metodi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56
Utilizzo di parametri facoltativi e argomenti denominati . . . . . . . . . . . . . . . . . . . . . . . . . . 63
Definizione di parametri facoltativi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
Comunicazione di argomenti denominati . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
Eliminare le ambiguità con l’utilizzo di parametri facoltativi
e argomenti denominati . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66
Riferimenti rapidi del capitolo 3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
4
Utilizzo delle istruzioni decisionali . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
Dichiarazione di variabili booleane . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
Utilizzo degli operatori booleani . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74
Nozioni su operatori di uguaglianza e relazionali. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74
Nozioni sugli operatori logici condizionali . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
Corto circuito . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
Riepilogo della precedenza e dell’associatività degli operatori . . . . . . . . . . . . . . . . . . 76
Utilizzo delle istruzioni if per le decisioni . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
Nozioni sulla sintassi dell’istruzione if . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
Utilizzo di blocchi per raggruppare le istruzioni . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78
Azioni a cascata associate alle istruzioni if . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
Utilizzo delle istruzioni switch. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
Nozioni sulla sintassi delle istruzioni switch . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
Rispetto delle regole delle istruzioni switch . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86
Riferimenti rapidi del capitolo 4 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89
5
Utilizzo degli operatori di assegnazione composti e delle istruzioni
di iterazione . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
Utilizzo degli operatori di assegnazione composti . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
Scrittura di istruzioni while . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92
Sommario parte I-III
vii
Scrittura di istruzioni for . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97
Nozioni sull’ambito delle istruzioni for . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98
Scrittura di istruzioni do . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99
Riferimenti rapidi del capitolo 5 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108
6
Gestione di errori ed eccezioni . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .109
Gestione degli errori . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109
Verifica del codice e rilevamento delle eccezioni . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110
Eccezioni non gestite . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111
Utilizzo di più gestori catch. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112
Rilevamento di più eccezioni . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113
Utilizzo dei calcoli dei valori integer checked e unchecked . . . . . . . . . . . . . . . . . . . . . . . 117
Scrittura di istruzioni checked . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 118
Scrittura di espressioni checked . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119
Generazione di eccezioni. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 120
Utilizzo di un blocco finally . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 124
Riferimenti rapidi del capitolo 6 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 126
Parte II
7
Analisi del linguaggio C#
Creazione e gestione di classi e oggetti . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .129
Comprendere la classificazione . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129
Lo scopo dell’incapsulamento . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130
Definizione e utilizzo di una classe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130
Controllo dell’accessibilità . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 132
Uso dei costruttori . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 133
Overload dei costruttori . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 134
Come comprendere dati e metodi statici . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 142
Creazione di un campo condiviso . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 143
Creazione di un campo statico mediante la parola chiave const . . . . . . . . . . . . . . . . 143
Classi statiche. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 144
Classi anonime. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 147
Riferimenti rapidi del capitolo 7 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148
8
Introduzione a valori e riferimenti . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 151
Copia di variabili di tipo valore e classi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 151
Introduzione ai valori Null e ai tipi Nullable . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 156
Utilizzo dei tipi nullable . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 157
Introduzione alle proprietà dei tipi nullable . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 158
viii
Sommario parte I-III
Utilizzo dei parametri ref e out . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 158
Creazione di parametri ref . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 159
Creazione di parametri out . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 160
Organizzazione della memoria del computer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 162
Utilizzo di stack e heap . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 163
La classe System.Object . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 164
Boxing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 165
Unboxing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 166
Casting sicuro dei dati . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 167
L’operatore is . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 168
L’operatore as . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 168
Riferimenti rapidi del capitolo 8 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 171
9
Creazione di tipi valore con enumerazioni e strutture . . . . . . . . . . . . . . . . . . . . . . . 173
Utilizzo delle enumerazioni. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 173
Dichiarazione di un’enumerazione . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 173
Utilizzo di un’enumerazione . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 174
Scelta dei valori della costante di enumerazione . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 175
Scelta del tipo sottostante un’enumerazione . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 176
Utilizzo delle strutture . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 178
Dichiarazione di una struttura . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 179
Introduzione alle differenze tra struttura e classe . . . . . . . . . . . . . . . . . . . . . . . . . . . . 180
Dichiarazione delle variabili di struttura . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 182
Introduzione all’inizializzazione delle strutture . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 182
Copia delle variabili struttura . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 187
Riferimenti rapidi del capitolo 9 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 190
10 Utilizzo di matrici e insiemi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .191
Che cos’è una matrice?. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 191
Dichiarazione delle variabili della matrice . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 191
Creazione di un’istanza di una matrice . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 192
Inizializzazione delle variabili di matrice. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 193
Creazione di una matrice di tipi impliciti . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 194
Accesso a un singolo elemento di una matrice . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 195
Iterazioni in una matrice . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 195
Copia di matrici . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 197
Utilizzo di matrici multidimensionali . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 198
Utilizzo delle matrici per giocare a carte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 199
Sommario parte I-III
Che cosa sono le classi di insiemi? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 206
La classe di insiemi ArrayList . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 208
La classe di insiemi Queue . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 210
La classe di insiemi Stack . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 210
La classe di insiemi Hashtable. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 211
La classe di insiemi SortedList. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 213
Utilizzo degli inizializzatori di insiemi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 214
Confronto di matrici e insiemi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 214
Utilizzo delle classi di insiemi per giocare alle carte . . . . . . . . . . . . . . . . . . . . . . . . . . . 214
Riferimenti rapidi del capitolo 10 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 218
11 Introduzione alle matrici di parametri . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .219
Utilizzo degli argomenti di matrici . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 220
Dichiarazione di una matrice params . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 221
Utilizzo di params object[ ] . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 223
Utilizzo di una matrice params . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 224
Confronto tra matrici di parametri e parametri facoltativi . . . . . . . . . . . . . . . . . . . . . . . . 226
Riferimenti rapidi del capitolo 11 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 229
12 Utilizzo dell’ereditarietà . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .231
Che cos’è l’ereditarietà? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 231
Utilizzo dell’ereditarietà . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 232
Chiamata dei costruttori di classe base. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 234
Assegnazione delle classi. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 235
Dichiarazione di nuovi metodi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 237
Dichiarazione dei metodi virtuali . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 238
Dichiarazione di metodi in override . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 239
Introduzione all’accesso protetto. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 242
Introduzione ai metodi di estensione . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 247
Riferimenti rapidi del capitolo 12. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 251
13 Creazione di interfacce e definizione di classi astratte . . . . . . . . . . . . . . . . . . . . . . .253
Introduzione alle interfacce . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 253
Definizione di un’interfaccia . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 254
Implementazione di un’interfaccia . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 255
Riferimento a una classe attraverso la sua interfaccia . . . . . . . . . . . . . . . . . . . . . . . . . 256
Utilizzo di più interfacce . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 257
Implementazione esplicita di un’interfaccia . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 257
Limitazioni dell’interfaccia. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 259
Definizione e utilizzo delle interfacce . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 259
ix
x
Sommario parte I-III
Classi astratte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 269
Metodi astratti. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 270
Classi sigillate . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 271
Metodi sigillati . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 271
Implementazione e utilizzo di una classe astratta . . . . . . . . . . . . . . . . . . . . . . . . . . . . 272
Riferimenti rapidi del capitolo 13. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 277
14 Utilizzo di Garbage collection e gestione delle risorse . . . . . . . . . . . . . . . . . . . . . . .279
Fasi della vita di un oggetto . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 279
Scrittura dei distruttori. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 280
Perché utilizzare il garbage collector? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 282
Come funziona il garbage collector? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 283
Consigli . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 284
Gestione delle risorse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 284
I metodi disposal. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 285
Metodo disposal esente da eccezioni . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 285
L’istruzione using. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 286
Chiamata del metodo Dispose da un distruttore . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 288
Implementazione del metodo disposal esente da eccezioni . . . . . . . . . . . . . . . . . . . . . . 289
Riferimenti rapidi del capitolo 14 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 292
Parte III
Creazione dei componenti
15 Implementazione delle proprietà per accedere ai campi . . . . . . . . . . . . . . . . . . . . .295
Implementazione dell’incapsulamento utilizzando i metodi . . . . . . . . . . . . . . . . . . . . . . 296
Che cosa sono le proprietà? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 297
Utilizzo delle proprietà . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 299
Proprietà di sola lettura . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .300
Proprietà di sola scrittura . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .300
Accessibilità alle proprietà. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 301
Introduzione alle limitazioni delle proprietà . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 302
Dichiarazione delle proprietà dell’interfaccia . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .304
Utilizzo delle proprietà in un’applicazione Windows . . . . . . . . . . . . . . . . . . . . . . . . . . 305
Generazione di proprietà automatiche . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 307
Inizializzazione degli oggetti utilizzando le proprietà . . . . . . . . . . . . . . . . . . . . . . . . . . . . 308
Riferimenti rapidi del capitolo 15 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 313
16 Utilizzo degli indicizzatori . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .315
Che cos’è un indicizzatore? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 315
Esempio che non utilizza gli indicizzatori. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 315
Sommario parte I-III
Lo stesso esempio utilizzando gli indicizzatori . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 317
Comprensione delle funzioni di accesso degli indicizzatori . . . . . . . . . . . . . . . . . . . . 319
Confronto fra indicizzatori e matrici . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 320
Indicizzatori nelle interfacce . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 322
Utilizzo degli indicizzatori in un’applicazione Windows . . . . . . . . . . . . . . . . . . . . . . . . . . 323
Riferimenti rapidi del capitolo 16 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 328
17 Interruzione del programma e gestione degli eventi . . . . . . . . . . . . . . . . . . . . . . . .329
Dichiarazione e utilizzo dei delegati . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 329
Lo scenario della fabbrica automatizzata . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 330
Implementazione in fabbrica senza utilizzare i delegati . . . . . . . . . . . . . . . . . . . . . . . 330
Implementazione della fabbrica utilizzando un delegato . . . . . . . . . . . . . . . . . . . . . . 331
Utilizzo dei delegati . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 333
Espressioni lambda e delegati . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 338
Creazione di un adattatore per i metodi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 339
Utilizzo di una espressione lambda come adattatore . . . . . . . . . . . . . . . . . . . . . . . . . 339
Le forme delle espressioni lambda . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .340
Abilita di notifiche con eventi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 342
Dichiarazione di un evento . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 342
Sottoscrizione di un evento . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 343
Annullamento della sottoscrizione di un evento . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .344
Generazione di un evento . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .344
Introduzione agli eventi dell’interfaccia utente WPF . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 345
Utilizzo degli eventi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .346
Riferimenti rapidi del capitolo 17 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 350
18 Introduzione ai generics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .353
Problemi del tipo object . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 353
La soluzione generics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 355
Confronto tra generics e classi generalizzate . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 357
Rapporto tra generics e vincoli . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 358
Creazione di una classe generica . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 358
Teoria delle strutture binarie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 358
Creazione di una classe struttura binaria mediante l’uso di generics . . . . . . . . . . . . 361
Creazione di un metodo generico . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 370
Definizione di un metodo generico per creare una struttura binaria . . . . . . . . . . . . 371
Interfacce generiche e di varianza . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 373
Interfacce covarianti . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 375
Interfacce controvarianti . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 377
Riferimenti rapidi del capitolo 18 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 379
xi
xii
Sommario parte I-III
19 Enumerazione degli insiemi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .381
Enumerazione degli elementi di un insieme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 381
Implementazione manuale di un enumeratore . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 383
Implementazione dell’interfaccia IEnumerable . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 387
Implementazione di un enumeratore mediante un operatore di iterazione . . . . . . . . . 389
Esempio di un semplice operatore di iterazione . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 389
Definizione di un enumeratore per la classe Tree<TItem>
mediante un operatore di iterazione . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 391
Riferimenti rapidi del capitolo 19 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 394
20 Interrogazione dei dati in memoria mediante espressioni di query . . . . . . . . . . . .395
LINQ (Language Integrated Query) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 395
Utilizzo di LINQ in un’applicazione C# . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 396
Selezione dei dati . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 398
Filtro dei dati . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .400
Ordinamento, raggruppamento e aggregazione dei dati . . . . . . . . . . . . . . . . . . . . . . 401
Unione dei dati . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .404
Utilizzo degli operatori nelle query . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 405
Query dei dati negli oggetti Tree<TItem> . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 407
LINQ e la valutazione differita . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 412
Riferimenti rapidi del capitolo 20. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 416
21 Overload degli operatori . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .419
Utilizzo degli operatori . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 419
Vincoli degli operatori . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 420
Operatori soggetti a overload . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 420
Creazione di operatori simmetrici . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 422
Utilizzo di assegnazioni composte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 424
Dichiarazione di operatori incremento e decremento . . . . . . . . . . . . . . . . . . . . . . . . . . . . 425
Confronto degli operatori in strutture e classi. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 426
Definizione di coppie di operatori . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 426
Implementazione degli operatori . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 427
Utilizzo degli operatori di conversione . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 434
Come fornire conversioni incorporate . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 434
Implementazione di operatori di conversione definiti dall’utente . . . . . . . . . . . . . . . 435
Creazione di operatori simmetrici, parte due . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 436
Creazione di operatori di conversione . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 437
Riferimenti rapidi del capitolo 21 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .440
Sommario breve parte IV-VI
Scaricabile dal sito Mondadori Informatica
Parte IV
22
23
24
Parte V
25
26
Parte VI
27
28
29
Creazione di applicazioni WPF
Introduzione a Windows Presentation Foundation . . . . . . . . . 443
Raccolta dell’input utente . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 477
Esecuzione della convalida . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 509
Gestione dei dati
Interrogazione delle informazioni in un database . . . . . . . . . . .535
Visualizzazione e modifica dei dati mediante
Entity Framework e associazione dei dati . . . . . . . . . . . . . . . . . .565
Creazione di soluzioni professionali con Visual Studio 2010
Introduzione alla TPL (Task Parallel Library) . . . . . . . . . . . . . . . .599
Esecuzione di accessi simultanei ai dati . . . . . . . . . . . . . . . . . . . .649
Creazione e utilizzo di un servizio Web . . . . . . . . . . . . . . . . . . . .683
Appendice
Interazione con i linguaggi dinamici . . . . . . . . . . . . . . . . . . . . . . . . . . . 717
xiii
Sommario parte IV-VI
Scaricabile dal sito Mondadori Informatica
Parte IV Creazione di applicazioni WPF
22 Introduzione a Windows Presentation Foundation . . . . . . . . . . . . . . . . . . . . . . . . .443
Creazione di un’applicazione WPF . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 443
Creazione dell’applicazione WPF . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .444
Aggiunta di controlli al form. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 458
Utilizzo dei controlli WPF . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 458
Modifica dinamica delle proprietà. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 466
Gestione degli eventi in un form WPF . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 470
Elaborazione degli eventi nei form Windows . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 471
Riferimenti rapidi del capitolo 22. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 476
23 Raccolta dell’input utente . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .477
Linee guida e stile dei menu . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 477
Menu ed eventi di menu . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 478
Creazione di un menu . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 478
Gestione degli eventi di menu . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .484
Menu di scelta rapida. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 491
Creazione dei menu di scelta rapida . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 491
Finestre di dialogo comuni di Windows . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 495
Utilizzo della classe SaveFileDialog . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 495
Incremento della velocità di risposta di un’applicazione WPF . . . . . . . . . . . . . . . . . . . . . 498
Riferimenti rapidi del capitolo 23 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 508
24 Esecuzione della convalida . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .509
Convalida dei dati . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 509
Strategie di convalida dei dati immessi dall’utente . . . . . . . . . . . . . . . . . . . . . . . . . . . . 509
Un esempio: ordine di biglietti per eventi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 510
Esecuzione della convalida mediante l’uso dell’associazione . . . . . . . . . . . . . . . . . . . 511
Modifica del punto di esecuzione della convalida . . . . . . . . . . . . . . . . . . . . . . . . . . . . 527
Riferimenti rapidi del capitolo 24 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 531
xv
xvi
Sommario parte IV-VI Scaricabile dal sito Mondadori Informatica
Parte V
Gestione dei dati
25 Interrogazione delle informazioni in un database . . . . . . . . . . . . . . . . . . . . . . . . . .535
Interrogazione di un database mediante ADO.NET . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 535
Il database Northwind . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 536
Creazione del database . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 536
Utilizzo di ADO.NET per interrogare le informazioni relative a un ordine . . . . . . . . 538
Interrogazione di un database mediante LINQ to SQL . . . . . . . . . . . . . . . . . . . . . . . . . . . 549
Definizione di una classe entità . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 549
Creazione ed esecuzione di una query LINQ to SQL . . . . . . . . . . . . . . . . . . . . . . . . . . 551
Recupero differito e immediato . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 553
Unione di tabelle e creazione di relazioni . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 554
Recupero differito e immediato, parte due . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 558
Definizione di una classe DataContext personalizzata. . . . . . . . . . . . . . . . . . . . . . . . . 559
Utilizzo di LINQ to SQL per interrogare le informazioni sull’ordine. . . . . . . . . . . . . . 560
Riferimenti rapidi del capitolo 25 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .564
26 Visualizzazione e modifica dei dati mediante
Entity Framework e associazione dei dati. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .565
Utilizzo dell’associazione di dati con Entity Framework . . . . . . . . . . . . . . . . . . . . . . . . . . . 566
Utilizzo dell’associazione di dati per modificarli . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 583
Aggiornamento dei dati esistenti. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 583
Gestione dei conflitti di aggiornamento. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .584
Aggiunta ed eliminazione dei dati . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 587
Riferimenti rapidi del capitolo 26 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 596
Parte VI Creazione di soluzioni professionali con Visual Studio 2010
27 Introduzione alla TPL (Task Parallel Library) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .599
Perché eseguire il multitasking mediante l’elaborazione parallela?. . . . . . . . . . . . . . . . .600
La crescita dei processori multicore . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 601
Implementazione del multitasking in un’applicazione desktop . . . . . . . . . . . . . . . . . . . . 602
Attività, thread e ThreadPool . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 603
Creazione, esecuzione e controllo delle attività . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .604
Utilizzo della classe Task per implementare l’esecuzione
in parallelo. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .608
Astrazione delle attività mediante la classe Parallel . . . . . . . . . . . . . . . . . . . . . . . . . . . 617
Restituzione di un valore da un’attività . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 624
Utilizzo di attività e di thread di interfaccia utente insieme . . . . . . . . . . . . . . . . . . . . . . . 628
Sommario parte IV-VI Scaricabile dal sito Mondadori Informatica
xvii
Annullamento delle attività e gestione delle eccezioni . . . . . . . . . . . . . . . . . . . . . . . . . . . 632
Funzionamento dell’annullamento cooperativo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 633
Gestione delle eccezioni delle attività mediante l’uso
della classe AggregateException . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 641
Utilizzo delle condizioni di proseguimento con attività annullate e fallite. . . . . . . .644
Riferimenti rapidi del capitolo 27. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 645
28 Esecuzione di accessi simultanei ai dati . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .649
Utilizzo di PLINQ per l’accesso ai dati in modo dichiarativo e parallelo . . . . . . . . . . . . . 650
Utilizzo di PLINQ per migliorare la performance durante
un’iterazione all’interno di un insieme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 650
Definizione delle opzioni per una query PLINQ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 655
Annullamento di una query PLINQ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 656
Sincronizzazione degli accessi simultanei imperativi ai dati . . . . . . . . . . . . . . . . . . . . . . 656
Blocco dei dati . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 659
Primitivi di sincronizzazione nella TPL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 661
Annullamento e sincronizzazione di primitivi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 668
Classi d’insieme concorrenti . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 668
Utilizzo di insiemi concorrenti e di un blocco per implementare
accessi ai dati in modo thread-safe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 670
Riferimenti rapidi del capitolo 28 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 681
29 Creazione e utilizzo di un servizio Web . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .683
Che cos’è un servizio Web? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .684
Il ruolo di Windows Communication Foundation . . . . . . . . . . . . . . . . . . . . . . . . . . . . .684
Architetture dei servizi WEB . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 685
Servizi Web SOAP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 685
Servizi Web REST . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 688
Creazione di Servizi Web . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 689
Creazione del servizio Web SOAP ProductInformation . . . . . . . . . . . . . . . . . . . . . . . . 689
Servizi Web SOAP, client e proxy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 697
Usufruire del servizio Web SOAP ProductInformation. . . . . . . . . . . . . . . . . . . . . . . . . 698
Creazione del servizio Web REST ProductDetails . . . . . . . . . . . . . . . . . . . . . . . . . . . . 704
Utilizzo del servizio Web REST ProductDetails . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 711
Riferimenti rapidi del capitolo 29. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 715
Appendice . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 717
Interazione con i linguaggi dinamici . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 717
Indice analitico . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .727
L’autore . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 747
Parte IV
Creazione di applicazioni WPF
In questa parte:
Introduzione a Windows Presentation Foundation . . . . . . . . . . . . . . . . . . . . . . . 443
Raccolta dell’input utente . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 477
Esecuzione della convalida . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 509
441
Capitolo 22
Introduzione a Windows Presentation
Foundation
Gli argomenti trattati in questo capitolo sono:
■
Creazione di applicazioni Microsoft WPF (Windows Presentation Foundation).
■
Utilizzo di controlli WPF quali etichette, caselle di testo e pulsanti.
■
Definizione degli stili per i controlli WPF.
■
Modifica delle proprietà dei form e dei controlli WPF in fase di progettazione e mediante
codice in fase di esecuzione.
■
Gestione degli eventi esposti da form e controlli WPF.
Ora che hai completato le esercitazioni ed esaminati gli esempi delle prime tre parti di questo
manuale, dovresti avere acquisito una buona familiarità con il linguaggio C#. Leggendo, hai
appreso come scrivere programmi e creare componenti utilizzando Microsoft C#, e dovresti
conoscere molti dei punti più importanti di questo linguaggio, ad esempio i metodi estensione,
le espressioni lambda e la distinzione tra i tipi di valore e i tipi riferimento. Ora che disponi delle
nozioni base relative al linguaggio, nella Parte IV potrai usarle per espandere la tue capacità e
utilizzare C# per sfruttarle le librerie GUI (Graphical User Interface, interfaccia grafica utente)
fornite da Microsoft .NET Framework. In particolare, potrai vedere come utilizzare gli oggetti dello
spazio dei nomi System.Windows per creare applicazioni WPF.
Questo capitolo illustra come creare un’applicazione WPF base utilizzando i comuni componenti
presenti nella maggior parte delle applicazioni GUI. L’impostazione delle proprietà di form e
controlli WPF viene eseguita mediante le finestre Progettazione e Proprietà, oltre che utilizzando
il linguaggio XAML (eXtensible Application Markup Language). Viene mostrato anche come
utilizzare gli stili WPF per creare interfacce utente facilmente adattabili agli standard di
presentazione aziendali. Infine, viene spiegato come rilevare e gestire alcuni degli eventi esposti
da form e controlli WPF.
Creazione di un’applicazione WPF
L’esempio usato di seguito permette di creare un’applicazione che l’utente potrà usare per
immettere e visualizzare le informazioni relative ai membri della Middleshire Bell Ringers
Association, un gruppo stimato formato dai migliori campanari. Inizialmente l’applicazione
sarà creata in modo da essere molto semplice, concentrando lo sviluppo sull’aspetto del form e
verificando che funzioni come desiderato. Procedendo sarà possibile apprendere alcune delle
funzioni fornite da WPF per la creazione di interfacce molto personalizzabili. Nei capitoli successivi
verranno quindi introdotti i menu e tecniche di implementazione della convalida per garantire la
443
444
Parte IV
Creazione di applicazioni WPF
correttezza dei dati immessi. L’immagine che segue mostra l’aspetto dell’applicazione una volta
completata (la versione completa è disponibile compilando ed eseguendo il progetto BellRingers
nella cartella \Microsoft Press\Visual CSharp Step by Step\Chapter 22\BellRingers - Complete\
all’interno della tua cartella Documenti).
Creazione dell’applicazione WPF
In questo esercizio è possibile iniziare a costruire l’applicazione Middleshire Bell Ringers
Association creando un nuovo progetto, definendo il layout del form e aggiungendo i controlli
necessari. Nei capitoli precedenti hai utilizzato applicazioni WPF esistenti in Microsoft Visual
Studio 2010, quindi buona parte delle prime due esercitazioni sono un semplice ripasso.
Creazione del progetto Middleshire Bell Ringers Association
1. Avvia Visual Studio 2010 se non è già in esecuzione.
2. Se utilizzi Visual Studio 2010 Standard o Visual Studio 2010 Professional, esegui le
operazioni seguenti per creare una nuova applicazione WPF:
2.1. Nel menu File, seleziona Nuovo e poi Progetto.
Viene visualizzata la finestra di dialogo Nuovo progetto.
2.2. Nel riquadro a sinistra, espandi il riquadro dei modelli installati (se non già visibile),
espandi Visual C#, quindi fai clic su Windows.
2.3. Nel riquadro centrale, fai clic sull’icona Applicazione WPF.
Capitolo 22
Introduzione a Windows Presentation Foundation
445
2.4. Nel campo Percorso, digita \Microsoft Press\Visual CSharp Step By Step\Chapter
22 all’interno della tua cartella Documenti.
2.5. Nel campo relativo al nome, digita BellRingers.
2.6. Fai clic su OK.
3. Se utilizzi Microsoft Visual C# 2010 Express, esegui i passaggi seguenti per creare una nuova
applicazione grafica.
3.1. Nel menu File, fai clic su Nuovo progetto.
3.2. Nella finestra di dialogo Nuovo progetto, nel riquadro a sinistra, sotto i modelli
installati, fai clic su Visual C#.
3.3. Nel riquadro centrale, fai clic su Applicazione WPF, nel campo relativo ai nomi, digita
BellRingers, quindi fai clic su OK.
3.4. Quando Visual Studio ha creato il progetto, nel menu File, fai clic su Salva tutto.
3.5. Nella finestra di dialogo Salva progetto, nel campo Percorso specifica Microsoft
Press\Visual CSharp Step By Step\Chapter 22 nella cartella Documenti e poi fai clic
su Salva.
Il nuovo progetto viene creato e contiene un form vuoto chiamato MainWindow.
Esame del form e del layout del riquadro Grid
1. Esamina il form nel riquadro XAML visualizzato sotto la finestra Progettazione. Nota che la
definizione XAML del form ha il seguente aspetto:
<Window x:Class="BellRingers.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
</Grid>
</Window>
L’attributo Class specifica il nome completo della classe che implementa il form. In questo
caso, il nome è MainWindow nello spazio dei nomi BellRingers. Il modello Applicazione WPF
utilizza il nome dell’applicazione come nome dello spazio dei nomi predefinito per i form.
Gli attributi xmlns specificano gli spazi dei nomi XML che definiscono gli schemi usati da
WPF; tutti i controlli e gli altri elementi che possono essere incorporati in un’applicazione
WPF hanno definizioni all’interno di questi spazi dei nomi. Chi non ha familiarità con gli
spazi dei nomi XML può ignorare gli attributi xmlns per ora. L’attributo Title specifica il testo
che appare nella barra del titolo del form, mentre gli attributi Height e Width ne specificano
l’altezza e la larghezza predefinite. Questi valori possono essere modificati cambiandoli
nel riquadro XAML o utilizzando la finestra Proprietà. I valori di queste e altre proprietà
possono essere modificati dinamicamente anche mediante codice C# eseguito con il form.
446
Parte IV
Creazione di applicazioni WPF
2. Fai clic sul form MainWindow nella finestra Progettazione. Nella finestra Proprietà, localizza
e fai clic sulla proprietà Title, quindi digita Middleshire Bell Ringers Association –
Members e premi Invio per modificare il testo visualizzato nella barra del titolo del form.
Nota che il valore nell’attributo Title del form cambia anche nel riquadro XAML, e che il
nuovo titolo viene visualizzato nella barra del titolo del form nella finestra Progettazione.
Nota Il form MainWindow contiene un controllo figlio che verrà esaminato nel punto
successivo. Se nella finestra Proprietà appaiono le proprietà del controllo System.Windows.
Controls.Grid, fai clic sul testo MainWindow nel form MainWindow. Questa operazione
seleziona il form invece della griglia, e la finestra Proprietà passa quindi a visualizzare le
proprietà del controllo System.Windows.Window.
3. Come puoi vedere nel riquadro XAML, l’elemento Window contiene un elemento figlio
chiamato Grid.
In un’applicazione WPF, i controlli come pulsanti, caselle di testo ed etichette vengono
inseriti in un riquadro di un form. Il riquadro gestisce quindi il layout dei controlli presenti al
suo interno. Il riquadro predefinito aggiunto dal modello Applicazione WPF si chiama Grid
e permette di specificare esattamente l’ubicazione dei controlli in fase di progettazione.
Sono disponibili altri riquadri che forniscono stili di layout differenti. Ad esempio, StackPanel
posiziona automaticamente i controlli verticalmente, ognuno direttamente sotto il proprio
predecessore diretto. Un altro esempio è WrapPanel, che dispone i controlli in una riga
da sinistra a destra andando a capo come necessario. Lo scopo principale di un riquadro
di layout è gestire il modo in cui sono posizionati i controlli quando l’utente ridimensiona
la finestra in fase di esecuzione; i controlli vengono ridimensionati e riposizionati
automaticamente in base al tipo di riquadro.
Nota Il riquadro Grid è flessibile ma complesso. Per impostazione predefinita, puoi pensare al riquadro Grid come alla definizione di una singola cella all’interno della quale è
possibile trascinare dei controlli. Tuttavia, puoi impostare le proprietà di un riquadro Grid
in modo da definire più righe e colonne (da cui il suo nome), trascinando quindi i controlli
in ognuna delle celle così definite. Per semplicità, gli esempi di questo capitolo utilizzano
una singola cella.
4. Nella finestra Progettazione, fai clic sul form MainWindow, quindi sulla scheda Casella degli
strumenti.
5. Nella sezione dei controlli WPF comuni, fai clic su Button, quindi fai clic nella parte in alto a
destra del form.
Viene aggiunto un pulsante controllo con due connettori che lo ancorano ai bordi superiore
e destro del form, come qui mostrato:
Capitolo 22
Introduzione a Windows Presentation Foundation
447
Connettori
Punti di ancoraggio
Pur avendo fatto clic sul form, il controllo Button è stato aggiunto al controllo Grid
presente al suo interno. La griglia occupa tutto il form, tranne la barra del titolo nella parte
superiore. I connettori mostrano che il pulsante è ancorato ai bordi superiore e destro della
griglia. In fase di esecuzione, se ridimensioni il form, il pulsante si sposta per mantenere
queste connessioni e per mantenere la stessa distanza tra il pulsante e i bordi collegati.
Puoi ancorare il pulsante a diversi bordi del form facendo clic sui punti di ancoraggio del
controllo oppure modificando le proprietà HorizontalAlignment e VerticalAlignment del
pulsante, come illustrato nel prossimo passaggio.
6. Esamina il codice nel riquadro XAML. Anche se i valori della proprietà Margin possono
essere diversi da quelli riportati qui, l’elemento Grid e il suo contenuto dovrebbero avere un
aspetto simile all’esempio che segue:
<Grid>
<Button Content="Button" HorizontalAlignment="Left"
Margin="0,84,34,0" Name="button1" Width="75" Height="23"
VerticalAlignment="Top"/>
</Grid>
Nota In tutto questo capitolo le righe del riquadro XAML vengono mostrate suddivise e
rientrate in modo da poter rientrare nella pagina stampata.
Quando posizioni un controllo in una griglia, puoi connettere uno o più punti di ancoraggio
al bordo corrispondente della griglia in questione. Se sposti il controllo, resta collegato
agli stessi bordi fino a quando non vengono modificate le proprietà di allineamento del
controllo.
Le proprietà HorizontalAlignment e VerticalAlignment del pulsante indicano i bordi a cui
il pulsante è attualmente connesso, mentre la proprietà Margin indica la distanza da tali
bordi. Come ricorderai dal capitolo 1, “Introduzione a C#”, la proprietà Margin contiene
quattro valori che specificano rispettivamente la distanza dai bordi sinistro, superiore,
destro e inferiore della griglia. Nell’esempio XAML appena visto, il pulsante è a 84 unità
dal bordo superiore e a e 34 unità dal bordo destro della griglia (ogni unità rappresenta
1/96 di pollice). I valori di Margin pari a 0 indicano che il pulsante non è connesso al bordo
corrispondente. Come detto nel passaggio precedente, quando esegui l’applicazione,
il runtime WPF si sforza di mantenere queste distanze anche quando il form viene
ridimensionato.
448
Parte IV
Creazione di applicazioni WPF
7. Nel menu Debug, fai clic su Avvia senza eseguire debug per costruire ed eseguire
l’applicazione.
8. Una volta visualizzato il form, ridimensiona la finestra facendo clic e trascinando ogni bordo.
Nota che mentre trascini i bordi del form, la distanza del pulsante dai bordi superiore e
destro del form rimane invariata.
9. Chiudi il form e ritorna a Visual Studio 2010.
10. Nella finestra Progettazione, fai clic sul controllo pulsante, quindi sul punto di ancoraggio
sinistro per connettere il controllo al bordo sinistro del form, come mostrato nell’immagine
che segue:
Nel riquadro XAML, nota che non è più specificata la proprietà HorizontalAlignment. Il
valore predefinito delle proprietà HorizontalAlignment e VerticalAlignment è chiamato
Stretch, e indica che il controllo è ancorato contemporaneamente ai due bordi opposti.
Come puoi vedere, ora la proprietà Margin relativa al margine sinistro indica un valore
diverso da zero.
Nota Per rimuovere una connessione, fai clic sul punto di ancoraggio connesso al bordo
della griglia.
11. Nel menu Debug, fai clic su Avvia senza eseguire debug per costruire ed eseguire
nuovamente l’applicazione.
12. Una volta visualizzato il form, prova a modificarne le dimensioni restringendolo o
allargandolo. Nota che il pulsante non può più spostarsi verso destra o sinistra poiché è
ancorato ai bordi sinistro e destro del form. Pertanto, al variare della larghezza del form il
pulsante si allarga o restringe di conseguenza.
13. Chiudi il form e ritorna a Visual Studio 2010.
14. Nella finestra Progettazione, aggiungi un secondo controllo Button dalla Casella degli
strumenti posizionandolo al centro del form.
Capitolo 22
Introduzione a Windows Presentation Foundation
449
15. Nel riquadro XAML, imposta la proprietà Margin a 0,0,0,0, elimina le proprietà
VerticalAlignment e HorizontalAlignment, quindi imposta le proprietà Width e Height come
indicato di seguito:
<Button Content="Button" Margin="0,0,0,0" Name="button2"
Width="75" Height="23"/>
Suggerimento Nella finestra Proprietà è possibile impostare molte delle proprietà di un
controllo, ad esempio Margin. Tuttavia, a volte è più semplice digitare i valori direttamente
nel riquadro XAML, se vengono inseriti con attenzione.
Nota Non impostando le proprietà Width e Height del controllo pulsante, questo riempie
l’intero form.
16. Nel menu Debug, fai clic su Avvia senza eseguire debug per costruire ed eseguire
l’applicazione.
17. Una volta visualizzato il form, ridimensiona il form. Nota che allargando o restringendo il
form, il nuovo pulsante si sposta tentando di mantenere la posizione relativa ai quattro lati,
rimanendo così al centro del form. Riducendo l’altezza verticale del form, il nuovo controllo
pulsante arriva persino a superare la parte superiore del primo controllo pulsante.
18. Chiudi il form e ritorna a Visual Studio 2010.
Mantenendo un approccio coerente, l’uso di riquadri di layout come Grid permette di creare form
con un aspetto professionale, indipendentemente dalla risoluzione dello schermo dell’utente
e senza dover scrivere parti di codice complesse per determinare se egli ha ridimensionato la
finestra. Inoltre, WPF permette di modificare l’aspetto dei controlli usati in un’applicazione,
evitando anche in questo caso di scrivere grandi quantità di codice. La combinazione di queste
funzioni consente di creare applicazioni facilmente personalizzabili per adattarle allo stile
aziendale desiderato. Alcune di queste funzioni saranno esaminate nelle esercitazioni che
seguono.
Aggiunta di un’immagine allo sfondo del form
1. Nella finestra Progettazione, fai clic sul form MainWindow.
2. Nella sezione dei controlli WPF comuni della Casella degli strumenti, fai clic su Image, quindi
fai clic in una posizione qualsiasi all’interno del form. Questo controllo immagine sarà usato
per visualizzare un’immagine sullo sfondo del form.
Nota Puoi utilizzare molte altre tecniche per visualizzare un’immagine come sfondo di un
Grid. Il metodo mostrato in questo esercizio è forse il più semplice, benché altre soluzioni
possano offrire una maggiore flessibilità.
450
Parte IV
Creazione di applicazioni WPF
3. Nel riquadro XAML, imposta la proprietà Margin del controllo immagine e rimuovi tutti gli
altri valori di proprietà tranne Name, come qui mostrato:
<Image Margin="0,0,0,0" Name="image1"/>
Il controllo immagine si espande fino a occupare completamente la griglia, ma i due
controlli pulsante rimangono visibili.
4. In Esplora soluzioni, fai clic con il tasto destro del mouse sul progetto BellRingers, quindi
seleziona Elemento esistente. Nella finestra di dialogo Aggiungi elemento esistente –
BellRingers, accedi alla cartella Microsoft Press\Visual CSharp Step By Step\Chapter 22
all’interno della tua cartella Documenti. Nella casella di riepilogo a discesa accanto alla
casella di testo del nome del file, seleziona Tutti i file (*.*). Seleziona il file bell.gif, quindi fai
clic su Aggiungi.
Questa operazione aggiunge il file di immagine bell.gif come risorsa all’applicazione. Il file
bell.gif contiene un disegno di una campana che suona.
5. Nel riquadro XAML, modifica la definizione del controllo immagine come qui mostrato in
grassetto. La proprietà Image.Source è un esempio di proprietà composta contenente uno
o più elementi figlio. Nota che è necessario sostituire il delimitatore di chiusura del tag (/>)
del controllo immagine con un delimitatore normale (>) e aggiungere un tag di chiusura </
Image> per racchiudere la proprietà Image.Source:
<Image Margin="0,0,0,0" Name="image1" >
<Image.Source>
<BitmapImage UriSource="bell.gif" />
</Image.Source>
</Image>
Lo scopo del controllo immagine è visualizzare un’immagine. L’origine di tale immagine può
essere specificata in molto modi differenti. L’esempio qui mostrato carica l’immagine dal file
bell.gif appena aggiunto come risorsa del progetto.
L’immagine dovrebbe ora apparire nel form come mostrato di seguito:
Capitolo 22
Introduzione a Windows Presentation Foundation
451
In questo caso è necessario tuttavia risolvere un piccolo problema, perché l’immagine non
è sullo sfondo e copre completamente i due controlli pulsante. Questo comportamento
è causato dal fatto che, non specificando diversamente, tutti i controlli inseriti in un
riquadro di layout prevedono un ordine z implicito che sovrappone i controlli alla fine della
descrizione XAML ai controlli aggiunti per primi.
Nota Il termine ordine z si riferisce alle posizioni di profondità relativa degli elementi
sull’asse z di uno spazio tridimensionale, in cui y e x sono rispettivamente l’asse verticale e
orizzontale. Gli elementi con un valore di ordine z superiore appaiono davanti a quelli con
un valore inferiore.
Vi sono due modi per spostare il controllo immagine dietro i pulsanti. Il primo consiste nello
spostare le definizioni XAML dei pulsanti in modo che appaiano dopo il controllo immagine,
mentre il secondo richiede di specificare in modo esplicito il valore della proprietà ZIndex
del controllo. I controlli con un valore ZIndex superiore appaiono davanti a quelli dello
stesso riquadro con un valore Zindex inferiore. Se due controlli hanno lo stesso valore
ZIndex, la precedenza relativa viene determinata dall’ordine con cui essi compaiono nella
descrizione XAML, come visto in precedenza.
6. Nel riquadro XAML, imposta la proprietà ZIndex dei controlli pulsante e immagine come
mostrato in grassetto nel codice di esempio che segue:
<Button Panel.ZIndex="1" Content="Button" Margin="379,84,49,0"
Name="button1" Height="23" VerticalAlignment="Top" />
<Button Panel.ZIndex="1" Content="Button" Height="23" Margin="0,0,0,0"
Name="button2" Width="75" />
<Image Panel.ZIndex="0" Margin="0,0,0,0" Name="image1" >
<Image.Source>
<BitmapImage UriSource="Bell.gif" />
</Image.Source>
</Image>
Fatto ciò, i due pulsanti dovrebbero apparire nuovamente davanti all’immagine.
Con WPF puoi modificare il modo in cui i controlli come pulsanti, caselle di testo ed etichette
appaiono in un form. Questa funzione è l’argomento del prossimo esercizio.
Creazione di un uno stile per gestire l’aspetto dei controlli di un form
1. Nel riquadro XAML, modifica la definizione del primo pulsante del form come mostrato
in grassetto nel codice di esempio che segue. La proprietà Button.Resources è un altro
esempio di proprietà composta; è necessario modificare la definizione dell’elemento Button
per incapsulare questa proprietà, sostituendo il delimitatore di chiusura del tag (/>) del
controllo pulsante con un delimitatore normale (>) e aggiungendo un tag di chiusura </
Button>. Ricorda che è buona norma suddividere su più righe la descrizione XAML di un
452
Parte IV
Creazione di applicazioni WPF
controllo che contiene valori di proprietà figlio composte come Button.Resource, in modo
da facilitarne la lettura e la gestione:
<Button Panel.ZIndex="1" Content="Button" Margin="169,84,34,0"
Name="button1" Height="23" VerticalAlignment="Top">
<Button.Resources>
<Style x:Key="buttonStyle">
<Setter Property="Button.Background" Value="Gray"/>
<Setter Property="Button.Foreground" Value="White"/>
<Setter Property="Button.FontFamily" Value="Comic Sans MS"/>
</Style>
</Button.Resources>
</Button>
Questo esempio specifica i valori dei colori di sfondo e primo piano e il carattere usato per il
testo di uno dei due pulsanti. Gli stili sono risorse che è possibile aggiungere a un elemento
Resources del controllo. A ogni stile è possibile assegnare un nome univoco mediante la
proprietà Key.
Nota Quando compili una finestra WPF, Visual Studio aggiunge eventuali risorse presenti
al suo interno all’insieme associato alla finestra in questione. Dal punto di vista tecnico, la
proprietà Key non specifica il nome dello stile, ma un identificatore della risorsa presente
in questo insieme. Inoltre, puoi specificare la proprietà Name per manipolare la risorsa nel
codice C#, ma i controlli fanno riferimento alle risorse specificandone il relativo valore Key.
Controlli e altri elementi aggiungi a un form dovrebbero avere impostate le rispettive proprietà Name, poiché questo è il metodo usato nel codice per fare riferimento a tali risorse.
Benché sia stato definito uno stile come parte della definizione del pulsante, il relativo
aspetto non è cambiato. È necessario specificare lo stile da applicare a un controllo
utilizzando la proprietà Style.
2. Modifica la definizione del pulsante in modo che faccia riferimento allo stile buttonStyle,
come indicato di seguito in grassetto:
<Button Style="{DynamicResource buttonStyle}" Panel.ZIndex="1"
Content="Button" Margin ="169,84,34,0" Name="button1" Height="23"
VerticalAlignment="Top">
<Button.Resources>
<Style x:Key="buttonStyle">
...
</Style>
</Button.Resources>
Button
</Button>
La sintassi {DynamicResource buttonStyle} crea un nuovo oggetto stile basato sullo
stile specificato, che viene applicato al pulsante tramite la proprietà Style. L’aspetto del
pulsante sul form dovrebbe cambiare.
Capitolo 22
Introduzione a Windows Presentation Foundation
453
Gli stili hanno un ambito. Quando tenti di fare riferimento allo stile buttonStyle dal secondo
pulsante del form, l’operazione non ha alcun effetto. In alternativa, puoi creare una copia
di questo stile e aggiungerla all’elemento Resources del secondo pulsante, quindi farvi
riferimento come mostrato di seguito:
<Grid>
<Button Style="{DynamicResource buttonStyle}" Content="Button"
Panel.ZIndex="1" Margin ="169,84,34,0" Name="button1" Height="23"
VerticalAlignment="Top">
<Button.Resources>
<Style x:Key="buttonStyle">
<Setter Property="Button.Background" Value="Gray"/>
<Setter Property="Button.Foreground" Value="White"/>
<Setter Property="Button.FontFamily" Value="Comic Sans MS"/>
</Style>
</Button.Resources>
</Button>
<Button Style="{DynamicResource buttonStyle}" Content="Button"
Panel.ZIndex="1" Height="23" Margin="0,0,0,0" Name="button2"
Width="76">
<Button.Resources>
<Style x:Key="buttonStyle">
<Setter Property="Button.Background" Value="Gray"/>
<Setter Property="Button.Foreground" Value="White"/>
<Setter Property="Button.FontFamily" Value="Comic Sans MS"/>
</Style>
</Button.Resources>
</Button>
...
</Grid>
Tuttavia, questo approccio può diventare molto ripetitivo e complicare notevolmente la
gestione qualora diventi necessario modificare lo stile dei pulsanti. Una strategia molto
migliore consiste nel definire lo stile come risorsa della finestra, quindi fare riferimento allo
stile da tutti i controlli presenti nella finestra in questione.
3. Nel riquadro XAML, aggiungi un elemento <Window.Resources> sopra la griglia, sposta
la definizione dello stile buttonStyle in questo nuovo elemento, quindi elimina l’elemento
<Button.Resources> dai due pulsanti. Aggiungi o modifica la proprietà Style di entrambi i
pulsanti in modo che faccia riferimento a questo stile. Di seguito viene riportato il codice
aggiornato dell’intera descrizione XAML del form, con la definizione e i riferimenti alla
risorsa mostrati in grassetto:
<Window x:Class="BellRingers.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Middleshire Bell Ringers Association - Members"
Height="350" Width="525">
<Window.Resources>
<Style x:Key="buttonStyle">
<Setter Property="Button.Background" Value="Gray"/>
<Setter Property="Button.Foreground" Value="White"/>
454
Parte IV
Creazione di applicazioni WPF
<Setter Property="Button.FontFamily" Value="Comic Sans MS"/>
</Style>
</Window.Resources>
<Grid>
<Button Style="{StaticResource buttonStyle}" Panel.ZIndex="1"
Content="Button" Margin ="169,84,34,0" Name="button1" Height="23"
VerticalAlignment="Top">
</Button>
<Button Style="{StaticResource buttonStyle}" Panel.ZIndex="1"
Content="Button" Height="23" Margin="0,0,0,0" Name="button2"
Width="76" />
<Image Panel.ZIndex="0" Margin="0,0,0,0" Name ="image1">
<Image.Source>
<BitmapImage UriSource="Bell.gif" />
</Image.Source>
</Image>
</Grid>
</Window>
Entrambi i pulsanti appaiono ora nella finestra Progettazione con lo stesso stile.
Il codice appena immesso fa riferimento allo stile di pulsante utilizzando la parola chiave
StaticResource anziché DynamicResource. Le regole di ambito delle risorse statiche sono
simili a quelle di C#, poiché richiedono di definire una risorsa prima di poterla referenziare.
Nel punto 1 di questo esercizio hai utilizzato un riferimento allo stile buttonStyle prima del
codice XAML che lo definisce, pertanto lo stile non rientrava realmente nell’ambito. Questo
riferimento fuori ambito funziona perché l’uso di DynamicResource rimanda il momento
in cui il riferimento alla risorsa viene risolto alla fase di esecuzione, punto in cui la risorsa
dovrebbe essere stata creata.
In generale, le risorse statiche sono più efficienti di quelle dinamiche poiché vengono risolte
quando l’applicazione viene compilata, ma le risorse dinamiche offrono una maggiore
flessibilità. Ad esempio, se la risorsa in questione viene modificata durante l’esecuzione
dell’applicazione, ad esempio mediante codice che cambia stile in fase di esecuzione,
i controlli che fanno riferimento allo stile tramite la parola chiave StaticResource non
vengono modificati, mentre ciò avviene regolarmente per quelli utilizzano la parola chiave
DynamicResource.
Nota Vi sono molte altre differenze di comportamento tra risorse statiche e risorse di-
namiche, oltre a limitazioni sul momento in cui è possibile fare riferimento a una risorsa in
modo dinamico. Per ulteriori informazioni, consulta la documentazione di .NET Framework
fornita con Visual Studio 2010.
Nella definizione dello stile è comunque presente un certo livello di ripetitività; infatti
ogni proprietà (sfondo, primo piano e famiglia di caratteri) indica in modo esplicito la
sua appartenenza alle proprietà di un pulsante. Per rimuovere queste ripetizioni, puoi
specificare l’attributo TargetType nel tag Style.
Capitolo 22
Introduzione a Windows Presentation Foundation
455
4. Modifica la definizione dello stile per specificare l’attributo TargetType, quindi rimuovi il
riferimento Button da ogni proprietà, come mostrato di seguito:
<Style x:Key="buttonStyle" TargetType="Button">
<Setter Property="Background" Value="Gray"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="FontFamily" Value="Comic Sans MS"/>
</Style>
Ora puoi aggiungere al form un numero qualsiasi di pulsanti e assegnare a ognuno lo stile
buttonStyle. Ma che dire degli altri controlli, ad esempio etichette e caselle di testo?
5. Nella finestra Progettazione, fai clic sul form MainWindow, quindi sulla scheda Casella degli
strumenti. Nella sezione Comuni, fai clic su TextBox, quindi fai clic in una posizione qualsiasi
nella metà inferiore del form.
Viene inserito un controllo TextBox nel form.
6. Nel riquadro XAML, modifica la definizione del controllo casella di testo e specifica
l’attributo Style mostrato in grassetto nell’esempio che segue, tentando così di applicare lo
stile buttonStyle:
<TextBox Style="{StaticResource buttonStyle}" Height="21"
Margin="114,0,44,58" Name="textBox1" VerticalAlignment="Bottom" />
Come è facile intuire, il tentativo di impostare lo stile di una casella di testo con un stile
concepito per i pulsanti non riesce. Il riquadro XAML visualizza una sottolineatura blu sotto
il riferimento allo stile nel controllo TextBox. Se posizioni il mouse sopra l’elemento, compre
una descrizione con il messaggio che indica che il TargetType ‘Button’ non corrisponde
al tipo di elemento ‘TextBox’. Se tenti di creare l’applicazione, verrà visualizzato lo stesso
messaggio di errore.
7. Per risolvere il problema, nel riquadro XAML, cambia TargetType a Control nella definizione
dello stile, cambia la proprietà Key a bellRingersStyle (un nome più significativo), quindi
modifica i riferimenti allo stile nei controlli del pulsante e della casella di testo, come
mostrato di seguito in grassetto:
<Window x:Class="BellRingers.MainWindow"
...>
<Window.Resources>
<Style x:Key="bellRingersStyle" TargetType="Control">
<Setter Property="Background" Value="Gray"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="FontFamily" Value="Comic Sans MS"/>
</Style>
</Window.Resources>
<Grid>
<Button Style="{StaticResource bellRingersStyle}" ...>
</Button>
<Button Style="{StaticResource bellRingersStyle}" ... />
...
<TextBox ... Style="{StaticResource bellRingersStyle}" ... />
</Grid>
</Window>
456
Parte IV
Creazione di applicazioni WPF
L’impostazione dell’attributo TargetType di uno stile su Control specifica che lo stile in
questione può essere applicato a qualsiasi controllo che erediti dalla classe Control. Nel
modello WPF, ciò avviene per molti differenti tipi di controllo, tra cui caselle di testo e
pulsanti, ereditati dalla classe Control. Tuttavia, è possibile fornire elementi Setter solo per
le proprietà che appartengono esplicitamente alla classe Control (i pulsanti dispongono di
alcune proprietà aggiuntive che non fanno parte della classe Control; specificando una di
queste proprietà esclusive dei pulsanti, ciò impedisce di impostare TargetType su Control).
8. Nel menu Debug, fai clic su Avvia senza eseguire debug per costruire ed eseguire
l’applicazione. Verifica che quanto immesso nella casella di testo appaia di colore bianco con
il carattere Comic Sans MS font.
Sfortunatamente, la scelta dei colori rende difficile vedere il cursore quando fai clic sulla
casella di testo per digitare. Questo problema sarà risolto in un punto successivo.
9. Chiudi il form e ritorna a Visual Studio 2010.
10. Nel riquadro XAML, modifica lo stile bellRingersStyle e aggiungi l’elemento <Style.Triggers>
mostrato in grassetto nell’esempio di codice che segue. Se appare un messaggio di errore
indicante che TriggerCollection è bloccato, ricompila la soluzione.
<Style x:Key="bellRingersStyle" TargetType="Control">
<Setter Property="Background" Value="Gray"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="FontFamily" Value="Comic Sans MS"/>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="Blue" />
</Trigger>
</Style.Triggers>
</Style>
Il trigger specifica un’azione da eseguire quando una proprietà valore viene modificata.
Lo stile bellRingersStyle rileva una variazione nella proprietà IsMouseOver per modificare
temporaneamente il colore dello sfondo del controllo su cui si trova il puntatore del mouse.
Nota È molto importante non confondere i trigger con gli eventi. I trigger rispondono
a cambiamenti transitori nei valori delle proprietà. Quando il valore della proprietà torna
all’impostazione originale, l’azione eseguita dal trigger viene annullata. Nel precedente
esempio, quando la proprietà IsMouseOver di un controllo non ha più il valore true, la
proprietà Background viene reimpostata al suo valore originale. Gli eventi specificano
un’azione da eseguire quando in un’applicazione si verifica un avvenimento significativo,
ad esempio quando l’utente fa clic su un pulsante; le azioni eseguite da un evento non
possono essere annullate con il termine dell’avvenimento.
11. Nel menu Debug, fai clic su Avvia senza eseguire debug per costruire ed eseguire
nuovamente l’applicazione. Questa volta, quando sposti il mouse sulla casella di testo
questa diventa blu, pertanto puoi vedere più facilmente il cursore testo. La casella di
testo torna al suo colore originale non appena il puntatore del mouse viene spostato.
Nota che i pulsanti non si comportano esattamente allo stesso modo. I controlli pulsante
Capitolo 22
Introduzione a Windows Presentation Foundation
457
implementano già questa funzionalità e assumono un tono di blu più chiaro quando
il puntatore del mouse si trova su di loro. Questo comportamento predefinito ha la
precedenza sul trigger specificato nello stile.
12. Chiudi il form e ritorna a Visual Studio 2010.
Nota Un approccio alternativo utilizzabile per applicare un carattere globalmente a tutti i
controlli di un form consiste nell’impostare le proprietà del testo della finestra che contiene i controlli. Queste proprietà comprendono FontFamily, FontSize e FontWeight. Tuttavia,
gli stili forniscono funzionalità aggiuntive, come ad esempio i trigger, non limitando le
impostazioni alla gestione delle proprietà relative ai caratteri. Se specifichi le proprietà
del testo per una finestra e applichi uno stile ai controlli presenti al suo interno, lo stile dei
controlli ha la precedenza sulle proprietà del testo della finestra.
Funzionamento di un’applicazione WPF
Un’applicazione WPF può contenere un numero qualsiasi di form; per aggiungere i form
a un’applicazione puoi utilizzare il comando Aggiungi finestra del menu Progetto di Visual
Studio 2010. Ma come può un’applicazione sapere quale form visualizzare quando viene
avviata? Come ricorderai dal capitolo 1, questo è lo scopo del file App.xaml. Se apri il file
App.xaml del progetto BellRingers, puoi vedere che contiene quanto mostrato di seguito:
<Application x:Class="BellRingers.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="MainWindow.xaml">
<Application.Resources>
</Application.Resources>
</Application>
Quando compili un’applicazione WPF, il compilatore converte questa definizione XAML
in un oggetto Application. Questo oggetto controlla il ciclo di vita dell’applicazione ed è
responsabile della creazione del form visualizzato inizialmente. Puoi pensare all’oggetto
Application come all’elemento che fornisce il metodo Main dell’applicazione. La proprietà
chiave è StartupUri, la quale specifica il file XAML della finestra che l’oggetto Application
deve creare. Quando compili l’applicazione, questa proprietà viene convertita nel codice che
crea e apre il form WPF specificato. Per visualizzare un form differente, deve semplicemente
modificare il valore della proprietà StartupUri.
È importante notare che la proprietà StartupUri fa riferimento al nome del file XAML e non
alla classe che implementa la finestra all’interno di tale file. Se cambi il nome predefinito
della classe (MainWindow), il nome del file non cambia (resta sempre MainWindow.xaml).
Allo stesso modo, se cambi il nome del file, non cambia il nome della classe della finestra
definita in questo file. L’uso di nomi diversi per la classe window e il file XAML può causare
confusione, pertanto se desideri usare nomi diversi si consiglia di farlo in modo coerente
cambiandoli entrambi.
458
Parte IV
Creazione di applicazioni WPF
Aggiunta di controlli al form
Fino a questo punto hai creato un form, impostate alcune sue proprietà, aggiunti alcuni controlli e
definito uno stile. Tuttavia, per renderlo davvero utile è necessario aggiungere alcuni altri controlli
e scrivere del codice personalizzato in modo da implementare alcune funzionalità significative.
La libreria WPF contiene un’insieme di controlli di vati tipi. Lo scopo di alcuni di questi è ovvio, ad
esempio per i controlli TextBox, ListBox, CheckBox e ComboBox, mentre altri più potenti possono
non essere così familiari.
Utilizzo dei controlli WPF
Nel prossimo esercizio puoi aggiungere al form nuovi controlli che l’utente utilizzerà per
immettere informazioni relative ai membri dell’associazione Bell Ringers. Per fare ciò utilizzerai
numerosi controlli, ciascuno adatto a un tipo particolare di immissione dati.
I controlli TextBox saranno impiegati per immettere nome e cognome dei membri. Ognuno di essi
appartiene a una “torre” nella quale si trovano le campane. Il distretto Middleshire ha numerose
torri, ma l’elenco usato è statico poiché non vi sono nuove torri costruite frequentemente,
e fortunatamente quelle esistenti non crollano molto spesso. L’elemento ideale per gestire
questo tipo di dati è un controllo ComboBox. Il form deve memorizzare anche se un membro è
il “capitano” della propria torre, cioè la persona incaricata di guidare gli altri campanari. Anche
in questo caso il tipo migliore è il controllo CheckBox, che può risultare selezionato (True) o
deselezionato (False).
Suggerimento I controlli CheckBox possono di fatto avere tre stati se la proprietà IsThreeState
è impostata su True. Questi tre stati sono true, false e null. Questi stati sono utili quando si desidera visualizzare informazioni recuperate da un database relazionale. Alcune colonne di una
tabella di un database consentono l’immissione di valori null, indicando così che il valore memorizzato è non definito o sconosciuto.
L’applicazione deve anche raccogliere informazioni statistiche su quando i membri sono entrati
a farne parte e sull’esperienza che essi hanno, ad esempio fino a un anno, da 1 a 4 anni, da 5
a 9 anni e oltre 10 anni. Per fare ciò, puoi usare un gruppo di opzioni, o pulsanti di opzione,
per indicare l’esperienza di ciascun membro; i pulsanti di opzione costituiscono un gruppo di
valori mutuamente esclusivi. La vecchia libreria di Microsoft Windows Forms fornisce il controllo
DateTimePicker per selezionare e visualizzare le date, e questo controllo è l’ideale per indicare la
data in cui ogni membro è entrato nell’associazione.
Infine, l’applicazione deve memorizzare le melodie che ogni membro è in grado di suonare; anche
se spesso è motivo di confusione, nella comunità dei campanari queste melodie sono note come
“metodi”. Nonostante ogni campanaro possa suonare una sola campana alla volta, un gruppo
di campanari sotto la direzione del capitano della torre può suonare le campane a disposizione
in sequenze differenti per dare vita a semplici melodie. Esistono molti metodi per suonare le
campane, ognuno con un nome decisamente pittoresco come Plain Bob, Reverse Canterbury,
Capitolo 22
Introduzione a Windows Presentation Foundation
459
Grandsire, Stedman, Kent Treble Bob e Old Oxford Delight. Nuovi metodi vengono aggiunti con
allarmante regolarità, pertanto l’elenco dei metodi può variare nel tempo. In un’applicazione
reale questo elenco dovrebbe essere archiviato in un database. In questa applicazione, utilizzerai
una piccola selezione di metodi immessi direttamente nel form. Nota che i sistemi per accedere e
recuperare i dati archiviati in un database sono discussi nella Parte V di questo manuale, “Gestione
dei dati”. Un controllo adatto per la visualizzazione di queste informazioni e per indicare se un
membro è in grado di suonare un metodo particolare è un controllo ListBox contenente un elenco
di controlli CheckBox.
Una volta immesse le informazioni relative a un membro, il pulsante Add permetterà di
convalidare e memorizzati i dati. L’utente potrà fare clic su Clear per reimpostare i controlli del
form e annullare i dati immessi.
Aggiunta di controlli a un form
1. Assicurati che nella finestra Progettazione sia visualizzato il form MainWindow.xaml. Rimuovi
i due controlli pulsante e il controllo casella di testo dal form.
Suggerimento Per spostare un controllo da un form, fai clic sul controllo, quindi premi il
tasto Canc.
2. Nel riquadro XAML, modifica le proprietà del form Height in 470 e Width in 600, come qui
mostrato in grassetto:
<Window x:Class="BellRingers.MainWindow"
...
Title="..." Height="470" Width="600">
...
</Window>
3. Nella finestra Progettazione, fai clic sul form MainWindow. Dalla Casella degli strumenti,
trascina nel form un controllo Label posizionandolo vicino all’angolo superiore sinistro.
Non preoccuparti della posizione e delle dimensioni esatte dell’etichetta, poiché questa
operazione sarà ripetuta per numerosi controlli più avanti.
4. Nel riquadro XAML, modifica il testo dell’etichetta in First Name, come qui mostrato in
grassetto:
<Label Content="First Name" ... />
Suggerimento Il testo visualizzato nell’etichetta e da molti altri controlli può essere
modificato anche impostandone la proprietà Content nella finestra Proprietà.
5. Nella finestra Progettazione, fai clic sul form MainWindow. Dalla Casella degli strumenti,
trascina nel form un controllo TextBox posizionandolo a destra dell’etichetta.
460
Parte IV
Creazione di applicazioni WPF
Suggerimento Puoi utilizzare le linee guida visualizzata nella finestra Progettazione per
allineare più facilmente i controlli (le linee guida vengono visualizzate dopo aver trascinato
il controllo sul form).
6. Nel riquadro XAML, modifica la proprietà Name della casella di testo con firstName, come
qui mostrato in grassetto:
<TextBox ... Name="firstName" .../>
7. Aggiungi un secondo controllo Label al form. Posizionalo a destra della casella di testo
firstName. Nel riquadro XAML, modifica la proprietà Content per cambiare il testo
dell’etichetta con Last Name.
8. Aggiungi un altro controllo TextBox al form e posizionalo a destra dell’etichetta Last Name.
Nel riquadro XAML, modifica la proprietà Name di questa casella di testo in lastName.
9. Aggiungi un terzo controllo Label al form, posizionandolo direttamente sotto l’etichetta
First Name. Nel riquadro XAML, modifica il testo dell’etichetta in Tower.
10. Aggiungi un controllo ComboBox al form. Posiziona questo controllo sotto la casella di testo
firstName e a destra dell’etichetta Tower. Nel riquadro XAML, modifica la proprietà Name di
questa casella combinata in towerNames.
11. Aggiungi un controllo CheckBox al form. Posizionalo sotto la casella di testo lastName e
a destra della casella combinata towerNames. Nel riquadro XAML, modifica la proprietà
Name della casella di controllo in isCaptain e il testo visualizzato al suo interno in Captain.
12. Aggiungi un quarto controllo Label al form, posizionandolo sotto l’etichetta Tower. Nel
riquadro XAML, modifica il testo dell’etichetta in Member Since.
13. Nella Casella degli strumenti, espandi la categoria Controlli. Aggiungi un controllo
DatePicker al form, posizionandolo sotto la casella combinata towerNames. Modifica il
nome della proprietà Name di questo controllo in MemberSince.
14. Dalla sezione Controlli della Casella degli strumenti, aggiungi al form un controllo GroupBox
posizionandolo sotto l’etichetta Member Since. Nel riquadro XAML, modifica le proprietà
Name e Header rispettivamente in yearsExperience ed Experience. La proprietà Header
cambia il testo visualizzato all’interno della casella gruppo nel form. Imposta la proprietà
Height a 200.
15. Aggiungi un controllo StackPanel al form. Nel riquadro XAML, imposta la proprietà Margin
del controllo a “0,0,0,0”. Rimuovi i valori di tutte le altre proprietà, tranne Name. Il codice
del controllo StackPanel dovrebbe avere l’aspetto mostrato di seguito:
<StackPanel Margin="0,0,0,0" Name="stackPanel1" />
Capitolo 22
Introduzione a Windows Presentation Foundation
461
16. Nel riquadro XAML, modifica la definizione del controllo yearsExperience GroupBox ed
elimina gli elementi <Grid></Grid>. Sposta la definizione del controllo StackPanel nel codice
XAML per il controllo GroupBox, come qui mostrato:
<GroupBox Header="Experience" ... Name="yearsExperience" ...>
<StackPanel Margin="0,0,0,0" Name="stackPanel1" />
</GroupBox>
17. Aggiungi un controllo RadioButton al form, posizionandolo in alto all’interno del controllo
StackPanel appena aggiunto. Ripeti l’operazione aggiungendo altri tre controlli RadioButton
al controllo StackPanel. Questi vengono automaticamente disposti in modo verticale.
18. Nel riquadro XAML, modifica le proprietà Name e il testo visualizzato dei pulsanti di
opzione nella proprietà Content, come qui mostrato in grassetto:
<GroupBox...>
<StackPanel ...>
<RadioButton
<RadioButton
<RadioButton
<RadioButton
</StackPanel>
</GroupBox>
...
...
...
...
Content="Up to 1 year" ...
Content="1 to 4 years" ...
Content="5 to 9 years" ...
Content="10 or more years"
Name="novice" ... />
Name="intermediate" ... />
Name="experienced" ... />
... Name="accomplished" ... />
19. Aggiungi al form un controllo ListBox posizionandolo a destra del controllo GroupBox. Nel
riquadro XAML, modifica la proprietà Name di questa casella di riepilogo in methods.
20. Aggiungi al form un controllo Button e posizionalo nell’angolo in basso a sinistra sotto il
controllo GroupBox. Nel riquadro XAML, modifica la proprietà Name di questo pulsante in
add e il testo visualizzato nella proprietà Content in Add.
21. Aggiungi al form un altro controllo Button e posizionalo in basso a destra del pulsante
Add. Nel riquadro XAML, modifica la proprietà Name di questo pulsante in clear e il testo
visualizzato nella proprietà Content in Clear.
A questo punto al form sono stati aggiunti tutti i controlli necessari. Il punto successivo consiste
nel perfezionare il layout. La tabella che segue mostra le proprietà e i valori di layout da assegnare
a ciascun controllo.
Nella finestra Progettazione, fai clic su ogni controllo, quindi usando la finestra Proprietà, apporta
le varie modifiche. I margini e l’allineamento dei controlli sono progettati per mantenerli nella
giusta posizione anche qualora l’utente ridimensioni il form.
Nota anche che i valori dei margini specificati per i pulsanti di opzione sono relativi a ogni
elemento precedente nel controllo StackPanel che li contiene; il primo pulsante di opzione è a
10 unità dal margine superiore del controllo StackPanel, mentre i pulsanti di opzione rimanenti
hanno uno spazio verticale di 20 unità tra loro.
462
Parte IV
Creazione di applicazioni WPF
Controllo
Proprietà
Valore
label1
Height
28
Margin
29, 25, 0, 0
VerticalAlignment
Top
HorizontalAlignment
Sinistra
Width
75
firstName
label2
lastName
label3
towerNames
isCaptain
Height
23
Margin
121, 25, 0, 0
VerticalAlignment
Top
HorizontalAlignment
Sinistra
Width
175
Height
28
Margin
305, 25, 0, 0
VerticalAlignment
Top
HorizontalAlignment
Sinistra
Width
75
Height
23
Margin
380, 25, 0, 0
VerticalAlignment
Top
HorizontalAlignment
Sinistra
Width
175
Height
28
Margin
29, 72, 0, 0
VerticalAlignment
Top
HorizontalAlignment
Sinistra
Width
75
Height
23
Margin
121, 72, 0, 0
VerticalAlignment
Top
HorizontalAlignment
Sinistra
Width
275
Height
23
Margin
420, 72, 0, 0
VerticalAlignment
Top
HorizontalAlignment
Sinistra
Capitolo 22
Controllo
label4
memberSince
Introduzione a Windows Presentation Foundation
Proprietà
Valore
Width
75
Height
28
Margin
29, 134, 0, 0
VerticalAlignment
Top
HorizontalAlignment
Sinistra
Width
90
Height
23
Margin
121, 134, 0, 0
VerticalAlignment
Top
HorizontalAlignment
Sinistra
Width
275
Height
200
Margin
29, 174, 0, 0
VerticalAlignment
Top
HorizontalAlignment
Sinistra
Width
258
stackPanel1
Margin
0, 0, 0, 0
novice
Height
16
Margin
0, 10, 0, 0
Width
120
Height
16
Margin
0, 20, 0, 0
Width
120
Height
16
Margin
0, 20, 0, 0
Width
120
Height
16
Margin
0, 20, 0, 0
yearsExperience
intermediate
experienced
accomplished
methods
Width
120
Height
200
Margin
310, 174, 0, 0
VerticalAlignment
Top
HorizontalAlignment
Sinistra
Width
245
463
464
Parte IV
Creazione di applicazioni WPF
Controllo
Proprietà
Valore
add
Height
23
Margin
188, 388, 0, 0
VerticalAlignment
Top
HorizontalAlignment
Sinistra
Width
75
Height
23
Margin
313, 388, 0, 0
VerticalAlignment
Top
HorizontalAlignment
Sinistra
Width
75
clear
Per finire, è possibile applicare uno stile ai controlli. Per fare ciò, puoi utilizzare lo stile
bellRingersStyle per pulsanti e caselle di testo, mentre etichette, caselle combinate, caselle gruppo
e pulsanti di opzione dovranno probabilmente non essere visualizzati su sfondo grigio.
Applicazione degli stili ai controlli e verifica del funzionamento del form
1. Nel riquadro XAML, aggiungi all’elemento <Windows.Resources> lo stile
bellRingersFontStyle mostrato in grassetto nel codice che segue. Lascia lo stile
bellRingersStyle esistente dove si trova. Nota che questo nuovo stile cambia solo il carattere.
<Window.Resources>
<Style x:Key="bellRingersFontStyle" TargetType="Control">
<Setter Property="FontFamily" Value="Comic Sans MS"/>
</Style>
<Style x:Key="bellRingersStyle" TargetType="Control">
...
</Style>
</Window.Resources>
2. Nel form, fai clic sul controllo Label label1 che visualizza il testo First Name. Nella finestra
Proprietà, cerca la proprietà Style del controllo. Fai clic sull’etichetta Resource… mostrata
come valore di questa proprietà. Nel form viene visualizzato un elenco di stili disponibili
come risorse, come mostrato nell’immagine seguente.
Capitolo 22
Introduzione a Windows Presentation Foundation
465
3. Verifica che l’elenco a discesa nell’angolo inferiore destro della casella di riepilogo sia
impostato su Static, quindi fai doppio clic su bellRingersFontStyle.
4. Nel riquadro XAML, verifica che lo stile bellRingersFontStyle sia stato applicato al controllo
label1, come qui mostrato in grassetto:
<Label Content="First Name" ... Style="{StaticResource bellRingersFontStyle}"/>
5. Applica lo stesso stile ai controlli che seguono. Puoi utilizzare l’editor delle risorse delle
proprietà o aggiungere lo stile manualmente a ogni controllo modificando le definizioni
XAML:
■
label2
■
label3
■
isCaptain
■
towerNames
■
label4
■
yearsExperience
■
methods
Nota L’applicazione dello stile alla casella gruppo yearsExperience e alla casella di riepi-
logo methods comporta automaticamente il suo uso da parte degli elementi visualizzati in
questi controlli.
6. Applica lo stile bellRingersStyle ai controlli che seguono:
■
firstName
■
lastName
■
add
■
clear
466
Parte IV
Creazione di applicazioni WPF
7. Nel menu Debug, fai clic su Avvia senza eseguire debug.
Una volta eseguito, il form dovrebbe apparire simile a quanto mostrato nell’immagine che
segue:
Nota che attualmente la casella di riepilogo methods è vuota. Il codice necessario per
popolarla sarà aggiunto in un esercizio successivo.
8. Fai clic sulla freccia in giù nella casella combinata Tower. L’elenco delle torri attualmente è
vuoto. Anche in questo caso, il codice relativo a questa casella combinata sarà inserito in un
esercizio successivo.
9. Chiudi il form e ritorna a Visual Studio 2010.
Modifica dinamica delle proprietà
Le finestre Progettazione e Proprietà e il riquadro XAML sono stati utilizzati per impostare le
proprietà in modo statico. Durante l’esecuzione del form può risultare utile poter reimpostare
il valore di ciascun controllo in base a un valore iniziale predefinito. Per fare ciò, è necessario
scrivere del codice. Le esercitazioni che seguono mostrano come creare un metodo privato
chiamato Reset. In seguito, questo metodo Reset verrà richiamato all’avvio del form e ogni volta
che l’utente farà clic sul pulsante Clear.
Creazione del metodo Reset
1. Nella finestra Progettazione, fai clic con il tasto destro del mouse sul form, quindi fai clic su
Visualizza codice. Viene visualizzata la finestra dell’editor di testo e di codice contenente il
file MainWindow.xaml.cs che permette di aggiungere il codice C# al form.
Capitolo 22
Introduzione a Windows Presentation Foundation
467
2. Aggiungi il metodo Reset qui mostrato in grassetto alla classe MainWindow:
public partial class MainWindow : Window
{
...
public void Reset()
{
firstName.Text = String.Empty;
lastName.Text = String.Empty;
}
}
Le due istruzioni presenti in questo metodo garantiscono che le caselle di testo firstName e
lastName siano vuote assegnando una stringa vuota alle loro proprietà Text.
Inoltre, può essere anche necessario inizializzare le proprietà degli altri controlli del form e
popolare la casella combinata towerNames e la casella di riepilogo methods.
Come ricorderai, la casella combinata towerName conterrà un elenco di tutte le torri
campanarie presenti nel distretto Middleshire. Solitamente queste informazioni vengono
archiviate in un database, ed è pertanto necessario scrivere il codice che recupera l’elenco
delle torri e popola il controllo ComboBox. In questo esempio tuttavia l’applicazione utilizza
un gruppo di informazioni memorizzate nel codice. Il controllo ComboBox ha una proprietà
chiamata Items che contiene un elenco di dati da visualizzare.
3. Aggiungi alla classe MainWindow la matrice di stringhe chiamata towers qui mostrata in
grassetto e contenente un elenco fisso di nomi di torri:
public partial class MainWindow : Window
{
private string[] towers = { "Great Shevington", "Little Mudford",
"Upper Gumtree", "Downley Hatch" };
...
}
4. Aggiungi le seguenti istruzioni mostrate in grassetto alla fine del metodo Reset. Questo
codice cancella la casella combinata towerNames (importante perché altrimenti potrebbero
venire creati molti valori doppi nell’elenco) e aggiunge le torri trovate nella matrice towers.
Una casella combinata contiene una proprietà chiamata Items contenente un insieme di
elementi da visualizzare. L’istruzione che segue il ciclo foreach fa in modo che la prima torre
venga visualizzata come valore predefinito:
public void Reset()
{
...
towerNames.Items.Clear();
foreach (string towerName in towers)
{
towerNames.Items.Add(towerName);
}
towerNames.Text = towerNames.Items[0] as string;
}
468
Parte IV
Creazione di applicazioni WPF
Nota I valori fissi possono essere specificati in fase di progettazione anche nella descrizione XAML della casella combinata, come mostrato di seguito:
<ComboBox Text="towerNames">
<ComboBox.Items>
<ComboBoxItem>
Great Shevington
</ComboBoxItem>
<ComboBoxItem>
Little Mudford
</ComboBoxItem>
<ComboBoxItem>
Upper Gumtree
</ComboBoxItem>
<ComboBoxItem>
Downley Hatch
</ComboBoxItem>
</ComboBox.Items>
</ComboBox>
5. Ora devi popolare la casella di riepilogo methods con l’elenco dei metodi di suonare le
campane. Come la casella combinata, la casella di riepilogo dispone di una proprietà
chiamata Items che può contenere un gruppo di valori da visualizzare. Inoltre, come avviene
per il controllo ComboBox, questa proprietà può essere popolata con i dati provenienti
da un database. Tuttavia in questo esempio verranno utilizzati solamente dei valori
fissi. Aggiungi alla classe MainWindow la matrice di stringhe qui mostrata in grassetto e
contenente l’elenco dei metodi:
public partial class MainWindow : Window
{
...
private string[] ringingMethods = { "Plain Bob", "Reverse Canterbury",
"Grandsire", "Stedman", "Kent Treble Bob", "Old Oxford Delight",
"Winchendon Place", "Norwich Surprise", "Crayford Little Court" };
...
}
6. Per consentire all’utente di specifica quali metodi può suonare un membro, la casella di
riepilogo methods dovrebbe ora visualizzare un elenco di caselle di controllo invece delle
normali stringhe di testo. Grazie alla flessibilità del modello WPF, è possibile specificare
un’ampia gamma di tipi di contenuto differenti per i controlli, come ad esempio caselle di
riepilogo e caselle combinate. Aggiungi al metodo Reset il codice qui mostrato in grassetto
per riempire la casella di riepilogo methods con i metodi della matrice ringingMethods.
Nota che in questo caso ogni elemento è una casella di controllo. Il testo visualizzato dalla
casella di controllo può essere specificato impostandone la proprietà Content, mentre la
spaziatura tra gli elementi dell’elenco può essere definita nella proprietà Margin; questo
codice inserisce una spaziatura di 10 unità dopo ciascuna voce:
public void Reset()
{
...
methods.Items.Clear();
Capitolo 22
Introduzione a Windows Presentation Foundation
469
CheckBox method = null;
foreach (string methodName in ringingMethods)
{
method = new CheckBox();
method.Margin = new Thickness(0, 0, 0, 10);
method.Content = methodName;
methods.Items.Add(method);
}
}
Nota La maggior parte dei controlli WPF dispone di una proprietà Content che è pos-
sibile utilizzare per impostare e leggere il valore visualizzato dal controllo in questione.
Questa proprietà è in realtà un oggetto, pertanto è possibile impostarla su pressoché qualsiasi tipo, a condizione che abbia senso visualizzarla.
7. La casella di controllo isCaptain dovrebbe avere false come valore predefinito. Per fare ciò,
devi impostare la proprietà IsChecked. Aggiungi l’istruzione qui mostrata in grassetto al
metodo Reset:
public void Reset()
{
...
isCaptain.IsChecked = false;
}
8. Il form contiene quattro pulsanti di opzione che indicano il numero di anni di esperienza
di ciascun membro. Un pulsante di opzione è simile a un controllo CheckBox, poiché
contiene un valore true o false. Tuttavia, la potenza dei pulsanti di opzione aumenta quando
vengono raggruppati in un controllo GroupBox. In questo caso, i pulsanti di opzione
formano un insieme mutuamente esclusivo in cui un solo pulsante di opzione del gruppo
può essere selezionato (impostato su true), mentre tutti gli altri risultano automaticamente
deselezionati (impostati su false). Per impostazione predefinita, nessuno dei pulsanti risulta
selezionato. Questa condizione deve essere corretta impostando la proprietà IsChecked del
pulsante di opzione novice. Aggiungi l’istruzione qui mostrata in grassetto al metodo Reset:
public void Reset()
{
...
novice.IsChecked = true;
}
9. Verifica che il controllo DateTimePicker chiamato Member Since abbia la data corrente
come valore predefinito. Per fare ciò, puoi impostare la proprietà Text del controllo. La data
corrente può essere ottenuta dal metodo statico Today della classe DateTime.
Aggiungi al metodo Reset il codice qui mostrato in grassetto per inizializzare il controllo
DatePicker.
public void Reset()
{
...
memberSince.Text = DateTime.Today.ToString();
}
470
Parte IV
Creazione di applicazioni WPF
10. Infine, devi disporre il metodo Reset in modo che possa essere richiamato durante la
visualizzazione iniziale del form. Una buona ubicazione in cui inserirlo è il costruttore
MainWindow. Inserisci una chiamata al metodo Reset dopo l’istruzione che richiama il
metodo InitializeComponent, come qui mostrato in grassetto:
public MainWindow()
{
InitializeComponent();
this.Reset();
}
11. Nel menu Debug, fai clic su Avvia senza eseguire debug per verificare che il progetto possa
essere compilato ed eseguito correttamente.
12. Una volta aperto il form, fai clic sulla casella combinata Tower.
Viene mostrato l’elenco delle torri campanarie ed è possibile selezionarne una.
13. Fai clic sulla freccia in giù a destra dello strumento data e ora Member Since.
Sullo schermo appare un calendario di date. Il valore predefinito è la data corrente. Puoi
fare clic su una data e utilizzare le frecce per selezionare il mese desiderato. Puoi anche fare
clic sul nome del mese per visualizzare i mesi sotto forma di un elenco a discesa, oppure
sull’anno per visualizzare l’elenco degli anni.
14. Fai clic su ciascuno dei pulsanti di opzione nella casella gruppo Experience.
Nota che non è possibile selezionare più di un pulsante di opzione alla volta.
15. Nella casella di riepilogo Methods, fai clic su alcuni dei metodi per selezionare la casella
di controllo corrispondente. Facendo clic un’altra volta su a metodo, la casella di controllo
corrispondente viene deselezionata come previsto.
16. Fai clic sui pulsanti Add e Clear.
Attualmente questi pulsanti non svolgono alcun compito. Le loro funzionalità verranno
aggiunte nelle esercitazioni alla fine di questo capitolo.
17. Chiudi il form e ritorna a Visual Studio 2010.
Gestione degli eventi in un form WPF
Se hai familiarità con Microsoft Visual Basic, Microsoft Foundation Classes (MFC) o qualsiasi altro
strumento disponibile per la creazione di applicazioni GUI per Windows, sai certamente che
Windows utilizza un modello pilotato dagli eventi per determinare quando eseguire il codice.
Nel capitolo 17, “Interruzione del flusso del programma e gestione degli eventi”, hai visto come
pubblicare eventi personali e sottoscriverli. Form e controlli WPF dispongono di eventi predefiniti
che puoi sottoscrivere e che dovrebbero essere sufficienti per gestire i requisiti della maggior
parte delle interfacce utente.
Capitolo 22
Introduzione a Windows Presentation Foundation
471
Elaborazione degli eventi nei form Windows
Il compito dello sviluppatore è di rilevare gli eventi che hanno importanza per l’applicazione e
creare il codice incaricato di gestirli. Un esempio familiare è il controllo Button, il quale genera
un evento “qualcuno ha fatto clic su di me” ogni volta che un utente fa clic con il mouse o preme
Invio quando questo pulsante ha il controllo. Se desideri che il pulsante svolga un compito, devi
scrivere il codice che risponde a questo evento. Questo è l’argomento dell’esercizio che segue.
Gestione dell’evento Click
k per i pulsanti Clear e Add
1. Visualizza il file MainWindow.xaml nella finestra Progettazione. Fai doppio clic sul pulsante
Clear nel form.
Nota Quando modifichi il codice alla base di un form WPF e crei l’applicazione, la volta
successive che viene visualizzato il form nella finestra Progettazione, nella parte superiore
della finestra potrebbe comparire un messaggio che dice che un assembly o il documento
correlato, è stato aggiornato, quindi è necessario riavviare. Se ciò accade, fai clic sul messaggio per consentire il riavvio del form.
Viene visualizzata la finestra dell’editor di testo e di codice e creato un metodo chiamato
clear_Click. Questo è il metodo evento che viene richiamato ogni volta che l’utente fa clic sul
pulsante Clear. Il metodo evento accetta due parametri:sender (un oggetto) e un parametro
di argomento aggiuntivo (un oggetto RoutedEventArgs). Il runtime WPF popola questi
parametri con informazioni relative all’origine dell’evento e altre informazioni aggiuntive
utili per gestire l’evento. Questi parametri non saranno utilizzati in questo esercizio.
I controlli WPF possono generare numerosi eventi. Quando fai doppio clic su un controllo
o un form nella finestra Progettazione, Visual Studio genera lo stub di un metodo evento
per l’evento predefinito per il controllo; nel caso di un pulsante, l’evento predefinito è Click.
Quando invece fai doppio clic su a controllo casella di testo, Visual Studio genera lo stub di
un metodo evento per la gestione dell’evento TextChanged.
2. Quando l’utente fa clic sul pulsante Clear, si desidera che il form venga reimpostato in base
ai valori predefiniti. Nel corpo del metodo clear_Click, richiama il metodo Reset come qui
mostrato in grassetto:
private void clear_Click(object sender, RoutedEventArgs e)
{
this.Reset();
}
Una volta terminato di immettere i dati relativi a un membro, gli utenti faranno clic sul
pulsante Add per memorizzare le informazioni. L’evento Click del pulsante Add dovrebbe
convalidare le informazioni immesse per verificare che siano corrette, ad esempio per
controllare che il capitano di una torre non abbia meno di un anno di esperienza; in caso
472
Parte IV
Creazione di applicazioni WPF
affermativo, dovrebbe predisporre i dati per l’invio a un database o altro archivio. Ulteriori
informazioni sulla convalida sono disponibili nei capitoli successivi. Per ora, il codice per
l’evento Click del pulsante Add dovrà semplicemente visualizzare una finestra di dialogo
contenente la ripetizione dei dati immessi.
3. Torna alla finestra Progettazione relativa al form MainWindow.xaml. Nel riquadro XAML,
localizza l’elemento che definisce il pulsante Add e inizia a immettere il codice qui mostrato
in grassetto:
<Button Content="Add" ... Click= />
Nota che mentre digiti il carattere =, compare un menu di scelta rapida che visualizza due
elementi: <New Event Handler> e clear_Click. Se due pulsanti eseguono un’operazione
comune, essi possono condividere lo stesso metodo gestore eventi, ad esempio clear_Click.
Se desideri generare un metodo gestore eventi completamente nuovo, puoi selezionare il
comando <Nuovo gestore eventi>.
4. Nel menu di scelta rapida, fai doppio clic sul comando <Nuovo gestore eventi>.
Nel codice XAML relativo al pulsante appare il testo add_Click.
Nota Ricorda che non sei limitato a gestire l’evento Click per un pulsante. Quando modi-
fichi il codice XAML di un controllo, l’elenco IntelliSense visualizza proprietà ed eventi del
controllo in questione. Per gestire un evento diverso dall’evento Click, è sufficiente digitare
il nome dell’evento, quindi selezionare o digitare il nome del metodo che desideri gestisca
tale evento. L’elenco completo degli eventi supportati da ciascun controllo è disponibile
nella documentazione di Visual Studio 2010.
5. Passa alla finestra dell’editor di codice e di testo che visualizza il file MainWindow.xaml.cs.
Nota che il metodo add_Click è stato aggiunto alla classe MainWindow.
Suggerimento Non è necessario utilizzare i nomi predefiniti generati da Visual Studio
2010 per i metodi gestori evento. Invece di fare clic sul comando <Nuovo gestore eventi>
nel menu di scelta rapida, puoi digitare direttamente il nome di un metodo. Tuttavia,
in questo caso devi aggiungere manualmente tale metodo alla classe window. Questo
metodo deve utilizzare la grafia corretta; inoltre deve restituire un void e accettare due
argomenti, un parametro object e un parametro RoutedEventArgs.
Importante Qualora in seguito tu decida di rimuovere un metodo evento come
add_Click dal file MainWindow.xaml.cs, dovrai anche modificare la definizione XAML del
controllo corrispondente per rimuovere il riferimento Click=”add_Click” all’evento; in caso
contrario, l’applicazione non potrà essere compilata.
Capitolo 22
Introduzione a Windows Presentation Foundation
473
6. Aggiungi il codice qui mostrato in grassetto al metodo add_Click:
private void add_Click(object sender, RoutedEventArgs e)
{
string nameAndTower = String.Format(
"Member name: {0} {1} from the tower at {2} rings the following methods:",
firstName.Text, lastName.Text, towerNames.Text);
StringBuilder details = new StringBuilder();
details.AppendLine(nameAndTower);
foreach (CheckBox cb in methods.Items)
{
if (cb.IsChecked.Value)
{
details.AppendLine(cb.Content.ToString());
}
}
MessageBox.Show(details.ToString(), "Member Information");
}
Questo blocco di codice crea una variabile string chiamata nameAndTower che riempie con
il nome del membro e della torre a cui egli appartiene.
Nota che il codice accede alla proprietà Text dei controlli casella di testo e casella combinata
per leggere i valori attuali di tali controlli. Inoltre, il codice utilizza il metodo statico String.
Format per formattare il risultato. Il metodo String.Format opera in modo simile al metodo
Console.WriteLine, ma restituisce come risultato la stringa formattata invece di visualizzarla
sullo schermo.
Il codice crea quindi un oggetto StringBuilder chiamato details. Il metodo utilizza questo
oggetto StringBuilder per creare una rappresentazione stringa delle informazioni da
visualizzare. Il testo nella stringa nameAndTower viene impiegato inizialmente per popolare
l’oggetto details. Il codice esegue quindi un’iterazione all’interno dell’insieme Items nella
casella di riepilogo methods. Come ricorderai, questa casella di riepilogo contiene controlli
casella di controllo. Ogni casella di controllo viene esaminata a turno, e se selezionata
dall’utente, il testo nella proprietà Content della casella di controllo viene aggiunto
all’oggetto StringBuilder details. In questo caso è necessario tuttavia risolvere un piccolo
problema: Ricorda che CheckBox può essere impostato su true, false o null. La proprietà
IsChecked di fatto restituisce un valore bool? di tipo null. Per accedere al valore booleano
della proprietà IsChecked si utilizza la proprietà Value.
Infine, la classe MessageBox fornisce metodi statici per la visualizzazione di finestre di
dialogo sullo schermo. Il metodo Show qui usato visualizza il contenuto della stringa details
nel corpo della finestra di dialogo, e inserisce il testo “Member Information” nella barra
del titolo. Show è un metodo sottoposto a overload e vi sono altre varianti che è possibile
utilizzare per specificare icone e pulsanti da visualizzare nella finestra di dialogo.
474
Parte IV
Creazione di applicazioni WPF
Nota In alternativa, è possibile utilizzare la normale concatenazione di stringhe invece
di un oggetto StringBuilder, ma la classe StringBuilder è decisamente più efficiente e
costituisce l’approccio consigliato per eseguire il tipo di attività richiesto in questo codice.
In .NET Framework e C#, il tipo di dati string è invariabile; quando modifichi il valore di
una stringa, il runtime in realtà crea una nuova stringa contenente il valore modificato ed
elimina la vecchia stringa. Modificando più volte una stringa, il codice può diventare inefficiente, poiché a ogni modifica deve essere creata una nuova stringa in memoria (le vecchie stringhe vengono destinate a essere eliminate mediante garbage collection). La classe
StringBuilder nello spazio dei nomi System.Text è progettata per eliminare questa inefficienza. Per aggiungere o rimuovere i caratteri da un oggetto StringBuilder, puoi utilizzare i
metodi Append, Insert e Remove senza creare un nuovo oggetto ogni volta.
7. Nel menu Debug, fai clic su Avvia senza eseguire debug per costruire ed eseguire
l’applicazione.
8. Digita i dati di prova relativi a nome e cognome di un membro, seleziona una torre e scegli
alcuni metodi. Fai clic sul pulsante Add e verifica che sullo schermo appaia la finestra di
dialogo Member Information contenente le informazioni relative al nuovo membro e ai
metodi che egli è in grado di suonare. Fai clic su OK nella finestra di dialogo.
9. Fai clic sul pulsante Clear e verifica che i controlli del form vengano reimpostati in base ai
rispettivi valori predefiniti.
10. Chiudi il form e ritorna a Visual Studio 2010.
L’esercizio finale di questo capitolo mostra come aggiungere un gestore eventi per gestire l’evento
Closing della finestra, in modo che gli utenti possano confermare che desiderano realmente uscire
dall’applicazione. L’evento Closing viene generato quando l’utente tenta di chiudere il form ma
prima che ciò accada realmente. Questo evento può essere impiegato per chiedere all’utente di
salvare i dati non ancora memorizzati, o addirittura per chiedere se desidera davvero chiudere il
form; in caso contrario, è possibile annullare l’evento nel gestore eventi e impedire la chiusura del
form.
Gestione dell’evento Closing per il form
1. Nel riquadro XAML della finestra Progettazione, inizia a immettere il codice qui mostrato in
grassetto nella descrizione XAML della finestra MainWindow:
<Window x:Class="BellRingers.MainWindow"
...
Title="..." ... Closing=>
2. Una volta digitato il carattere =, appare il menu di scelta rapida in cui puoi fare doppio clic
sul comando <Nuovo gestore eventi>.
Visual Studio genera un metodo evento chiamato Window_Closing e lo associa all’evento
Closing del form, come mostrato di seguito:
<Window x:Class="BellRingers.MainWindow"
...
Title="..." ... Closing="Window_Closing">
Capitolo 22
Introduzione a Windows Presentation Foundation
475
3. Passa alla finestra dell’editor di codice e di testo che visualizza il file MainWindow.xaml.cs.
Alla classe MainWindow è stato aggiunto uno stub per il metodo evento Window_Closing:
private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
}
Come puoi vedere, il secondo parametro di questo metodo è del tipo CancelEventArgs.
La classe CancelEventArgs dispone di una proprietà booleana chiamata Cancel. Se imposti
Cancel su true nel gestore eventi, il form non potrà esser chiuso. Se imposti Cancel su false
(valore predefinito), il form verrà chiuso al termine del gestore eventi.
4. Aggiungi l’istruzione qui mostrata in grassetto al metodo memberFormClosing:
private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
MessageBoxResult key = MessageBox.Show(
"Are you sure you want to quit",
"Confirm",
MessageBoxButton.YesNo,
MessageBoxImage.Question,
MessageBoxResult.No);
e.Cancel = (key == MessageBoxResult.No);
}
Queste istruzioni vengono visualizzate in una finestra di dialogo che chiede all’utente
di confermare se desidera uscire dall’applicazione. La finestra di dialogo conterrà i
pulsanti Sì e No (Yes o No) e un’icona con il punto di domanda. Il parametro finale,
MessageBoxResult.No, indica il pulsante predefinito da selezionare quando l’utente preme
semplicemente il tasto Invio; è consigliabile presumere che l’utente non desideri uscire
dall’applicazione piuttosto che rischiare perdite accidentali di dati appena immessi e non
ancora salvati. Quando l’utente fa clic su un pulsante, la finestra di dialogo si chiude e il
pulsante selezionato viene restituito come valore del metodo come MessageBoxResult,
un’enumerazione che identifica quale pulsante è stato premuto. Se l’utente fa clic su No, la
seconda istruzione imposta la proprietà Cancel del parametro CancelEventArgs (e) su true,
impedendo così la chiusura del form.
5. Nel menu Debug, fai clic su Avvia senza eseguire debug per eseguire l’applicazione.
6. Prova a chiudere il form. Fai clic su No nella finestra di dialogo che appare.
Il form dovrebbe continuare nella sua esecuzione.
7. Prova di nuovo a chiudere il form. Questa volta, fai clic su Sì.
Il form si chiude e l’applicazione viene terminata.
In questo capitolo hai così visto come utilizzare le funzioni essenziali di WPF per creare un
interfaccia utente funzionale. WPF contiene molte altre funzioni che per motivi di spazio non
possono essere esaminate in questo manuale, in particolar modo quelle relative ad alcune
funzionalità davvero utili per la gestione di grafica e animazioni bi- e tridimensionali.
476
Parte IV
Creazione di applicazioni WPF
■ Se desideri continuare con il capitolo successivo
Continua a eseguire Visual Studio 2010 e passa al capitolo 23.
■ Se desideri uscire ora da Visual Studio 2010
Nel menu File, fai clic su Esci. Se sullo schermo è visualizzata una finestra di dialogo Salva, fai
clic su Sì per salvare il progetto.
Riferimenti rapidi del capitolo 22
Obiettivo
Azione
Creazione di un’applicazione WPF
Utilizza il modello Applicazione WPF.
Aggiunta di controlli a un
form
Trascina il controllo dalla Casella degli strumenti al form.
Modifica delle proprietà
di un form o un controllo
Fai clic sul form o controllo nella finestra Progettazione. Fatto ciò, esegui una delle
seguenti operazioni:
Visualizzazione del codice
alla base di un form
■
Nella finestra Proprietà, seleziona la proprietà che desideri modificare e immetti un nuovo valore.
■
Nel riquadro XAML, specifica le proprietà e il valore nell’elemento <Window>
o nell’elemento che definisce il controllo.
Esegui una delle seguenti operazioni:
■
Fai clic su Codice nel menu Visualizza.
■
Fai clic con il tasto destro del mouse nella finestra Progettazione, quindi fai
clic su Visualizza codice.
■
In Esplora soluzioni, espandi la cartella corrispondente al file .xaml del form,
quindi fai doppio clic sul file .xaml.cs che appare.
Definizione di un gruppo
di pulsanti di opzione
mutuamente esclusivi.
Aggiungi al form un controllo pannello, ad esempio StackPanel. Aggiungi i pulsanti
di opzione al pannello. Tutti i pulsanti di controllo di un pannello sono mutuamente esclusivi.
Riempimento di una
casella combinata o di
una casella di riepilogo
tramite codice C#
Utilizza il metodo Add della proprietà Items. Ad esempio:
towerNames.Items.Add("Upper Gumtree");
A seconda che si desideri conservare il contenuto attuale dell’elenco o meno, può
essere necessario prima reimpostare la proprietà Items. Ad esempio:
towerNames.Items.Clear();
Inizializzazione di un
controllo casella di
controllo o pulsante di
opzione
Gestione di un evento per
un controllo o form
Imposta su true o false la proprietà IsChecked. Ad esempio:
novice.IsChecked = true;
Nel riquadro XAML, aggiungi il codice che specifica l’evento, quindi seleziona un
metodo esistente con la firma appropriata, oppure fai clic sul comando <Aggiungi
nuovo evento> nel menu di scelta rapida che appare, quindi immetti il codice che
gestisce l’evento nel metodo evento che viene creato.
Capitolo 23
Raccolta dell’input utente
Gli argomenti trattati in questo capitolo sono:
■
Creazione di menu per applicazioni Microsoft WPF (Windows Presentation Foundation)
utilizzando le classi Menu e MenuItem.
■
Esecuzione di elaborazioni in risposta a eventi di menu quando un utente fa clic su un
comando di menu.
■
Creazione di menu di scelta rapida contestuali tramite la classe ContextMenu.
■
Manipolazione dei menu mediante il codice e creazione di menu dinamici.
■
Utilizzo di finestre di dialogo comuni di Windows in un’applicazione per chiedere all’utente
il nome di un file.
■
Creazione di applicazioni WPF che sfruttano più thread per incrementare la velocità di
risposta.
Nel capitolo 22, “Introduzione a Windows Presentation Foundation”, hai appreso come creare una
semplice applicazione WPF formata da una selezione di controlli ed eventi. Molte applicazioni
commerciali basate su Microsoft Windows forniscono anche menu contenenti comandi e opzioni
che permettono all’utente di eseguire numerose attività relative all’applicazione. In questo
capitolo potrai apprendere come creare i menu e aggiungerli ai form utilizzando il controllo
Menu. Vedrai così come reagire quando l’utente fa clic su un comando di un menu. Inoltre,
apprenderai come creare menu di scelta rapida il cui contenuto varia in base al contesto corrente.
Infine, potrai avere informazioni sulle classi comuni delle finestre di dialogo fornite come parte
della libreria WPF. Queste ultime consentono di interrogare l’utente in modo facile e familiare in
merito a elementi utilizzati frequentemente, ad esempio file e stampati.
Linee guida e stile dei menu
Se esamini la maggior parte delle applicazioni basate su Windows, noterai che alcuni elementi
della barra dei menu tendono ad apparire costantemente allo stesso posto, e che il contenuto di
tali elementi risulta spesso prevedibile. Ad esempio, solitamente il menu File è la prima voce della
barra dei menu e al suo interno è possibile quasi sempre trovare i comandi relativi alla creazione
di un nuovo documento, all’apertura dei documenti esistenti, al salvataggio e alla stampa dei
documenti e per l’uscita dall’applicazione.
Nota Il termine documento indica i dati gestiti da un’applicazione. Ad esempio, in Microsoft
Office Excel questo termine fa riferimento a un foglio di lavoro, mentre nell’applicazione
BellRingers creata nel capitolo 22 può riferirsi ai dettagli relativi a un nuovo membro.
477
478
Parte IV
Creazione di applicazioni WPF
L’ordine in cui i vari comandi appaiono tende a essere costante nelle diverse applicazioni; ad
esempio, il comando Esci è sempre l’ultimo del menu File. Nel menu File possono essere presenti
anche comandi specifici dell’applicazione.
Spesso le applicazioni dispongono di un menu Modifica contenente comandi come Taglia, Incolla,
Cancella e Trova. Nella barra dei menu sono normalmente presenti alcuni menu aggiuntivi specifici
dell’applicazione, ma anche in questo caso le convenzioni seguite indicano come ultimo menu
quello indicato dal carattere ?, il quale permette di accedere agli argomenti della Guida e alle
informazioni generiche quali copyright e condizioni di licenza dell’applicazione. Nelle applicazioni
progettate correttamente, la maggior parte dei menu è caratterizzata da un contenuto
prevedibile e ciò aiuta a garantire che l’applicazione sia facile da apprendere e utilizzare.
Suggerimento Microsoft pubblica un pacchetto completo di linee guida sulla creazione di
interfacce utente intuitive, comprese note sulla progettazione dei menu, nel sito Web Microsoft
all’indirizzo http://msdn2.microsoft.com/en-us/library/Aa286531.aspx.
Menu ed eventi di menu
WPF fornisce il controllo Menu come contenitore per le voci del menu. Il controllo Menu dispone
di una struttura base per la definizione di un menu. Come molti altri aspetti di WPF, il controllo
Menu è molto flessibile e consente di definire una struttura di menu formata da pressoché
qualsiasi tipo di controllo WPF. Probabilmente, hai già familiarità con menu contenenti elementi
di testo su cui è possibile fare clic per eseguire un comando. I menu WPF possono contenere
anche pulsanti, caselle di testo, caselle combinate e così via. Per definire i menu, puoi utilizzare il
riquadro XAML della finestra Progettazione, oltre a crearli in fase di esecuzione tramite il codice
di Microsoft Visual C#. La progettazione dell’aspetto del menu rappresenta solo metà del lavoro
da svolgere. Facendo clic su un comando di menu, l’utente si aspetta che succeda qualcosa.
L’applicazione reagisce ai comandi rilevando gli eventi di menu ed eseguendo il codice allo stesso
modo in cui gestisce gli eventi di controllo.
Creazione di un menu
Nell’esercitazione che segue, utilizzerai il riquadro XAML per creare i menu dell’applicazione della
Middleshire Bell Ringers Association. Potrai apprendere come manipolare e creare menu tramite
codice più avanti in questo capitolo.
Creazione del menu di un’applicazione
1. Avvia Microsoft Visual Studio 2010 se non è già in esecuzione.
2. Apri la soluzione BellRingers disponibile nella cartella \Microsoft Press\Visual CSharp Step by
Step\Chapter 23\BellRingers della cartella Documenti. Questa è una copia dell’applicazione
creata nel capitolo 22.
Capitolo 23
Raccolta dell’input utente
479
3. Visualizza il file MainWindow.xaml nella finestra Progettazione.
4. Dalla Casella degli strumenti, trascina un controllo DockPanel dalla sezione Tutti i controlli
di WPF a un punto qualsiasi del form. (Accertati di trascinarlo nel form e non in uno dei
controlli dello stesso.) Nella finestra Proprietà, imposta la proprietà Width di DockPanel su
Auto, la proprietà HorizontalAlignment su Stretch, la proprietà VerticalAlignment su Top e la
proprietà Margin su 0.
Nota L’impostazione su 0 della proprietà Margin equivale a specificare i valori 0, 0, 0, 0.
Il controllo DockControl dovrebbe apparire nella parte superiore del form e occupare la sua
intera larghezza. In pratica, il controllo dovrebbe coprire gli elementi dell’interfaccia utente
First Name, Last Name, Tower e Captain.
Il controllo DockPanel è un controllo pannello che puoi utilizzare per controllare la
disposizione di altri controlli inseriti al suo interno, ad esempio i controlli Grid e StackPanel
incontrati nel capitolo 22. Puoi anche aggiungere un menu direttamente a un form, ma è
consigliabile posizionarlo in un DockPanel poiché in questo modo è possibile manipolare
più facilmente il menu e il suo posizionamento all’interno del form. Ad esempio, per
posizionarlo nella parte inferiore o su un lato, puoi ricollocare l’intero menu in un altro
punto del form semplicemente spostando il pannello in fase di progettazione o tramite
codice in fase di esecuzione.
5. Dalla Casella degli strumenti, trascina un controllo Menu dalla sezione Tutti i controlli di
WPF nel controllo DockPanel. Nella finestra Proprietà, imposta la proprietà DockPanel.Dock
su Top, imposta la proprietà Width su Auto, imposta la proprietà HorizontalAlignment su
Stretch e infine imposta la proprietà VerticalAlignment su Top.
Il controllo Menu viene visualizzato come una banda di colore grigio che attraversa la parte
superiore di DockPanel. Se esamini il codice dei controlli DockPanel e Menu nel riquadro
XAML, essi dovrebbero avere il seguente aspetto:
<DockPanel Height="100" HorizontalAlignment="Stretch" Margin="0"
Name="dockPanel1" VerticalAlignment="Top" Width="Auto">
<Menu Height="23" Name="menu1" Width="Auto" DockPanel.Dock="Top"
VerticalAlignment="Top">
</DockPanel>
La proprietà HorizontalAlignment non compare nel codice XAML, poiché il valore “Stretch” è
la sua impostazione predefinita.
Nota In tutto questo capitolo le righe del riquadro XAML vengono mostrate suddivise e
rientrate in modo da poter rientrare nella pagina stampata.
6. Fai clic sul controllo Menu del form. Nella finestra Proprietà, cerca la proprietà Items. Il
valore della stessa è riportato come (Collection). Un controllo Menu contiene un insieme
480
Parte IV
Creazione di applicazioni WPF
di elementi MenuItem. Correntemente, il menu non presenta alcun elemento, pertanto
l’insieme è vuoto. Fai clic sul pulsante di ellissi (…) vicino al valore.
Viene visualizzata la finestra di dialogo Editor dell’insieme: Elementi, illustrata nell'immagine
seguente:
7. Fai clic su Aggiungi. Viene creato un nuovo elemento MenuItem e visualizzato nella finestra
di dialogo. Nel riquadro delle proprietà, imposta la proprietà Header su _File (incluso il
trattino di sottolineatura iniziale).
L’attributo Header dell’elemento MenuItem specifica il testo che deve apparire come voce
di menu. Il trattino di sottolineatura (_) che appare prima di ogni lettera fornisce l’accesso
rapido alle voci di menu mediante la pressione da parte dell’utente del tasto Alt seguito
dalla lettera che segue la sottolineatura (in questo caso, Alt+F per File). Anche questa è
una convenzione comune dell’ambiente Windows. In fase di esecuzione, la F all’inizio di
File appare sottolineata quando l’utente preme il tasto Alt. Le lettere per l’accesso rapido
possono essere utilizzate una sola volta per ogni menu, poiché una loro duplicazione può
confondere l’utente (e probabilmente anche l’applicazione).
8. Fai nuovamente clic su Aggiungi. Nel riquadro delle proprietà, imposta la proprietà Header
del secondo elemento MenuItem su _Help e poi fai clic su OK per chiudere la finestra di
dialogo.
9. Nel riquadro XAML, analizza la definizione del controllo Menu. Dovrebbe essere simile alla
seguente (i nuovi elementi sono visualizzati in grassetto):
<Menu Height="22" Name="menu1" Width="Auto" DockPanel.Dock="Top"
VerticalAlignment="Top" HorizontalAlignment="Stretch" >
<MenuItem Header="_File" />
<MenuItem Header="_Help" />
</Menu>
Capitolo 23
Raccolta dell’input utente
481
Osserva che gli elementi MenuItem vengono visualizzati come elementi figlio del controllo
Menu. Puoi creare elementi di menu digitando il codice direttamente nel riquadro XAML
anziché utilizzare la finestra di dialogo Editor dell’insieme, se lo preferisci.
10. Nel menu Debug, fai clic su Avvia senza eseguire debug per costruire ed eseguire
l’applicazione.
Quando il form viene visualizzato, il menu dovrebbe apparire subito sotto la barra del titolo
nella parte superiore della finestra. Premi il tasto Alt; il menu dovrebbe prendere il controllo
e la “F” di “File” e la “H” di “Help” dovrebbero apparire sottolineate come qui mostrato:
Facendo clic su una delle voci di menu non accade nulla, poiché non sono ancora stati
definiti i menu figlio di queste voci.
11. Chiudi il form e ritorna a Visual Studio 2010.
12. Nel riquadro XAML modifica la definizione dell’elemento di menu _File, rimuovi il carattere
“/” dalla fine del tag e aggiungi le voci del menu figlio e un elemento di chiusura </
MenuItem>, come illustrato di seguito in grassetto:
<MenuItem Header="_File" >
<MenuItem Header="_New Member" Name="newMember" />
<MenuItem Header="_Save Member Details" Name="saveMember" />
<Separator/>
<MenuItem Header="E_xit" Name="exit" />
</MenuItem>
Questo codice XAML aggiunge i comandi New Member, Save Member Details e Exit al menu
File. Quando viene visualizzato il menu, l’elemento <Separator/> appare come una linea
orizzontale ed è convenzionalmente impiegato per raggruppare le voci di menu correlate
tra loro. A parte il separatore, ogni voce di menu possiede un nome perché in seguito
dovrai fare riferimento a esso nell'applicazione.
Suggerimento Puoi aggiungere anche voci di menu figlio a un elemento MenuItem
utilizzando la finestra di dialogo Editor dell’insieme: Elementi. Come il controllo Menu,
ogni elemento MenuItem ha una proprietà chiamata Items, ovvero un insieme di elementi
MenuItem. Puoi fare clic sul pulsante di ellissi visualizzato nella proprietà Items del riquadro Properties per un elemento MenuItem al fine di aprire un’altra istanza della finestra
di dialogo Editor dell’insieme: Elementi. Tutte le voci aggiunte vengono visualizzate come
elementi figlio dell’elemento MenuItem.
13. Modifica la definizione della voce di menu _Help aggiungendo la voce di menu figlio qui
mostrata in grassetto:
<MenuItem Header="_Help" >
<MenuItem Header="_About Middleshire Bell Ringers" Name="about" />
</MenuItem>
482
Parte IV
Creazione di applicazioni WPF
14. Nel menu Debug, fai clic su Avvia senza eseguire debug per costruire ed eseguire
l’applicazione.
Quando appare il form, fai clic sul menu File. Sullo schermo dovrebbero apparire le voci di
menu figlio mostrate di seguito:
Puoi anche fare clic sul menu Help per visualizzare la voce di menu figlio About Middleshire
Bell Ringers.
Tuttavia, nessuna delle voci di menu figlio esegue un’azione quando viene selezionata.
Apprenderai come associare le voci di menu alle azioni nella sezione successiva.
15. Chiudi il form e ritorna a Visual Studio 2010.
Per proseguire, puoi aggiungere delle icone alle voci di menu. Molte applicazioni, utilizzano
icone nei menu per fornire un ulteriore aiuto visivo all’utente.
16. In Esplora soluzioni, fai clic con il tasto destro del mouse sul progetto BellRingers, quindi
seleziona Elemento esistente. Nella finestra di dialogo Aggiungi elemento esistente –
BellRingers, accedi alla cartella Microsoft Press\Visual CSharp Step By Step\Chapter 23
all’interno della tua cartella Documenti. Nella casella di riepilogo a discesa accanto alla
casella di testo del nome del file, seleziona Tutti i file (*.*). Seleziona Face.bmp, Note.bmp e
Ring.bmp e fai clic su Aggiungi.
Questa operazione aggiunge all’applicazione tre immagini come risorse.
17. Nel riquadro XAML, modifica le definizioni delle voci newMember, saveMember e about e
aggiungi gli elementi figlio MenuItem.Icon, che fanno riferimento a ciascuna delle tre icone
appena aggiunte al progetto, come qui mostrato in grassetto:. Ricorda che devi rimuovere
anche il carattere “/” dal tag di chiusura per ogni elemento MenuItem e che devi aggiungere
il tag </MenuItem>:
<Menu Height="22" Name="menu1" ... >
<MenuItem Header="_File" >
<MenuItem Header="_New Member" Name="newMember" >
<MenuItem.Icon>
<Image Source="Face.bmp"/>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="_Save Member Details" Name="saveMember" >
<MenuItem.Icon>
<Image Source="Note.bmp"/>
</MenuItem.Icon>
</MenuItem>
<Separator/>
<MenuItem Header="E_xit" Name="exit"/>
</MenuItem>
Capitolo 23
Raccolta dell’input utente
483
<MenuItem Header="_Help">
<MenuItem Header="_About Middleshire Bell Ringers" Name="about" >
<MenuItem.Icon>
<Image Source="Ring.bmp"/>
</MenuItem.Icon>
</MenuItem>
</MenuItem>
</Menu>
18. Il tocco finale consiste nel verificare che il testo delle voci di menu impieghi uno stile
coerente con il resto del form. Nel riquadro XAML, modifica la definizione dell’elemento di
livello superiore menu1 e imposta la proprietà Style sullo stile BellRingersFontStyle, come qui
mostrato in grassetto:
<Menu Style="{StaticResource bellRingersFontStyle}" ... Name="menu1" ... >
Nota che le voci di menu figlie ereditano automaticamente lo stile del menu di livello
superiore che le contiene.
19. Nel menu Debug, fai clic su Avvia senza eseguire debug per costruire ed eseguire
nuovamente l’applicazione.
Quando appare il form, fai clic sul menu File. A questo punto dovresti poter vedere che il
testo delle voci di menu viene visualizzato con il carattere corretto e affiancato dalle icone
relative alle varie voci di menu figlie, come mostrato di seguito:
20. Chiudi il form e ritorna a Visual Studio 2010.
Tipi di voci di menu
In questa esercitazione hai utilizzato l’elemento MenuItem per aggiungere voci di menu
figlie a un controllo Menu. Inoltre hai visto che è possibile specificare le voci del menu di
livello superiore come elementi MenuItem, quindi hai appreso come nidificare gli elementi
MenuItem per definire una struttura di menu personalizzata. Se desideri creare menu a
cascata, ogni elemento MenuItem nidificato può a sua volta contenere ulteriori elementi
MenuItem nidificati. In teoria, questo processo può essere ripetuto molte volte, ma in realtà
raramente si va oltre il secondo livello di nidificazione.
484
Parte IV
Creazione di applicazioni WPF
Ricorda tuttavia che non sei limitato a utilizzare gli elementi MenuItem. Se necessario, ai
menu WPF è possibile aggiungere anche caselle combinate, caselle di testo e la maggior
parte degli altri tipi di controlli. Ad esempio, la struttura di menu che segue contiene un
pulsante e una casella combinata:
<Menu ...>
<MenuItem Header="Miscellaneous">
<Button Content="Add new member" />
<ComboBox>
<ComboBox.Items>
<ComboBoxItem>
Great Shevington
</ComboBoxItem>
<ComboBoxItem>
Little Mudford
</ComboBoxItem>
<ComboBoxItem>
Upper Gumtree
</ComboBoxItem>
<ComboBoxItem>
Downley Hatch
</ComboBoxItem>
</ComboBox.Items>
</ComboBox>
</MenuItem>
</Menu>
In fase di esecuzione, questa struttura di menu assumerebbe il seguente aspetto:
Nonostante la grande libertà disponibile durante la progettazione dei menu, si consiglia
di sforzarsi di mantenere le cose semplici e senza eccessive elaborazioni, poiché un menu
come quello di questo esempio non è molto intuitivo!
Gestione degli eventi di menu
Il menu creato fino a questo punto ha un aspetto eccellente, ma non accade nulla quando l’utente
fa clic sulle sue voci. Per renderle funzionanti, è necessario immettere il codice che gestisce i vari
eventi del menu. Quando un utente seleziona una voce di menu, possono verificarsi numerosi
Capitolo 23
Raccolta dell’input utente
485
eventi differenti, alcuni dei quali sono più utili di altri. L’evento utilizzato più frequentemente è
l’evento Click, che si verifica quando l’utente fa clic sulla voce di menu. Solitamente, questo evento
viene rilevato al fine di eseguire i compiti associati alla voce di menu relativa.
Nell’esercitazione che segue, apprenderai ulteriori informazioni sugli eventi di menu e su come
elaborarli. In seguito potrai creare eventi Click per le voci di menu newMember ed exit.
Lo scopo del comando New Member è di fare in modo che l’utente possa immettere le
informazioni relative a un nuovo membro. Pertanto, tutti i campi del form dovrebbero essere
disabilitati fino al momento in cui l’utente fa clic su New Member, così come dovrebbe avvenire
per il comando Save Member Details. Quando l’utente fa clic sul comando New Member, si
desidera abilitare tutti i campi, reimpostare il contenuto del form in modo che egli possa iniziare
ad aggiungere le informazioni relative a un nuovo membro, e infine abilitare il comando Save
Member Details.
Gestione degli eventi delle voci di menu New Memberr ed Exit
1. Nel riquadro XAML, fai clic sulla definizione della casella di testo firstName. Nella finestra
Proprietà, deseleziona la proprietà IsEnabled. Questa operazione imposta IsEnabled su False
nella definizione XAML.
Ripeti questa operazione anche per i controlli lastName, towerNames, isCaptain,
memberSince, yearsExperience, methods e clear e per la voce di menu saveMember.
2. Nel riquadro XAML della finestra Progettazione, inizia a immettere il codice qui mostrato in
grassetto nella descrizione XAML della voce di menu _New Member:
<MenuItem Header="_New Member" Name="newMember" Click=>
3. Una volta digitato il carattere =, appare il menu di scelta rapida in cui puoi fare doppio clic
sul comando <Nuovo gestore eventi>.
Visual Studio genera un metodo evento chiamato newMember_Click e lo associa all’evento
Click della voce di menu.
Suggerimento Durante la definizione di metodi evento per le voci di menu, assegna
sempre nomi comprensibili. In caso contrario, Visual Studio genera un metodo evento
chiamato MenuItem_Click per l’evento Click. Se in seguito crei metodi evento Click per
altre voci di menu prive di nome, queste verranno chiamate MenuItem_Click_1, MenuItem_
Click_2 e così via. In presenza di più metodi evento di questo tipo, può risultare difficile
capire quale metodo evento appartiene a una specifica voce di menu.
4. Passa alla finestra dell’editor di codice e di testo che visualizza il file MainWindow.xaml.cs.
(Nel menu Visualizza, fai clic su Codice.)
Come puoi vedere, alla fine della definizione della classe MainWindow è stato aggiunto il
metodo evento newMember_Click:
486
Parte IV
Creazione di applicazioni WPF
private void newMember_Click(object sender, RoutedEventArgs e)
{
}
5. Aggiungi le istruzioni seguenti illustrate in grassetto al metodo newMember_Click:
private void newMember_Click(object sender, RoutedEventArgs e)
{
this.Reset();
saveMember.IsEnabled = true;
firstName.IsEnabled = true;
lastName.IsEnabled = true;
towerNames.IsEnabled = true;
isCaptain.IsEnabled = true;
memberSince.IsEnabled = true;
yearsExperience.IsEnabled = true;
methods.IsEnabled = true;
clear.IsEnabled = true;
}
Questo codice richiama il metodo Reset e abilita tutti i controlli. Ricorda quanto visto nel
capitolo 22, il metodo Reset reimposta i controlli presenti nel form in base ai rispettivi valori
predefiniti. (Se non ricordi come funziona il metodo Reset, scorri il contenuto della finestra
dell’editor di codice e di testo fino a visualizzare il metodo.)
A questo punto, è necessario creare un metodo evento Click per il comando Exit. Scopo di
questo metodo è di causare la chiusura del form.
6. Torna alla finestra Progettazione e visualizza il file MainWindow.xaml. Utilizza la tecnica
appresa nel punto 2 per creare un metodo evento Click per la voce di menu exit chiamata
exit_Click. (Nota che questo è il nome predefinito generato dal comando <Nuovo gestore
eventi>.)
7. Passa alla finestra dell’editor di codice e di testo. Nel corpo del metodo exitClick, immetti
l’istruzione qui mostrata in grassetto:
private void exit_Click(object sender, RoutedEventArgs e)
{
this.Close();
}
Il metodo Close di un form tenta di chiuderlo. Ricorda che se il form intercetta l’evento
Closing, ciò può impedirne la chiusura. L’applicazione della Middleshire Bell Ringers
Association fa esattamente questo, e chiede all’utente se desidera uscire. Se l’utente
risponde negativamente, il form non viene chiuso e l’applicazione continua a essere
eseguita.
Il passo successivo è la gestione della voce di menu saveMember. Quando l’utente fa clic su
questa voce di menu, i dati presenti nel form dovrebbero essere salvati in un file. Per il momento,
le informazioni saranno salvate in un normale file di testo chiamato Members.txt ubicato nella
Capitolo 23
Raccolta dell’input utente
487
cartella corrente. In seguito, potrai modificare il codice in modo che l’utente possa selezionare
un’ubicazione e un nome alternativi per il file.
Gestione dell’evento della voce di menu Save Memberr Details
1. Torna alla finestra Progettazione e visualizza il file MainWindow.xaml. In Nel riquadro XAML,
cerca la definizione della voce di menu saveMember e utilizza il comando <New Event
Handler> per generare un evento Click chiamato saveMember_Click. (Nota che questo è il
nome predefinito generato dal comando <Nuovo gestore eventi>.)
2. Nella finestra dell’editor di codice e di testo che mostra il contenuto di MainWindow.xaml.cs,
passa all’inizio del file e aggiungi l’istruzione using che segue all’elenco:
using System.IO;
3. Localizza il metodo evento saveMember_Click alla fine del file. Aggiungi le istruzioni qui
mostrate in grassetto al corpo del metodo:
private void saveMember_Click(object sender, RoutedEventArgs e)
{
using (StreamWriter writer = new StreamWriter("Members.txt"))
{
writer.WriteLine("First Name: {0}", firstName.Text);
writer.WriteLine("Last Name: {0}", lastName.Text);
writer.WriteLine("Tower: {0}", towerNames.Text);
writer.WriteLine("Captain: {0}", isCaptain.IsChecked.ToString());
writer.WriteLine("Member Since: {0}", memberSince.Text);
writer.WriteLine("Methods: ");
foreach (CheckBox cb in methods.Items)
{
if (cb.IsChecked.Value)
{
writer.WriteLine(cb.Content.ToString());
}
}
MessageBox.Show("Member details saved", "Saved");
}
}
Questo blocco di codice crea un oggetto StreamWriter usato dal metodo per memorizzare
il testo nel file Member.txt. L’uso della classe StreamWriter è molto simile alla visualizzazione
di testo in un’applicazione console mediante l’oggetto Console poiché è sufficiente utilizzare
il metodo WriteLine.
Una volta salvate tutte le informazioni, sullo schermo appare una finestra di messaggio che
fornisce alcuni commenti all’utente.
4. A questo punto, il pulsante Add e il metodo evento associato sono obsoleti, pertanto puoi
eliminare tale pulsante dalla finestra Progettazione. Nella finestra dell’editor di codice e di
testo, contrassegna come commento il metodo add_Click.
488
Parte IV
Creazione di applicazioni WPF
Fatto ciò rimane la voce di menu about, la quale dovrebbe visualizzare una finestra di dialogo
che fornisce informazioni sulla versione dell’applicazione, l’autore e altri dati utili. Nella prossima
esercitazione, aggiungerai un metodo evento in grado di gestire questo evento.
Gestione dell’evento della voce di menu About Middleshire Bell Ringers
1. Fai clic su Aggiungi finestra nel menu Progetto.
2. Nella finestra di dialogo Aggiungi nuovo elementi – BellRingers, nel riquadro al centro, fai
clic su Window (WPF). Nella casella di testo Nome, digita About.xaml e fai clic su Aggiungi.
Una volta aggiunti i controlli appropriati, dovrai visualizzare questa finestra quando l’utente
fa clic sul comando About Middleshire Bell Ringers nel menu Help.
Nota Visual Studio fornisce il modello delle finestre About Box. Tuttavia, questo modello
genera una finestra Windows Form invece di una finestra WPF.
3. Nella finestra Progettazione, fai clic sul form About.xaml. Nella finestra Proprietà, modifica
la proprietà Title su About Middleshire Bell Ringers, imposta la proprietà Width su 300
e imposta la proprietà su Height su 156. Imposta la proprietà ResizeMode su NoResize per
impedire il ridimensionamento della finestra da parte dell’utente. (Questa è la convenzione
per questo tipo di finestra di dialogo.)
4. Digita AboutBellRingers nella casella Name nella parte superiore della finestra Proprietà.
5. Dalla Casella degli strumenti, aggiungi due controlli etichetta e un controllo pulsante al
form. Nel riquadro XAML, modifica la proprietà di questi tre controlli come qui mostrato in
grassetto (se desideri, cambia il testo visualizzato nell’etichetta buildDate):
<Window x:Class="BellRingers.About"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="About Middleshire Bell Ringers" Height="156" Width="300"
Name="AboutBellRingers" ResizeMode="NoResize">
<Grid>
<Label Content="Version 1.0" Height="28" HorizontalAlignment="Left"
Margin="80,20,0,0" Name="version" VerticalAlignment="Top"
Width="75" />
<Label Content="Build date: September 2009" Height="28"
HorizontalAlignment="Left" Margin="80,50,0,0" Name="buildDate"
VerticalAlignment="Top" Width="160" />
<Button Content="OK" Height="23" HorizontalAlignment="Left"
Margin="100,85,0,0" Name="ok" VerticalAlignment="Top"
Width="78" />
</Grid>
</Window>
Capitolo 23
Raccolta dell’input utente
489
Il form completato dovrebbe apparire simile a quanto mostrato di seguito:
6. Nella finestra Progettazione, fai doppio clic sul pulsante OK.
Visual Studio genera un metodo evento per l’evento Click del pulsante e aggiunge il metodo
ok_Click al file About.xaml.cs.
7. Nella finestra dell’editor di codice e di testo che visualizza il contenuto del file About.xaml.cs,
aggiungi l’istruzione qui mostrata in grassetto al metodo ok_Click:
private void ok_Click(object sender, RoutedEventArgs e)
{
this.Close();
}
Quando l’utente fa clic sul pulsante OK, la finestra About Middleshire Bell Ringers viene
chiusa.
8. Torna alla finestra Progettazione e visualizza il file MainWindow.xaml. Nel riquadro XAML,
localizza la definizione della voce di menu about e utilizza il comando <Nuovo gestore
eventi> per specificare un metodo evento Click chiamato about_Click. (Questo è il nome
predefinito.)
9. Nella finestra dell’editor di codice e di testo relativa al file MainWindow.xaml.cs, aggiungi le
istruzioni qui mostrate in grassetto al metodo about_Click:
private void about_Click(object sender, RoutedEventArgs e)
{
About aboutWindow = new About();
aboutWindow.ShowDialog();
}
In realtà i form WPF IN sono solo classi che ereditano dalla classe System.Windows.Windows.
Per creare un’istanza di un form WPF utilizzi la stessa modalità delle altre classi. Questo
codice crea una nuova istanza della finestra About e richiama quindi il metodo ShowDialog
per visualizzarla. Il metodo ShowDialog viene ereditato dalla classe Windows e visualizza il
form WPF sulla schermo. Il metodo ShowDialog tiene il controllo fino a quando la finestra
About non viene chiusa dall’utente facendo clic sul pulsante OK.
Verifica del funzionamento degli eventi di menu
1. Nel menu Debug, fai clic su Avvia senza eseguire debug per costruire ed eseguire
l’applicazione.
490
Parte IV
Creazione di applicazioni WPF
Nota che tutti i campi presenti nel form sono disabilitati.
2. Fai clic sul menu File.
Il comando Save Member Details appare disabilitato.
3. Nel menu File, fai clic su New Member.
I campi del form diventano disponibili.
4. Immetti le informazioni relative a un nuovo membro.
5. Fai clic di nuovo sul menu File.
Il comando Save Member Details diventa disponibile.
6. Nel menu File, fai clic su Save Member Details.
Dopo una breve attesa, sullo schermo appare il messaggio “Member details saved”. Fai clic
su OK nella finestra di messaggio.
7. Utilizzando Esplora risorse, accedi alla cartella \Microsoft Press\Visual CSharp Step by Step\
Chapter 23\BellRingers\BellRingers\bin\Debug all’interno della cartella Documenti.
Qui dovresti trovare un file chiamato Members.txt.
8. Fai doppio clic su Members.txt per visualizzarne il contenuto in Blocco note.
Questo file dovrebbe contenere le informazioni relative al nuovo membro. Il testo seguente
illustra un esempio:
First Name: John
Last Name: Sharp
Tower: Little Mudford
Captain: False
Member Since: 15/01/2000
Methods:
Plain Bob
Reverse Canterbury
Grandsire
Stedman
Kent Treble Bob
Old Oxford Delight
Winchendon Place
9. Chiudi Blocco note e torna all’applicazione della Middleshire Bell Ringers.
10. Fai clic su About Middleshire Bell Ringers nel menu Help.
Viene visualizzata la finestra About. Nota che non è possibile ridimensionarla e che mentre è
visualizzata non puoi fare clic su alcuno degli elementi presenti nel form Members.
11. Fai clic su OK per tornare al form Members.
12. Nel menu File, fai clic su Esci.
L’applicazione tenta di chiudere il form e chiede se confermi di voler procedere. Se fai clic su
No, il form rimane aperto; in caso contrario, il form viene chiuso e l’applicazione termina.
13. Fai clic su Yes per chiudere il form.
Capitolo 23
Raccolta dell’input utente
491
Menu di scelta rapida
Molte applicazioni basate su Windows utilizzano menu di scelta rapida che appaiono quando
fai clic con il tasto destro del mouse su un form o un controllo. Solitamente questi menu sono
sensibili al contesto e visualizzano i comandi che possono essere applicati solo al controllo o form
attualmente attivo. Questi menu vengono solitamente indicati con il termine menu contestuali o
di scelta rapida. I menu di scelta rapida possono essere aggiunti facilmente alle applicazioni WPF
utilizzando la classe ContextMenu.
Creazione dei menu di scelta rapida
Nelle esercitazioni che seguono apprenderai come creare due menu di scelta rapida. Il primo di
questi menu è associato ai controlli casella di testo firstName e lastName e permette all’utente di
deselezionarli. Il secondo menu di scelta rapida è associato al form e contiene i comandi necessari
per salvare le informazioni relative al membro attualmente visualizzato e reimpostare il form.
Nota I controlli TextBox associate a un menu di scelta rapida predefinito che fornisce I comandi
Taglia, Copia e Incolla per eseguire modifiche di testo. Il menu di scelta rapida che andrai a definire nell’esercitazione che segue sovrascriverà il menu predefinito.
Creazione dei menu di scelta rapida firstName e lastName
1. Nella finestra Progettazione relativa a MainWindow.xaml, aggiungi l’elemento ContextMenu
qui mostrato in grassetto alla fine delle risorse finestra del riquadro XAML, dopo le
definizioni di stile:
<Window.Resources>
...
<ContextMenu x:Key="textBoxMenu" Style="{StaticResource bellRingersFontStyle}" >
</ContextMenu>
</Window.Resources>
Questo menu di scelta rapida sarà condiviso dalle caselle di testo firstName e lastName.
Aggiungendolo alle risorse finestra, il menu di scelta rapida diventa disponibile per qualsiasi
controllo presente nella finestra.
2. Aggiungi l’elemento MenuItem qui mostrato in grassetto al menu di scelta rapida
textBoxMenu:
<Window.Resources>
...
<ContextMenu x:Key="textBoxMenu" Style="{StaticResource bellRingersFontStyle}">
<MenuItem Header="Clear Name" Name="clearName" />
</ContextMenu>
</Window.Resources>
Questo codice aggiunge al menu di scelta rapida una voce di menu chiamata clearName
con la legenda “Clear Name”.
492
Parte IV
Creazione di applicazioni WPF
3. Nel riquadro XAML, modifica le definizioni dei controlli casella di testo firstName e
lastName e aggiungi la proprietà ContextMenu, illustrata di seguito in grassetto:
<TextBox ... Name="firstName" ContextMenu="{StaticResource textBoxMenu}" ... />
...
<TextBox ... Name="lastName" ContextMenu="{StaticResource textBoxMenu}" ... />
La proprietà ContextMenu determina quale menu (se presente) deve essere visualizzato
quando l’utente fa clic con il pulsante destro del mouse sul controllo.
4. Torna alla definizione dello stile textBoxMenu e aggiungi un metodo di evento Click
chiamato clearName_Click alla voce di menu clearName. Nota che questo è il nome
predefinito generato dal comando <Nuovo gestore eventi>.
<MenuItem Header="Clear Name" Name="clearName" Click="clearName_Click" />
5. Nella finestra dell’editor di codice e di testo relativa a MainWindow.xaml.cs, aggiungi le
seguenti istruzioni al metodo evento clearName_Click generato dal comando <Nuovo
gestore eventi>:
firstName.Clear();
lastName.Clear();
Questo codice reimposta entrambe le caselle di testo ogni volta che l’utente fa clic sul
comando Clear Name nel menu di scelta rapida.
6. Nel menu Debug, fai clic su Avvia senza eseguire debug per costruire ed eseguire
l’applicazione. Una volta visualizzato il form, fai clic su File quindi su New Member.
7. Immetti un nome e un cognome nelle caselle di testo First Name e Last Name. Fai clic con il
tasto destro del mouse sulla casella di testo First Name. Nel menu di scelta rapida, fai clic sul
comando Clear Name e verifica che entrambe le caselle di testo vengano reimpostate.
8. Immetti un nome e un cognome nelle caselle di testo First Name e Last Name. Questa volta,
fai clic con il pulsante destro del mouse sulla casella di testo Last Name. Nel menu di scelta
rapida, fai clic sul comando Clear Name e verifica nuovamente che entrambe le caselle di
testo vengano reimpostate.
9. Fai clic con il pulsante destro su uno dei controlli ad eccezione di Member Since. Fai clic con
il pulsante destro del mouse in un punto qualsiasi del form fuori dalle caselle di testo First
Name e Last Name.
Ad eccezione del controllo Member Since, solo le caselle di testo First Name e Last Name
dispongono di un menu di scelta rapida, pertanto non dovrebbe apparire nessun altro
menu.
Nota Il controllo Member Since visualizza un menu di scelta rapdia con i comandi Tglia,
Copia e Incolla. Questa funzionalità è contenuta nel controllo DatePicker per impostazione
predefinita.
10. Chiudi il form e ritorna a Visual Studio 2010.
Capitolo 23
Raccolta dell’input utente
493
Ora puoi procedere ad aggiungere il secondo menu di scelta rapida, il quale contiene i comandi
che l’utente può utilizzare per salvare le informazioni relative ai membri e reimpostare i campi
presenti nel form. Per cambiare un po’ le cose e dimostrare la facilità con cui è possibile creare un
menu di scelta rapida in modo dinamico, nell’esercitazione che segue creerai il menu mediante il
codice. Il luogo migliore per questo scopo è il costruttore del form. Pertanto aggiungerai il codice
necessario per abilitare il menu di scelta rapida per la finestra quando l’utente crea un nuovo
membro.
Creazione del menu di scelta rapida di una finestra
1. Passa alla finestra dell’editor di codice e di testo che visualizza il file MainWindow.xaml.cs.
2. Aggiungi la variabile privata qui mostrata in grassetto alla classe MainWindow:
public partial class MainWindow : Window
{
...
private ContextMenu windowContextMenu = null;
...
}
3. Cerca il costruttore predefinito della classe MainWindow. Questo è il primo metodo della
classe ed è chiamato MainWindow. Aggiungi le istruzioni qui mostrate in grassetto dopo
il codice che richiama il metodo Reset per creare le voci di menu necessarie per salvare le
informazioni relative al membro:
public MainWindow()
{
InitializeComponent();
this.Reset();
MenuItem saveMemberMenuItem = new MenuItem();
saveMemberMenuItem.Header = "Save Member Details";
saveMemberMenuItem.Click += new RoutedEventHandler(saveMember_Click);
}
Questo codice imposta la proprietà Header per la voce di menu, quindi specifica che
l’evento Click deve richiamare il metodo evento saveMember_Click; nota che si tratta
dello stesso metodo utilizzato in una precedente esercitazione di questo capitolo. Il tipo
RoutedEventHandler è un delegato che rappresenta i metodi che permettono di gestire
gli eventi generati da molti controlli WPF. Per ulteriori informazioni su delegati ed eventi,
consulta il capitolo 17, “Interruzione del flusso del programma e gestione degli eventi”.
4. Nel costruttore MainWindow, aggiungi le istruzioni qui mostrate in grassetto per creare
le voci di menu che reimpostano i campi del form e azzerarli in base ai rispettivi valori
predefiniti:
public MainWindow()
{
...
MenuItem clearFormMenuItem = new MenuItem();
clearFormMenuItem.Header = "Clear Form";
clearFormMenuItem.Click += new RoutedEventHandler(clear_Click);
}
494
Parte IV
Creazione di applicazioni WPF
Questa voce di menu richiama il metodo evento clear_Click ogni volta che l’utente vi fa clic.
5. Nel costruttore MainWindow, aggiungi le istruzioni qui mostrate in grassetto per creare il
menu di scelta rapida e popolarlo con le due voci di menu appena create:
public MainWindow()
{
...
windowContextMenu = new ContextMenu();
windowContextMenu.Items.Add(saveMemberMenuItem);
windowContextMenu.Items.Add(clearFormMenuItem);
}
Il tipo ContextMenu contiene un insieme chiamato Items che raccoglie le voci di menu.
6. Alla fine del metodo evento newMember_Click, aggiungi l’istruzione qui mostrata in
grassetto per associare il menu contestuale al form:
private void newMember_Click(object sender, RoutedEventArgs e)
{
...
this.ContextMenu = windowContextMenu;
}
Nota che l’applicazione associa il menu di scelta rapida al form solo se è disponibile la
funzionalità relativa al nuovo membro. Dovendo impostare la proprietà ContextMenu
del form nel costruttore, le voci Save Member Details e Clear Details del menu di scelta
rapida sarebbero disponibili anche quando i controlli presenti nel form sono disabilitati, un
comportamento da evitare in questa applicazione.
Suggerimento Se necessario, puoi annullare l’associazione tra menu di scelta rapida e
form impostando la proprietà ContextMenu del form su null.
7. Nel menu Debug, fai clic su Avvia senza eseguire debug per costruire ed eseguire
l’applicazione.
8. Una volta visualizzato il form, fai clic con il pulsante destro del mouse sul form e verifica che
il menu di scelta rapida non compaia.
9. Nel menu File, fai clic su New Member e immetti le informazioni relative a un nuovo
membro.
10. Fai clic con il pulsante destro del mouse sul form. Nel menu di scelta rapida, fai clic su
Clear Form e verifica che i campi del form vengano reimpostati in base ai rispettivi valori
predefiniti.
11. Immetti nuovamente le informazioni relative a un membro. Fai clic con il pulsante destro del
mouse sul form. Nel menu di scelta rapida, fai clic su Save Member Details. Verifica che sullo
schermo appaia la finestra di messaggio “Member details saved”, quindi fai clic su OK.
12. Chiudi il form e ritorna a Visual Studio 2010.
Capitolo 23
Raccolta dell’input utente
495
Finestre di dialogo comuni di Windows
L’applicazione BellRingers ora ti permette di salvare le informazioni sui membri, ma ciò avviene
sempre nello stesso file, sovrascrivendo ogni volta quanto salvato in precedenza. Di seguito viene
illustrato come risolvere questo problema.
Un grande numero di attività quotidiane richiede che l’utente possa specificare alcune
informazioni, indipendentemente dalle funzionalità dell’applicazione eseguita. Ad esempio,
se l’utente desidera aprire o salvare un file, normalmente gli viene chiesto di indicare il file
da aprire o l’ubicazione in cui desidera salvarlo. Come puoi aver notato, le stesse finestre di
dialogo vengono impiegate da molte applicazioni differenti. Ciò non è dovuto alla mancanza
di immaginazione da parte degli sviluppatori di applicazioni, ma al fatto che tali funzionalità
sono così comuni che Microsoft le ha standardizzate e rese disponibili come “finestre di dialogo
comuni”, un componente fornito dal sistema operativo Microsoft Windows che puoi utilizzare
anche nelle tue applicazioni. La libreria di classi di Microsoft .NET Framework fornisce le classi
OpenFileDialog e SaveFileDialog, le quali operano come gestore delle finestre di dialogo comuni.
Utilizzo della classe SaveFileDialog
Nell’esercitazione che segue, utilizzerai la classe SaveFileDialog. Quando l’utente salva le
informazioni in un file nell’applicazione BellRingers, puoi richiedere il nome e l’ubicazione di tale
file visualizzando la finestra di dialogo comune Salva file.
Utilizzo della classe SaveFileDialog
1. Nella finestra dell’editor di codice e di testo relativa a MainWindow.xaml.cs, all’inizio del file
aggiungi l’istruzione using seguente all’elenco:
using Microsoft.Win32;
La classe SaveFileDialog si trova nello spazio dei nomi Microsoft.Win32 (anche nelle versioni
a 64 bit del sistema operativo Windows).
2. Localizza il metodo saveMember_Click e aggiungi il codice qui mostrato in grassetto
all’inizio del metodo, sostituendo YourName con il nome del tuo account:
private void saveMember_Click(object sender, RoutedEventArgs e)
{
SaveFileDialog saveDialog = new SaveFileDialog();
saveDialog.DefaultExt = "txt";
saveDialog.AddExtension = true;
saveDialog.FileName = "Members";
saveDialog.InitialDirectory = @"C:\Users\YourName\Documents\";
saveDialog.OverwritePrompt = true;
saveDialog.Title = "Bell Ringers";
saveDialog.ValidateNames = true;
...
}
496
Parte IV
Creazione di applicazioni WPF
Questo codice crea una nuova istanza della classe SaveFileDialog e ne imposta le proprietà.
La tabella che segue descrive lo scopo di queste proprietà.
Proprietà
Descrizione
DefaultExt
Estensione predefinita da utilizzare se l’utente non la specifica quando
fornisce il nome del file.
AddExtension
Attiva la finestra di dialogo per l’aggiunta dell’estensione indicata dalla
proprietà DefaultExt (se omessa) al file specificato dall’utente.
FileName
Nome di file attualmente selezionato. Può essere popolata per
specificare un nome di file predefinito, oppure reimpostata se non si
desidera disporne.
InitialDirectory
Directory predefinita da utilizzare nella finestra di dialogo.
OverwritePrompt
Fa in modo che la finestra di dialogo avvisi l’utente qualora si tenti
di sovrascrivere un file esistente. Perché funzioni, la proprietà
ValidateNames deve essere impostata su true.
Title
Stringa visualizzata nella barra del titolo della finestra di dialogo.
ValidateNames
Indica se i nomi di file sono convalidati. Utilizza da alcune altre
proprietà, ad esempio OverwritePrompt. Se la proprietà ValidateNames
è impostata su true, la finestra di dialogo verifica anche che i nomi di file
digitati dall’utente contengano solo caratteri validi.
3. Aggiungi al metodo saveMember_Click l’istruzione if seguente (e una parentesi graffa
di chiusura) illustrata di seguito in grassetto. Essa include il codice precedente che crea
l’oggetto StreamWriter e memorizza i dati del membro in un file:
if (saveDialog.ShowDialog().Value)
{
using (StreamWriter writer = new StreamWriter("Members.txt"))
{
// existing code
...
}
}
Il metodo ShowDialog visualizza la finestra di dialogo Salva file. Questa finestra di dialogo
è di tipo modale, pertanto l’utente non può utilizzare altri form dell’applicazione fino a
quando non la chiude facendo clic su uno dei suoi pulsanti. La finestra di dialogo Salva
file dispone dei pulsanti Salva e Annulla. Se l’utente fa clic su Salva, il valore restituito dal
metodo ShowDialog è true; altrimenti, è false.
Il metodo ShowDialog chiede all’utente il nome del file da salvare, ma non esegue
fisicamente l’operazione; pertanto devi fornire personalmente il codice che compie questa
compito. In realtà il compito di questo metodo è di fornire il nome di file selezionato
dall’utente nella proprietà FileName.
4. Nel metodo saveMember_Click, modifica l’istruzione che crea l’oggetto StreamWriter qui
mostrata in grassetto:
Capitolo 23
Raccolta dell’input utente
497
using (StreamWriter writer = new StreamWriter(saveDialog.FileName))
{
...
}
Il metodo saveMember_Click può così salvare le informazioni nel file specificato dall’utente
invece che in Members.txt.
5. Nel menu Debug, fai clic su Avvia senza eseguire debug per costruire ed eseguire
l’applicazione.
6. Nel menu File, fai clic su New Member, quindi aggiungi le informazioni relative a un nuovo
membro.
7. Nel menu File, fai clic su Save Member Details.
Viene visualizzata la finestra di dialogo Salva file con la didascalia “Bell Ringers”. La cartella
predefinita dovrebbe essere la tua cartella Documenti, mentre il nome di file predefinito
proposto dovrebbe essere Members, come mostrato nell’immagine che segue:
Se ometti l’estensione del file, l’applicazione aggiunge automaticamente .txt al momento
del salvataggio. Se scegli un file esistente, la finestra di dialogo te lo segnala prima di venire
chiusa.
8. Modifica il valore nella casella di testo Nome file in TestMember, quindi fai clic su Salva.
9. Nell’applicazione Bell Ringers, verifica che venga visualizzato il messaggio “Member details
saved”, fai clic su OK e chiudi l’applicazione.
10. In Esplora risorse, spostati nella tua cartella Documenti.
Verifica che sia stato creato il file TestMember.txt.
498
Parte IV
Creazione di applicazioni WPF
11. Fai doppio clic sul file e verifica che contenga le informazioni relative al membro appena
aggiunto. Al termine, chiudi Blocco note.
Questa tecnica può essere utilizzata anche per aprire un file:crea un oggetto OpenFileDialog,
attivalo utilizzando il metodo ShowDialog e recupera la proprietà FileName quanto il metodo
restituisce il controllo dopo che l’utente ha fatto clic sul pulsante Apri. A questo punto puoi aprire
il file, leggerne il contenuto e popolare i campi visibili sullo schermo. Per ulteriori informazioni
sull’uso della classe OpenFileDialog, consulta la sezione della libreria MSDN relativa a Visual Studio
2010.
Incremento della velocità di risposta di un’applicazione WPF
La funzione delle librerie WPF è quello di fornire gli elementi fondamentali per creare applicazioni
per interfacce utente grafiche. Pertanto, per natura le applicazioni WPF sono interattive. Quando
un utente esegue un’applicazione WPF, deve conoscere approfonditamente i controlli che
costituiscono l’interfaccia utente. Hai appreso che un’applicazione risponde all’utente quando egli
effettua operazioni come la selezione di un pulsante, la digitazione di un testo in caselle, la scelta
di voci di menu attraverso un codice eseguito all’attivazione degli eventi corrispondenti. Pertanto,
cosa succede se l’esecuzione del codice che risponde a un determinato evento è molto lenta? Ad
esempio, supponiamo che l’applicazione BellRingers abbia salvato i propri dati in un database
remoto, situato da qualche parte in Internet. Servono alcuni secondi per trasmettere realmente
questi dati e archiviarli. Quale affetto ha questo ritardo sull’utilizzabilità dell’applicazione?
Nell’esercitazione seguente, simulerai questo scenario e ne osserverai i risultati.
Simulazione di un gestore eventi lento in un’applicazione WPF
1. In Visual Studio, passa alla finestra dell’editor di codice e di testo e visualizza il file
MainWindow.xaml.cs.
2. All’inizio dell'elenco, aggiungi le seguenti istruzioni using:
using System.Threading;
3. Aggiungi all’istruzione using l’istruzione seguente illustrata in grassetto che memorizza i
dati del membro in un file specificato dall'utente.
private void saveMember_Click(object sender, RoutedEventArgs e)
{
...
if (saveDialog.ShowDialog().Value)
{
using (StreamWriter writer = new StreamWriter(saveDialog.FileName))
{
...
Thread.Sleep(10000);
MessageBox,Show("Member details saved", "Saved");
}
}
}
Capitolo 23
Raccolta dell’input utente
499
Il metodo statico Sleep della classe Thread nello spazio dei nomi System.Threading
comporta l’interruzione della risposta da parte del thread corrente dell’applicazione per
un determinato periodo di tempo. Questo tempo è specificato in millisecondi, ovvero
l’interruzione è di 10 secondi.
Nota Un thread è un percorso di esecuzione in un’applicazione. Tutte le applicazioni
hanno almeno un thread ed è possibile creare applicazioni con più thread. Se un computer
ha più CPU o un processore multicore, può eseguire più thread simultaneamente. Se si
creano più thread delle CPU o dei core processore disponibili, il sistema operativo alloca
una quantità di tempo CPU ad ognuno per dare l’apparenza di esecuzione simultanea.
Apprenderai maggiori informazioni sui thread e sull’esecuzione di operazioni in parallelo
nel capitolo 27, relativo alla libreria parallela di attività.
4. Nel menu Debug, fai clic su Avvia senza eseguire debug.
5. Quando viene visualizzato il form WPF, nel menu File fai clic su New Member e inserisci
alcune informazioni del nuovo membro.
6. Nel menu File, fai clic su Save Member Details. Nella finestra di dialogo Bell Ringers, accetta
il nome predefinito del file e fai clic su Salva (e sovrascrivi il file, se richiesto).
7. Quando viene chiusa la finestra di dialogo Bell Ringers, tenta di selezionare uno dei
controllo del form WPF. Osserva che il form non risponde. (Potrebbe apparire vuoto e la
barra del titolo potrebbe visualizzare un messaggio di non risposta.)
8. Quando viene visualizzata la finestra di dialogo del form salvato, fai clic su OK.
9. A questo punto, seleziona uno dei controlli del form WPF. Il from risponde correttamente.
10. Chiudi il form e ritorna a Visual Studio 2010.
Per impostazione predefinita, le applicazioni WPF hanno un solo thread. Di conseguenza, un
gestore eventi lento può interrompere l’applicazione. Questo naturalmente non è accettabile in
un programma professionale. Tuttavia, .NET Framework consente di creare più thread, nei quali
eseguire attività che richiedono molto tempo. Ricorda, però, che esistono alcune restrizioni per
questi thread in un’applicazione WPF, che conoscerai nell’esercizio seguente.
Esecuzione di un’operazione lenta in un nuovo thread
1. In Visual Studio, visualizza il codice del file MainWindow.xaml.cs nella finestra dell’editor di
codice e di testo.
2. Aggiungi un nuovo metodo private alla classe MainWindow chiamato saveData. Esso
dovrebbe accettare un parametro stringa che specifica il nome di un file e non dovrebbe
restituire valori, come segue:
private void saveData(string fileName)
{
}
500
Parte IV
Creazione di applicazioni WPF
3. Cerca il metodo saveMember_Click. Copia l’istruzione using e il codice incluso da questo
metodo a saveData. Il metodo saveData dovrebbe essere simile al seguente:
private void saveData(string fileName)
{
using (StreamWriter writer = new StreamWriter(saveDialog.FileName))
{
writer.WriteLine("First Name: {0}", firstName.Text);
writer.WriteLine("Last Name: {0}", lastName.Text);
writer.WriteLine("Tower: {0}", towerNames.Text);
writer.WriteLine("Captain: {0}", isCaptain.IsChecked.ToString());
writer.WriteLine("Member Since: {0}", memberSince.Text);
writer.WriteLine("Methods: ");
foreach (CheckBox cb in methods.Items)
{
if (cb.IsChecked.Value)
{
writer.WriteLine(cb.Content.ToString());
}
}
Thread.Sleep(10000);
MessageBox.Show("Member details saved", "Saved");
}
}
4. Nell’istruzione using, modifica il codice che richiama il costruttore dell’oggetto StreamWriter
e sostituisci il riferimento a saveDialog.FileName con il parametro fileName, come illustrato
di seguito in grassetto:
using (StreamWriter writer = new StreamWriter(fileName))
{
...
}
5. Nel metodo saveMember_Click, rimuovi l’istruzione using e il blocco di codice incluso e
sostituiscila con le istruzioni illustrate di seguito in grassetto:
private void saveMember_Click(object sender, RoutedEventArgs e)
{
...
if (saveDialog.ShowDialog().Value)
{
Thread workerThread = new Thread(
() => this.saveData(saveDialog.FileName));
workerThread.Start();
}
}
Capitolo 23
Raccolta dell’input utente
501
Questo codice crea un nuovo oggetto Thread chiamato workerThread. Il costruttore della
classe Thread prevede l’esecuzione di un delegato che faccia riferimento a un metodo
quando viene avviato il thread. Questo esempio utilizza un’espressione lambda per creare
un delegato anonimo che richiama il metodo saveData.
Il metodo Start della classe Thread avvia l’esecuzione del thread. Esso viene eseguito in
modo asincrono, ovvero il metodo Start non attende che si concluda il metodo eseguito dal
thread.
6. Sul menu Debug, fai clic su Avvia debug.
Importante Non eseguire l’applicazione senza debug.
7. Quando viene visualizzato il form WPF, nel menu File, seleziona New Member, fornisci alcuni
dati del nuovo membro e fai clic su Save Member Details. Nella finestra di dialogo Bell
Ringers, seleziona il file Members.txt e fai clic su Salva (e sovrascrivi il file, se richiesto).
L’applicazione si interrompe al metodo saveData e indica l’eccezione illustrata nell’immagine
seguente.
502
Parte IV
Creazione di applicazioni WPF
Il testo dell’eccezione indica che il thread chiamante non è in grado di accedere all’oggetto
perché di proprietà di un altro thread e la riga evidenziata è il codice che legge i dati dalla
casella di testo firstName.
8. Nel menu Debug, fai clic su Termina debug e torna a Visual Studio 2010.
Hai tentato di utilizzare un thread per eseguire un’operazione lunga in background. È un
approccio valido. Il problema è che il modello di protezione implementato da WPF impedisce a
tutti i thread diversi da quello che ha creato un oggetto di interfaccia utente, come un controllo,
di accedere a esso. Questa restrizione evita che due o più thread tentino di acquisire il controllo
dell’input utente e di modificare i dati sullo schermo, perché potrebbero causare una corruzione
dei dati.
È possibile evitare questa restrizione in molti modi, anche se la soluzione più semplice è quella di
raccogliere i dati da salvare in una struttura del metodo eseguito dal thread dell’interfaccia utente
e poi passare la struttura al metodo eseguito dal thread di background. Questo è l’argomento
dell’esercizio che segue.
Copia di dati dal thread dell’interfaccia utente al thread di background
1. In Visual Studio, nel menu Progetto, fai clic su Aggiungi nuovo elemento.
2. Nella finestra di dialogo Aggiungi nuovo elemento – Bell Ringers, nel riquadro a sinistra
espandi Visual C# e fai clic sull’opzione del codice. Nel riquadro centrale, fai clic sul file di
codice. Nella casella di testo Nome, digita Member.cs, e poi fai clic su Aggiungi.
Visual Studio aggiunge un file di codice vuoto chiamato Member.cs al progetto e lo
visualizza nella finestra dell’editor di codice e di testo.
3. Aggiungi le istruzioni using seguenti all'inizio del file:
using System;
using System.Collections.Generic;
4. Nel file Member.cs, definisci la struttura Member come illustrato di seguito. Essa contiene
le proprietà pubbliche che corrispondono a ognuno dei campi del form. L’elenco di metodi
che il membro può avviare è memorizzata come insieme List<string> (non come proprietà).
struct Member
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string TowerName { get; set; }
public bool IsCaptain { get; set; }
public DateTime MemberSince { get; set; }
public List<string> Methods;
}
5. Torna al file MainWindow.xaml.cs nella finestra dell’editor di codice e di testo.
Capitolo 23
Raccolta dell’input utente
503
6. Modifica il metodo saveData affinché accetti una struttura Member come secondo
parametro. Nel corpo del metodo saveData, modifica il codice per leggere i dati del
membro da questa struttura anziché dai campi del form WPF. L’esempio di codice seguente
illustra il metodo completato. Osserva che il ciclo for esegue un’iterazione nell’insieme
List<string> anziché nelle caselle di controllo del form:
private void saveData(string fileName, Member member)
{
using (StreamWriter writer = new StreamWriter(fileName))
{
writer.WriteLine("First Name: {0}", member.FirstName);
writer.WriteLine("Last Name: {0}", member.LastName);
writer.WriteLine("Tower: {0}", member.TowerName);
writer.WriteLine("Captain: {0}", member.IsCaptain.ToString());
writer.WriteLine("Member Since: {0}", member.MemberSince.ToString());
writer.WriteLine("Methods: ");
foreach (string method in member.Methods)
{
writer.WriteLine(method);
}
Thread.Sleep(10000);
MessageBox.Show("Member details saved", "Saved");
}
}
7. Nel metodo saveMember_Click, nel blocco dell’istruzione if che avvia il thread di
background, crea una variabile Member e compilala con i dati del form. Passa la variabile
Member come secondo parametro al metodo saveData eseguito dal thread di background.
Il codice seguente in grassetto illustra le modifiche da apportare:
private void saveMember_Click(object sender, RoutedEventArgs e)
{
...
if (saveDialog.ShowDialog().Value)
{
Member member = new Member();
member.FirstName = firstName.Text;
member.LastName = lastName.Text;
member.TowerName = towerNames.Text;
member.IsCaptain = isCaptain.IsChecked.Value;
member.MemberSince = memberSince.SelectedDate.Value;
member.Methods = new List<string>();
foreach (CheckBox cb in methods.Items)
{
if (cb.IsChecked.Value)
{
member.Methods.Add(cb.Content.ToString());
}
}
Thread workerThread = new Thread(
() => this.saveData(saveDialog.FileName, member));
workerThread.Start();
}
}
504
Parte IV
Creazione di applicazioni WPF
Nota Il controllo isCaptain nel form è un controllo CheckBox e la proprietà IsChecked è
null pertanto, per determinare se il controllo è verificato, analizza la proprietà Value. Allo
stesso modo, la proprietà SelectedDate del controllo DatePicker è anch’essa null, pertanto
il codice utilizza la proprietà Value per recuperare i dati relativi all’appartenenza.
8. Nel menu Debug, fai clic su Avvia senza eseguire debug. Nel menu File, fai clic su New
Member e aggiungi alcuni dati. Poi, nel menu File, fai clic su Save MemberDetails. Nella
finestra di dialogo Bell Ringers, specifica il nome del file predefinito e fai clic su Salva.
Osserva che adesso il form WPF funziona correttamente, ovvero risponde quando fai clic in
uno dei campi o tenti di inserire dei dati.
9. Dopo 10 secondi, viene visualizzata la finestra di dialogo che conferma il salvataggio dei
dati. Fai clic su OK, chiudi il form e ritorna a Visual Studio 2010.
I thread e la classe BackgroundWorker
L’esempio illustrato nell’esercizio precedente ha creato un oggetto Thread per eseguire
un metodo in un thread nuovo. Un approccio alternativo è quello di creare un oggetto
BackgroundWorker. La classe BackgroundWorker si trova nello spazio dei nomi System.
ComponentModel e fornisce un involucro per i thread. Il codice seguente illustra come
eseguire il metodo saveData utilizzando un oggetto BackgroundWorker.
BackgroundWorker workerThread = new BackgroundWorker();
workerThread.DoWork += (x, y) => this.saveData(saveDialog.FileName, member);
workerThread.RunWorkerAsync();
Si specifica il metodo eseguito da un oggetto BackgroundWorker eseguendo la
sottoscrizione dell’evento DoWork. Esso richiede all’utente di fornire un delegato
DoWorkEventHandler (specificato come espressione lambda in questo esempio). Il delegato
DoWorkEventHandler fa riferimento a un metodo che accetta due parametri, un parametro
object che indica l’elemento che ha richiamato l’oggetto BackgroundWorker e un parametro
DoWorkEventArgs che contiene informazioni specifiche passate a BackgroundWorker,
affinché esso possa svolgere la sua funzione. Il codice di esempio illustrato non utilizza
questi due parametri.
L’esecuzione di un thread ha inizio richiamando il metodo RunWorkerAsync dell’oggetto
BackgroundWorker.
Per eseguire operazioni semplici in background, la classe Thread è ideale. Tuttavia, in scenari
più complessi la classe BackgroundWorker fornisce alcuni vantaggi rispetto alla classe
Thread. In modo specifico, fornisce l’evento ProgressChanged, utilizzabile da un thread per
riportare lo stato di avanzamento di un’operazione lunga e l’evento RunWorkerCompleted
che il thread usa per indicare il completamento della propria attività.
Per ulteriori informazioni sulla classe BackgroundWorker, consulta la documentazione fornita
con Visual Studio.
Capitolo 23
Raccolta dell’input utente
505
A questo punto, hai migliorato la velocità di risposta dell’applicazione utilizzando un thread
separato per eseguire un'operazione lunga e passando i dati richiesti da un thread come
parametro. Tuttavia, c’è un ultimo problema da risolvere. Quando l’utente salva i dati per un
membro, viene visualizzata la finestra di dialogo che conferma il salvataggio corretto dei dati.
In origine era utile, quando le operazioni di salvataggio erano rapide e l'utente non poteva
eseguire altre attività contemporaneamente. Adesso, invece, a volte questa finestra viene
visualizzata mentre l'utente sta inserendo dei dati per un membro e può causare distrazione.
L’utente deve leggere il messaggio prima di poter continuare l’inserimento dei dati e questa
azione può interrompere la concentrazione, provocando errori di inserimento. Una soluzione a
questo problema è quella di visualizzare il messaggio in una barra di stato alla fine del form. Essa
costituisce un mezzo non fastidioso per informare l’utente che il salvataggio è stato completato.
Tuttavia, esiste un problema con questo approccio, perché la barra di stato è un controllo
creato dal thread dell'interfaccia utente e di proprietà dello stesso. Come può accedere il thread
di background a questo controllo e visualizzare un messaggio? La risposta è l’oggetto WPF
Dispatcher.
Puoi utilizzarlo per indicare la thread dell’interfaccia utente di eseguire un metodo per conto di un
altro thread. L’oggetto Dispatcher accoda queste richieste e le esegue nel thread dell’interfaccia
utente in un momento appropriato; ad esempio, puoi assegnare una priorità a una richiesta
indicando all’oggetto Dispatcher di eseguire la richiesta solo quando il thread dell’interfaccia non
è attivo. Puoi accedere all’oggetto Dispatcher attraverso la proprietà Dispatcher di un controllo
WPF, incluso il form stesso.
Devi inviare una richiesta all’oggetto Dispatcher richiamando il metodo Invoke. Questo è un
metodo di overload, ma tutti gli overload prevedono un oggetto Delegate che racchiuda un
riferimento a un metodo eseguibile dall’oggetto Dispatcher.
Nell’esercizio finale di questo capitolo, modificherai l’applicazione BellRingers per visualizzare lo
stato dell’operazione di salvataggio in una barra di stato alla fine del form, utilizzando l’oggetto
Dispatcher.
Utilizzo dell’oggetto Dispatcherr per visualizzare un messaggio di stato
1. In Visual Studio, visualizza il file MainWindow.xaml nella finestra Progettazione.
2. Nella Casella degli strumenti, seleziona il controllo StatusBar nella sezione Controlli e
aggiungilo al form WPF.
3. Utilizzando la finestra Proprietà, imposta le proprietà del controllo con i valori riportati nella
tabella che segue.
506
Parte IV
Creazione di applicazioni WPF
Proprietà
Valore
Name
status
Height
23
HorizontalAlignment
Stretch
Margin
0, 0, 0, 0
Style
{StaticResource bellRingersFontStyle}
VerticalAlignment
Bottom
Width
Auto
A causa di queste proprietà, la barra di stato occupa una riga alla fine del form.
4. Il pulsante Clear è parzialmente coperto dalla barra di stato, pertanto deve essere spostato.
Nella finestra Proprietà, modifica la proprietà Margin del pulsante Clear con 313,378,0,0.
5. Visualizza il file MainWindow.xaml.cs nella finestra dell’editor di codice e di testo.
6. All’inizio dell'elenco, aggiungi le seguenti istruzioni using:
using System.Windows.Threading;
7. Nel metodo saveData, sostituisci l’istruzione che visualizza la finestra di messaggio con il
codice illustrato di seguito in grassetto:
private void saveData(string fileName, Member member)
{
using (StreamWriter writer = new StreamWriter(fileName))
{
...
Thread.Sleep(10000);
Action action = new Action(() => {
status.Items.Add("Member details saved");
});
this.Dispatcher.Invoke(action, DispatcherPriority.ApplicationIdle);
}
}
Il metodo Invoke dell’oggetto Dispatcher prevede una richiesta nel form di un parametro
Delegate che fa riferimento a un metodo da eseguire. Tuttavia, Delegate è una classe
astratta. La classe Action dello spazio dei nomi System è un’implementazione concreta della
classe Delegate, progettata per fare riferimento a un metodo che non accetta parametri e
non restituisce un risultato. (In altre parole, il metodo esegue semplicemente un’azione.)
Nota Il tipo generico Func<T>, illustrato brevemente nel capitolo 20, “Interrogazione dei
dati in memoria mediante espressioni di query”, è un'altra implementazione della classe
Delegate. Un oggetto Func<T> fa riferimento a un metodo che restituisce un oggetto di
tipo T, ed è utile per richiamare un metodo che restituisce un valore attraverso un delegato.
Capitolo 23
Raccolta dell’input utente
507
Il codice illustrato qui utilizza un’espressione per definire un metodo anonimo che visualizza
il messaggio “Member details saved” nella barra di stato. Un oggetto StatusBar può
visualizzare più informazioni e per visualizzare un elemento deve essere aggiunto alla classe
Items.
L’esempio del metodo Invoke illustrato qui ottiene un riferimento all’oggetto Dispatcher
utilizzando la proprietà Dispatcher del form. Il secondo parametro del metodo Invoke
specifica la priorità che l’oggetto Dispatcher dovrebbe assegnare alla richiesta. Si tratta
di un valore dell’enumerazione DispatcherPriority dello spazio dei nomi System.Windows.
Threading. Il valore ApplicationIdle indica all’oggetto Dispatcher di eseguire la richiesta
quando l’applicazione non sta eseguendo alter attività.
8. Nel menu Debug, fai clic su Avvia senza eseguire debug. Quando viene visualizzato il form
WPF, nel menu File fai clic su New Member e aggiungi alcune informazioni del membro. Nel
menu File, fai clic su Save Member Details. Nella finestra di dialogo Bell Ringers, specifica il
nome del file predefinito e fai clic su Salva.
Verifica che il form WPF risponda sempre mentre il thread di background è in esecuzione.
9. Dopo 10 secondi, verifica che venga visualizzata la finestra di messaggio “Member details
saved” alla fine del form.
10. Chiudi il form WPF e ritorna a Visual Studio 2010.
I thread sono estremamente preziosi per gestire la velocità di risposta in un’interfaccia utente.
Tuttavia, a volte possono essere complessi da gestire in modo efficace; può essere difficile
sincronizzare le operazioni concorrenti se si deve attendere che l’esecuzione di uno o più thread
sia stata completata prima di continuare un processo, oppure se si creano troppi thread, può
verificarsi un sovraccarico e un conseguente rallentamento delle attività del computer. .NET
Framework fornisce un’astrazione di thread chiamata Tasks, utilizzabile per creare e controllare i
thread in modo gestibile. Per ulteriori informazioni su Tasks, consulta il capitolo 27.
In questo capitolo, ha appreso come creare menu e consentire agli utenti di eseguire operazioni in
un’applicazione; inoltre hai creato menu di scelta rapida visualizzati quando l’utente fa clic con il
pulsante destro su un controllo o form.
Hai imparato a utilizzare classi comuni delle finestre di dialogo per richiedere all’utente il nome e
il percorso di un file. Infine, hai conosciuto il modello di thread, utilizzato dalle applicazioni WPF, e
hai appreso come usare i thread per incrementare la velocità di risposta delle applicazioni.
■ Se desideri continuare con il capitolo successivo
Continua a eseguire Visual Studio 2010 e passa al capitolo 24.
■ Se desideri uscire ora da Visual Studio 2010
Nel menu File, fai clic su Esci. Se sullo schermo è visualizzata una finestra di dialogo Salva, fai
clic su Sì per salvare il progetto.
508
Parte IV
Creazione di applicazioni WPF
Riferimenti rapidi del capitolo 23
Obiettivo
Azione
Creazione di un menu per un form
Aggiungi un controllo DockPanel e posizionalo all’inizio del form.
Aggiungi un controllo Menu al controllo DockPanel.
Aggiunta di voci di menu a un
menu
Aggiungi elementi MenuItem al controllo Menu. Specifica il testo della
voce di menu impostandone la proprietà Header, quindi assegna un
nome a ciascuna voce specificandone la proprietà Name. (Facoltativo)
Se necessario, puoi specificare le proprietà in modo da poter visualizzare funzionalità quali icone e menu figlio. Per aggiungere una chiave di
accesso alle voci di menu, aggiungi un carattere di sottolineatura come
prefisso della lettera desiderata.
Creazione di una barra separatrice
in un menu
Aggiungi un elemento Separator al menu.
Attivazione o disattivazione di una
voce di menu
Imposta la proprietà IsEnabled su True o False nella finestra Proprietà in
fase di progettazione, oppure scrivi un codice per impostare la proprietà
IsEnabled della voce di menu su true o false in fase di esecuzione.
Esecuzione di un’azione quando
l’utente fa clic su una voce di menu
Seleziona la voce di menu desiderata e specifica un metodo evento per
l’evento Click. Aggiungi il codice necessario al metodo evento.
Creazione di un menu di scelta
rapida
Aggiungi una proprietà ContextMenu alle risorse della finestra. Aggiungi
le voci al menu di scelta rapida come avviene quando si desidera aggiungere le voci di un menu normale.
Associazione di un menu di scelta
rapida a un form o controllo
Imposta la proprietà ContextMenu del form o del controllo in modo da
fare riferimento al menu di scelta rapida.
Creazione di un menu di scelta
rapida in modo dinamico
Crea un oggetto ContextMenu. Compila l’insieme Items di questo oggetto
con oggetti MenuItem che definiscono le varie voci di menu. Imposta la
proprietà ContextMenu del form o del controllo in modo da fare riferimento al menu di scelta rapida.
Richiesta del nome del file da salvare all’utente
Utilizza la classe SaveFileDialog. Visualizza la finestra di dialogo mediante
il metodo ShowDialog. Alla chiusura della finestra di dialogo, la proprietà
FileName dell’istanza di SaveFileDialog contiene il nome di file selezionato dall’utente.
Esecuzione di un’operazione in un
thread di background
Crea un oggetto Thread che faccia riferimento a un metodo da eseguire.
Richiama il metodo Start dell’oggetto Thread per richiamare il metodo.
Ad esempio:
Thread workerThread new Thread(
() => doWork(...));
workerThread,Start();
Accesso di un thread di
background a controlli gestiti dal
thread dell’interfaccia utente
Crea un delegato Action che faccia riferimento a un metodo che accede
ai controlli. Esegui il metodo utilizzando il metodo Invoke dell’oggetto
Dispatcher e, a scelta, specifica una priorità. Ad esempio:
Action action = new Action(() => {
status.Items.Add("Member details added");
});
this.Dispatcher.Invoke(action,
DispatcherPriority.ApplicationIdle);
Capitolo 24
Esecuzione della convalida
Gli argomenti trattati in questo capitolo sono:
■
Verifica delle informazioni immesse dall’utente per verificare che non violino alcuna regola
di applicazione o business.
■
Associazione delle proprietà di un controllo su un form con le proprietà di altri controlli.
■
Utilizzo di regole di convalida dell’associazione dei dati per verificare le informazioni
immesse dall’utente.
■
Esecuzione della convalida in modo efficace e discreto.
Nei due capitoli precedenti hai visto come creare un’applicazione Microsoft WPF (Windows
Presentation Foundation) che utilizza più controlli per l’immissione dei dati. Hai creato dei menu
per semplificare l’utilizzo dell’applicazione. Inoltre hai appreso come rilevare gli eventi generati
da menu, form e controlli in modo che l’applicazione possa essere davvero utile oltre ad apparire
esteticamente valida. Hai poi utilizzato dei thread per rendere efficace l’applicazione.
Nonostante un’attenta progettazione dei form e l’uso appropriato dei controlli possano aiutare a
garantire che l’utente immetta le informazioni corrette, spesso succede di dover eseguire ulteriori
controlli. In questo capitolo potrai apprendere come convalidare i dati immessi dall’utente che
utilizza l’applicazione per verificare che essi corrispondano alle regole aziendali specificate nei
requisiti dell’applicazione stessa.
Convalida dei dati
Il concetto di convalida dell’input è semplice, ma non è sempre facile da implementare,
specialmente se la convalida comporta un controllo incrociato dei dati che l’utente ha immesso in
due o più controlli. La regola aziendale sottostante può essere semplice, ma spesso la convalida
viene eseguita in un momento non appropriato, e ciò rende il form difficile da usare.
Strategie di convalida dei dati immessi dall’utente
Esistono molte strategie di convalida delle informazioni immesse nelle applicazioni dagli utenti.
Una tecnica comune familiare a molti sviluppatori di applicazioni per Microsoft Windows che
utilizzano versioni precedenti di Microsoft .NET Framework consiste nel gestire l’evento di controlli
LostFocus. L’evento LostFocus viene generato quando l’utente si sposta da un controllo a un altro.
A questo evento è possibile aggiungere il codice che consente di esaminare i dati presenti nel
controllo che l’utente sta abbandonando per verificare che soddisfino i requisiti dell’applicazione
prima di consentire lo spostamento del cursore. Il problema di questa strategia è che spesso è
necessario eseguire un controllo incrociato dei dati immessi in un controllo con i valori presenti
509
510
Parte IV
Creazione di applicazioni WPF
in altri, e la logica di convalida può diventare molto complessa; in molti casi si finisce per ripetere
una logica simile nel gestore eventi LostFocus per più controlli. Inoltre, non è possibile in alcun
modo avere il controllo dell’ordine con cui l’utente si sposta da un controllo all’altro. Gli utenti
possono infatti muoversi tra i controlli di un form in qualsiasi ordine, pertanto non puoi sempre
presumere che ogni controllo contenga un valore valido se è in corso il controllo incrociato di un
controllo particolare con i dati di altri elementi presenti nel form.
Un altro problema fondamentale di questa strategia è che può vincolare in modo eccessivo la
logica di convalida degli elementi di presentazione di un’applicazione alla logica aziendale. In caso
di modifica dei requisiti aziendali, può risultare necessario cambiare la logica di convalida, con il
conseguente aumento nella complessità della gestione.
Con WPF puoi definire le regole di convalida come parte del modello aziendale usato nelle tue
applicazioni. Puoi quindi fare riferimento a queste regole nella descrizione XAML (Extensible
Application Markup Language) dell’interfaccia utente. Per fare ciò, devi definire le classi richieste
dal modello aziendale, quindi associare le proprietà dei controlli dell’interfaccia utente alle
proprietà esposte da tali classi. In fase di esecuzione, WPF può creare istanze di queste classi.
Quando modifichi i dati di un controllo, questi possono essere automaticamente copiati a ritroso
nella proprietà specificata dell’istanza di classe del modello aziendale e quindi convalidati.
Ulteriori informazioni sull’associazione dei dati sono disponibili nella Parte V di questo manuale,
“Gestione dei dati”. Questo capitolo si concentra sulle regole di convalida che è possibile abbinare
con l’associazione dei dati.
Un esempio: ordine di biglietti per eventi
Considera il seguente semplice scenario. ti è stato chiesto di creare un’applicazione che consenta
ai clienti di ordinare biglietti per degli eventi. Parte dell’applicazione deve consentire al cliente di
inserire le proprie informazioni personali, specificare un evento e selezionare il numero di biglietti
richiesto. Un cliente ha un livello elevato (Standard, Premium, Executive o Premium Executive);
maggiore è il livello, più biglietti potranno essere ordinati. I clienti standard possono ordinare
massimo due biglietti per un evento, i clienti premium possono ordinare quattro biglietti, quelli
executive otto biglietti e quelli premium executive 10 biglietti. A tal fine decidi di creare un form
come quello mostrato nell’immagine che segue.
Capitolo 24
Esecuzione della convalida
511
Devi assicurare che l’input sia valido e coerente. In particolare, deve poter fare quanto segue:
■ Selezionare un evento. L’applicazione di prova utilizza un insieme di eventi memorizzati nel
codice. In un ambiente di produzione, l’insieme di eventi viene memorizzato in un database
e si utilizza l’associazione dei dati per recuperare e visualizzare gli eventi. Vedrai come fare
tutto ciò nel capitolo 26, “Visualizzazione e modifica dei dati mediante Strumentazione di
entità Framework e associazione”.
■ Inserire un numero di riferimento cliente. L’applicazione di prova non verifica tale numero.
■ Specificare un livello di privilegi. Ancora una volta, l’applicazione di prova non verifica che il
cliente abbia di fatto tale livello.
■ Scegliere un numero di biglietti superiore a 0 e inferiore o uguale al valore consentito dal
livello di privilegi del cliente.
Quando l’utente ha completato l’inserimento dei dati, fa clic sull’elemento Purchase nel menu
File. L’applicazione vera visualizza uno schermo per l’utente che gli consente di inserire i dettagli
per il pagamento. Nell’applicazione di prova, viene visualizzato semplicemente un messaggio che
conferma l’inserimento dei dati dell’utente.
Esecuzione della convalida mediante l’uso dell’associazione
Nelle esercitazioni che seguono dovrai esaminare l’applicazione Ticket Ordering e aggiungere
le regole di convalida utilizzando l’associazione dei dati. A titolo informativo, in uno degli
esercizi potrai vedere come sia facile usare tempi di convalida errati e rendere un’applicazione
praticamente inutilizzabile.
Esame del form Ticket Orders
1. Avvia Microsoft Visual Studio 2010 se non è già in esecuzione.
2. Apri il progetto OrderTickets disponibile nella cartella \Microsoft Press\Visual CSharp Step
By Step\Chapter 24\OrderTickets della tua cartella Documenti.
3. Nel menu Debug, fai clic su Avvia senza eseguire debug per costruire ed eseguire
l’applicazione.
4. Una volta visualizzato il form, non inserire i dati; fai subito clic su Purchase nel menu File.
L’applicazione visualizza una finestra di messaggio con indicato l’acquisto di 0 biglietti
per un dato cliente e un dato evento. L’applicazione attualmente consente all’utente di
acquistare i biglietti senza specificare per chi siano, l’evento o anche il numero di biglietti
richiesto.
5. Fai clic su OK per tornare al form Members.
6. Nella casella combinata Event, fai clic su Little Mudford Festival.
512
Parte IV
Creazione di applicazioni WPF
7. Nella casella Customer Reference digita C1234.
8. Nella casella combinata Privilege Level, fai clic su Premium.
9. Usando il controllo di scorrimento Tickets, fai clic sul cursore e trascinalo a destra del
controllo in modo da specificare 10 biglietti.
10. Nel menu File, fai clic su Purchase.
L’applicazione visualizza una finestra di messaggio con indicato l’acquisto di 10 biglietti per
un cliente Premium e per l’evento C1234: Little Mudford Festival. Nota che l’applicazione
non controlla se il numero di biglietti supera quello consentito per il livello di privilegi.
11. Fai clic su OK, quindi chiudi il form e torna a Visual Studio 2010.
attualmente, questo form non è molto utile. Non convalida i dati inseriti dall’utente, e il controllo
di scorrimento rende difficile stabilire esattamente quanti biglietti sono stati selezionati dall’utente
fino a quando non fa clic sul pulsante Purchase (devi contare le tacche sotto il controllo). Questo
secondo problema è facile da risolvere, quindi si inizierà da qui. Nel prossimo esercizio puoi
aggiungere al form un controllo TextBox e utilizzare l’associazione dei dati per visualizzare il
valore attuale della barra di scorrimento del controllo.
Utilizzo dell’associazione dei dati per visualizzare il numero di biglietti richiesto
1. In Esplora soluzioni, fai doppio clic sul file TicketForm.xaml.
2. Dalla Casella degli strumenti, aggiungi nel form un controllo TextBox posizionandolo a
destra del controllo di scorrimento. Utilizzando la finestra Proprietà, imposta le proprietà di
questo controllo sui valori riportati nella tabella che segue.
Proprietà
Valore
Name
tickets
Height
23
Width
25
Margin
380, 170, 0, 0
IsReadOnly
True (selected)
TextAlignment
Right
HorizontalAlignment
Left
VerticalAlignment
Top
3. Nel riquadro XAML, modifica la definizione del controllo TextBox tickets. Aggiungi
l’elemento figlio TextBox.Text come mostrato di seguito in grassetto, assicurandoti di
sostituire il delimitatore di chiusura del tag (/>) del controllo TextBox con un delimitatore
normale (>) e aggiungere un tag di chiusura </TextBox>:
<TextBox Height="23" HorizontalAlignment="Left" Margin="380,170,0,0"
Name="tickets" VerticalAlignment="Top" Width="25" TextAlignment="Right"
IsReadOnly="True">
<TextBox.Text>
</TextBox.Text>
</TextBox>
Capitolo 24
Esecuzione della convalida
513
4. Nell’elemento figlio TextBox.Text aggiungi l’elemento Binding mostrato di seguito in
grassetto:
<TextBox.Text>
<Binding ElementName="numberOfTickets" Path="Value" />
</TextBox.Text>
</TextBox>
Questo elemento Binding associa la proprietà Text del controllo TextBox con la proprietà
Value del controllo Slider (il controllo Slider viene denominato numberOfTickets). Quando
l’utente modifica questo valore, TextBox verrà aggiornato automaticamente. Nota che il
controllo TextBox visualizza il valore 0 nella finestra Progettazione, ossia il valore predefinito
del controllo Slider.
5. Nel menu Debug, fai clic su Avvia senza eseguire debug.
6. Quando viene visualizzato il form, trascina il cursore sul controllo Slider e verifica che il
numero dei biglietti venga visualizzato nel controllo TextBox a destra.
7. Chiudi il form e torna a Visual Studio.
Ora puoi concentrarti sulla convalida dei dati inseriti dall’utente. Si possono utilizzare molti
approcci, ma in casi come questo si consiglia di creare una classe in grado di modellare l’entità per
cui si stanno inserendo i dati. Puoi aggiungere la logica di convalida a questa classe, poi associare
le proprietà nella classe ai vari campi sul form. Se nel form vengono inseriti dati validi, le regole di
convalida nella classe possono generare un’eccezione che può essere catturata e visualizzata sul
form.
Inizierai creando una classe per modellare un ordine di un cliente, poi apprenderai come utilizzare
tale classe per assicurare che l’utente specifichi sempre un evento e inserisca un numero di
riferimento.
Creazione della classe TicketOrder con la logica di convalida per specificare un
evento e imporre l’inserimento di un numero cliente di riferimento
1. In Esplora soluzioni, fai clic con il tasto destro del mouse sul progetto OrderTickets, quindi
seleziona Aggiungi e fai clic su Classe.
2. Nella finestra di dialogo Aggiungi nuovo elemento - OrderTickets, nella casella di testo
Nome, immetti TicketOrder.cs, quindi fai clic su Aggiungi.
3. Nella finestra dell’editor di codice e di testo che mostra il file TickerOrder.cs, aggiungi
i campi privati eventName e customerReference qui mostrati in grassetto alla classe
TicketOrder:
class TicketOrder
{
private string eventName;
private string customerReference;
}
514
Parte IV
Creazione di applicazioni WPF
4. Aggiungi la seguente proprietà EventName pubblica alla classe TicketOrder mostrata in
grassetto e basata sul campo eventName aggiunto nel punto precedente:
class TicketOrder
{
...
public string EventName
{
get { return this.eventName; }
set
{
if (String.IsNullOrEmpty(value))
{
throw new ApplicationException
("Specify an event");
}
else
{
this.eventName = value;
}
}
}
}
La funzione di accesso della proprietà set esamina il valore fornito per il nome dell’evento;
se il campo è vuoto viene generata un’eccezione con relativo messaggio esplicativo.
5. Aggiungi la proprietà pubblica CustomerReference alla classe TicketOrder come mostrato in
grassetto:
class TicketOrder
{
...
public string CustomerReference
{
get { return this.customerReference; }
set
{
if (String.IsNullOrEmpty(value))
{
throw new ApplicationException
("Specify the customer reference number");
}
else
{
this.customerReference = value;
}
}
}
}
È simile alla proprietà EventName. La funzione di accesso della proprietà set esamina il
valore fornito per il numero cliente di riferimento; se il campo è vuoto viene generata
un’eccezione.
Capitolo 24
Esecuzione della convalida
515
Una volta creata la classe TicketOrder, il passaggio successivo consiste nell’associare le caselle di
testo customerReference del form alle proprietà CustomerReference della classe.
Associazione dei controlli delle caselle di testo del form alle proprietà della classe
TicketOrder
1. In Esplora soluzioni, fai doppio clic sul file TicketForm.xaml per visualizzare il form nella
finestra Progettazione.
2. Nel riquadro XAML, aggiungi alla definizione Window la dichiarazione dello spazio dei nomi
XML qui mostrata in grassetto:
<Window x:Class="OrderTickets.TicketForm"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ticketOrder="clr-namespace:OrderTickets"
Title="Ticket Orders" Height="250" Width="480" ResizeMode="NoResize">
...
Questa dichiarazione è simile a un’istruzione using nel codice Microsoft C#. Consente di fare
riferimento ai tipi nello spazio dei nomi OrderTickets nel codice XAML della finestra.
3. Aggiungi alla finestra l’elemento Window.Resources qui mostrato in grassetto:
<Window x:Class=" OrderTickets.TicketForm"
...
...ResizeMode="NoResize">
<Window.Resources>
<ticketOrder:TicketOrder x:Key="orderData" />
</Window.Resources>
<Grid>
...
Questa risorsa crea una nuova istanza della classe TicketOrder. Mediante il valore chiave
orderData, puoi fare riferimento a questa istanza in altri punti della definizione XAML della
finestra.
4. Trova la definizione della casella di testo customerReference nel riquadro XAML e modificala
come mostrato di seguito in grassetto, assicurandoti di sostituire il delimitatore di chiusura
del tag (/>) del controllo TextBox con un delimitatore normale (>) e aggiungere un tag di
chiusura </TextBox>:
<TextBox Height="23" HorizontalAlignment="Left" Margin="156,78,0,0"
Name="customerReference" VerticalAlignment="Top" Width="205">
<TextBox.Text>
<Binding Source="{StaticResource orderData}"
Path="CustomerReference" />
</TextBox.Text>
</TextBox>
Questo codice associa i dati visualizzati nella proprietà Text della casella di testo al
valore nella proprietà CustomerReference dell’oggetto orderData. Se l’utente aggiorna
il valore nella casella di testo customerReference del form, i nuovi dati vengono copiati
516
Parte IV
Creazione di applicazioni WPF
automaticamente nell’oggetto orderData. Ricorda che la proprietà CustomerReference nella
classe TicketOrder controlla che l’utente abbia di fatto specificato un valore.
5. Modifica la definizione dell’associazione immessa nel punto precedente e aggiungi un
elemento Binding.ValidationRules figlio, come qui mostrato in grassetto:
<TextBox Height="23" HorizontalAlignment="Left" Margin="156,78,0,0"
Name="customerReference" VerticalAlignment="Top" Width="205">
<TextBox.Text>
<Binding Source="{StaticResource orderData}"
Path="CustomerReference">
<Binding.ValidationRules>
<ExceptionValidationRule/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
Con l’elemento ValidationRules di un’associazione puoi specificare l’operazione di convalida
che applicazione deve eseguire quando l’utente immette i dati nel controllo. L’elemento
ExceptionValidationRule è una regola incorporata che permette di verificare eventuali
eccezioni generate dall’applicazione in caso di variazione dei dati presenti nel controllo. Se
viene rilevata un’eccezione, questo elemento evidenzia il controllo interessato, in modo che
l’utente possa accorgersi della presenza di un problema di immissione dei dati.
6. Aggiungi l’associazione equivalente e la regola di associazione alla proprietà Text della
casella combinata eventList, associandola con la proprietà EventName dell’oggetto
orderData, come mostrato di seguito in grassetto:
<ComboBox Height="23" HorizontalAlignment="Left" Margin="156,29,0,0"
Name="eventList" VerticalAlignment="Top" Width="205" >
<ComboBox.Text>
<Binding Source="{StaticResource orderData}" Path="EventName" >
<Binding.ValidationRules>
<ExceptionValidationRule/>
</Binding.ValidationRules>
</Binding>
</ ComboBox.Text>
<ComboBox.Items>
...
</ComboBox.Items>
</ComboBox >
7. Nel menu Debug, fai clic su Avvia senza eseguire debug per costruire ed eseguire
l’applicazione.
8. Una volta visualizzato il form, fai clic su Purchase nel menu File senza inserire i dati.
Viene visualizzato il messaggio che comunica l’acquisto di 0 biglietti per il cliente specificato
e un dato evento.
9. Fai clic su OK.
Capitolo 24
Esecuzione della convalida
517
10. Nel form Ticket Orders, seleziona Little Mudford Festival dalla casella combinata Event,
digita C1234 nella casella di testo customerReference, quindi seleziona Premium nella
casella combinata Privilege Level.
Non dovrebbe succedere nulla di visibile.
11. Fai clic sulla casella di testo customerReference, elimina il numero di riferimento che hai
immesso e fai clic nuovamente sulla casella combinata Privilege Level.
Questa volta la casella di testo customerReference viene evidenziata con un bordo di colore
rosso. Quando l’associazione ha tentato di copiare il valore inserito dall’utente nell’oggetto
TickerOrder, il valore era una stringa vuota, quindi la proprietà CustomerReference ha
generato un’eccezione ApplicationException. In questi casi, un controllo che utilizza
l’associazione indica che si è verificata un’eccezione visualizzando un bordo di colore rosso.
12. Digita C1234 nella casella di testo customerReference, quindi fai clic sulla casella combinata
Privilege Level.
La casella rossa attorno alla casella di testo customerReference sparisce.
13. Elimina nuovamente il valore nella casella di testo customerReference. Nel menu File, fai clic
su Purchase.
Sorprendentemente, il bordo di colore rosso non appare attorno alla casella di testo
customerReference.
14. Fai clic su OK nella finestra di messaggio, quindi fai clic sulla casella combinata Privilege
Level.
Il bordo di colore rosso appare ora attorno alla casella di testo customerReference.
15. Chiudi il form e ritorna a Visual Studio 2010.
A questo punto ci sono almeno due domande che dovresti porti:
■ Per quale motivo il form non rileva sempre se l’utente dimentica di immettere un valore in
una casella di testo? La risposta è che la convalida avviene solo quando il puntatore lascia
la casella di testo. Questo, a sua volta, avviene solo quando l’utente sposta il cursore su un
altro controllo sul form. I menu non vengono di fatto trattati come se fossero parte del
form. Vengono trattati in modo diverso. Quando si seleziona un elemento di menu, non ci
si sposta su un altro controllo del form, quindi il cursore non si è spostato dalla casella di
testo. Il cursore si sposta solamente quando fai clic sulla casella combinata Privilege Level (o
su un altro controllo), causando la convalida. Inoltre, la casella di testo customerReference e
la casella combinata Event inizialmente sono vuote. Se ci si sposta da uno di questi controlli
senza selezionare o digitare qualcosa, la convalida non avviene. Infatti, ciò avviene soltanto
quando selezioni o immetti delle informazioni e quindi le elimini. Questi problemi saranno
risolti più avanti in questo capitolo.
518
Parte IV
Creazione di applicazioni WPF
■ Come è possibile fare in modo che il form visualizzi un messaggio di errore esplicativo
invece di limitarsi a evidenziare che si è verificato un problema di immissione dei dati in un
controllo? A tal fine, puoi intercettare il messaggio generato dall’eccezione e visualizzarlo in
un altro punto del form. L’esercitazione che segue illustra come procedere.
Nel prossimo esercizio verrà data risposta a queste domande.
Aggiunta di uno stile per la visualizzazione dei messaggi di eccezione
1. Nel riquadro XAML della finestra Progettazione relativa al file TicketForm.xaml, aggiungi lo
stile qui mostrato in grassetto all’elemento Window.Resources:
<Window.Resources>
<ticketOrder:Customer x:Key="orderData" />
<Style x:Key="errorStyle" TargetType="Control">
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="True">
<Setter Property="ToolTip"
Value="{Binding RelativeSource={x:Static RelativeSource.Self},
Path=(Validation.Errors)[0].ErrorContent}" />
</Trigger>
</Style.Triggers>
</Style>
</Window.Resources>
Questo stile contiene un trigger che rileva quando la proprietà Validation.HasError del
controllo viene impostata su true. Ciò accade ogni volta che una regola di convalida
dell’associazione del controllo genera un’eccezione. Il trigger imposta la proprietà ToolTip
del controllo corrente in modo da visualizzare il testo dell’eccezione. I dettagli relativi alla
sintassi di associazione qui mostrata esulano dallo scopo di questo manuale, ma l’origine
di associazione {Binding RelativeSource={x:Static RelativeSource.Self} è
un riferimento al controllo corrente e il percorso di associazione (Validation.Errors)
[0].ErrorContent associa il primo messaggio di eccezione rilevato in questa origine
di associazione alla proprietà ToolTip. Ricorda che un’eccezione può a sua volta generare
ulteriori eccezioni, ognuna delle quali genera propri messaggi. Solitamente, il primo
messaggio è anche il più significativo.
2. Applica lo stile errorStyle ai controlli eventList e customerReference, come qui mostrato in
grassetto:
<ComboBox Style="{StaticResource errorStyle}" ... Name="eventList" ... >
...
</ComboBox>
<TextBox Style="{StaticResource errorStyle}" ... Name="customerReference" ... >
...
</TextBox>
3. Nel menu Debug, fai clic su Avvia senza eseguire debug per costruire ed eseguire
l’applicazione.
Capitolo 24
Esecuzione della convalida
519
4. Quando compare il form, seleziona Little Mudford Festival dalla casella combinata Event,
digita C1234 nella casella di testo customerReference, quindi fai clic sulla casella combinata
Privilege Level.
5. Fai clic sulla casella di testo customerReference, elimina il numero di riferimento che hai
immesso e fai clic nuovamente sulla casella combinata Privilege Level.
La casella di testo customerReference viene evidenziata con un bordo di colore rosso.
Nota Assicurati di eliminare il contenuto della casella di testo foreName invece di sovrascriverlo con spazi.
6. Posiziona il puntatore del mouse sulla casella di testo customerReference. Sullo schermo
appare una descrizione comandi con il messaggio “Specify the customer reference number”
come mostrato nella figura che segue:
È il messaggio dell’eccezione ApplicationException generato dalla proprietà
CustomerReference della classe TicketOrder.
7. Chiudi il form e ritorna a Visual Studio 2010.
Vi sono ancora alcuni problemi di risolvere, ma ciò avverrà dopo aver esaminato come convalidare
il livello di privilegi e il numero di biglietti e aver assicurato che siano coerenti.
Aggiunta delle proprietà per la convalida del livello di privilegi e del numero di
biglietti
1. Passa alla finestra dell’editor di codice e di testo che visualizza il contenuto del file
TicketOrder.cs.
2. Aggiungi l’enumerazione PrivilegeLevel qui mostrata in grassetto, sopra la classe
TicketOrder:
enum PrivilegeLevel { Standard, Premium, Executive, PremiumExecutive }
class TicketOrder
{
...
}
520
Parte IV
Creazione di applicazioni WPF
Utilizzerai questa enumerazione per specificare il tipo della proprietà PrivilegeLevel nella
classe TicketOrder.
3. Aggiungi i campi privati privilegeLevel e numberOfTickets alla classe TicketOrder come
mostrato in grassetto:
class TicketOrder
{
private string eventName;
private string customerReference;
private PrivilegeLevel privilegeLevel;
private short numberOfTickets;
...
}
4. Aggiungi il metodo booleano privato checkPrivilegeAndNumberOfTickets alla classe
TicketOrder, come qui mostrato in grassetto:
class TicketOrder
{
...
private bool checkPrivilegeAndNumberOfTickets(
PrivilegeLevel proposedPrivilegeLevel,
short proposedNumberOfTickets)
{
bool retVal = false;
switch (proposedPrivilegeLevel)
{
case PrivilegeLevel.Standard:
retVal = (proposedNumberOfTickets <= 2);
break;
case PrivilegeLevel.Premium:
retVal = (proposedNumberOfTickets <= 4);
break;
case PrivilegeLevel.Executive:
retVal = (proposedNumberOfTickets <= 8);
break;
case PrivilegeLevel.PremiumExecutive:
retVal = (proposedNumberOfTickets <= 10);
break;
}
return retVal;
}
}
Questo metodo esamina i valori nei parametri proposedPrivilegeLevel e
proposedNumberOfTickets e ne verifica la coerenza in base alle regole aziendali descritte
precedentemente nel capitolo. Se i valori sono coerenti, il metodo restituisce il valore true;
in caso contrario, restituisce il valore false.
Capitolo 24
Esecuzione della convalida
521
5. Aggiungi le proprietà PrivilegeLevel e NumberOfTickets pubbliche qui mostrate in grassetto
alla classe TicketOrder. Nota che il tipo della proprietà PrivilegeLevel è l’enumerazione
PrivilegeLevel.
class Customer
{
...
public PrivilegeLevel PrivilegeLevel
{
get { return this.privilegeLevel; }
set
{
this.privilegeLevel = value;
if (!this.checkPrivilegeAndNumberOfTickets(value, this.numberOfTickets))
{
throw new ApplicationException(
"Privilege level too low for this number of tickets");
}
}
}
public short NumberOfTickets
{
get { return this.numberOfTickets; }
set
{
this.numberOfTickets = value;
if (!this.checkPrivilegeAndNumberOfTickets(this.privilegeLevel, value))
{
throw new ApplicationException(
"Too many tickets for this privilege level");
}
if (this.numberOfTickets <=0)
{
throw new ApplicationException(
"You must buy at least one ticket");
}
}
}
}
Le funzioni di accesso set di queste proprietà richiamano il metodo
CheckPrivilegeAndNumberOfTickets per verificare che i campi privilegeLevel e
numberOfTickets corrispondano, generando un’eccezione in caso contrario.
Inoltre, le funzioni di accesso set della proprietà NumberOfTickets verificano che l’utente
abbia specificato almeno un biglietto. Non è necessario verificare che l’utente abbia
specificato un valore per la proprietà PrivilegeLevel, in quanto il valore predefinito è
Standard (il primo elemento nell’enumerazione PrivilegeLevel).
522
Parte IV
Creazione di applicazioni WPF
6. Aggiungi il metodo ToString qui mostrato in grassetto alla classe TicketOrder:
class TicketOrder
{
...
public override string ToString()
{
string formattedString = String.Format("Event: {0}\tCustomer: {1}\tPrivilege:
{2}\tTickets: {3}",
this.eventName, this.customerReference,
this.privilegeLevel.ToString(), this.numberOfTickets.ToString());
return formattedString;
}
}
Questo metodo verrà usato per visualizzare i dettagli relativi agli ordini dei biglietti per
verificare che i dati siano corretti.
Il passaggio successivo consiste nell’associare a queste nuove proprietà la casella combinata
privilegeLevel e il controllo di scorrimento numberOfTickets. Tuttavia, se ti fermi un attimo a
pensare, puoi notare che c’è un piccolo problema con la proprietà PrivilegeLevel. Devi associare
la proprietà Text della casella combinata privilegeLevel alla proprietà PrivilegeLevel dell’oggetto
TicketOrder creato dal form. Il tipo della proprietà Text è string. Il tipo della proprietà PrivilegeLevel
è PrivilegeLevel (un’enumerazione). Affinché l’associazione possa funzionare, devi convertire i
valori string e PrivilegeLevel. Fortunatamente, con il meccanismo di associazione implementato da
WPF, puoi specificare una classe convertitrice in grado di eseguire questo tipo di operazioni.
Nota Un’associazione WPF può convertire automaticamente un’enumerazione e una stringa se i
valori stringa sono identici ai nomi di ogni elemento nell’enumerazione. Nell’applicazione Ticket
Order i primi tre elementi nella casella combinata privilegeLevel (Standard, Premium e Executive)
corrispondono direttamente agli elementi con gli stessi nomi nell’enumerazione PrivilegeLevel.
Tuttavia, l’ultimo elemento nella casella combinata è Premium Executive (con uno spazio), ma
l’elemento corrispondente nell’enumerazione si chiama PremiumExecutive (senza uno spazio).
L’associazione WPF non può convertire questi due valori, quindi è necessaria una classe convertitrice.
I metodi convertitori sono ubicati nelle rispettive classi che devono implementare l’interfaccia
IValueConverter. Questa interfaccia definisce due metodi: Convert, che converte il tipo usato
dalla proprietà nella classe che fornisce i dati per l’associazione nel tipo visualizzato nel form, e
ConvertBack, che converte i dati dal tipo visualizzato nel form al tipo richiesto dalla classe.
Creazione di classi e metodi convertitori
1. Nel file TicketOrder.cs aggiungi l’istruzione using che segue nell’elenco all’inizio del file:
using System.Windows.Data;
L’interfaccia IValueConverter viene definita in questo spazio dei nomi.
2. Aggiungi la classe PrivilegeLevelConverter mostrata di seguito alla fine del file, dopo la
classe Customer:
Capitolo 24
Esecuzione della convalida
523
[ValueConversion(typeof(string), typeof(PrivilegeLevel))]
public class PrivilegeLevelConverter : IValueConverter
{
}
Il testo tra parentesi direttamente sopra la classe è un esempio di attributo. Un attributo
fornisce metadati descrittivi per una classe. L’attributo ValueConversion viene impiegato
da strumenti quali la progettazione di WPF nella finestra Progettazione per verificare
che la classe cui fai riferimento venga applicata correttamente. I parametri dell’attributo
ValueConversion specificano il tipo di valore visualizzato nel form (string) e il tipo di valore
presente nella corrispondente proprietà della classe (PrivilegeLevel). Altri esempi di attributi
sono illustrati più avanti in questo manuale.
3. Nella classe PrivilegeLevelConverter, aggiungi il metodo Convert qui mostrato in grassetto:
[ValueConversion(typeof(string), typeof(PrivilegeLevel))]
public class PrivilegeLevelConverter: IValueConverter
{
public object Convert(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
PrivilegeLevel privilegeLevel = (PrivilegeLevel)value;
string convertedPrivilegeLevel = String.Empty;
switch (privilegeLevel)
{
case PrivilegeLevel.Standard:
convertedPrivilegeLevel = "Standard";
break;
case PrivilegeLevel.Premium:
convertedPrivilegeLevel = "Premium";
break;
case PrivilegeLevel.Executive:
convertedPrivilegeLevel = "Executive";
break;
case PrivilegeLevel.PremiumExecutive:
convertedPrivilegeLevel = "Premium Executive";
break;
}
return convertedPrivilegeLevel;
}
}
La firma del metodo Convert è definita dall’interfaccia IValueConverter. Nota che per il
momento puoi ignorare gli altri parametri. Il valore restituito da questo metodo contiene i
dati associati alla proprietà del form. In questo caso, il metodo Convert converte un valore
PrivilegeLevel in un tipo string. Come puoi vedere, il parametro value viene passato come
tipo object, pertanto devi eseguirne il cast al tipo appropriato prima di poterlo utilizzare.
524
Parte IV
Creazione di applicazioni WPF
4. Aggiungi il metodo ConvertBack qui mostrato in grassetto alla classe
PrivilegeLevelConverter:
[ValueConversion(typeof(string), typeof(PrivilegeLevel))]
public class PrivilegeLevelConverter: IValueConverter
{
...
public object ConvertBack(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
PrivilegeLevel privilegeLevel = PrivilegeLevel.Standard;
switch ((string)value)
{
case "Standard":
privilegeLevel = PrivilegeLevel.Standard;
break;
case "Premium":
privilegeLevel = PrivilegeLevel.Premium;
break;
case "Executive":
privilegeLevel = PrivilegeLevel.Executive;
break;
case "Premium Executive":
privilegeLevel = PrivilegeLevel.PremiumExecutive;
break;
}
return privilegeLevel;
}
}
Il metodo ConvertBack è fornito come parte dell’interfaccia IValueConverter. Nel metodo
ConvertBack, il parametro value è ora il valore del form che viene convertito nuovamente
in un valore del tipo appropriato per la classe. In questo caso, il metodo ConvertBack
converte i dati da un tipo string, visualizzato nella proprietà Text della casella combinata, nel
corrispondente valore Title.
5. Nel menu Compila, fai clic su Compila soluzione. Verifica che la soluzione possa essere
compilata senza errori, correggendoli se necessario.
Associazione dei controlli delle caselle combinate e dei controlli di scorrimento del
form alle proprietà della classe TicketOrder
1. Torna alla finestra Progettazione relativa al form TicketForm.xaml.
2. Nel riquadro XAML, aggiungi un oggetto TitleConverter come risorsa della finestra, quindi
specifica un valore chiave di privilegeLevelConverter, come mostrato di seguito in grassetto:
<Window.Resources>
<ticketOrder:TicketOrder x:Key="orderData" />
<ticketOrder:PrivilegeLevelConverter x:Key="privilegeLevelConverter" />
Capitolo 24
Esecuzione della convalida
525
...
</Window.Resources>
3. Localizza la definizione del controllo casella combinata privilegeLevel e assegnale lo stile
errorStyle. Dopo l’elenco degli elementi della casella combinata, aggiungi il codice XAML
qui mostrato in grassetto per associare la proprietà Text della casella combinata alla
proprietà Title nell’oggetto orderData, specificando la risorsa titleConverter come oggetto
che fornisce i metodi convertitore:
<ComboBox Style="{StaticResource errorStyle}" ... Name="privilegeLevel" ...>
<ComboBox.Text>
<Binding Source="{StaticResource orderData}" Path="PrivilegeLevel"
Converter="{StaticResource privilegeLevelConverter}" >
<Binding.ValidationRules>
<ExceptionValidationRule />
</Binding.ValidationRules>
</Binding>
</ComboBox.Text>
<ComboBox.Items>
...
</ComboBox.Items>
</ComboBox>
4. Modifica la definizione del controllo di scorrimento numberOfTickets. Applica lo stile
errorStyle, quindi associa la proprietà Value alla proprietà NumberOfTickets dell’oggetto
orderData, come mostrato di seguito in grassetto:
<Slider Style="{StaticResource errorStyle}" Height="22"
HorizontalAlignment="Left" Margin="156,171,0,0", Name="numberOfTickets"
VerticalAlignment="Top" Width="205" SmallChange="1"
TickPlacement="BottomRight" Maximum="10" IsSnapToTickEnabled="True" >
<Slider.Value>
<Binding Source="{StaticResource orderData}" Path="NumberOfTickets">
<Binding.ValidationRules>
<ExceptionValidationRule />
</Binding.ValidationRules>
</Binding>
</Slider.Value>
</Slider>
5. Nel menu Visualizza, fai clic su Codice per passare alla finestra dell’editor di codice e di testo
che visualizza il contenuto del file TicketForm.xaml.cs.
6. Modifica il codice nel metodo purchaseTickets_Click come qui mostrato in grassetto:
private void purchaseTickets_Click(object sender, RoutedEventArgs e)
{
Binding ticketOrderBinding =
BindingOperations.GetBinding(privilegeLevel, ComboBox.TextProperty);
TicketOrder ticketOrder = ticketOrderBinding.Source as TicketOrder;
MessageBox.Show(ticketOrder.ToString(), "Purchased");
}
Questo codice visualizza i dettagli dell’ordine nella finestra di messaggio (di fatto
ancora non salva l’ordine da nessuna parte). Il metodo GetBinding statico della classe
BindingOperations restituisce un riferimento all’oggetto cui è associata la proprietà
526
Parte IV
Creazione di applicazioni WPF
specificata. In questo caso, il metodo GetBinding recupera l’oggetto associato alla
proprietà Text della casella combinata title. Questo dovrebbe essere lo stesso oggetto
a cui fa riferimento la risorsa orderData. In realtà, per recuperare lo stesso riferimento il
codice potrebbe avere interrogato una qualsiasi delle proprietà associate dei controlli ,
customerReference, privilegeLevel o numberOfTickets. Il riferimento viene restituito come
oggetto Binding. Il codice esegue quindi il cast di questo oggetto Binding in un oggetto
TicketOrder prima di visualizzarne i dettagli.
Ora puoi eseguire nuovamente l’applicazione e vedere come viene eseguita la convalida.
Esecuzione dell’applicazione e verifica del funzionamento della convalida
1. Nel menu Debug, fai clic su Avvia senza eseguire debug per costruire ed eseguire
l’applicazione.
2. Nella casella combinata Privilege Level, fai clic su Premium.
3. Imposta il controllo di scorrimento Tickets a 5.
Il metodo CheckPrivilegeAndNumberOfTickets nella classe TicketOrder genera un’eccezione
perché il livello di privilegi e il numero di biglietti non corrisponde. Il controllo di
scorrimento Tickets viene evidenziato con un bordo di colore rosso. Posiziona il puntatore
del mouse su Tickets e verifica che venga visualizzato il messaggio “Too many tickets for this
privilege level”.
4. Nella casella combinata Privilege Level, fai clic su Executive.
Anche se ora il livello di privilegi è sufficiente per consentire al cliente di ordinare 5 biglietti,
il controllo resta evidenziato.
5. Imposta il controllo di scorrimento Tickets a 6.
Verifica che l’evidenziatura rossa scompaia. La convalida si verifica solo quando viene
modificato il valore nel controllo, non quando viene cambiato il valore di un controllo
diverso.
6. Nella casella combinata Privilege Level, fai clic su Standard.
La casella combinata viene evidenziata. Se posizioni il cursore del mouse sulla casella
combinata, dovrebbe comparire il messaggio “Privilege level too low for this number of
tickets”.
7. Imposta il controllo di scorrimento Tickets a 5.
Il controllo di scorrimento viene evidenziato.
8. Nel menu File, fai clic su Purchase.
Compare una finestra di messaggio che visualizza il livello di privilegi (Standard) e il numero
di biglietti (5) per l’ordine. Inoltre, l’evento e il numero di riferimento del cliente sono
entrambi vuoti. Nonostante il form contenga dati errati o mancanti, puoi comunque salvare
le informazioni presenti.
Capitolo 24
Esecuzione della convalida
527
9. Fai clic su OK, quindi digita C1234 nella casella di testo customerReference, ma non fare clic
al di fuori di questa casella di testo.
10. Nel menu File, fai nuovamente clic su Purchase.
Nella finestra di messaggio non è incluso il numero di riferimento del cliente. Ciò è dovuto
al fatto che la casella di testo customerReference del form non è attiva. Come già visto
nelle esercitazioni precedenti, la convalida dell’associazione dei dati di una casella di testo
avviane solo quando l’utente fa clic su un altro controllo del form. Ciò accade anche per i
dati stessi; per impostazione predefinita, questi vengono copiati nell’oggetto orderDetails
solo quando il puntatore lascia la casella di testo. In realtà, la convalida viene avviata dalla
copia dei dati dal form all’oggetto orderDetails.
11. Fai clic su OK, quindi fai clic sulla casella combinata Event e seleziona Little Mudford Festival.
12. Nel menu File, fai clic su Purchase.
Questa volta, nella finestra di messaggio vengono visualizzati tutti i dettagli del form.
13. Fai clic su OK, chiudi l’applicazione e torna a Visual Studio 2010.
Da questo esercizio puoi notare che benché la convalida esegua un controllo incrociato corretto
dei controlli Privilege Level e Tickets, c’è ancora qualcosa da fare prima di poter utilizzare
l’applicazione.
Modifica del punto di esecuzione della convalida
Le difficoltà dell’applicazione consistono nel fatto che la convalida viene eseguita nel momento
errato, è applicata in modo incoerente e non impedisce all’utente di salvare dati incoerenti.
È pertanto necessario trovare un approccio alternativo per gestire la convalida. La soluzione
è di verificare i dati immessi dall’utente solo quando tenta di salvarli. In questo modo puoi
assicurarti che egli abbia effettivamente immesso tutti i dati e che questi siano coerenti. In caso
di problemi, puoi visualizzare un messaggio di errore e impedire il salvataggio delle informazioni
fino a quando gli errori non sono stati corretti. Nell’esercitazione che segue, apprenderai come
modificare l’applicazione per rinviare la convalida fino al momento in cui l’utente tenta di
acquistare i biglietti.
Convalida dei dati in modo esplicito
1. Torna alla finestra Progettazione relativa al file TicketForm.xaml. Nel riquadro XAML,
modifica l’associazione della casella combinata privilegeLevel e imposta la proprietà
UpdateSourceTrigger su “Explicit” come qui mostrato in grassetto:
<ComboBox ... Name="privilegeLevel" ...>
...
<ComboBox.Text>
<Binding Source="{StaticResource orderData}" Path="PrivilegeLevel"
Converter="{StaticResource privilegeLevelConverter}"
UpdateSourceTrigger="Explicit" >
...
528
Parte IV
Creazione di applicazioni WPF
</Binding>
</ComboBox.Text>
</ComboBox>
La proprietà UpdateSourceTrigger regola quando le informazioni immesse dall’utente
devono essere reinviate all’oggetto TicketOrder sottostante e convalidate. Impostando
questa proprietà su “Explicit”, puoi rimandare la sincronizzazione fino a quando
l’applicazione non la esegue in modo esplicito tramite codice.
2. Modifica le associazioni dei controlli eventList, customerReference e numberOfTickets per
impostare la proprietà UpdateSourceTrigger su “Explicit”:
<ComboBox ... Name="eventList" ... >
<ComboBox.Text>
<Binding Source="{StaticResource orderData}" Path="EventName"
UpdateSourceTrigger="Explicit" >
...
</Binding>
</ComboBox.Text>
...
</ComboBox>
...
<TextBox ... Name="customerReference" ... >
<TextBox.Text>
<Binding Source="{StaticResource orderData}" Path="CustomerReference"
UpdateSourceTrigger="Explicit" >
...
</Binding>
</TextBox.Text>
</TextBox>
...
<Slider ...Name="numberOfTickets" ...>
<Slider.Value>
<Binding Source="{StaticResource orderData}" Path="NumberOfTickets"
UpdateSourceTrigger="Explicit" >
...
</Binding>
</Slider.Value>
</Slider>
3. Visualizza il file TicketForm.xaml.cs nella finestra dell’editor di testo e di codice. Nel metodo
purchaseTickets_Click, aggiungi le istruzioni che seguono in grassetto all’inizio del metodo:
private void purchaseTickets_Click(object sender, RoutedEventArgs e)
{
BindingExpression eventBe =
eventList.GetBindingExpression(ComboBox.TextProperty);
BindingExpression customerReferenceBe =
customerReference.GetBindingExpression(TextBox.TextProperty);
BindingExpression privilegeLevelBe =
privilegeLevel.GetBindingExpression(ComboBox.TextProperty);
BindingExpression numberOfTicketsBe =
numberOfTickets.GetBindingExpression(Slider.ValueProperty);
...
}
Capitolo 24
Esecuzione della convalida
529
Queste istruzioni creano oggetti BindingExpression per ognuno dei tre controlli con
regole di convalida dell’associazione. Utilizzerai questi oggetti nel prossimo passaggio per
propagare i valori nel form all’oggetto TicketOrder e attivare le regole di convalida.
4. Aggiungi le seguenti istruzioni illustrate in grassetto al metodo purchaseTickets_Click, dopo
il codice che hai inserito nel passaggio precedente:
private void purchaseTickets_Click(object sender, RoutedEventArgs e)
{
...
eventBe.UpdateSource();
customerReferenceBe.UpdateSource();
privilegeLevelBe.UpdateSource();
numberOfTicketsBe.UpdateSource();
...
}
Il metodo UpdateSource della classe BindingExpression sincronizza i dati in un oggetto con
i controlli che fanno riferimento all’oggetto attraverso le associazioni. Invia i valori nelle
proprietà associate dei controlli sul form di nuovo all’oggetto TicketOrder. I dati vengono
convalidati anche in questo caso.
Queste istruzioni aggiunge nel passaggio aggiornano le proprietà dell’oggetto TicketOrder
con i valori immessi dall’utente nel form, quindi eseguono la convalida dei dati. La classe
BindingExpression fornisce una proprietà chiamata HasError che indica se il metodo
UpdateSource ha avuto esito positivo o se ha causato un’eccezione.
5. Aggiungi il codice qui mostrato in grassetto al metodo purchaseTickets_Click per verificare
la proprietà HasError di ogni oggetto BindingExpression e visualizzare un messaggio in caso
di errore nella convalida. Sposta il codice originale che visualizza i dettagli relativi al cliente
nella part else dell’istruzione if.
private void purchaseTickets_Click(object sender, RoutedEventArgs e)
{
...
if (eventBe.HasError || customerReferenceBe.HasError ||
privilegeLevelBe.HasError || numberOfTicketsBe.HasError)
{
MessageBox.Show("Please correct errors", "Purchase aborted");
}
else
{
Binding ticketOrderBinding =
BindingOperations.GetBinding(privilegeLevel, ComboBox.TextProperty);
TicketOrder ticketOrder = ticketOrderBinding.Source as TicketOrder;
MessageBox.Show(ticketOrder.ToString(), "Purchased");
}
}
530
Parte IV
Creazione di applicazioni WPF
Nuova verifica dell’applicazione
1. Nel menu Debug, fai clic su Avvia senza eseguire debug per costruire ed eseguire
l’applicazione.
2. Una volta visualizzato il form Ticket Orders, fai clic su Purchase nel menu File.
Verifica che venga visualizzata la finestra di messaggio Purchase aborted con il testo “Please
correct errors” e che i controlli Event, Customer Reference e Tickets siano evidenziati.
Nota Privilege Level non è evidenziato perché viene impostato al valore predefinito
Standard, come descritto precedentemente nel capitolo.
3. Fai clic su OK per tornare al form Ticket Orders. Sposta il cursore del mouse sopra ogni
controllo evidenziato, quindi verifica che i messaggi dell’eccezione ApplicationException
di ogni proprietà nell’oggetto selezionato TicketOrder vengano visualizzati come
suggerimenti.
4. Nella casella combinata Event, seleziona Little Mudford Festival. Nella casella di testo
Customer Reference digita C1234. Nella casella combinata Privilege Level, seleziona
Premium. Imposta il controllo di scorrimento Tickets su 8, quindi fai clic su Purchase nel
menu File.
Verifica che venga nuovamente visualizzata la finestra di messaggio Purchase aborted, ma
che questa volta venga evidenziato solo il controllo di scorrimento Tickets.
5. Fai clic su OK, poi sposta il cursore del mouse sopra il controllo Tickets.
Verifica che il suggerimento visualizzi il messaggio “Too many tickets for this privilege level”.
6. Nella casella combinata Privilege Level seleziona Premium Executive, quindi nel menu File fai
clic su Purchase.
Verifica che ora venga visualizzata una finestra di messaggio Purchased con il testo “Event:
Little Mudford Festival Customer: C1234 Privilege: PremiumExecutive Tickets: 8" e che
nessuno dei controlli del form sia evidenziato. Questa volta i dati sono completi e coerenti.
7. Prova con altre combinazioni di valori, poi verifica che la convalida funzioni correttamente.
Al termine delle prove, chiudi il form e torna a Visual Studio.
In questo capitolo hai visto come eseguire alcune operazioni base di convalida utilizzando
l’elaborazione della regola di convalida predefinita fornita mediante l’associazione dei dati.
Hai appreso come definire regole di convalida personalizzate se desideri eseguire verifiche più
complesse.
Capitolo 24
Esecuzione della convalida
531
■ Se desideri continuare con il capitolo successivo
Continua a eseguire Visual Studio 2010 e passa al capitolo 25.
■ Se desideri uscire ora da Visual Studio 2010
Nel menu File, fai clic su Esci. Se sullo schermo è visualizzata una finestra di dialogo Salva, fai
clic su Sì per salvare il progetto.
Riferimenti rapidi del capitolo 24
Obiettivo
Azione
Uso dell’associazione dei dati per
abbinare una proprietà di un controllo in un form a una proprietà di
un altro controllo sullo stesso form
Nel codice XAML della proprietà del controllo crea un’associazione. Fai
riferimento al controllo contenente la proprietà da associare utilizzando
il tag ElementName e la proprietà da associare utilizzando il tag Path. Ad
esempio:
<TextBox ...>
<TextBox.Text>
<Binding ElementName="numberOfTickets"
Path="Value" />
</TextBox.Text>
</TextBox>
Uso dell’associazione dei dati per
abbinare una proprietà di un controllo in un form a una proprietà di
un oggetto
Nel codice XAML della proprietà del controllo, specifica un origine di associazione che identifichi l’oggetto e il nome della proprietà dell’oggetto
cui desideri associare la proprietà. Ad esempio:
Abilitazione di un’associazione dei
dati per convalidare i dati immessi
dall’utente
Specifica l’elemento Binding.ValidationRules come parte dell’associazione.
Ad esempio:
<TextBox ...>
<TextBox.Text>
<Binding Source="{StaticResource orderData}"
Path="ForeName" />
</TextBox.Text>
</TextBox>
<Binding Source="{StaticResource orderData}"
Path="ForeName" />
<Binding.ValidationRules>
<ExceptionValidationRule/>
</Binding.ValidationRules>
</Binding>
532
Parte IV
Creazione di applicazioni WPF
Obiettivo
Azione
Visualizzazione delle informazioni
relative a un errore in modo discreto
Definisci uno stile che rilevi i cambiamenti nella proprietà Validation.
HasError del controllo, quindi imposta il messaggio restituito dall’eccezione nella proprietà ToolTip del controllo. Applica questo stile a tutti i controlli che richiedono l’esecuzione della convalida. Ad esempio:
<Style x:Key="errorStyle" TargetType="Control">
<Style.Triggers>
<Trigger Property="Validation.HasError"
Value="True">
<Setter Property="ToolTip"
Value="{Binding RelativeSource=
{x:Static RelativeSource.Self},
Path=(Validation.Errors)[0].ErrorContent}" />
</Trigger>
</Style.Triggers>
</Style>
Convalida di tutti i controlli di un
form mediante codice di controllo
invece che quando l’utente si sposta da un controllo all’altro
Nel codice XAML di associazione, imposta la proprietà
UpdateSourceTrigger dell’associazione su “Explicit” per rimandare la convalida fino a quando non viene richiesta dall’applicazione. Per convalidare i dati presenti in tutti i controlli, crea un oggetto BindingExpression
per ciascuna proprietà associata di ogni controllo, quindi richiama il
metodo UpdateSource. Esamina la proprietà HasError di ogni oggetto
BindingExpression. Se la proprietà ha il valore true, la convalida non è riuscita.
Parte V
Gestione dei dati
In questa parte:
Interrogazione delle informazioni in un database . . . . . . . . . . . . . . . . . . . . . . . . 535
Visualizzazione e modifica dei dati mediante
Entity Framework e associazione dei dati . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 565
533
Capitolo 25
Interrogazione delle informazioni in
un database
Gli argomenti trattati in questo capitolo sono:
■
Richiamo e visualizzazione di dati provenienti da un database Microsoft SQL Server
mediante Microsoft ADO.NET.
■
Definizione delle classi entità per la memorizzazione dei dati recuperati da un database.
■
Utilizzo di LINQ to SQL per interrogare un database e popolare le istanze delle classi entità.
■
Creazione di una classe DataContext personalizzata per accedere a un database in modo
sicuro.
Nella Parte IV di questo manuale, “Creazione di applicazioni Windows Presentation Foundation”,
hai appreso come utilizzare Microsoft Visual C# per creare interfacce utente e presentare e
convalidare le informazioni. Nella Parte V, potrai apprendere come gestire i dati utilizzando
le funzioni di accesso ai dati disponibili in Microsoft Visual Studio 2010 e Microsoft .NET
Framework. I capitoli in questa parte del manuale descrivono ADO.NET, una libreria di oggetti
progettata appositamente per facilitare la creazione di applicazioni che utilizzano i database.
In questo capitolo, potrai apprendere anche come interrogare i dati mediante LINQ to SQL,
estensioni di LINQ basate su ADO.NET progettate per il recupero di dati da un database. Nel
capitolo 26, “Visualizzazione e modifica dei dati mediante Strumentazione di entità Framework e
associazione”, sono disponibili ulteriori informazioni sull’aggiornamento dei dati mediante ADO.
NET e LINQ to SQL.
Importante Per eseguire gli esercizi di questo capitolo, devi aver installato Microsoft SQL
Server 2008 Express. Questo software è disponibile come materiale di riferimento di Microsoft
Visual Studio 2010 e Visual C# 2010 Express.
Importante Per eseguire le esercitazioni di questo capitolo e del resto del manuale, si raccomanda di utilizzare un account con privilegi di amministrazione.
Interrogazione di un database mediante ADO.NET
La libreria di classi ADO.NET contiene una struttura completa per la creazione di applicazioni
con il compito di recuperare e aggiornare i dati memorizzati in un database relazionale. Il
modello definito da ADO.NET si basa sul concetto di provider di dati. Ogni sistema di gestione
di database, ad esempio SQL Server, Oracle, IBM DB2 e così via, dispone di un proprio provider
535
536
Parte V
Gestione dei dati
di dati che implementa una separazione tra i meccanismi di connessione al database, l’invio di
query e l’aggiornamento dei dati. Grazie a queste separazioni è possibile creare codice portabile
indipendente dal sistema di gestione del database sottostante. In questo capitolo, apprenderai
come connetterti a un database gestito da SQL Server 2008 Express Edition, ma ricorda che le
tecniche utilizzate per farlo sono ugualmente valide anche per sistemi di gestione di database
differenti.
Il database Northwind
Northwind Traders è una società immaginaria che vende prodotti alimentari con nomi esotici.
Il database Northwind contiene numerose tabelle con informazioni su prodotti distribuiti da
Northwind Traders, acquirenti di tali prodotti e loro ordini, fornitori da cui viene acquistata la
merce da rivendere, spedizionieri usati per il trasporto dei prodotti ai clienti e dipendenti della
società. Nella prossima immagine vengono mostrate tutte le tabelle presenti nel database
Northwind e le relazioni presenti tra loro. Le tabelle utilizzate in questo capitolo sono Orders e
Products.
Creazione del database
Prima di procedere oltre, devi creare il database Northwind.
Creazione del database Northwind
1. Nel menu Start di Windows, fai clic su Programmi, Accessori, fai clic con il pulsante destro
del mouse su Prompt dei comandi, quindi seleziona Esegui come amministratore.
Capitolo 25
Interrogazione delle informazioni in un database
537
Se hai effettuato l’accesso con un account che ha i diritti di amministratore, nella finestra di
dialogo Controllo account utente, fai clic su Sì.
Se hai effettuato l’accesso con un account che non ha i diritti di amministratore, nella
finestra di dialogo Controllo account utente inserisci la password dell’amministratore, quindi
fai clic su Sì.
Viene visualizzata la finestra del prompt dei comandi eseguita come amministratore.
2. Nella finestra del prompt dei comandi, digita il comando che segue:
sqlcmd -S.\SQLExpress -E
Questo comando avvia l’utilità sqlcmd per la connessione all’istanza locale di SQL Server
2008 Express. Dovrebbe comparire un prompt “1>”.
Suggerimento Assicurati che SQL Server 2008 Express sia in esecuzione prima di tentare
di provare a eseguire l’utilità sqlcmd. Per impostazione predefinita, il programma è configurato per essere avviato automaticamente. Qualora non sia in stato avviato, riceverai un
messaggio di errore non appena eseguito il comando sqlcmd. Per verificare lo stato di SQL
Server 2008 Express e avviarlo se necessario, puoi utilizzare Gestione configurazione SQL
Server disponibile nella cartella Strumenti di configurazione del gruppo di programmi di
Microsoft SQL Server 2008.
3. Al prompt 1> digita il seguente comando includendo le parentesi quadre, poi premi
Invio. Sostituisci computer con il nome del tuo computer, e login con il nome dell’account
utilizzato per accedere a Windows.
CREATE LOGIN [computer\login] FROM WINDOWS
Dovrebbe comparire un prompt “2>”.
4. Al prompt 2>, digita GO, quindi premi Invio.
SQL Server tenta di creare un accesso per l’account utente in modo che possa essere creato
il database Northwind. Se il comando è corretto, dovrebbe comparire il prompt “1>”. Se
il comando visualizza il messaggio “The server principal ‘computer\login’ already exists.”,
hai già un accesso SQL Server, quindi puoi ignorare il messaggio. Se il comando visualizza
un qualunque altro messaggio, controlla di aver specificato i valori corretti per computer e
login, quindi ripeti i passaggi 3 e 4.
5. Al prompt 1>, digita il seguente comando, quindi premi Invio (come prima sostituisci
computer con il nome del tuo computer e login con il nome dell’account utilizzato per
accedere a Windows).
GRANT CREATE DATABASE TO [computer\login]
6. Al prompt 2>, digita GO, quindi premi Invio.
7. Al prompt 1>, digita EXIT, quindi premi Invio.
Questo comando consente di uscire dall’utilità sqlcmd e di tornare al prompt dei comandi di
Windows.
538
Parte V
Gestione dei dati
8. Chiudi la finestra del prompt dei comandi.
9. Nel menu Start di Windows, fai clic su Programmi, Accessori, quindi su Prompt dei comandi.
In questo modo si apre una finestra del prompt dei comandi con le credenziali personali
invece che con quelle dell’amministratore.
10. Nella finestra del prompt dei comandi digita il seguente comando per accedere alla
cartella \Microsoft Press\Visual CSharp Step by Step\Chapter 25 all’interno della tua cartella
Documenti. Sostituisci Nome con il tuo nome utente.
cd "\Users\Name\Documents\Microsoft Press\Visual CSharp Step By Step\Chapter 25"
11. Nella finestra del prompt dei comandi, digita il comando che segue:
sqlcmd -S.\SQLExpress -E -iinstnwnd.sql
Questo comando utilizza l’utilità sqlcmd per eseguire lo script instnwnd.sql. Questo script
contiene i comandi SQL che creano il database Northwind Traders e le tabelle del database,
inoltre le riempie con alcuni dati di esempio.
12. Al termine dell’esecuzione dello script, chiudi la finestra del prompt dei comandi.
Nota Il comando indicato nel punto 11 può essere eseguito ogni volta che desideri ripri-
stinare il database Northwind Traders alle condizioni originali. Lo script instnwnd.sql elimina automaticamente il database presente e lo ricrea.
Utilizzo di ADO.NET per interrogare le informazioni relative a
un ordine
Nel prossimo gruppo di esercitazioni apprenderai come creare il codice che permette di accedere
al database Northwind e visualizzare le informazioni in una semplice applicazione console. Lo
scopo dell’esercitazione è di aiutarti a saperne di più su ADO.NET e comprendere il modello di
oggetto implementato. Nelle esercitazioni successive utilizzerai LINQ to SQL per interrogare il
database. Nel capitolo 26 potrai vedere come utilizzare le procedure guidate fornite con Visual
Studio 2010 per generare il codice che recupera e aggiorna i dati e li visualizza graficamente in
un’applicazione WPF (Windows Presentation Foundation).
L’applicazione che dovrai creare produrrà per prima cosa un semplice report contenente le
informazioni relative agli ordini dei clienti. Il programma chiederà l’ID del cliente all’utente, quindi
ne visualizzerà gli ordini.
Connessione al database
1. Avvia Visual Studio 2010 se non è già in esecuzione.
Capitolo 25
Interrogazione delle informazioni in un database
539
2. Crea un nuovo progetto chiamato ReportOrders utilizzando il modello Applicazione console.
Salvalo nella cartella \Microsoft Press\Visual CSharp Step By Step\Chapter 25 all’interno della
tua cartella Documenti.
Nota Ricorda che se utilizzi Visual C# 2010 Express puoi specificare il percorso per salvare
il progetto quando lo salvi utilizzando Salva ReportOrders nel menu File.
3. In Esplora soluzioni, fai clic con il pulsante destro del mouse sul file Program.cs e rinominalo
Report.cs. Nel messaggio Microsoft Visual Studio, fai clic su Sì per cambiare tutti i
riferimenti della classe Program in Report.
4. Nella finestra dell’editor di codice e di testo, aggiungi le istruzioni using che seguono
all’elenco all’inizio del file Report.cs:
using System.Data;
using System.Data.SqlClient;
Lo spazio dei nomi System.Data contiene molti tipi utilizzati da ADO.NET. Lo spazio dei
nomi System.Data.SqlClient contiene le classi del provider di dati di SQL Server per ADO.
NET. Queste classi sono versioni specializzate delle classi di ADO.NET, ottimizzate per
operare con SQL Server.
5. Nel metodo Main della classe Report, aggiungi l’istruzione qui mostrata in grassetto, la
quale dichiara un oggetto SqlConnection:
static void Main(string[] args)
{
SqlConnection dataConnection = new SqlConnection();
}
SqlConnection è una sottoclasse della classe Connection di ADO.NET ed è progettata per
gestire le connessioni con i database SQL Server.
6. Dopo la dichiarazione di variabile, aggiungi un blocco try/catch al metodo Main come
mostrato di seguito in grassetto. Tutto il codice necessario per ottenere l’accesso al
database deve rientrare nella parte try di questo blocco. Nel blocco catch, aggiungi un
semplice gestore per rilevare le eccezioni SqlException.
static void Main(string[] args)
{
...
try
{
// You will add your code here in a moment
}
catch (SqlException e)
{
Console.WriteLine("Error accessing the database: {0}", e.Message);
}
}
540
Parte V
Gestione dei dati
In caso di errore durante l’accesso a un database SQL Server viene generata un’eccezione
SqlException.
7. Sostituisci il commento nel blocco try con il codice qui mostrato in grassetto:
try
{
SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder();
builder.DataSource = ".\\SQLExpress";
builder.InitialCatalog = "Northwind";
builder.IntegratedSecurity = true;
dataConnection.ConnectionString = builder.ConnectionString;
}
Per connettere un database SQL Server, devi costruire una stringa di connessione che
specifica il database a cui connettersi, l’istanza di SQL Server in cui si trova il database
e il modo in cui l’applicazione si identificherà come utente valido del database in SQL
Server. Il modo più semplice per farlo è utilizzare un oggetto SqlConnectionStringBuilder.
La classe SqlConnectionStringBuilder espone le proprietà di ogni elemento della stringa di
connessione. Puoi poi leggere una stringa di connessione completa che combina tutti questi
elementi nel formato corretto dalla proprietà ConnectionString.
Questo codice utilizza un oggetto SqlConnectionStringBuilder per creare una stringa di
connessione per accedere al database Northwind eseguito nell’istanza di SQL Server Express
del computer. Il codice specifica che la connessione utilizzerà Autenticazione di Windows
per la connessione al database. Questo è il metodo di accesso preferito perché evita di
dover chiedere nome o password dell’utente, oppure di immettere tali informazioni nel
codice dell’applicazione.
La stringa di connessione viene salvata nella proprietà ConnectionString dell’oggetto
SqlConnection, che utilizzerai nel prossimo passaggio.
Se necessario, nella stringa di connessione puoi immettere anche molti altri elementi
utilizzando la classe SqlConnectionStringBuilder; le proprietà mostrate in questo esempio
sono l’insieme minimo sufficiente. Per ulteriori informazioni, consulta la documentazione
fornita con Visual Studio 2010.
8. Aggiungi l’istruzione seguente mostrata in grassetto al codice nel blocco try:
try
{
...
dataConnection.Open();
}
Questa istruzione utilizza la stringa di connessione specificata dalla proprietà
ConnectionString dell’oggetto dataConnection per aprire una connessione al database.
Se la connessione riesce, puoi utilizzare l’oggetto dataConnection per eseguire i comandi
del database e le query. Se la connessione non riesce, l’istruzione genera un’eccezione
SqlException.
Capitolo 25
Interrogazione delle informazioni in un database
541
Utilizzo dell’autenticazione di SQL Server
Autenticazione di Windows è utile per autenticare gli utenti che sono membri di un
dominio Windows. Tuttavia, possono esservi occasioni in cui l’utente che accede al database
non dispone di un account Windows, ad esempio quando stai creando un’applicazione
progettata per l’accesso da parte di utenti remoti attraverso Internet. In questi casi puoi
utilizzare i parametri User ID e Password come mostrato di seguito:
string userName = ...;
string password = ...;
// Prompt the user for his name and password, and fill these variables
string connString = String.Format(
"User ID={0};Password={1};Initial Catalog=Northwind;" +
"Data Source=YourComputer\\SQLExpress", username, password);
myConnection.ConnectionString = connString;
A questo punto è utile considerare il seguente avvertimento: non inserire mai nomi e
password utente nel codice delle applicazioni. Chiunque sia in grado di ottenere una copia
del codice sorgente o di decompilare il codice eseguibile può vedere queste informazioni, e
ciò vanifica ogni sforzo per garantire la sicurezza dei dati.
Il punto successivo consiste nel chiedere l’ID del cliente all’utente, quindi interrogare il database
per trovare tutti i suoi ordini.
Interrogazione della tabella Orders
1. Aggiungi le istruzioni qui mostrate in grassetto al blocco try dopo l’istruzione
dataConnection.Open();:
try
{
...
Console.Write("Please enter a customer ID (5 characters): ");
string customerId = Console.ReadLine();
}
Queste istruzioni permettono di chiedere all’utente l’ID del cliente e passano la risposta alla
variabile stringa customerId.
542
Parte V
Gestione dei dati
2. Immetti le istruzioni qui mostrate in grassetto dopo il codice appena aggiunto:
try
{
...
SqlCommand dataCommand = new SqlCommand();
dataCommand.Connection = dataConnection;
dataCommand.CommandType = CommandType.Text;
dataCommand.CommandText =
"SELECT OrderID, OrderDate, ShippedDate, ShipName, ShipAddress, " +
"ShipCity, ShipCountry " +
"FROM Orders WHERE CustomerID = @CustomerIdParam";
}
La prima istruzione crea un oggetto SqlCommand. Come SqlConnection, questa è una
versione specializzata della classe Command di ADO.NET, progettata per eseguire le query
nei database SQL Server. Gli oggetti Command di ADO.NET vengono impiegati per eseguire
un comando su un’origine dati. Nel caso dei database relazionali, il testo del comando è
un’istruzione SQL.
La seconda riga di codice imposta la proprietà Connection dell’oggetto SqlCommand in
modo da impiegare la connessione al database aperta nell’esercitazione precedente. Le due
istruzioni successive specificano che l’oggetto SqlCommand contiene il testo di un’istruzione
SQL (puoi anche specificare il nome di una stored procedure o il nome di una singola
tabella del database) e popolano la proprietà CommandText con un’istruzione SQL SELECT
che recupera le informazioni dalla tabella Orders per tutti gli ordini con un CustomerID
specificato. Il testo @CustomerIdParam è un segnaposto per un parametro SQL (il simbolo
@ indica al provider di dati che questo è un parametro e non il nome di una colonna del
database). Il valore di CustomerID sarà passato come oggetto SqlParameter nel prossimo
passaggio.
3. Aggiungi l’istruzione seguente mostrata in grassetto al codice nel blocco try, dopo il codice
inserito nel passaggio precedente:
try
{
...
SqlParameter param = new SqlParameter("@CustomerIdParam", SqlDbType.Char, 5);
param.Value = customerId;
dataCommand.Parameters.Add(param);
}
Queste istruzioni creano un oggetto SqlParameter che può essere sostituito per @
CustomerIdParam quando l’oggetto SqlCommand viene eseguito. Il parametro viene
contrassegnato come database di tipo Char (l’equivalente SQL Server di una stringa
a lunghezza fissa), e la lunghezza di questa stringa viene specificata come 5 caratteri.
SqlParameter viene popolato con la stringa inserita dall'utente nella variabile customerId,
quindi aggiunta all‘insieme Parameter di SqlCommand. Quando SQL Server esegue questo
comando, esaminerà l’insieme Parameters del comando per un parametro denominato @
Capitolo 25
Interrogazione delle informazioni in un database
543
CustomerIdParam, quindi sostituirà il valore di questo parametro nel testo dell’istruzione
SQL.
Importante Se ti dedichi da poco alla creazione di applicazioni di database, ti chiederai
perché il codice crea un oggetto SqlParameter e non crea semplicemente un’istruzione
SQL che incorpora il valore della variabile customerId, come in questo caso:
dataCommand.CommandText =
"SELECT OrderID, OrderDate, ShippedDate, ShipName, ShipAddress, " +
"ShipCity, ShipCountry " +
"FROM Orders WHERE CustomerID = '" + customerId + "'";
Questo approccio rende le applicazioni vulnerabili agli attacchi SQL injection. Si sconsiglia
tuttavia di scrivere codice come questo per le applicazioni di produzione. Per una
descrizione di cosa sia un attacco SQL injection e di quanto possa essere pericoloso,
consulta l’argomento Attacco intrusivo nel codice SQL in SQL Server Books Online
all’indirizzo http://msdn2.microsoft.com/it-it/library/ms161953.aspx.
4. Aggiungi le istruzioni qui mostrate in grassetto dopo il codice appena immesso:
try
{
...
Console.WriteLine("About to find orders for customer {0}\n\n", customerId);
SqlDataReader dataReader = dataCommand.ExecuteReader();
}
Il metodo ExecuteReader di un oggetto SqlCommand crea un oggetto SqlDataReader
che può essere impiegato per prelevare le righe identificate dall’istruzione SQL. La classe
SqlDataReader fornisce il meccanismo più rapido tra quelli disponibili per il recupero dei
dati da SQL Server.
L’operazione che segue esegue l’iterazione tra tutti gli ordini (se presenti) e li visualizza.
Prelievo dei dati e visualizzazione degli ordini
1. Nel file Report.cs, aggiungi il ciclo while qui mostrato in grassetto dopo l’istruzione che crea
l’oggetto SqlDataReader:
try
{
...
while (dataReader.Read())
{
// Code to display the current row
}
}
Il metodo Read della classe SqlDataReader preleva la riga successiva dal database.
Restituisce true se era stata recuperata correttamente un’altra riga, altrimenti restituisce
544
Parte V
Gestione dei dati
false, in genere perché non ci sono altre righe. Il ciclo while appena aggiunto continua
a leggere i dati dalla variabile dataReader e termina quando non vi sono altre righe da
leggere.
2. Aggiungi le istruzioni qui mostrate in grassetto al corpo del ciclo while creato nel punto
precedente:
while (dataReader.Read())
{
int orderId = dataReader.GetInt32(0);
DateTime orderDate = dataReader.GetDateTime(1);
DateTime shipDate = dataReader.GetDateTime(2);
string shipName = dataReader.GetString(3);
string shipAddress = dataReader.GetString(4);
string shipCity = dataReader.GetString(5);
string shipCountry = dataReader.GetString(6);
Console.WriteLine(
"Order: {0}\nPlaced: {1}\nShipped: {2}\n" +
"To Address: {3}\n{4}\n{5}\n{6}\n\n", orderId, orderDate,
shipDate, shipName, shipAddress, shipCity, shipCountry);
}
Questo blocco di codice mostra come leggere i dati dal database utilizzando un oggetto
SqlDataReader. L’oggetto SqlDataReader contiene l’ultima riga prelevata dal database. Per
estrarre le informazioni da ciascuna colonna della riga puoi utilizzare i metodi GetXXX; nota
che è disponibile un metodo GetXXX per ogni tipo di dati comune. Ad esempio, per leggere
un valore int puoi usare il metodo GetInt32, per una stringa puoi usare il metodo GetString,
ed è facile indovinare quale metodo usare per leggere un valore DateTime. I metodi GetXXX
accettano un parametro indicante la colonna da leggere: 0 è la prima colonna, 1 è la
seconda, e così via. Il codice precedente legge le varie colonne dalla riga Orders corrente,
memorizza i valori in un set di variabili e infine ne stampa i valori.
I cursori firehose
Uno dei principali svantaggi delle applicazioni database multiutente è i blocco dei dati.
Sfortunatamente, è comune vedere applicazioni che recuperano righe di dati da un
database e le tengono bloccate per impedire ad altri utenti di modificarne le informazioni
mentre l’applicazione li utilizza. In alcuni casi estremi, l’applicazione può persino impedire
agli altri utenti la lettura dei dati bloccati. Se l’applicazione recupera una grande quantità di
righe, essa blocca una parte proporzionalmente grande della tabella. In presenza di molti
utenti che eseguono la stessa applicazione contemporaneamente, questi possono finire
per dover aspettare che altre persone sblocchino i dati, con frustrazione e rallentamenti
conseguenti.
La classe SqlDataReader è stata progettata per eliminare questo tipo di problemi. Essa
preleva una riga alla volta senza creare blocchi di alcun tipo dopo averla letta. Questo
comportamento garantisce un notevole miglioramento della concorrenza nelle applicazioni.
La classe SqlDataReader a volte viene definita cursore firehose perché trova i dati il più
velocemente possibile. Il termine cursore è la traduzione dell’acronimo della definizione
inglese “CUrrent Set Of Rows”.
Capitolo 25
Interrogazione delle informazioni in un database
545
Una volta terminate le operazioni nel database, è buona norma chiudere la connessione e
rilasciare le risorse utilizzate.
Disconnessione da un database e verifica del funzionamento dell’applicazione
1. Aggiungi l’istruzione seguente mostrata in grassetto dopo il ciclo while nel blocco try:
try
{
...
while(dataReader.Read())
{
...
}
dataReader.Close();
}
Questa istruzione chiude l’oggetto SqlDataReader. Una volta terminato di utilizzarlo, si
consiglia di chiudere sempre l’oggetto SqlDataReader poiché ciò è necessario prima che
sia possibile utilizzare l’oggetto SqlConnection corrente per eseguire altri comandi. Inoltre,
questa operazione è consigliabile anche se l’unica azione successiva sarà la chiusura di
SqlConnection.
Nota Attivando MARS (Multiple Active Result Sets) con SQL Server 2008, è possibile aprire più oggetti SqlDataReader su un unico oggetto SqlConnection ed elaborare più set di
dati. MARS è disabilitato per impostazione predefinita. Per ulteriori informazioni su MARS
e su come attivarlo e utilizzarlo, consulta SQL Server 2008 Books Online.
2. Dopo il blocco catch, aggiungi il blocco finally che segue:
catch(SqlException e)
{
...
}
finally
{
dataConnection.Close();
}
Le connessioni di database sono risorse preziose, pertanto devi assicurarti che esse vengano
chiuse una volta finito di utilizzarle. L’inserimento di questa istruzione in un blocco finally
garantisce che la SqlConnection sarà chiusa anche in presenza di un’eccezione; ricorda che il
codice nel blocco finally viene eseguito dopo il termine del gestore catch.
546
Parte V
Gestione dei dati
Suggerimento Un approccio alternativo all’uso di un blocco finally consiste nel racchiudere il codice che crea l’oggetto SqlDataConnection in un’istruzione using, come mostrato
nell’esempio di codice che segue. Alla fine del blocco definito dall’istruzione using, l’oggetto SqlConnection viene chiuso automaticamente anche qualora sia stata rilevata un’eccezione:
using (SqlConnection dataConnection = new SqlConnection())
{
try
{
SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder();
...
}
catch (SqlException e)
{
Console.WriteLine("Error accessing the database: {0}", e.Message);
}
}
3. Nel menu Debug, fai clic su Avvia senza eseguire debug per costruire ed eseguire
l’applicazione.
4. Alla richiesta dell’ID del cliente, immetti l’ID VINET e premi INVIO.
Viene visualizzata l’istruzione SQL SELECT seguita dagli ordini del cliente specificato, come
mostrato nell’immagine che segue:
Spostati all’indietro nella finestra della console per vedere tutti i dati. Al termine, premi il
tasto Invio per chiudere la finestra di console.
5. Esegui nuovamente l’applicazione senza eseguire il debug e digita BONAP come ID del
cliente.
Vengono visualizzate alcune righe, ma poi si verifica un errore e sullo schermo appare
una finestra di messaggio con il testo “ReportOrders has stopped working”. Se compare il
Capitolo 25
Interrogazione delle informazioni in un database
547
messaggio “Do you want to send more information about the problem?”, fai clic sul pulsante
per cancellare.
Nella finestra di console appare il messaggio di errore “Unhandled Exception: System.Data.
SqlTypes.SqlNullValueException: Data is Null. This method or property cannot be called on
Null values”.
Il problema è causato dal fatto che i database relazionali consentono ad alcune colonne
di contenere dati null. Un valore null è simile a una variabile null in C#: non contiene alcun
valore, ma causa la generazione di un errore se tenti di leggerla. Nella tabella Orders, la
colonna ShippedDate può contenere un valore null se l’ordine non è ancora stato spedito.
Nota anche che questa è un’eccezione SqlNullValueException, e di conseguenza non viene
rilevata dal gestore SqlException.
6. Premi Invio per chiudere la finestra di console e tornare a Visual Studio 2010.
Chiusura delle connessioni
In molte delle applicazioni più vecchie, puoi notare la tendenza ad aprire una connessione
all’avvio dell’applicazione e a chiuderla solamente una volta terminata l’applicazione stessa.
Il motivo logico di questa strategia era che l’apertura e la chiusura delle connessioni ai
database erano operazioni costose in termini di tempo. Questa strategia aveva un forte
effetto sulla scalabilità delle applicazioni, poiché ogni utente che eseguiva l’applicazione
aveva una connessione aperta con il database per tutto il tempo di esecuzione del
programma, anche quando egli si recava a pranzo. La maggior parte dei database limita il
numero delle connessioni concorrenti permesse (a volte questo a causa della licenza, ma
in genere perché ogni connessione consuma risorse nel server di database, che non sono
infinite). Alla fine il database raggiunge il numero limite di utenti che possono operare
contemporaneamente.
La maggior parte dei provider di dati .NET Framework implementano l’uso di un pool
di connessione, come fa anche il provider SQL Server. In questo modo, le connessioni ai
database vengono create e gestite all’interno di un pool. Quando un’applicazione richiede
la connessione, il provider di accesso ai dati estrae la connessione disponibile successiva
dal pool. Quando l’applicazione chiude la connessione, questa viene restituita al pool e
resa disponibile per l’applicazione successiva che la richiede. Ciò significa che l’apertura e
la chiusura delle connessioni ai database non sono più operazioni dispendiose. La chiusura
di una connessione non disconnette il database, ma restituisce solamente la connessione al
pool. L’apertura di una connessione comporta solamente l’ottenimento di una connessione
già aperta dal pool. Pertanto, si consiglia di non trattenere le connessioni più a lungo del
necessario e di aprire la connessione quando serve, per poi chiuderla non appena si finisce
di utilizzarla.
548
Parte V
Gestione dei dati
Nota L’overload dal metodo ExecuteReader della classe SqlCommand, il quale crea un
SqlDataReader. Puoi specificare un parametro System.Data.CommandBehavior che chiuda automaticamente la connessione usata da SqlDataReader quando questo viene terminato come
mostrato di seguito:
SqlDataReader dataReader =
dataCommand.ExecuteReader(System.Data.CommandBehavior.CloseConnection);
Quando leggi i dati dall’oggetto SqlDataReader, dovresti verificare che essi non siano null.
La procedura per eseguire questa operazione è illustrata di seguito.
Gestione di valori null di un database
1. Nel metodo Main della classe Report, modifica il codice nel corpo del ciclo while
inserendovi il blocco if…else qui mostrato in grassetto:
while (dataReader.Read())
{
int orderId = dataReader.GetInt32(0);
if (dataReader.IsDBNull(2))
{
Console.WriteLine("Order {0} not yet shipped\n\n", orderId);
}
else
{
DateTime orderDate = dataReader.GetDateTime(1);
DateTime shipDate = dataReader.GetDateTime(2);
string shipName = dataReader.GetString(3);
string shipAddress = dataReader.GetString(4);
string shipCity = dataReader.GetString(5);
string shipCountry = dataReader.GetString(6);
Console.WriteLine(
"Order {0}\nPlaced {1}\nShipped{2}\n" +
"To Address {3}\n{4}\n{5}\n{6}\n\n", orderId, orderDate,
shipDate, shipName, shipAddress, shipCity, shipCountry);
}
}
L’istruzione if utilizza il metodo IsDBNull per determinare se la colonna ShippedDate (la
colonna 2 della tabella) è null. In caso affermativo, non viene fatto alcun tentativo di
prelevarne i dati o di prelevarli da altre colonne che dovrebbero anch’esse essere null se non
vi sono valori di ShippedDate; in caso contrario, le colonne vengono lette e stampate come
prima.
2. Compila ed esegui nuovamente l’applicazione.
3. Alla richiesta, digita l’ID cliente BONAP.
Questa volta non viene rilevato alcun errore, e viene visualizzato un elenco di ordini non
ancora evasi.
4. Al termine dell’applicazione, premi Invio per tornare a Visual Studio 2010.
Capitolo 25
Interrogazione delle informazioni in un database
549
Interrogazione di un database mediante LINQ to SQL
Nel campitolo 20, “Interrogazione dei dati in memoria mediante espressioni di query”, hai visto
come utilizzare LINQ per esaminare il contenuto di insiemi enumerabili presenti in memoria.
LINQ fornisce espressioni di interrogazione che impiegano una sintassi simile a quella di SQL per
eseguire query e generare un gruppo di risultati che possono essere consultati. Di conseguenza
non dovrebbe essere una sorpresa scoprire che esiste una forma estesa di LINQ, chiamata LINQ to
SQL, che permette di interrogare e manipolare il contenuto di un database. LINQ to SQL è basata
su ADO.NET e fornisce un alto livello di separazione, eliminando la necessità di gestire i dettagli
di creazione di un oggetto Command di ADO.NET, eseguire l’iterazione in un gruppo di risultati
restituiti da un oggetto DataReader o prelevare i dati colonna per colonna mediante i differenti
metodi GetXXX.
Definizione di una classe entità
Nel capitolo 20 viene dimostrato che l’uso di LINQ richiede che gli oggetti da interrogare siano
enumerabili; essi devono essere insiemi che implementano l’interfaccia IEnumerable. LINQ to
SQL può creare i propri insiemi enumerabili di oggetti basati su classi personalizzate e mappate
direttamente sulle tabelle di un database. Queste classi sono chiamate classi entità. Quando ti
connetti a un database ed esegui una query, LINQ to SQL può recuperare i dati identificati dalla
query e creare un’istanza di una classe entità per ogni riga prelevata.
Il modo migliore per spiegare l’uso di LINQ to SQL consiste nell’esaminare un esempio. La tabella
Products del database Northwind è composta da colonne in cui sono presenti informazioni
relative ai vari aspetti dei diversi prodotti commercializzati. La parte dello script instnwnd.sql
che hai eseguito nella prima esercitazione del capitolo contiene un’istruzione CREATE TABLE che
sembra simile a questa (nota però che alcuni vincoli, colonne e altri dettagli sono stati omessi):
CREATE TABLE "Products" (
"ProductID" "int" NOT NULL ,
"ProductName" nvarchar (40) NOT NULL ,
"SupplierID" "int" NULL ,
"UnitPrice" "money" NULL,
CONSTRAINT "PK_Products" PRIMARY KEY CLUSTERED ("ProductID"),
CONSTRAINT "FK_Products_Suppliers" FOREIGN KEY ("SupplierID")
REFERENCES "dbo"."Suppliers" ("SupplierID")
)
Puoi definire una classe entità corrispondente alla tabella Products come mostrato di seguito:
[Table(Name = "Products")]
public class Product
{
[Column(IsPrimaryKey = true, CanBeNull = false)]
public int ProductID { get; set; }
[Column(CanBeNull = false)]
public string ProductName { get; set; }
550
Parte V
Gestione dei dati
[Column]
public int? SupplierID { get; set; }
[Column(DbType = "money")]
public decimal? UnitPrice { get; set; }
}
La classe Product contiene una proprietà per ognuna delle colonne della tabella Products cui sei
interessato. Non è necessario specificare ogni colonna della tabella sottostante, ma le colonne
omesse non saranno prelevate durante l’esecuzione di una query basata su questa classe entità. I
punti importanti da notare sono gli attributi Table e Column.
L’attributo Table identifica la classe come una classe entità. Il parametro Name specifica il nome
della tabella corrispondente nel database. Se ometti il parametro Name, LINQ to SQL presume
che il nome della classe entità sia lo stesso della tabella corrispondente nel database.
L’attributo Column descrive il modo in cui la colonna della tabella Products è mappata sulla
proprietà della classe Product. L’attributo Column può accetta un numero di parametri. Quelli
mostrati in questo esempio e descritti nel seguente elenco sono i più comuni:
■ IsPrimaryKey indica che la proprietà fa parte della chiave principale. Se la tabella dispone
di una chiave primaria composita che copre più colonne, devi specificare il parametro
IsPrimaryKey per ogni proprietà corrispondente nella classe entità.
■ DbType specifica il tipo di colonna sottostante del database. In molti casi, LINQ to SQL può
rilevare i dati di una colonna del database e convertirli nella corrispondente proprietà della
classe entità, mentre in altre situazioni è necessario specificare personalmente la mappatura
del tipo di dati. Ad esempio, la colonna UnitPrice della tabella Products utilizza il tipo money
di SQL Server. La classe entità specifica la proprietà corrispondente come valore decimal.
Nota La mappatura predefinita per i dati di tipo money in SQL Server è sul tipo decimal
di una classe entità, pertanto il parametro DbType qui mostrato è ridondante. Tuttavia, è
stato lasciato per mostrare la sintassi.
■ Il parametro CanBeNull indica se la colonna del database può contenere valori null. Il valore
predefinito del parametro CanBeNull è true. Nota che le due proprietà della classe Product
che corrispondono alle colonne che permettono l’immissione di valori null nel database
(SupplierID e UnitPrice) sono definite come valori nullable nella classe entità.
Capitolo 25
Interrogazione delle informazioni in un database
551
Nota Puoi utilizzare LINQ to SQL anche per creare nuovi database e tabelle basati
sulle definizioni delle tue classi entità mediante il metodo CreateDatabase dell’oggetto
DataContext. LINQ to SQL utilizza la definizione del parametro DbType per specificare se
una colonna deve consentire valori null. Utilizzando LINQ to SQL per creare un nuovo database, è necessario specificare il supporto di valori null nelle colonne di ciascuna tabella
nel parametro DbType come mostrato di seguito:
[Column(DbType = "NVarChar(40) NOT NULL", CanBeNull = false)]
public string ProductName { get; set; }
...
[Column(DbType = "Int NULL", CanBeNull = true)]
public int? SupplierID { get; set; }
Come l’attributo Table, l’attributo Column fornisce un parametro Name che puoi
utilizzare per specificare il nome della colonna sottostante del database. Se ometti questo
parametro, LINQ to SQL presume che il nome della colonna sia lo stesso della proprietà
nella classe entità.
Creazione ed esecuzione di una query LINQ to SQL
Una volta definita una classe entità, puoi utilizzarla per prelevare e visualizzare i dati delle tabella
Products. Il codice che segue mostra le operazioni base da eseguire per questo scopo:
SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder();
builder.DataSource = ".\\SQLExpress";
builder.InitialCatalog = "Northwind";
builder.IntegratedSecurity = true;
DataContext db = new DataContext(builder.ConnectionString);
Table<Product> products = db.GetTable<Product>();
var productsQuery = from p in products
select p;
foreach (var product in productsQuery)
{
Console.WriteLine("ID: {0}, Name: {1}, Supplier: {2}, Price: {3:C}",
product.ProductID, product.ProductName,
product.SupplierID, product.UnitPrice);
}
Nota Ricorda che in questo contesto le parole chiave from, in e select sono identificatori di C# e
non elementi della sintassi SQL. Pertanto, esse devono essere immesse in caratteri minuscoli.
La classe DataContext è responsabile della gestione della relazione tra le classi entità e le tabelle
del database. Questa classe viene impiegata per stabilire una connessione con il database e
creare insiemi di classi entità. Il costruttore DataContext si aspetta come parametro una stringa
552
Parte V
Gestione dei dati
di connessione che specifichi il database che desideri utilizzare. Questa stringa connessione è
esattamente uguale alla stringa di connessione che si utilizzerebbe nella connessione tramite un
oggetto Connection di ADO.NET (la classe DataContext di fatto crea una connessione ADO.NET
nascosta).
Il metodo GetTable<TEntity> generico della classe DataContext si aspetta una classe entità come
parametro tipo TEntity. Questo metodo crea un insieme enumerabile basato su questo tipo e
restituisce l’insieme come tipo Table<TEntity>.
Su questo insieme è possibile eseguire le query LINQ to SQL. La query mostrata in questo
esempio preleva semplicemente ogni oggetto presente nella tabella Products.
Nota Per un riassunto delle nozioni già viste sulle espressioni di query LINQ, consulta il
capitolo 20.
L’istruzione foreach esegue un’iterazione tra i risultati della query e visualizza i dettagli relativi
a ciascun prodotto. L’immagine che segue mostra i risultati dell’esecuzione del codice. I prezzi
mostrati sono per ordine, non per singole voci.
L’oggetto DataContext controlla automaticamente la connessione al database, aprendola
immediatamente prima di prelevare la prima riga di dati nell’istruzione foreach, quindi
chiudendola una volta prelevata l’ultima riga.
La query LINQ to SQL mostrata nell’esempio precedente recupera ogni colonna per ogni riga
della tabella Products. In questo caso, puoi eseguire un’iterazione direttamente all’interno
dell’insieme products come mostrato di seguito:
Table<Product> products = db.GetTable<Product>();
foreach (Product product in products)
{
...
}
Capitolo 25
Interrogazione delle informazioni in un database
553
Quando viene eseguita l’istruzione foreach, l’oggetto DataContext crea un’istruzione SQL SELECT
che preleva tutti i dati dalla tabella Products. Se desideri recuperare una singola riga della tabella
Products, puoi richiamare il metodo Single della classe entità Products. Single è un metodo
estensione che accetta un metodo che identifica la riga cercata e la restituisce come istanza della
classe entità (invece che come insieme di righe di un insieme Table). Puoi specificare il parametro
metodo come espressione lambda. Se l’espressione lambda non identifica esattamente una
riga, il metodo Single restituisce un’eccezione InvalidOperationException. L’esempio di codice
che segue interroga il database Northwind in merito al prodotto il cui valore ProductID è 27.
Il valore restituito è un’istanza della classe Product, e l’istruzione Console.WriteLine stampa il
nome del prodotto. Come in precedenza, la connessione al database viene aperta e chiusa
automaticamente dall’oggetto DataContext.
Product singleProduct = products.Single(p => p.ProductID == 27);
Console.WriteLine("Name: {0}", singleProduct.ProductName);
Recupero differito e immediato
Un punto importante da sottolineare è che per impostazione predefinita LINQ to SQL recupera
i dati dal database solo quando lo richiedi, e non quando definisci una query LINQ to SQL o
crei un insieme Table. Questo comportamento è noto come recupero differito. Nell’esempio che
visualizza tutti i tipi di prodotto presenti nella tabella Products visto precedentemente, l’insieme
productsQuery viene popolato solo al momento dell’esecuzione del ciclo foreach. Questo modo
di procedere corrisponde a quello di LINQ durante l’interrogazione degli oggetti in memoria; così
facendo è sempre disponibile la versione più aggiornata dei dati, anche se questi cambiano dopo
aver eseguito l’istruzione che crea l’insieme enumerabile productsQuery.
All’avvio del ciclo foreach, LINQ to SQL crea ed esegue un’istruzione SQL SELECT derivata dalla
query LINQ to SQL che crea un oggetto DataReader di ADO.NET. Ogni iterazione del ciclo foreach
esegue i metodi GetXXX necessari per prelevare i dati della riga interessata. Una volta prelevata
ed elaborata l’ultima riga nel ciclo foreach, LINQ to SQL chiude la connessione con il database.
Il recupero differito assicura che dal database vengano prelevati solamente i dati realmente usati
dall’applicazione. Tuttavia, se accedi a un database eseguito su un’istanza remota di SQL Server, il
prelievo dei dati una riga alla volta non rappresenta l’utilizzo migliore dell’ampiezza di banda della
rete. In questo scenario, è possibile prelevare e inserire nella cache tutti i dati in un’unica richiesta
di rete forzando la valutazione immediata della query LINQ to SQL. Ciò è possibile richiamando i
metodi estensione ToList o ToArray, i quali prelevano i dati in un elenco o matrice quando definisci
la query LINQ to SQL come mostrato di seguito:
var productsQuery = from p in products.ToList()
select p;
554
Parte V
Gestione dei dati
In questo esempio di codice, productsQuery è ora un elenco enumerabile popolato con
informazioni provenienti dalla tabella Products. Quando esegui l’iterazione nei dati, LINQ to SQL li
recupera da questo elenco invece di inviare una richiesta di prelievo al database.
Unione di tabelle e creazione di relazioni
LINQ to SQL supporta l’operatore di query join per riunire e recuperare i dati correlati
memorizzati in più tabelle. Ad esempio, la tabella Products nel database Northwind contiene
l’ID del fornitore di ciascun prodotto. Pertanto, per conoscere il nome di ogni fornitore devi
interrogare la tabella Suppliers. La tabella Suppliers contiene la colonna CompanyName, la quale
specifica il nome della società fornitrice, e ContactName, la quale contiene il nome della persona
della società fornitrice che gestisce gli ordini di Northwind Traders.
Per definire una classe entità contenente le informazioni principali sul fornitore, puoi procedere
come mostrato di seguito (la colonna SupplierName nel database è obbligatoria, ma la colonna
ContactName consente i valori null):
[Table(Name = "Suppliers")]
public class Supplier
{
[Column(IsPrimaryKey = true, CanBeNull = false)]
public int SupplierID { get; set; }
[Column(CanBeNull = false)]
public string CompanyName { get; set; }
[Column]
public string ContactName { get; set; }
}
Fatto ciò puoi creare gli insiemi Table<Product> e Table<Supplier> e definire una query LINQ to
SQL per unire queste tabelle come mostrato di seguito:
DataContext db = new DataContext(...);
Table<Product> products = db.GetTable<Product>();
Table<Supplier> suppliers = db.GetTable<Supplier>();
var productsAndSuppliers = from p in products
join s in suppliers
on p.SupplierID equals s.SupplierID
select new { p.ProductName, s.CompanyName, s.ContactName };
Quando esegui un’iterazione nell’insieme productsAndSuppliers, LINQ to SQL esegue un’istruzione
SQL SELECT che unisce le tabelle Products e Suppliers del database in base alle loro colonne
SupplierID e preleva i dati.
Tuttavia, con LINQ to SQL puoi specificare le relazioni tra tabelle come parte della definizione
delle classi entità. LINQ to SQL può quindi prelevare automaticamente le informazioni relative
al fornitore per ogni prodotto, senza richiedere di creare un’istruzione join potenzialmente
complessa e soggetta a errori. Per tornare all’esempio di prodotti e fornitori, nel database
Capitolo 25
Interrogazione delle informazioni in un database
555
Northwind queste tabelle presentano una relazione molti-a-uno in cui ogni prodotto è fornito
da un unico fornitore, ma ogni fornitore può fornire più prodotti. Per descrivere questa relazione
in modo differente, una riga della tabella Product può fare riferimento a una singola riga della
tabella Suppliers attraverso le colonne SupplierID delle due tabelle, ma una riga della tabella
Suppliers può fare riferimento a un intero gruppo di righe nella tabella Products. LINQ to SQL
fornisce i tipi generici EntityRef<TEntity> e EntitySet<TEntity> che permettono di modellare il
tipo di relazione. Prendendo per prima la classe entità Product, puoi definire il lato “uno” della
relazione con la classe entità Supplier utilizzando il tipo EntityRef<Supplier> come qui mostrato in
grassetto:
[Table(Name = "Products")]
public class Product
{
[Column(IsPrimaryKey = true, CanBeNull = false)]
public int ProductID { get; set; }
...
[Column]
public int? SupplierID { get; set; }
...
private EntityRef<Supplier> supplier;
[Association(Storage = "supplier", ThisKey = "SupplierID", OtherKey = "SupplierID")]
public Supplier Supplier
{
get { return this.supplier.Entity; }
set { this.supplier.Entity = value; }
}
}
Il campo privato supplier è un riferimento a un’istanza della classe entità Supplier. La proprietà
pubblica Supplier fornisce l’accesso a questo riferimento. L’attributo Association specifica il modo
in cui LINQ to SQL deve localizzare e popolare i dati di questa proprietà. Il parametro Storage
identifica il campo private usato per memorizzare il riferimento all’oggetto Supplier. Il parametro
ThisKey indica quale proprietà della classe entità Product debba essere impiegata da LINQ to
SQL per localizzare il riferimento a Supplier per questo prodotto, mentre il parametro OtherKey
specifica quale proprietà della tabella Supplier debba essere usata da LINQ to SQL per trovare
la corrispondenza con il valore del parametro ThisKey. In questo esempio, le tabelle Product e
Supplier vengono unite tramite la proprietà SupplierID di entrambe le entità.
Nota In realtà il parametro Storage è facoltativo. Se lo specifichi, LINQ to SQL accede direttamente al corrispondente membro dati mentre viene popolato invece di passare attraverso la
funzione di accesso set. La funzione di accesso set è richiesta per applicazioni che riempiono
o modificano manualmente l’oggetto entità cui fa riferimento la proprietà EntityRef<TEntity>.
Nonostante in questo esempio il parametro Storage sia ridondante, la procedura raccomandata
richiede di includerlo comunque.
La funzione di accesso get della proprietà Supplier restituisce un riferimento all’entità Supplier
utilizzando la proprietà Entity del tipo EntityRef<Supplier>. La funzione di accesso set popola
questa proprietà con un riferimento a un’entità Supplier.
556
Parte V
Gestione dei dati
I vari aspetti della relazione possono essere definiti nella classe Supplier con il tipo
EntitySet<Product> come mostrato di seguito:
[Table(Name = "Suppliers")]
public class Supplier
{
[Column(IsPrimaryKey = true, CanBeNull = false)]
public int SupplierID { get; set; }
...
private EntitySet<Product> products = null;
[Association(Storage = "products", OtherKey = "SupplierID", ThisKey = "SupplierID")]
public EntitySet<Product> Products
{
get { return this.products; }
set { this.products.Assign(value); }
}
}
Suggerimento Per convenzione, i nomi di una classe entità e delle sue proprietà utilizzano
sostantivi singolari. L’eccezione a queste regola è che le proprietà EntitySet<TEntity> solitamente
prendono la forma plurale poiché rappresentano un insieme invece di una singola entità.
In questo caso puoi notare che il parametro Storage dell’attributo Association specifica il campo
privato EntitySet<Product>. Un oggetto EntitySet<TEntity> contiene un insieme di riferimenti a
entità. La funzione di accesso get della proprietà pubblica Products restituisce questo insieme.
La funzione di accesso set utilizza il metodo Assign della classe EntitySet<Product> per popolare
l’insieme.
Pertanto, l’impiego dei tipi EntityRef<TEntity> e EntitySet<TEntity> consente di definire le
proprietà che possono modellare una relazione uno-a-molti, ma come è possibile riempire
praticamente queste proprietà con i dati? La risposta è che LINQ to SQL li riempie quando esegue
il prelievo dei dati. Il codice che segue crea un’istanza della classe Table<Product> ed esegue
una query LINQ to SQL per prelevare i dettagli relativi a tutti i prodotti. Questo codice è simile al
primo esempio LINQ to SQL visto precedentemente. L’unica differenza si trova nel ciclo foreach
che visualizza i dati.
DataContext db = new DataContext(...);
Table<Product> products = db.GetTable<Product>();
var productsAndSuppliers = from p in products
select p;
foreach (var product in productsAndSuppliers)
{
Console.WriteLine("Product {0} supplied by {1}",
product.ProductName, product.Supplier.CompanyName);
}
Capitolo 25
Interrogazione delle informazioni in un database
557
Come in precedenza, l’istruzione Console.WriteLine legge il valore nella proprietà ProductName
dell’entità prodotto, ma accede anche all’entità Supplier e visualizza la proprietà CompanyName
da tale entità. L’immagine che segue mostra quanto viene visualizzato eseguendo questo codice:
Quando il codice preleva ciascuna entità Product, LINQ to SQL esegue una seconda query differita
per recuperare i dettagli del fornitore del prodotto, in modo da poter popolare la proprietà
Supplier in base alla relazione specificata dall’attributo Association di questa proprietà nella classe
entità Product.
Una volta definite le entità Product e Supplier come dotate di una relazione uno-a-molti, una
logica simile si applica se esegui una query LINQ to SQL sull’insieme Table<Supplier> come
mostrato di seguito:
DataContext db = new DataContext(...);
Table<Supplier> suppliers = db.GetTable<Supplier>();
var suppliersAndProducts = from s in suppliers
select s;
foreach (var supplier in suppliersAndProducts)
{
Console.WriteLine("Supplier name: {0}", supplier.CompanyName);
Console.WriteLine("Products supplied");
foreach (var product in supplier.Products)
{
Console.WriteLine("\t{0}", product.ProductName);
}
Console.WriteLine();
}
In questo caso, quando il ciclo foreach preleva un fornitore, esegue una seconda query differita
per recuperare tutti i prodotti di tale fornitore e popolare la proprietà Products. Tuttavia questa
volta la proprietà è un insieme EntitySet<Product>, pertanto puoi inserire nel codice un’istruzione
foreach nidificata per eseguire l’iterazione all’interno del set, visualizzando il nome di ciascun
prodotto. Una volta eseguito, questo codice visualizza quanto mostrato nell’immagine che segue:
558
Parte V
Gestione dei dati
Recupero differito e immediato, parte due
Più indietro in questo capitolo hai appreso che LINQ to SQL differisce il prelievo dei dati fino a
quando questi non sono realmente richiesti, ma che è possibile applicare i metodi estensione
ToList o ToArray per recuperare i dati immediatamente. Questa tecnica non ha effetto sui dati cui
si fa riferimento come proprietà EntitySet<TEntity> o EntityRef<TEntity>; infatti, anche utilizzando
i metodi ToList o ToArray, questi dati vengono prelevati solamente al momento dell’accesso. Per
forzare LINQ to SQL a interrogare e prelevare immediatamente i dati referenziati, puoi impostare
la proprietà LoadOptions dell’oggetto DataContext come mostrato di seguito:
DataContext db = new DataContext(...);
Table<Supplier> suppliers = db.GetTable<Supplier>();
DataLoadOptions loadOptions = new DataLoadOptions();
loadOptions.LoadWith<Supplier>(s => s.Products);
db.LoadOptions = loadOptions;
var suppliersAndProducts = from s in suppliers
select s;
La classe DataLoadOptions fornisce il metodo generico LoadWith. Questo metodo consente di
specificare se una proprietà EntitySet<TEntity> in un’istanza debba essere caricata nel momento
in cui l’istanza in questione viene popolata. Il parametro al metodo LoadWith è un’espressione
lambda che identifica i dati correlati da recuperare quando vengono prelevati i dati per una
tabella. L’esempio qui mostrato fa in modo che la proprietà Products di ogni entità Supplier venga
popolata non appena i dati di ciascuna entità Product vengono prelevati, senza alcun ritardo.
Se specifichi la proprietà LoadOptions dell’oggetto DataContext assieme ai metodi estensione
ToList o ToArray di un insieme Table, LINQ to SQL carica in memoria l’intero insieme e i dati
delle proprietà di riferimento per l’entità in tale insieme non appena la query LINQ to SQL viene
valutata.
Suggerimento Se disponi di più proprietà EntitySet<TEntity>, puoi richiamare più volte
il metodo LoadWith dello stesso oggetto LoadOptions specificando ogni volta la proprietà
EntitySet<TEntity> da caricare.
Capitolo 25
Interrogazione delle informazioni in un database
559
Definizione di una classe DataContext personalizzata
La classe DataContext fornisce la funzionalità che permette di gestire i database e le connessioni
ai database, creare classi entità ed eseguire comandi per recuperare e aggiornare i dati presenti
in un database. Nonostante sia possibile utilizzare la classe DataContext base fornita con .NET
Framework, è buona norma usare l’ereditarietà e definire una versione personalizzata che dichiari
i diversi insiemi Table<TEntity> come membri pubblici. Ad esempio, l’esempio che segue mostra
una classe DataContext specializzata che espone gli insiemi Products e Suppliers Table come
membri pubblici:
public class Northwind : DataContext
{
public Table<Product> Products;
public Table<Supplier> Suppliers;
public Northwind(string connectionInfo) : base(connectionInfo)
{
}
}
Nota che la classe Northwind fornisce anche un costruttore che accetta come parametro una
stringa di connessione. Puoi creare una nuova istanza della classe Northwind e quindi definire ed
eseguire le query LINQ to SQL sulle classi di insieme Table esposte come mostrato di seguito:
Northwind nwindDB = new Northwind(...);
var suppliersQuery = from s in nwindDB.Suppliers
select s;
foreach (var supplier in suppliersQuery)
{
...
}
Questo approccio rende più semplice la gestione del codice. Utilizzando un oggetto DataContext
ordinario, puoi creare un’istanza di qualsiasi classe entità mediante il metodo GetTable,
indipendentemente dal database cui si connette l’oggetto DataContext. In precedenza potevi
sapere di aver utilizzato l’oggetto DataContext errato e di avere eseguito la connessione al
database errato solo in fase di esecuzione, cioè al momento del tentativo di prelievo dei dati. Con
la classe DataContext personalizzata, puoi fare riferimento agli insiemi Table attraverso l’oggetto
DataContext. Nota che il costruttore DataContext base utilizza un meccanismo chiamato reflection
per esaminarne i membri, e crea automaticamente istanze per qualsiasi membro che sia un
insieme Table; i dettagli del funzionamento della funzione reflection esulano dello scopo di questo
manuale. Il database cui devi connetterti per recuperare i dati per una specifica tabella è ovvio; se
IntelliSense non visualizza la tabella quando definisci la query LINQ to SQL, hai chiaramente scelto
la classe DataContext errata e il codice non potrà essere compilato.
560
Parte V
Gestione dei dati
Utilizzo di LINQ to SQL per interrogare le informazioni
sull’ordine
Nell’esercitazione che segue dovrai scrivere una versione dell’applicazione console sviluppata
nell’esercitazione precedente e che chiede all’utente l’ID di un cliente e visualizza le informazioni
relative ai suoi ordini. Per fare ciò, utilizzerai LINQ to SQL per recuperare i dati. Potrai quindi
confrontare LINQ to SQL con il codice equivalente creato utilizzando ADO.NET.
Definizione della classe entità Order
1. In Visual Studio 2010, crea un nuovo progetto chiamato LINQOrders basato sul modello
Applicazione console. Salvalo nella cartella \Microsoft Press\Visual CSharp Step By Step\
Chapter 25 all’interno della tua cartella Documenti.
2. In Esplora soluzioni, cambia il nome del file Program.cs in LINQReport.cs. Nel messaggio
Microsoft Visual Studio, fai clic su Sì per cambiare tutti i riferimenti della classe Program in
LINQReport.
3. Nel menu Progetto, fai clic su Aggiungi riferimento. Nella finestra di dialogo Aggiungi
riferimento, fai clic sulla scheda .NET, seleziona l’assembly System.Data.Linq e fai clic su OK.
Questo assembly contiene i tipi e gli attributi LINQ to SQL.
4. Nella finestra dell’editor di codice e di testo, aggiungi le istruzioni using che seguono
all’elenco all’inizio del file:
using System.Data.Linq;
using System.Data.Linq.Mapping;
using System.Data.SqlClient;
5. Aggiungi la classe entità Order al file LINQReport.cs dopo la classe LINQReport.
Contrassegna la classe Order con l’attributo Table, come mostrato di seguito:
[Table(Name = "Orders")]
public class Order
{
}
Nel database Northwind, la tabella viene chiamata Orders. Ricorda che è buona norma
utilizzare sostantivi singolari per il nome delle classi entità, poiché un oggetto entità
rappresenta una riga di dati del database.
6. Aggiungi le proprietà qui mostrate in grassetto alla classe Order:
[Table(Name = "Orders")]
public class Order
{
[Column(IsPrimaryKey = true, CanBeNull = false)]
public int OrderID { get; set; }
}
La colonna OrderID è la chiave primaria di questa tabella del database Northwind.
Capitolo 25
Interrogazione delle informazioni in un database
561
7. Aggiungi le proprietà qui mostrate in grassetto alla classe Order:
[Table(Name = "Orders")]
public class Order
{
...
[Column]
public string CustomerID { get; set; }
[Column]
public DateTime? OrderDate { get; set; }
[Column]
public DateTime? ShippedDate { get; set; }
[Column]
public string ShipName { get; set; }
[Column]
public string ShipAddress { get; set; }
[Column]
public string ShipCity { get; set; }
[Column]
public string ShipCountry { get; set; }
}
Queste proprietà contengono l’ID del cliente, la data dell’ordine e le relative informazioni di
spedizione. Nel database, tutte queste colonne consentono valori null, quindi è importante
utilizzare la versione nullable del tipo DateTime per le proprietà OrderDate e ShippedDate
(nota che string è un tipo di riferimento che consente automaticamente valori null). Nota
che LINQ to SQL mappa automaticamente il tipo NVarChar di SQL Server sul tipo string di
.NET Framework e il tipo DateTime di SQL Server sul tipo DateTime di .NET Framework.
8. Aggiungi la classe Northwind che segue al file LINQReport.cs dopo la classe entità Order:
public class Northwind : DataContext
{
public Table<Order> Orders;
public Northwind(string connectionInfo)
: base (connectionInfo)
{
}
}
La classe Northwind è una classe DataContext che espone una proprietà Table basata sulla
classe entità Order. Nella prossima esercitazione utilizzerai la versione specializzata della
classe DataContext per accedere alla tabella Orders nel database.
562
Parte V
Gestione dei dati
Recupero delle informazioni relative a un ordine mediante una query LINQ to SQL
1. Nel metodo Main della classe LINQReport, aggiungi il codice qui mostrato in grassetto, che
crea un oggetto Northwind:
static void Main(string[] args)
{
SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder();
builder.DataSource = ".\\SQLExpress";
builder.InitialCatalog = "Northwind";
builder.IntegratedSecurity = true;
Northwind northwindDB = new Northwind(builder.ConnectionString);
}
La stringa di connessione costruita usando l’oggetto SqlConnectionStringBuilder
è esattamente stessa di quella incontrata nell’esercitazione precedente. L’oggetto
northwindDB utilizza questa stringa per connettersi al database Northwind.
2. Dopo il codice aggiunto nel passaggio precedente, aggiungi un blocco try/catch al metodo
Main:
static void Main(string[] args)
{
...
try
{
// You will add your code here in a moment
}
catch (SqlException e)
{
Console.WriteLine("Error accessing the database: {0}", e.Message);
}
}
Come avviene utilizzando normale codice ADO.NET, LINQ to SQL genera un’eccezione
SqlException quando si verifica un errore durante l’accesso a un database SQL Server.
3. Sostituisci il commento nel blocco try con il codice qui mostrato in grassetto:
try
{
Console.Write("Please enter a customer ID (5 characters): ");
string customerId = Console.ReadLine();
}
Queste istruzioni permettono di chiedere all’utente l’ID del cliente e di salvare la risposta
alla variabile stringa customerId.
4. Aggiungi l’istruzione qui mostrata in grassetto dopo il codice appena immesso:
try
{
...
var ordersQuery = from o in northwindDB.Orders
where String.Equals(o.CustomerID, customerId)
Capitolo 25
Interrogazione delle informazioni in un database
563
select o;
}
Questa istruzione definisce la query LINQ to SQL che recupera gli ordini del cliente
specificato.
5. Aggiungi l’istruzione foreach e il blocco if…else qui mostrati in grassetto dopo il codice
immesso nel passaggio precedente:
try
{
...
foreach (var order in ordersQuery)
{
if (order.ShippedDate == null)
{
Console.WriteLine("Order {0} not yet shipped\n\n", order.OrderID);
}
else
{
// Display the order details
}
}
}
L’istruzione foreach esegue un’iterazione negli ordini del cliente. Se il valore nella colonna
ShippedDate del database è null, la corrispondente proprietà nell’oggetto entità Order è
anch’essa null, quindi l’istruzione if restituisce il messaggio appropriato.
6. Sostituisci il commento nella parte else dell’istruzione if aggiunta nel punto precedente con
il codice qui mostrato in grassetto:
if (order.ShippedDate == null)
{
...
}
else
{
Console.WriteLine("Order: {0}\nPlaced: {1}\nShipped: {2}\n" +
"To Address: {3}\n{4}\n{5}\n{6}\n\n", order.OrderID,
order.OrderDate, order.ShippedDate, order.ShipName,
order.ShipAddress, order.ShipCity,
order.ShipCountry);
}
7. Nel menu Debug, fai clic su Avvia senza eseguire debug per costruire ed eseguire
l’applicazione.
8. Digita VINET nella finestra di console in cui appare il messaggio “Please enter a customer ID
(5 characters):”.
L’applicazione dovrebbe visualizzare l’elenco degli ordini di questo cliente. Al termine
dell’applicazione, premi Invio per tornare a Visual Studio 2010.
564
Parte V
Gestione dei dati
9. Esegui nuovamente l’applicazione. Questa volta digita BONAP alla richiesta di immettere
l’ID del cliente.
L’ordine finale di questo cliente non è ancora stato evaso e contiene un valore null nella
colonna ShippedDate. Verifica che l’applicazione rilevi e gestisca correttamente il valore null.
Al termine dell’applicazione, premi Invio per tornare a Visual Studio 2010.
In questo capitolo hai esaminato gli elementi base forniti da LINQ to SQL per interrogare le
informazioni contenute in un database. LINQ to SQL dispone di molte altre funzioni che è
possibile utilizzare nelle tue applicazioni, tra cui la capacità di modificare i dati e aggiornare un
database. Alcuni di questi aspetti di LINQ to SQL saranno esaminati nel prossimo capitolo.
■ Se desideri continuare con il capitolo successivo
Continua a eseguire Visual Studio 2010 e passa al capitolo 26.
■ Se desideri uscire ora da Visual Studio 2010
Nel menu File, fai clic su Esci. Se sullo schermo è visualizzata una finestra di dialogo Salva, fai
clic su Sì per salvare il progetto.
Riferimenti rapidi del capitolo 25
Obiettivo
Azione
Connessione a un database SQL
Server mediante ADO.NET
Crea un oggetto SqlConnection, impostane la proprietà ConnectionString
con i dettagli relativi al database da utilizzare e richiama il metodo Open.
Creazione ed esecuzione di una
query di database mediante ADO.
NET
Crea un oggetto SqlCommand. Impostane la proprietà Connection su
un oggetto SqlConnection valido. Imposta la proprietà CommandText su
un’istruzione SQL SELECT valida. Richiama il metodo ExecuteReader per
eseguire la query e creare un oggetto SqlDataReader.
Richiamo dei dati mediante un oggetto SqlDataReader di ADO.NET
Verifica che i dati non siano null mediante il metodo IsDBNull. In caso
affermativo, utilizza il metodo GetXXX appropriato (ad esempio GetString
o GetInt32) per recuperare i dati.
Definizione di una classe entità
Definisci una classe con proprietà pubbliche per ogni colonna. Alla definizione della classe aggiungi come prefisso l’attributo Table, specificando il
nome della tabella del database sottostante. Aggiungi l’attributo Column
come prefisso a ogni proprietà e specifica i parametri che indicano nome,
tipo e supporto di valori null della colonna corrispondente del database.
Creazione ed esecuzione di una
query di database mediante ADO.
NET
Crea una variabile DataContext e specifica una stringa di connessione per
il database. Crea una variabile di insieme Table basata sulla classe entità
corrispondente alla tabella che desideri interrogare. Definisci una query
LINQ to SQL che identifichi i dati da recuperare dal database e restituisca
un insieme enumerabile di entità. Esegui un’iterazione nell’insieme enumerabile per recuperare i dati di ciascuna riga ed elaborare i risultati.
Capitolo 26
Visualizzazione e modifica dei dati
mediante Entity Framework e
associazione dei dati
Gli argomenti trattati in questo capitolo sono:
■
Utilizzo di ADO.NET Entity Framework per generare classi entità.
■
Utilizzo dell’associazione dei dati in un’applicazione Microsoft WPF (Windows Presentation
Foundation) per visualizzare e gestire i dati recuperati da un database.
■
Aggiornamento di un database utilizzando Entity Framework.
■
Rilevazione e risoluzione degli aggiornamenti causa di conflitti generati da più utenti.
Nel capitolo 25, “Interrogazione delle informazioni in un database”, hai appreso le nozioni base
dell’impiego di Microsoft ADO.NET e LINQ to SQL per eseguire query nei dati di un database. La
funzione principale di LINQ to SQL è quella di fornire un’interfaccia LINQ a Microsoft SQL Server.
Tuttavia, il modello sottostante utilizzato da LINQ to SQL è estensibile e alcuni produttori di terze
parti hanno creato provider di dati in grado di accedere a diversi sistemi di gestione database.
Visual Studio 2010 fornisce anche una tecnologia chiamata Entity Framework, utilizzabile per
interrogare e manipolare i database. Comunque, mentre LINQ to SQL genera un tipo di codice
molto simile alla struttura database, Entity Framework genera un modello logico di database
chiamato modello dati di entità, in base al quale è possibile scrivere il proprio codice. Utilizzando
Entity Framework, è possibile costruire classi che eseguono il mapping degli elementi nel
modello logico (o entità) a tabelle fisiche del database. Il layer di mapping può aiutare a isolare le
applicazioni rispetto a tutte le modifiche che potrebbero essere apportate successivamente nella
struttura del database e può essere utilizzato per fornire una certa indipendenza dalla tecnologia
utilizzata per implementare il database. Ad esempio, puoi creare un’applicazione che utilizza
Entity Framework per accedere ai dati di un database Oracle e poi trasferire il database in SQL
Server. La logica dell’applicazione non dovrebbe cambiare; tutto ciò che devi fare è aggiornare il
modo in cui le entità logiche vengono implementate nel layer di mapping.
565
566
Parte V
Gestione dei dati
Entity Framework può operare con una variante di LINQ chiamata LINQ to Entities. Utilizzando
LINQ to Entities, puoi interrogare e manipolare i dati tramite un modello oggetto entità con la
sintassi di LINQ.
In questo capitolo, apprenderai come utilizzare Entity Framework per generare un modello logico
di dati e poi scrivere applicazioni che utilizzano le associazioni di dati per visualizzare e modificare
i dati attraverso questo modello.
Nota Questo capitolo fornisce solo una breve introduzione a Entity Framework e LINQ to
Entities. Per ulteriori informazioni, consulta la documentazione fornita con Microsoft Visual
Studio 2010, o visita la pagina ADO.NET Entity Framework nel sito Web di Microsoft all’indirizzo
http://msdn.microsoft.com/en-us/library/bb399572(VS.100).aspx.
Utilizzo dell’associazione di dati con Entity Framework
Il primo incontro con il concetto di associazione dei dati in un’applicazione WPF è avvenuto nel
capitolo 24, “Esecuzione della convalida”, dove questa tecnica è stata utilizzata per associare le
proprietà dei controlli di un form WPF con quelle di un’istanza di una classe. Una strategia simile
può essere adottata associando le proprietà dei controlli a oggetti entità in modo da poter
visualizzare e gestire i dati presenti in un database utilizzando un’interfaccia utente di tipo grafico.
Tuttavia, prima di procedere è necessario definire le classi entità che eseguono il mapping alle
tabelle del database.
Nel capitolo 25, hai utilizzato LINQ to SQL per costruire una serie di classi entità e una classe di
contesto. Entity Framework opera in modo simile ma più espansivo e molti dei concetti appresi
nel capitolo 25 sono applicabili anche qui. Entity Framework fornisce il modello ADO.NET Entity
Data Model e procedure guidate in grado di generare classi entità dal database. (Puoi definire
un modello di entità anche manualmente e utilizzarlo per creare un database.) Entity Framework
genera anche una classe di contesto personalizzata, da utilizzare per accedere alle entità e
connettersi al database.
Nella prima esercitazione, utilizzerai il modello ADO.NET Entity Data Model per generare un
modello di dati che gestisce prodotti e forniture nel database Northwind.
Importante Le esercitazione di questo capitolo presuppongono che sia stato creato e compi-
lato il database Northwind. Per ulteriori informazioni, consultare l’esercizio della sezione relativa
alla creazione di un database nel capitolo 25.
Capitolo 26
Visualizzazione e modifica dei dati mediante Entity Framework e associazione dei dati
567
Come garantire l’accesso ai file di database SQL Server 2008 - Visual
C# 2010 Express Edition
Se utilizzi Microsoft Visual C# 2010 Express Edition, quando definisci una connessione a
un database Microsoft SQL Server per la procedura guidata delle entità, puoi connetterti
direttamente al database SQL Server. Visual C# 2010 Express Edition avvia una propria
istanza di SQL Server Express, per l’accesso al database. L’istanza viene eseguita con le
credenziali dell’utente che esegue l’applicazione. Se utilizzi Visual C# 2010 Express Edition,
devi scollegare il database dall’istanza predefinita di SQL Server Express, poiché questa non
consente la connessione dell’istanza utente a un database attualmente in uso. Le procedure
che seguono descrivono come eseguire queste attività.
Scollegamento del database Northwind
1. Nel menu Start di Windows, fai clic su Programmi - Accessori - Prompt dei comandi
per aprire una finestra del prompt dei comandi.
2. Nella finestra del prompt dei comandi, digita il comando seguente per trovare il
percorso \Microsoft Press\Visual CSharp Step by Step\Chapter 26 all’interno della
cartella Documenti. Sostituisci Nome con il tuo nome utente.
cd "\Users\Name\Documents\Microsoft Press\Visual CSharp Step By Step\Chapter 26"
3. Nella finestra del prompt dei comandi, digita il comando che segue:
sqlcmd -S.\SQLExpress -E -idetach.sql
Nota Lo script detach.sql contiene il comando SQL Server che segue, il quale scollega il
database Northwind dall’istanza di SQL Server:
sp_detach_db 'Northwind'
4. Al termine dell’esecuzione dello script, chiudi la finestra del prompt dei comandi.
Nota Se desideri ricreare il database Northwind, puoi eseguire lo script instnwnd.sql
come descritto nel capitolo 25. Tuttavia, se hai già scollegato il database Northwind devi
prima eliminare i file Northwind.mdf e Northwind_log.ldf dalla cartella C:\Programmi\
Microsoft SQL Server\MSSQL10.SQLEXPRESS\MSSQL\DATA. In caso contrario lo script non
potrà essere eseguito correttamente
Dopo aver scollegato il database da SQL Server, devi garantire all’account l’accesso alla
cartella contenente il database e il Controllo completo sui file dello stesso. La procedura che
segue illustra come procedere.
568
Parte V
Gestione dei dati
Come garantire l’accesso al file del database Northwind
1. Accedi al computer utilizzando un account che dispone di accesso di amministrazione.
2. In Esplora risorse, accedi alla cartella C:\Programmi\Microsoft SQL Server\MSSQL10.
SQLEXPRESS\MSSQL.
Nota Se stai utilizzando una versione di Windows Vista o Windows 7 a 64 bit, sostituisci
tutti i riferimenti alla cartella C:\Programmi in queste istruzioni con C:\Programmi (x86).
3. Se viene visualizzata una finestra di messaggio nella quale è indicato che non si
posseggono le autorizzazioni necessarie per accedere alla cartella, fai clic su Continua.
Nel messaggio Controllo dell’account utente che segue fai nuovamente clic su
Continua.
4. Accedi alla cartella DATA, fai clic con il tasto destro del mouse sul file Northwind,
quindi fai clic su Proprietà.
5. Nella finestra di dialogo Proprietà di Northwind, fai clic sulla scheda Protezione.
6. Se la pagina Protezione contiene il messaggio “Continuare?”, fai clic su Continua. Nella
finestra di messaggio Controllo dell’account utente, fai clic su Continua.
Se nella pagina Protezione appare il messaggio “Per cambiare le autorizzazioni fare
clic su Modifica”, fai clic su Modifica. Se compare una finestra di messaggio Controllo
dell’account utente, fai clic su Continua.
7. Se il tuo account utente non è presente nella casella di riepilogo Utenti e gruppi, fai
clic su Aggiungi nella finestra di dialogo Autenticazioni per Northwind. Nella finestra
di dialogo Selezionare gli utenti o i gruppi, immetti il nome del tuo account utente e
fai clic su OK.
8. Nella finestra di dialogo Autenticazioni per Northwind, fai clic sul tuo account utente
nella casella di riepilogo Utenti e gruppi.
9. Nella casella di riepilogo Autorizzazioni per account (dove Account corrisponde al
nome utente), seleziona la casella di controllo Consenti accanto alla voce Controllo
completo e fai clic su OK.
10. Nella finestra di dialogo Proprietà di Northwind, fai clic su OK.
11. Ripeti i passaggi da 4 a 10 per il file Northwind_log nella cartella DATA.
Creazione di un Entity Data Model per le tabelle Suppliers e Products
1. Avvia Visual Studio 2010 se non è già in esecuzione.
Capitolo 26
Visualizzazione e modifica dei dati mediante Entity Framework e associazione dei dati
569
2. Crea un nuovo progetto utilizzando il modello Applicazione WPF. Assegna il nome Suppliers
al progetto e salvalo nella cartella \Microsoft Press\Visual CSharp Step by Step\Chapter 26
all’interno della tua cartella Documenti.
Nota Ricorda che, se stai utilizzando Visual C# 2010 Express Edition, puoi specificare il
percorso di salvataggio del progetto facendo clic sull’opzione per salvare Suppliers nel
menu File.
3. In Esplora soluzioni, fai clic con il pulsante destro del mouse sul progetto Suppliers, quindi
seleziona Aggiungi e fai clic su Nuovo elemento.
4. Nella finestra di dialogo Aggiungi nuovo elemento – Suppliers, nel riquadro a sinistra espandi
Visual C# se non lo è già. Nel riquadro centrale, scorri le voci fino al modello ADO.NET
Entity Data Model, digita Northwind.edmx nella casella Nome e fai clic su Aggiungi.
Viene visualizzata la finestra di dialogo della Procedura guidata Entity Data Model. Questa
finestra permette di specificare le tabelle del database Northwind per cui si desidera creare
classi entità, selezionare le colonne da includere e definire le relazioni tra di loro.
5. Qui, scegli Genera da database e fai clic su Avanti.
La procedura guidata richiede di configurare una connessione a un database, quindi viene
visualizzata la pagina Scegliere la connessione dati.
6. Se utilizzi Visual Studio 2010 Professional Edition o Enterprise Edition, esegui le seguenti
operazioni:
6.1. Scegli Nuova connessione.
Se viene visualizzata la finestra di dialogo Scegli origine dati, fai clic sulla casella di
elenco Origine dati e poi su Microsoft SQL Server. Nell’elenco a discesa Provider di dati,
scegli Provider di dati .NET Framework per SQL Server se non è già selezionato e fai
clic su Continua.
Nota Se in precedenza hai già creato connessioni database, al posto di questa finestra di
dialogo potrebbe essere visualizzata la finestra delle proprietà di connessione. In questo
caso, fai clic sul pulsante di modifica vicino alla casella di testo Origine dati. Viene visualizzata la finestra di dialogo per modificare l’origine dati, uguale alla finestra Scegli origine
dati, con la differenza che il pulsante per continuare riporta la voce OK.
6.2. Nella finestra di dialogo Proprietà di connessione, nella casella combinata Nome server
digita .\SQLExpress. Nella sezione Accesso al server della finestra di dialogo, scegli
il pulsante di opzione Usa autenticazione di Windows. Nella sezione Connessione al
database della finestra di dialogo, nella casella combinata Seleziona o immetti nome di
database, digita Northwind e fai clic su OK.
570
Parte V
Gestione dei dati
7. Se utilizzi Visual C# 2010 Express Edition, esegui le seguenti operazioni.
7.1. Scegli Nuova connessione.
Se viene visualizzata la finestra di dialogo Scegli origine dati, fai clic sulla casella di
elenco Origine dati e poi su File di database Microsoft SQL Server. Nell’elenco a discesa
Provider di dati, scegli Provider di dati .NET Framework per SQL Server se non è già
selezionato e fai clic su Continua.
Nota Se in precedenza hai già creato connessioni database, al posto di questa finestra di
dialogo potrebbe essere visualizzata la finestra delle proprietà di connessione. In questo
caso, fai clic sul pulsante di modifica vicino alla casella di testo Origine dati. Viene visualizzata la finestra di dialogo per modificare l’origine dati, uguale alla finestra Scegli origine
dati, con la differenza che il pulsante per continuare riporta la voce OK.
7.2. Nella finestra di dialogo Proprietà di connessione, nella casella Nome file database fai
clic su Sfoglia.
7.3. Nella finestra di dialogo Selezione file di database SQL Server, accedi alla cartella C:\
Programmi\Microsoft SQL Server\MSSQL10.SQLEXPRESS\MSSQL\DATA, fai clic sul
database Northwind, quindi su Apri.
7.4. Nella sezione Accesso al server della finestra di dialogo, scegli il pulsante di opzione
Usa autenticazione di Windows.
8. Nella pagina Scegliere la connessione dati della Procedura guidata Entity Data Model Wizard,
seleziona la casella di controllo Salva impostazioni stringa di connessione entity in App.
Config come, digita NorthwindEntities (questo è il nome predefinito) e fai clic su Avanti.
Se stai utilizzando Visual C# 2010 Express Edition, viene visualizzata una finestra di
messaggio che chiede se si desidera aggiungere il file database al progetto. Fai clic su No.
9. Nella pagina Scegli oggetti di database, verifica che le caselle di controllo Rendi plurali o
singolari i nomi degli oggetti generati e Includi colonne di chiavi esterne nel modello siano
entrambe selezionate. Nell’elenco Scegliere gli oggetti di database da includere nel model,
espandi Tabelle e fai clic su Products (dbo) Suppliers (dbo). Nella casella di testo Spazio dei
nomi Model, digita NorthwindModel (questo è lo spazio dei nomi predefinito). L’immagine
seguente illustra la pagina completata.
Capitolo 26
Visualizzazione e modifica dei dati mediante Entity Framework e associazione dei dati
571
10. Fai clic su Fine.
La procedura guidata Entity Data Model Wizard genera classi entità chiamate Supplier e
Product in base alle tabelle Suppliers e Products, con campi proprietà per ogni colonna
delle tabelle, come illustrato nella pagina seguente. Il modello dati definisce le proprietà
di spostamento che collegano le due entità e gestiscono la relazione tra di esse. In questo
caso, una singola entità Supplier può essere correlata a più entità Product.
Per modificare le proprietà di una classe entità, seleziona la classe e modifica i valori delle
proprietà nella finestra Proprietà. Puoi anche utilizzare il riquadro dei dati di mapping
visualizzato alla fine della finestra per selezionare e modificare i campi che appaiono in una
classe entità.
572
Parte V
Gestione dei dati
Questo è il processo per modificare il mapping dalle proprietà logiche di un entità alle
colonne fisiche di una tabella.
Importante Questo esercizio presuppone che si stiano utilizzando le classi entità prede-
finite generate per le tabelle Suppliers e Products nel database, pertanto non modificare
niente!
11. In Esplora soluzioni, espandi la cartella Northwind.edmx e fai doppio clic su Northwind.
designer.cs.
Suggerimento Se Esplora soluzioni non è visualizzato, nel meni Visualizza seleziona
Esplora soluzioni.
Il codice generato dalla Procedura guidata Entity Data Model appare nella finestra
dell’editor di codice e di testo. Se espandi l’area dei contesti, noterai che contiene una classe
chiamata NorthwindEntities che deriva dalla classe ObjectContext. In Entity Framework, la
classe ObjectContext svolge un ruolo simile a quello della classe DataContext in LINQ to SQL
e può essere utilizzata per eseguire la connessione al database. La classe NorthwindEntities
estende la classe ObjectContext con una logica per eseguire la connessione al database
Northwind e compilare le entità Supplier e Product (come una classe personalizzata
DataContext di LINQ to SQL).
Le informazioni relative alla connessione specificate prima di creare le due classi entità
vengono salvate in un file di configurazione dell’applicazione. La memorizzazione della
stringa di connessione in un file di configurazione consente di modificare tale stringa senza
dover ricompilare l’applicazione, modificando semplicemente il file di configurazione stesso.
Ciò è molto utile se prevedi di dover riposizionare o rinominare il database, oppure di
passare dall’uso di un database di sviluppo locale a un database di produzione con lo stesso
insieme di tabelle.
Il codice delle due classi entità si trova nell’area delle entità del file. Queste classi sono un
po’ più complesse delle classi create manualmente nel capitolo 25, ma i principi generali
sono simili. L’ulteriore complessità è data dall’implementazione indiretta da parte delle classi
entità delle interfacce INotifyPropertyChanging e INotifyPropertyChanged e dalle proprietà
di spostamento utilizzate per collegare le entità correlate. Queste interfacce definiscono
gli eventi generati dalle classi entità quando modificano i valori delle loro proprietà. I vari
controlli dell’interfaccia utente presenti nella libreria WPF sottoscrivono questi eventi per
rilevare eventuali cambiamenti nei dati e garantire che le informazioni visualizzate in un
form WPF siano aggiornate.
Nota Le classi entità ereditano dalla classe System.Data.Objects.DataClasses.EntityObject
che, a sua volta, eredita dalla classe System.Data.Objects.DataClasses.StructuralObject.
La classe StructuralObject implementa le interfacce INotifyPropertyChanging e
INotifyPropertyChanged.
Capitolo 26
Visualizzazione e modifica dei dati mediante Entity Framework e associazione dei dati
Utilizzo del file di configurazione di un’applicazione
Il file di configurazione dell’applicazione è un meccanismo molto utile che permette
all’utente di modificare alcune delle risorse utilizzate dall’applicazione senza doverla
ricompilare. La stringa di connessione impiegata per connettersi a un database è un
esempio di risorsa modificabile.
Quando utilizzi la procedura guidata Entity Data Model per generare classi entità, al
progetto viene aggiunto un nuovo file chiamato App.config. Questa è l’origine del file di
configurazione dell’applicazione e appare nella finestra Esplora soluzioni. Per esaminare il
contenuto del file App.config, fai doppio clic sul suo nome. Come puoi vedere, si tratta di
un file XML; il testo dell’esempio che segue è stato riformattato per adattarlo alla pagina
stampata:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<connectionStrings>
<add name="NorthwindEntities" connectionString="metadata=res://*/Northwind.
csdl|res://*/Northwind.ssdl|res://*/Northwind.msl;provider=System.Data.
SqlClient;provider connection string="Data Source=.\SQLExpress;Initial
Catalog=Northwind;Integrated Security=True;MultipleActiveResultSets=True""
providerName="System.Data.EntityClient" />
</connectionStrings>
</configuration>
La stringa di connessione è memorizzata nell’elemento <connectionStrings> del file. Questa
stringa contiene un insieme di elementi nel form property=value. Questi elementi sono
separati da un carattere di punto e virgola. Le proprietà chiave sono gli elementi Data
Source, Initial Catalog e Integrated Security che dovresti conoscere dagli esercizi precedenti.
Quando compili l’applicazione, il compilatore C# copia il file app.config nella cartella del
codice compilato e lo rinomina come applicazione.exe.config, dove applicazione è il nome
della tua applicazione. Quando si connette al database, l’applicazione legge il valore della
stringa di connessione nel file di configurazione invece di usare una stringa di connessione
invariabile inserita nel codice C#. La tecnica che consente di svolgere questa operazione è
illustrata con l’uso delle classi entità più avanti in questo capitolo.
Il file di configurazione dell’applicazione (il file applicazione.exe.config) deve essere
distribuito assieme al codice eseguibile dell’applicazione stessa. Quando un utente
necessita di connettersi a un database differente, può modificare il file di configurazione
con un normale editor di testo per cambiare l’attributo <connectionString> dell’elemento
<connectionStrings>. Alla successiva esecuzione dell’applicazione, questa utilizzerà
automaticamente il nuovo valore.
Nota che il file di configurazione dell’applicazione deve essere protetto per impedire agli
utenti di apportare modifiche inappropriate.
573
574
Parte V
Gestione dei dati
Dopo aver creato un modello entità per l’applicazione, puoi generare l’interfaccia utente in grado
di visualizzare le informazioni recuperate utilizzando l’associazione di dati.
Creazione dell’interfaccia utente per l’applicazione Suppliers
1. In Esplora soluzioni, fai clic con il tasto destro del mouse sul file MainWindow.xaml, quindi fai
clic su Rinomina e digita il nuovo nome SupplierInfo.xaml.
2. Fai doppio clic sul file App.xaml per visualizzarlo nella finestra Progettazione. Nel riquadro
XAML, modifica l’elemento StartupUri in “SupplierInfo.xaml”, come qui mostrato in
grassetto:
<Application x:Class="Suppliers.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="SupplierInfo.xaml">
...
</Application>
3. In Esplora soluzioni, fai doppio clic sul file SupplierInfo.xaml per visualizzarlo nella finestra
Progettazione. Nel riquadro XAML, come illustrato in grassetto nel frammento di codice
seguente, modifica il valore dell’elemento x:Class con “Suppliers.SupplierInfo”, imposta Title
su “Supplier Information”, Height su ”362” e Width su “614”:
<Window x:Class="Suppliers.SupplierInfo"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Supplier Information" Height="362" Width="614">
...
</Window>
4. Visualizza il file SupplierInfo.xaml.cs nella finestra dell’editor di codice e di testo. Modifica il
nome della classe MainWindow in SupplierInfo, quindi cambia il nome del costruttore come
qui mostrato in grassetto:
public partial class SupplierInfo : Window
{
public SupplierInfo()
{
InitializeComponent();
}
}
5. In Esplora soluzioni, fai doppio clic sul file SupplierInfo.xaml per visualizzarlo nella finestra
Progettazione. Dalla sezione Controlli di WPF comuni della Casella degli strumenti, aggiungi
un controllo ComboBox e un controllo Button al form. (Posizionali in un punto qualunque
del form.) Dalla sezione Controlli di WPF comuni della Casella degli strumenti, aggiungi un
controllo ListView al form
6. Nella finestra Proprietà, imposta le proprietà di questi controlli con i valori indicati nella
seguente tabella.
Capitolo 26
Visualizzazione e modifica dei dati mediante Entity Framework e associazione dei dati
Controllo
Proprietà
Valore
comboBox1
Name
suppliersList
Height
23
Width
Auto
Margin
40,16,42,0
VerticalAlignment
Top
HorizontalAlignment
Stretch
Name
productsList
Height
Auto
Width
Auto
Margin
40,44,40,60
VerticalAlignment
Stretch
HorizontalAlignment
Stretch
Name
saveChanges
Content
Save Changes
IsEnabled
False (casella di controllo
deselezionata)
Height
23
Width
90
Margin
40,0,0,10
VerticalAlignment
Bottom
HorizontalAlignment
Left
listView1
button1
575
Il form Supplier Information nella finestra Progettazione dovrebbe avere il seguente aspetto:
576
Parte V
Gestione dei dati
7. Nel riquadro XAML, aggiungi l’elemento Window.Resources illustrato di seguito in grassetto
all’elemento Window, sopra l’elemento Grid:
<Window x:Class="Suppliers.SupplierInfo"
...>
<Window.Resources>
<DataTemplate x:Key="SuppliersTemplate">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Path=SupplierID}" />
<TextBlock Text=" : " />
<TextBlock Text="{Binding Path=CompanyName}" />
<TextBlock Text=" : " />
<TextBlock Text="{Binding Path=ContactName}" />
</StackPanel>
</DataTemplate>
</Window.Resources>
<Grid>
...
</Grid>
</Window>
Per specificare come desideri visualizzare i dati in un controllo puoi utilizzare DataTemplate.
Questo modello verrà applicato alla casella combinata suppliersList nel prossimo punto.
Questo modello contiene tre controlli TextBlock disposti orizzontalmente mediante uno
StackPanel. Il primo, il terzo e il quinto controllo TextBlock visualizzeranno rispettivamente i
dati delle proprietà SupplierID, CompanyName e ContactName dell’oggetto entità Supplier,
che sarà associato in seguito. Gli altri controlli TextBlock mostreranno solamente un
separatore.
8. Nel riquadro XAML, modifica la definizione della casella combinata suppliersList e specifica
le proprietà IsSynchronizedWithCurrentItem, ItemsSource e ItemTemplate, come illustrato di
seguito in grassetto:
<ComboBox ... Name="suppliersList" IsSynchronizedWithCurrentItem="True"
ItemsSource="{Binding}" ItemTemplate="{StaticResource SuppliersTemplate}" />
Suggerimento Se preferisci, puoi impostare queste proprietà anche utilizzando la finestra Proprietà della casella combinata suppliersList.
I dati di ogni fornitore saranno visualizzati nel controllo suppliersList. Ricorda il capitolo
25, nel quale LINQ to SQL utilizzava le classi di insiemi Table<T> per memorizzare
le righe di una tabella. Entity Framework segue un approccio simile, ma memorizza
le righe in una classe di insiemi ObjectSet<T>. L’impostazione della proprietà
IsSynchronizedWithCurrentItem garantisce che la proprietà SelectedItem del controllo sia
mantenuta sincronizzata con l’elemento corrente dell’insieme. Non impostando questa
proprietà su True, la casella combinata non potrà visualizzare automaticamente il primo
elemento dell’insieme stesso quando l’applicazione viene avviata e stabilisce l’associazione
con l’insieme.
Attualmente ItemsSource contiene un’associazione vuota. Nel capitolo 24 è stata definita
un’istanza di una classe come risorsa statica, quindi tale risorsa è stata specificata come
Capitolo 26
Visualizzazione e modifica dei dati mediante Entity Framework e associazione dei dati
577
origine dell’associazione. Se non specifichi un’origine di associazione, WPF si associa a
un oggetto specificato nella proprietà DataContext del controllo. (Dal momento che
hanno lo stesso nome, fai attenzione a non confondere la proprietà DataContext di un
controllo con un oggetto DataContext usato da LINQ to SQL per comunicare con un
database.) La proprietà DataContext del controllo verrà impostata in un oggetto insieme
ObjectSet<Supplier> utilizzando il codice.
La proprietà ItemTemplate specifica il modello da utilizzare per visualizzare i dati recuperati
dell’origine di associazione. In questo caso, il controllo suppliersList visualizzerà i campi
SupplierID, CompanyName e ContactName dall’origine di associazione.
9. Modifica la definizione di ListView productsList e specifica le proprietà
IsSynchronizedWithCurrentItem e ItemsSource:
<ListView ... Name="productsList" IsSynchronizedWithCurrentItem="True"
ItemsSource="{Binding}" />
La classe entità Supplier contiene una proprietà EntityCollection<Product> che fa riferimento
ai prodotti di cui dispone il fornitore. (La classe EntityCollection<T> è molto simile alla classe
EntitySet<T> di LINQ to SQL.) La proprietà DataContext del controllo productsList sarà
impostata sulla proprietà Products dell’oggetto Supplier attualmente selezionato utilizzando
il codice. In un’esercitazione successiva dovrai anche fornire la funzionalità necessaria per
consentire all’utente di aggiungere e rimuovere i prodotti. Questo codice modificherà
l’elenco dei prodotti che opera come origine di associazione. L’impostazione della proprietà
IsSynchronizedWithCurrentItem su True garantisce che il prodotto appena creato sia
selezionato nell’elenco quando l’utente ne aggiunge un altro, oppure che sia selezionato
l’elemento esistente se l’utente ne elimina uno. (Impostando questa proprietà su False,
nessun elemento risulterà selezionato dopo l’eliminazione di un prodotto, e ciò può causare
problemi nell’applicazione qualora il codice tenti di accedere all’elemento attualmente
selezionato.)
10. Aggiungi l’elemento figlio ListView.View, illsutrato di seguito in grassetto, che contiene
le definizioni di GridView e colonna al controllo productsList. Assicurati di sostituire il
delimitatore di chiusura (/>) dell’elemento ListView con un delimitatore normale (>) e di
aggiungere quindi un elemento </ListView> di chiusura.
<ListView ... Name="productsList" ...>
<ListView.View>
<GridView>
<GridView.Columns>
<GridViewColumn Width="75" Header="Product ID"
DisplayMemberBinding="{Binding Path=ProductID}" />
<GridViewColumn Width="225" Header="Name"
DisplayMemberBinding="{Binding Path=ProductName}" />
<GridViewColumn Width="135" Header="Quantity Per Unit"
DisplayMemberBinding="{Binding Path=QuantityPerUnit}" />
<GridViewColumn Width="75" Header="Unit Price"
DisplayMemberBinding="{Binding Path=UnitPrice}" />
</GridView.Columns>
</GridView>
</ListView.View>
</ListView>
578
Parte V
Gestione dei dati
Impostando la proprietà View, puoi fare in modo che il controllo ListView visualizzi i dati
in più formati. Questo codice XAML (Extensible Application Markup Language) utilizza un
componente GridView. Un GridView visualizza i dati in formato tabella, nella quale ogni riga
ha un insieme fisso di colonne definito dalle proprietà GridViewColumn. Ciascuna colonna
ha una propria intestazione che ne visualizza il nome. La proprietà DisplayMemberBinding
di ogni colonna specifica i dati che essa deve mostrare dall’origine di associazione.
I dati della colonna UnitPrice sono una proprietà Decimal della classe entità Product. WPF
converte queste informazioni in una stringa e applica un formato numerico predefinito.
Teoricamente, il dati di questa colonna dovrebbero apparire sotto forma di valuta. Se
desideri, puoi riformattare i dati di una colonna GridView creando una classe convertitrice.
Le classi convertitrici sono discusse nel capitolo 24, dove vengono impiegate per convertire
un’enumerazione in una stringa. In questo caso, la classe convertitrice trasformerà un valore
decimal? in un valore string contenente una rappresentazione di un valore valuta.
11. Passa alla finestra dell’editor di codice e di testo in cui viene visualizzato il file SupplierInfo.
xaml.cs. Dopo la classe SupplierInfo, aggiungi la classe PriceConverter, illustrata di seguito:
[ValueConversion(typeof(string), typeof(Decimal))]
class PriceConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
if (value != null)
return String.Format("{0:C}", value);
else
return "";
}
public object ConvertBack(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
Il metodo Convert richiama il metodo String.Format per creare una stringa in grado di
utilizzare il formato di valuta locale del computer. L’utente non modificherà realmente
il prezzo unitario nella visualizzazione elenco, pertanto non c’è alcuna necessità di
implementare il metodo ConvertBack per riconvertire un valore string in un valore Decimal.
12. Torna alla finestra Progettazione relativa al form SupplierInfo.xaml. Aggiungi la
dichiarazione di spazio dei nomi XML seguente all’elemento Windowe definisci un’istanza
della classe PriceConverter come risorsa Window, come illustrato di seguito:
<Window x:Class="Suppliers.SupplierInfo"
...
xmlns:app="clr-namespace:Suppliers"
...>
<Window.Resources>
<app:PriceConverter x:Key="priceConverter" />
...
Capitolo 26
Visualizzazione e modifica dei dati mediante Entity Framework e associazione dei dati
579
</Window.Resources>
...
</Window>
Nota La finestra Progettazione memorizza nella cache le definizioni dei controlli e
altri elementi dell’interfaccia utente e non sempre riconosce immediatamente spazi
dei nomi nuovi, aggiunti a un form. Se questa istruzione causa un errore nella finestra
Progettazione, nel menu Compila fai clic su Compila soluzione. La cache WPF verrà aggiornata e l’errore dovrebbe risolversi.
13. Modifica la definizione della GridViewColumn Unit Price e applica la classe convertitrice
all’associazione come mostrato di seguito in grassetto:
<GridViewColumn ... Header ="Unit Price" DisplayMemberBinding=
"{Binding Path=UnitPrice, Converter={StaticResource priceConverter}}" />
Hai così completato la definizione del form. Ora dovrai scrivere il codice necessario per recuperare
i dati visualizzati nel form, e dovrai selezionare le proprietà DataContext dei controlli suppliersList
e productsList in modo che le associazioni funzionino correttamente.
Creazione del codice per il recupero delle informazioni relative al fornitore e per
stabilire l’associazione dei dati
1. Nel file SupplierInfo.xaml, modifica la definizione dell’elemento Window e aggiungi un
metodo evento Loaded chiamato Window_Loaded. Nota che questo è il nome predefinito
del metodo generato quando fai clic su <Nuovo gestore eventi>. Il codice XAML
dell’elemento Window dovrebbe avere l’aspetto mostrato di seguito:
<Window x:Class="Suppliers.SupplierInfo"
...
Title="Supplier Information" ... Loaded="Window_Loaded">
...
</Window>
2. Nella finestra dell’editor di codice e di testo relativa a SupplierInfo.xaml.cs, all’inizio del file
aggiungi l’istruzione using seguente all’elenco:
using System.ComponentModel;
using System.Collections;
3. Aggiungi i tre campi privati qui mostrati in grassetto alla classe SupplierInfo:
public partial class SupplierInfo : Window
{
private NorthwindEntities northwindContext = null;
private Supplier supplier = null;
private IList productsInfo = null;
...
}
La variabile northwindContext sarà utilizzata per la connessione al database Northwind
e il recupero dei dati dalla tabella Suppliers. La variabile supplier conterrà i dati relativi
580
Parte V
Gestione dei dati
al fornitore attualmente visualizzato nel controllo suppliersList. La variabile productsInfo
conterrà i prodotti offerti dal fornitore attualmente visualizzato. Questa variabile sarà
associata al controllo productsList.
A questo punto, potrai essere incuriosito dalla definizione della variabile productsInfo;
nell’esercitaione precedente, hai appreso che la classe Supplier ha una proprietà
EntityCollection<Product> utilizzabile per accedere ai prodotti venduti da un fornitore.
Questa proprietà EntityCollection<Product> potrebbe essere associata al controllo
productsList, ma questo approccio può causare un problema importante. In precedenza,
si è visto che le classi entità Supplier e Product implementano indirettamente le interfacce
INotifyPropertyChanging e INotifyPropertyChanged mediante le classi EntityObject e
StructuralObject. Associando un controllo WPF a un’origine di dati, il controllo sottoscrive
automaticamente gli eventi esposti da tali interfacce per aggiornare quanto visualizzato a
ogni modifica dei dati. Tuttavia, la classe EntityCollection<Product> non implementa queste
interfacce, pertanto il controllo visualizzazione elenco non può essere aggiornato quando i
prodotti vengono aggiunti o rimossi dal fornitore. L’elenco viene però aggiornato al variare
di un prodotto esistente, poiché ogni elemento in EntityCollection<Product> è un oggetto
Product e invia le notifiche appropriate ai controlli WPF cui è associato.
4. Aggiungi il codice riportato in grassetto di seguito al metodo Window_Loaded:
private void Window_Loaded(object sender, RoutedEventArgs e)
{
this.northwindContext = new NorthwindEntities();
suppliersList.DataContext = this.northwindContext.Suppliers;
}
Quando un’applicazione viene avviata e carica la finestra, il codice crea una variabile
NorthwindDataContext che si connette al database Northwind. Ricorda che questa classe
è stata creata in precedenza dalla procedura guidata Entity Data Model. Il costruttore
predefinito di questa classe legge la stringa di connessione al database nel file di
configurazione dell’applicazione. Il metodo imposta quindi la proprietà DataContext della
casella combinata suppliersList sulla proprietà insieme Suppliers ObjectSet della variabile
northwindContext. Questa azione risolve l’associazione per la casella combinata e il modello
dati usato dalla casella combinata visualizza i valori in SupplierID, CompanyName e
ContactName per ogni oggetto Supplier dell’insieme.
Nota Se un controllo è figlio di un altro, come avviene ad esempio per un controllo
GridViewColumn in un controllo ListView, è necessario impostare solamente la proprietà
DataContext del controllo padre. Se la proprietà DataContext di un controllo figlio non
è impostata, il runtime WPF utilizzerà il valore DataContext del controllo padre. Questa
tecnica rende possibile la condivisione del contesto dei dati tra più controlli figlio e un
controllo padre.
Se il controllo padre immediato non dispone del contesto dei dati, il runtime WPF
esaminerà il controllo padre del padre e così via fino a raggiungere il controllo Window
che definisce il form. Se non è disponibile alcun contesto dei dati, viene ignorata qualsiasi
associazione dei dati per il controllo.
Capitolo 26
Visualizzazione e modifica dei dati mediante Entity Framework e associazione dei dati
581
5. Torna alla finestra Progettazione. Fai doppio clic sulla casella combinata suppliersList.
Questa azione crea il metodo evento suppliersList_SelectionChanged, eseguito ogni volta
che l’utente seleziona un elemento diverso nella casella combinata.
6. Nella finestra dell’editor di codice e di testo, aggiungi le istruzioni qui mostrate in grassetto
al metodo suppliersList_SelectionChanged:
private void suppliersList_SelectionChanged(object sender,
SelectionChangedEventArgs e)
{
this.supplier = suppliersList.SelectedItem as Supplier;
this.northwindContext.LoadProperty<Supplier>(this.supplier, s => s.Products);
this.productsInfo = ((IListSource)supplier.Products).GetList();
productsList.DataContext = this.productsInfo;
}
Questo metodo ottiene il fornitore attualmente selezionato dalla casella combinata e copia
i dati della proprietà EntityCollection<Product> del fornitore nella variabile productsInfo.
La classe EntityCollection<Product> implementa l’interfaccia IListSource, che fornisce il
metodo GetList per copiare i dati dall'insieme entità in un oggetto IList. Come LINQ to
SQL, tutti i dati relativi a un’entità non vengono recuperati automaticamente quando si
crea un’istanza di un’entità. In questo caso, ciò significa che ogni volta che l’applicazione
preleva i dati per un’entità Supplier dal database, non recupera automaticamente i dati dei
prodotti correlati a quel fornitore. Nel capitolo 25 hai appreso che LINQ to SQL fornisce il
metodo LoadWith della classe DataLoadOptions per specificare i dati correlati da recuperare
quando viene letta una riga dal database. Entity Framework fornisce un metodo generico
della classe ObjectContext, che esegue la stessa attività. Con la seconda istruzione del codice
precedente, Entity Framework recupera i prodotti associati a un fornitore ogni volta che lo
stesso viene prelevato.
Infine, il metodo imposta la proprietà DataContext del controllo productsList su questo
elenco di prodotti. Questa istruzione consente al controllo productsList di visualizzare gli
elementi nell’elenco dei prodotti.
7. Nel menu Debug, fai clic su Avvia senza eseguire debug per costruire ed eseguire
l’applicazione.
Una volta eseguito, il form visualizza i prodotti del primo fornitore, Exotic Liquids. Il form
dovrebbe apparire simile a quanto mostrato nell’immagine che segue.
582
Parte V
Gestione dei dati
8. Seleziona un fornitore differente dalla casella combinata e verifica che la visualizzazione
elenco ne mostri i prodotti. Una volta completata la consultazione dei dati, chiudi il form e
torna a Visual Studio 2010.
Utilizzo di LINQ to Entities per interrogare i dati
Nell’esercizio precedente è stata utilizzata l'associazione di dati per prelevare e visualizzare
le informazioni attraverso Entity Framework anziché creare e ed eseguire query ti tipo LINQ.
Tuttavia, come accennato in precedenza, puoi recuperare le informazioni anche da un
modello di dati creato con Entity Framework utilizzando LINQ to Entities. La sintassi è simile
quella di LINQ to SQL, con l’unica differenza che una query LINQ to Entities è basta su un
oggetto ObjectQuery<T>, dove T è un tipo EntityObject.
Ad esempio, puoi recuperare un elenco di nomi di prodotti da ObjectSet Products di
ObjectContext NorthwindEntities, illustrati nell’esempio precedente, e visualizzarli come
illustrato di seguito:
NorthwindEntities northwindContext = new NorthwindEntities();
ObjectQuery<Product> products = northwindContext.Products;
var productNames = from p in products
select p.ProductName;
foreach (var name in productNames)
{
Console.WriteLine("Product name: {0}", name);
}
Nota Dal punto di vista tecnico, la proprietà Products della classe NorthwindEntities
ha il tipo ObjectSet<Products>. Tuttavia, il tipo ObjectSet<T> eredita da ObjectQuery<T>,
pertanto puoi assegnare in modo sicuro la proprietà Products a una variabile
ObjectQuery<Products>.
Capitolo 26
Visualizzazione e modifica dei dati mediante Entity Framework e associazione dei dati
583
LINQ to Entities supporta la maggior parte degli operatori di query LINQ standard, anche
se esistono alcune eccezioni descritte nella documentazione fornita con Visual Studio 2010.
È possibile unire i dati da più tabelle, ordinare i risultati ed eseguire operazioni come il
raggruppamento di dati e il calcolo dei valori di aggregazione. Per ulteriori informazioni,
consulta il capitolo 20, “Interrogazione dei dati in memoria mediante espressioni di query”.
L’operazione finale da eseguire nell’applicazione Suppliers consiste nel fornire la funzionalità che
consente all’utente di modificare i dettagli relativi ai prodotti, e di rimuovere e creare i prodotti.
Prima di poterlo fare, dovrai apprendere come utilizzare Entity Framework per aggiornare i dati.
Utilizzo dell'associazione di dati per modificarli
Entity Framework fornisce un canale di comunicazione bidirezionale con un database. In
precedenza hai visto come utilizzare l’associazione di dati con Entity Framework per prelevare i
dati, ma puoi anche di modificare le informazioni recuperate e salvarle nuovamente nel database.
Aggiornamento dei dati esistenti
Quando recuperi i dati utilizzando un oggetto ObjectContext, gli oggetti creati da tali dati sono
memorizzati in una cache all’interno dell’applicazione. Puoi modificare i valori degli oggetti
contenuti in questa cache esattamente come avviene con i valori di un oggetto ordinario, ovvero
impostandone le proprietà. Tuttavia, l’aggiornamento di un oggetto in memoria non comporta
l’aggiornamento del database. Per memorizzare le modifiche nel database, devi generare i
comandi SQL UPDATE appropriati e fare in modo di eseguirli sul database server. Ciò è possibile in
modo molto semplice tramite Entity Framework. L’esempio di codice seguente illustra una query
LINQ to Entities che preleva il prodotto numero 14. Il codice poi ne modifica il nome in “Bean
Curd” (nel database Northwind originariamente il nome era “Tofu”) quindi reinvia tale modifica al
database:
NorthwindEntities northwindContext = new NorthwindEntities();
Product product = northwindContext.Products.Single(p => p.ProductID == 14);
product.ProductName = "Bean Curd";
northwindContext.SaveChanges();
L’istruzione chiave di questo esempio è la chiamata al metodo SaveChanges dell’oggetto
ObjectContext. (Ricorda che NorthwindEntities eredita da ObjectContext.) Quando modifichi le
informazioni in un oggetto entità compilato eseguendo una query, l’oggetto ObjectContext
che gestisce la connessione usata per eseguire la query originale tiene traccia delle modifiche
apportate ai dati. Il metodo SaveChanges propaga tali variazioni al database. Per fare ciò, in
background l’oggetto ObjectContext crea ed esegue un’istruzione SQL UPDATE.
Per prelevare e cambiare più prodotti insieme, è possibile richiamare SaveChanges una sola
volta dopo la modifica finale. Così facendo, il metodo SaveChanges raggruppa insieme tutti gli
aggiornamenti. L’oggetto ObjectContext crea una transazione del database al cui interno esegue
584
Parte V
Gestione dei dati
tutte le istruzioni SQL UPDATE. In caso di errore di uno degli aggiornamenti, la transazione viene
interrotta, le modifiche apportate dal metodo SaveChanges sono ripristinate nel database e il
metodo SaveChanges genera un’eccezione. Se tutti gli aggiornamenti sono completati con successo,
la transazione viene completata e le modifiche divengono permanenti nel database. Nota che se
il metodo SaveChanges non riesce, vengono ripristinate solamente le informazioni nel database,
mentre le modifiche rimangono presenti negli oggetti entità in memoria. L’eccezione generata
in caso di fallimento del metodo SaveChanges fornisce alcune informazioni sul motivo di quanto
accaduto. Puoi pertanto tentare di correggere il problema e richiamare nuovamente SaveChanges.
La classe ObjectContext fornisce anche il metodo Refresh. Con questo metodo puoi ripopolare
gli insiemi EntityObject con i dati presenti nel database e annullare così le modifiche apportate.
L’esempio che segue mostra come usarlo:
northwindContext.Refresh(RefreshMode.StoreWins, northwindContext.Products);
Il primo parametro è un membro dell’enumerazione System.Data.Objects.RefreshMode. La
specifica del valore RefreshMode.StoreWins impone l’aggiornamento dei dati dal database. Il
secondo parametro è l’entità nella cache da aggiornare.
Suggerimento L’esecuzione della traccia delle modifiche è un’operazione potenzialmente
dispendiosa per l’oggetto ObjectContext. Se sei certo di non dover modificare i dati (ad esempio
se l’applicazione genera un rapporto di sola lettura), è consigliabile disabilitare questa funzione
per un oggetto EntityObject impostando la proprietà MergeOption su MergeOption.NoTracking,
come illustrato di seguito:
northwindContext.Suppliers.MergeOption = MergeOption.NoTracking;
Puoi apportare modifiche a un’entità con il sistema di traccia delle modifiche disabilitato, ma tali
variazioni non verranno salvate quando si richiama SaveChanges e andranno perse quando si
chiude l’applicazione.
Gestione dei conflitti di aggiornamento
Possono esservi numerosi motivi per cui un’operazione di aggiornamento non può essere
completata con successo, ma una delle cause più comuni è un conflitto che si verifica quando
due utenti tentano di aggiornare gli stessi dati contemporaneamente. Se consideri ciò che accade
quando esegui un’applicazione che utilizza Entity Framework, puoi comprendere la facilità con
cui ciò può verificarsi. Quando recuperi i dati tramite un oggetto ObjectContext, essi vengono
memorizzati nella cache dell’applicazione. In seguito un altro utente può eseguire la stessa query
e recuperare così gli stessi dati. Se entrambe le persone modificano i dati e richiamano quindi il
metodo SaveChanges, una di esse sovrascriverà nel database le modifiche apportate dall’altra.
Questo fenomeno è noto con il termine lost update (perdita di aggiornamenti).
Questo fenomeno ha luogo perché Entity Framework implementa un tipo di concorrenza
ottimistica. In altre parole, quando preleva i dati da un database, non li blocca nel database.
Questa forma di concorrenza consente ad altri utenti di accedere agli stessi dati, ma presuppone
che la probabilità che due utenti modifichino gli stessi dati sia ridotta (da qui il termine
concorrenza ottimistica).
Capitolo 26
Visualizzazione e modifica dei dati mediante Entity Framework e associazione dei dati
585
Il contrario di concorrenza ottimistica è concorrenza pessimistica. In questo schema, tutti i
dati sono bloccati nel database quando vengono prelevati e nessun utente concorrente potrà
accedervi. Questo approccio garantisce che le modifiche non vadano perse, anche se risulta un po'
troppo rigido.
Entity Framework non supporta la concorrenza pessimistica in modo diretto. Fornisce, invece,
un sistema equilibrato. Ogni elemento di una classe EntityObject ha una proprietà chiamata
Concurrency Mode. Per impostazione predefinita, Concurrency Mode è impostata su None, ma
può essere modificata con Fixed, utilizzando il programma di progettazione di Entity Framework
L'immagine seguente illustra un modello entità creato in precedenza. L’utente ha selezionato
l'elemento ProductName dell’entità Product e ha modificato la proprietà Concurrency Mode su
Fixed nella finestra Proprietà.
Quando un’applicazione modifica il valore della proprietà ProductName in un’istanza della classe
Products EntityObject, Entity Framework conserva una copia del valore originale della proprietà
nella cache. Quando imposti Concurrency Mode per una proprietà e l’applicazione richiama il
metodo SaveChanges dell’oggetto ObjectContext, Entity Framework utilizza la copia del valore
originale contenuta nella cache per verificare che la colonna della riga corrispondente nel
database non sia stata modificata da un altro utente dopo essere stata prelevata. In caso negativo,
la riga viene aggiornata. Se, invece, la colonna è stata modificata, il metodo SaveChanges si
interrompe e rileva un’eccezione OptimisticConcurrencyException. Quando questo accade, tutte le
modifiche apportate dal metodo SaveChanges del database vengono annullate, anche se restano
nella cache dell’applicazione.
586
Parte V
Gestione dei dati
Quando viene rilevata un’eccezione OptimisticConcurrencyException, è possibile determinare
l’entità che ha causato il conflitto analizzando la proprietà StateEntries dell’oggetto eccezione.
Questa proprietà contiene un insieme di oggetti ObjectStateEntry. La classe ObjectStateEntry
stessa contiene una serie di proprietà. Le più importanti sono la proprietà Entity, che include
un riferimento all’entità che ha causato il conflitto, la proprietà CurrentValues, che include i dati
modificati dell’entità e la proprietà OriginalValues, che include i dati dell’entità recuperati in
origine dal database.
L’approccio consigliato per risolvere i conflitti è quello di utilizzare il metodo Refresh per ricaricare
la cache del database e chiamare nuovamente SaveChanges. Il metodo Refresh compila i valori
originali di un’entità specifica (passati come secondo parametro) con valori aggiornati provenienti
dal database. Se l’utente ha apportato numerose modifiche, è consigliabile non imporgli di
reimpostarne le chiavi. Fortunatamente, il parametro RefreshMode del metodo Refresh consente
di gestire questa situazione. L’enumerazione RefreshMode definisce due valori:
■ StoreWins I valori correnti dell’entità verranno sovrascritti con i valori aggiornati
provenienti dal database. Tutte le modifiche apportate all’entità dall’utente andranno perse.
■ ClientWins I valori correnti dell’entità non verranno sovrascritti con i valori aggiornati
provenienti dal database. Tutte le modifiche apportate all’entità dall’utente vengono
conservate nella cache e verranno propagate al database al richiamo successivo di
SaveChanges.
Il codice seguente illustra un esempio che tenta di modificare il nome del prodotto esistente
in Products ObjectSet con il ProductID 14 e poi di salvare la modifica nel database. Se un altro
utente ha già modificato gli stessi dati, il gestore OptimisticConcurrencyException aggiorna i valori
originali nella cache, ma conserva i dati modificati nei valori correnti della cache e poi chiama
nuovamente SaveChanges.
NorthwindEntities northwindContext = new NorthwindEntities();
try
{
Product product = northwindContext.Products.Single(p => p.ProductID == 14);
product.ProductName = "Bean Curd";
northwindContext.SaveChanges();
}
catch (OptimisticConcurrencyException ex)
{
northwindContext.Refresh(RefreshMode.ClientWins, northwindContext.Products);
northwindContext.SaveChanges();
}
Capitolo 26
Visualizzazione e modifica dei dati mediante Entity Framework e associazione dei dati
587
Importante Entity Framework si interrompe e rileva l’eccezione
OptimisticConcurrencyException, quando rileva il primo conflitto. Se hai modificato più righe, le
chiamate successive al metodo SaveChanges potrebbero causare altri conflitti.
Inoltre, esiste una piccola possibilità che un altro utente abbia modificato i dati tra le chiamate
a Refresh e SaveChanges nel gestore di eccezioni OptimisticConcurrencyException. In
un’applicazione commerciale, bisogna essere preparati a rilevare anche questo tipo di eccezione.
Aggiunta ed eliminazione dei dati
Oltre a modificare i dati esistenti, con Entity Framework puoi aggiungere nuove voci o rimuovere
voci esistenti in un insieme ObjectSet.
Quando si utilizza Entity Framework per generare un modello entità, la definizione di ogni entità
comprende un metodo factory chiamato CreateXXX (dove XXX corrisponde al nome della classe
entità), utilizzabile per creare una nuova entità. Esso richiede di fornire parametri per ognuna
delle colonne obbligatorie (non NULL) del database sottostante. Puoi impostare i valori di colonne
aggiuntive utilizzando le proprietà esposte dalla classe entità. Per aggiungere la nuova entità a
un insieme ObjectSet, si utilizza il metodo AddObject. Per salvare la nuova entità nel database, si
richiama il metodo SaveChanges dell’oggetto ObjectContext.
Il codice seguente crea una nuova entità Product e la aggiunge all’elenco di prodotti dell’insieme
gestito dall’oggetto di contesto NorthwindEntities. Il codice aggiunge anche un riferimento
al nuovo oggetto per il fornitore con il SupplierID 1. (Il metodo Add viene fornito da Entity
Framework per aiutare a gestire le relazioni tra le entità.) Il metodo SaveChanges inserisce il nuovo
prodotto nel database.
NorthwindEntities northwindContext = new NorthwindEntities();
Product newProduct = Product,CreateProduct(0, "Fried Bread", false);
newProduct.UnitPrice = 55;
newProduct.QuantityPerUnit = "10 boxes";
ObjectSet<Product> products = northwindContext.Products;
products.AddObject(newProduct);
Supplier supplier = northwindContext.Suppliers.Single(s => s.SupplierID == 1);
supplier.Products.Add(newProduct);
northwindContext.SaveChanges();
Nota In questo esempio, il primo parametro per il metodo CreateProduct è ProductID. Nel database Northwind, il ProductID è una colonna IDENTITY. Quando richiami SaveChanges, SQL Server
genera il proprio valore unico per questa colonna ed elimina il valore specificato.
588
Parte V
Gestione dei dati
L’eliminazione di un oggetto entità da un insieme ObjectSet è semplice. Devi richiamare il metodo
DeleteObject e specificare l’entità da eliminare. Il codice seguente elimina tutti i prodotti con
un ProductID maggiore o uguale a 79. I prodotti vengono rimossi dal database quando viene
eseguito il metodo SaveChanges.
NorthwindEntities northwindContext = new NorthwindEntities();
var productList = from p in northwindContext.Products
where p.productID >= 79
select p;
ObjectSet<Product> products = northwindContext.Products;
foreach (var product in productList)
{
products.DeleteObject(product);
}
northwindContext.SaveChanges();
Si consiglia di prestare la massima attenzione durante l’eliminazione delle righe da tabelle che
hanno relazioni con altre tabelle, poiché tali operazioni possono causare errori di integrità
referenziale quando aggiorni il database. Ad esempio, se tenti di eliminare un fornitore che
attualmente offre prodotti attivi nel database Northwind, l’aggiornamento non potrà essere
completato con successo. Prima devi rimuovere tutti i prodotti dal fornitore. Per farlo, utilizza il
metodo Remove della classe Supplier. (Come il metodo Add, anche il metodo Remove è fornito da
Entity Framework.)
Se si verifica un errore mentre salvi le modifiche dopo aver aggiunto o eliminato i dati, il metodo
SaveChanges rileva un’eccezione UpdateException. Bisogna essere preparati a rilevarla.
A questo punto, disponi di nozioni sufficienti per completare l’applicazione Suppliers.
Creazione del codice per modificare, eliminare e creare i prodotti
1. Torna all’applicazione Suppliers di Visual Studio 2010 e visualizza il file SupplierInfo.xaml file
nella finestra Progettazione.
2. Nel riquadro XAML, modifica la definizione del controllo productsList al fine di rilevare
l’evento KeyDown e richiamare un metodo evento productsList_KeyDown. (Nota che questo
è il nome predefinito del metodo evento.)
3. Nella finestra dell’editor di codice e di testo, aggiungi il codice qui mostrato in grassetto al
metodo productsList_KeyDown:
private void productsList_KeyDown(object sender, KeyEventArgs e)
{
switch (e.Key)
{
case Key.Enter: editProduct(this.productsList.SelectedItem as Product);
break;
Capitolo 26
Visualizzazione e modifica dei dati mediante Entity Framework e associazione dei dati
589
case Key.Insert: addNewProduct();
break;
case Key.Delete: deleteProduct(this.productsList.SelectedItem as Product);
break;
}
}
Questo metodo esamina i tasti premuti dall’utente. Se l’utente preme il tasto Invio, il codice
richiama il metodo editProduct passandogli come parametro i dettagli relativi al prodotto.
Se l’utente preme il tasto Ins, il codice richiama il metodo addNewProduct per creare e
aggiungere all’elenco un nuovo prodotto per il fornitore corrente; inoltre, se l’utente preme
il tasto Canc, il codice deve richiamare il metodo deleteProduct per eliminare il prodotto. Nei
prossimi punti creerai i metodi editProduct, addNewProduct e deleteProduct.
4. Torna alla finestra Progettazione. Nel riquadro XAML, modifica la definizione del controllo
productsList al fine di rilevare l’evento MouseDoubleClick e richiamare un metodo evento
productsList_MouseDoubleClick. (Ancora una volta, questo è il nome predefinito del
metodo evento.)
5. Nella finestra dell’editor di codice e di testo, aggiungi il codice qui mostrato in grassetto al
metodo productsList_MouseDoubleClick:
private void productsList_MouseDoubleClick(object sender, KeyEventArgs e)
{
editProduct(this.productsList.SelectedItem as Product);
}
Questo metodo richiama semplicemente il metodo editProducts. È comodo per l’utente, che
prevede di modificare i dati facendo doppio clic su di essi.
6. Aggiungi il metodo deleteProduct alla classe SupplierInfo come mostrato di seguito:
private void deleteProduct(Product product)
{
MessageBoxResult response = MessageBox.Show(
String.Format("Delete {0}", product.ProductName),
"Confirm", MessageBoxButton.YesNo, MessageBoxImage.Question,
MessageBoxResult.No);
if (response == MessageBoxResult.Yes)
{
this.northwindContext.Products.DeleteObject(product);
saveChanges.IsEnabled = true;
}
}
Questo metodo chiede all’utente di confermare se desidera realmente eliminare il prodotto
attualmente selezionato. L’istruzione if richiama il metodo DeleteObject dell’insieme
Products ObjectSet. Infine, il metodo attiva il pulsante saveChanges. Successivamente,
aggiungerai a questo pulsante la funzionalità per inviare al database le modifiche apportate
all'insieme Products ObjectSet.
590
Parte V
Gestione dei dati
7. Nel menu Progetto, fai clic su Aggiungi classe. Nella finestra di dialogo Aggiungi nuovo
elemento – Suppliers, seleziona il modello Window (WPF), digita ProductForm.xaml nella
casella Nome e fai clic su Aggiungi.
Esistono diversi approcci per aggiungere e modificare i prodotti. Le colonne del controllo
ListView sono elementi di testo di sola lettura, ma è possibile creare una visualizzazione
elenco personalizzata contenente caselle di testo o altri controlli che abilitano l’immissione
di dati da parte dell’utente. Tuttavia, la strategia più semplice consiste nel creare un altro
form che permetta all’utente di modificare o aggiungere le informazioni relative a un
prodotto.
8. Nella finestra Progettazione, fai clic sul form ProductForm, quindi nella finestra Proprietà
imposta la proprietà ResizeMode su NoResize, quindi le proprietà Height su 225 e Width su
515.
9. Aggiungi tre controlli Label, tre controlli TextBox e due controlli Button in un punto
qualunque del form. Nella finestra Proprietà, imposta le proprietà di questi controlli con i
valori indicati nella seguente tabella.
Controllo
Proprietà
Valore
label1
Content
Product Name
Height
23
Width
120
Margin
17,20,0,0
VerticalAlignment
Top
HorizontalAlignment
Left
label2
label3
textBox1
Content
Quantity Per Unit
Height
23
Width
120
Margin
17,60,0,0
VerticalAlignment
Top
HorizontalAlignment
Left
Content
Unit Price
Height
23
Width
120
Margin
17100,0,0
VerticalAlignment
Top
HorizontalAlignment
Left
Name
productName
Height
21
Width
340
Margin
130,24,0,0
Capitolo 26
Visualizzazione e modifica dei dati mediante Entity Framework e associazione dei dati
Controllo
textBox2
textBox3
button1
button2
Proprietà
Valore
VerticalAlignment
Top
HorizontalAlignment
Left
Name
quantityPerUnit
Height
21
Width
340
Margin
130,64,0,0
VerticalAlignment
Top
HorizontalAlignment
Left
Name
unitPrice
Height
21
Width
120
Margin
130104,0,0
VerticalAlignment
Top
HorizontalAlignment
Left
Name
ok
Content
OK
Height
23
Width
75
Margin
130150,0,0
VerticalAlignment
Top
HorizontalAlignment
Left
Name
cancel
Content
Cancel
Height
23
Width
75
Margin
300150,0,0
VerticalAlignment
Top
HorizontalAlignment
Left
591
Nella finestra Progettazione, il form dovrebbe apparire come mostrato nell’immagine che
segue:
592
Parte V
Gestione dei dati
10. Fai doppio clic sul pulsante OK per creare un gestore eventi per l’evento click. Nella finestra
dell’editor di codice e di testo relativo al file ProductForm.xaml.cs, aggiungi il codice qui
mostrato in grassetto.
private void ok_Click(object sender, RoutedEventArgs e)
{
if (String.IsNullOrEmpty(this.productName.Text))
{
MessageBox.Show("The product must have a name", "Error",
MessageBoxButton.OK, MessageBoxImage.Error);
return;
}
decimal result;
if (!Decimal.TryParse(this.unitPrice.Text, out result))
{
MessageBox.Show("The price must be a valid number", "Error",
MessageBoxButton.OK, MessageBoxImage.Error);
return;
}
if (result < 0)
{
MessageBox.Show("The price must not be less than zero", "Error",
MessageBoxButton.OK, MessageBoxImage.Error);
return;
}
this.DialogResult = true;
}
L’applicazione mostra il form richiamando il metodo ShowDialog. Questo metodo visualizza
il form come finestra di dialogo modale. Quando un utente fa clic su un pulsante del form,
questo viene chiuso automaticamente se il codice dell’evento click imposta la proprietà
DialogResult. Se l’utente fa clic su OK, il metodo esegue una semplice convalida delle
informazioni immesse dall’utente. La colonna Quantity Per Unit del database accetta i valori
null, pertanto l’utente può lasciare vuoto questo campo del form. Se egli immette il nome e
il prezzo di un prodotto valido, il metodo imposta su true la proprietà DialogResult del form.
Questo valore viene passato di nuovo alla chiamata del metodo ShowDialog.
Capitolo 26
Visualizzazione e modifica dei dati mediante Entity Framework e associazione dei dati
593
11. Torna alla finestra Progettazione relativa al file ProductForm.xaml. Seleziona il pulsante
Cancel e, nella finestra Proprietà, imposta la proprietà IsCancel su true. (Seleziona la casella
di controllo.)
Quando un utente fa clic sul pulsante Cancel del form, questo viene chiuso
automaticamente e restituisce un valore DialogResult di false al metodo ShowDialog.
12. Passa alla finestra dell’editor di codice e di testo in cui viene visualizzato il file SupplierInfo.
xaml.cs. Aggiungi il metodo addNewProduct illustrato di seguito alla classe SupplierInfo:
private void addNewProduct()
{
ProductForm pf = new ProductForm();
pf.Title = "New Product for " + supplier.CompanyName;
if (pf.ShowDialog().Value)
{
Product newProd = new Product();
newProd.ProductName = pf.productName.Text;
newProd.QuantityPerUnit = pf.quantityPerUnit.Text;
newProd.UnitPrice = Decimal.Parse(pf.unitPrice.Text);
this.supplier.Products.Add(newProd);
this.productsInfo.Add(newProd);
saveChanges.IsEnabled = true;
}
}
Il metodo addNewProduct crea una nuova istanza del form ProductForm, ne imposta la
proprietà Title in modo da contenere il nome del fornitore, quindi richiama il metodo
ShowDialog per visualizzare il form come finestra di dialogo modale. Se l’utente immette
dati validi e fa clic sul pulsante OK nel form, il codice del blocco if crea un nuovo oggetto
Product e lo popola con le informazioni presenti nell’istanza ProductForm. Il metodo lo
aggiunge quindi all’insieme Products del fornitore corrente e all’elenco visualizzato nel
controllo visualizzazione elenco presente nel form. Infine, il codice attiva il pulsante Save
Changes. In una fase successiva verrà aggiunto al gestore eventi click di questo pulsante il
codice necessario per consentire all’utente di salvare le modifiche nel database.
13. Aggiungi il metodo editProduct qui mostrato alla classe SupplierInfo.
private void editProduct(Product product)
{
ProductForm pf = new ProductForm();
pf.Title = "Edit Product Details";
pf.productName.Text = product.ProductName;
pf.quantityPerUnit.Text = product.QuantityPerUnit;
pf.unitPrice.Text = product.UnitPrice.ToString();
if (pf.ShowDialog().Value)
{
product.ProductName = pf.productName.Text;
product.QuantityPerUnit = pf.quantityPerUnit.Text;
product.UnitPrice = Decimal.Parse(pf.unitPrice.Text);
saveChanges.IsEnabled = true;
}
}
594
Parte V
Gestione dei dati
Il metodo editProduct crea anche un’istanza del form ProductForm. Questa volta, oltre a
impostare la proprietà Title, il codice popola anche i campi del form con le informazioni
relative al prodotto attualmente selezionato. Una volta visualizzato il form, l’utente
può procedere a modificare i valori. Facendo clic sul pulsante OK per chiudere il form,
il codice del blocco if copia i nuovi valori nel prodotto attualmente selezionato e
abilita il pulsante Save Changes. Nota che in questo caso non è necessario aggiornare
manualmente l’elemento corrente nell’elenco productsInfo, poiché la classe Product notifica
automaticamente le modifiche apportate ai dati al controllo visualizzazione elenco.
14. Torna alla finestra Progettazione e visualizza il file SupplierInfo.xaml. Fai doppio clic sul
pulsante Save Changes per creare il metodo gestore eventi click.
15. Nella finestra dell’editor di codice e di testo, aggiungi le istruzioni using che seguono
all’elenco all’inizio del file:
using System.Data;
using System.Data.Objects;
Questi spazi dei nomi contengono molti tipi utilizzati da Entity Framework.
16. Cerca il metodo saveChanges_Click e aggiungi il codice illustrato di seguito in grassetto:
private void saveChanges_Click(object sender, RoutedEventArgs e)
{
try
{
this.northwindContext.SaveChanges();
saveChanges.IsEnabled = false;
}
catch (OptimisticConcurrencyException)
{
this.northwindContext.Refresh(RefreshMode.ClientWins,
northwindContext.Products);
this.northwindContext.SaveChanges();
}
catch (UpdateException uEx)
{
MessageBox.Show(uEx.InnerException.Message, "Error saving changes");
this.northwindContext.Refresh(RefreshMode.StoreWins,
northwindContext.Products);
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "Error saving changes");
this.northwindContext.Refresh(RefreshMode.StoreWins,
northwindContext.Products);
}
}
Questo metodo richiama il metodo SaveChanges dell’oggetto ObjectContext per restituire
tutte le modifiche al database. I gestori rilevano tutte le eccezioni che possono verificarsi.
Il gestore OptimisticConcurrencyException utilizza la strategia descritta in precedenza
per aggiornare la cache e salvare nuovamente le modifiche. Il gestore UpdateException
riporta l’errore all’utente e poi aggiorna la cache dal database specificando il parametro
Capitolo 26
Visualizzazione e modifica dei dati mediante Entity Framework e associazione dei dati
595
RefreshMode.StoreWins. (In seguito a questa operazione, le modifiche apportate dall’utente
vengono eliminate.) Osserva che i dati più significativi di questa eccezione sono contenuti
nella proprietà InnerException della stessa (anche se normalmente non serve che l'utente li
visualizzi). Se si verifica un altro tipo di eccezione, il gestore Exception visualizza un semplice
messaggio e aggiorna la cache dal database.
Verifica del funzionamento dell’applicazione Suppliers
1. Nel menu Debug, fai clic su Avvia senza eseguire debug per costruire ed eseguire
l’applicazione. Una volta apparso il form che visualizza i prodotti forniti da Exotic Liquids,
fai clic sul terzo prodotto (Aniseed Syrup) e premi Invio o fai doppio clic sulla riga. Viene
visualizzato il form Edit Product Details. Modifica il valore nel campo Unit Price in 12,5,
quindi fai clic su OK. Verifica che il nuovo prezzo venga copiato nella visualizzazione elenco.
2. Premi il tasto Ins. Viene visualizzato il form New Product for Exotic Liquids. Immetti il nome
di un prodotto, la quantità unitaria e il prezzo, quindi fai clic su OK. Verifica che il nuovo
prodotto venga aggiunto alla visualizzazione elenco.
Il valore nella colonna Product ID dovrebbe essere 0. Questo valore è una colonna identity
nel database, pertanto SQL Server genera un valore univoco per tale colonna quando salvi
le modifiche.
3. Fai clic su Save Changes. Una volta salvati i dati, l’ID del nuovo prodotto viene visualizzato
nella visualizzazione elenco.
4. Fai clic sul nuovo prodotto, quindi premi il tasto Canc. Fai clic su Yes nella finestra di dialogo
Confirm. Verifica che il prodotto scompaia dal form. Fai nuovamente clic su Save Changes e
verifica che l’operazione venga completata senza errori.
Se desideri, prova ad aggiungere, rimuovere e modificare i prodotti di altri fornitori.
Puoi così eseguire alcune modifiche prima di fare clic su Save Changes, e il metodo
SubmitChanges salva tutte le variazioni apportate in seguito all’ultima operazione di
recupero o salvataggio dei dati.
Suggerimento Se elimini o sovrascrivi accidentalmente i dati di un prodotto che desideri conservare, chiudi l’applicazione senza fare clic su Save Changes. Nota che questa
versione dell’applicazione non segnala all’utente se egli tenta di uscire senza salvare le
modifiche.
In alternativa, puoi aggiungere all’applicazione un pulsante Discard Changes che richiami il
metodo Refresh dell’oggetto northwindContext ObjectContext per compilare nuovamente
le tabelle con i dati presenti nel database, come illustrato nei gestori di eccezioni
dell’esercizio precedente.
5. Chiudi il form e ritorna a Visual Studio 2010.
596
Parte V
Gestione dei dati
In questo capitolo, hai appreso come utilizzare Entity Framework per generare un modello entità
per il database. Hai imparato a utilizzare il modello entità da un’applicazione WPF associando i
controlli a insiemi di entità. Inoltre, hai appreso come utilizzare LINQ to Entities per accedere ai
dati attraverso un modello entità.
■ Se desideri continuare con il capitolo successivo
Continua a eseguire Visual Studio 2010 e passa al capitolo 27.
■ Se desideri uscire ora da Visual Studio 2010
Nel menu File, fai clic su Esci. Se sullo schermo è visualizzata una finestra di dialogo Salva, fai
clic su Sì per salvare il progetto.
Riferimenti rapidi del capitolo 26
Obiettivo
Azione
Creazione di classi entità utilizzando Entity Framework
Aggiungi una nuova classe al progetto utilizzando il modello ADO.NET
Entity Data Model. Utilizza la procedura guidata Entity Data Model per
eseguire la connessione al database contenente le tabelle che intendi modellare e seleziona le tabelle desiderate.
Visualizzazione dei dati in un controllo WPF da un oggetto entità o
da un insieme
Definisci un’associazione per la proprietà appropriata del controllo.
Se il controllo visualizza un elenco di oggetti, impostane la proprietà
DataContext a un insieme di oggetti entità. Se il controllo visualizza i dati
di un singolo oggetto, imposta la proprietà DataContext del controllo in
un oggetto entità e specifica la proprietà dell’oggetto entità da visualizzare nell’attributo Path dell’associazione.
Modifica delle informazioni di
un database utilizzando Entity
Framework
Per prima cosa, esegui una delle seguenti operazioni:
■
Per aggiornare una riga in una tabella del database, preleva i dati
della riga in un oggetto entità e assegna i nuovi valori alle proprietà
appropriate dell’oggetto entità.
■
Per inserire una nuova riga in una tabella del database, crea una
nuova istanza dell’oggetto entità corrispondente utilizzando il
metodo factory CreateXXX generato per questa classe entità (dove
XXX corrisponde al nome dell’entità). Impostane le proprietà e poi
richiama il metodo AddObject dell’insieme ObjectSet appropriato,
specificando il nuovo oggetto entità come parametro.
■
Per rimuovere una riga da una tabella del database, richiama
il metodo DeleteObject dell’insieme ObjectSet appropriato,
specificando l’oggetto entità da rimuovere come parametro.
Fatto ciò, una volta ultimate le modifiche, richiama il metodo
SaveChanges dell’oggetto SaveChanges per propagare le variazioni al
database.
Gestione dei conflitti in fase di aggiornamento di un database Entity
Framework
Fornisci un gestore per l’eccezione ChangeConflictException. Nel gestore
di eccezioni, richiama il metodo Refresh dell'oggetto ObjectContext al fine
di recuperare i dati più recenti dal database per i valori originali nella cache e poi richiama nuovamente SaveChanges.
Parte VI
Creazione di soluzioni professionali
con Visual Studio 2010
In questa parte:
Introduzione alla TPL (Task Parallel Library) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 599
Esecuzione di accessi simultanei ai dati . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 649
Creazione e utilizzo di un servizio Web . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 683
597
Capitolo 27
Introduzione alla TPL
(Task Parallel Library)
Gli argomenti trattati in questo capitolo sono:
■
Descrivere i vantaggi ottenibili dall'implementazione di operazioni parallele in
un'applicazione.
■
Illustrare perché la TPL è la piattaforma ottimale per l'implementazione di applicazioni in
grado di sfruttare i processori multicore.
■
Utilizzare la classe Task per creare ed eseguire operazioni parallele in un'applicazione.
■
Utilizzare la classe Parallel per eseguire in parallelo alcuni comuni costrutti di
programmazione.
■
Utilizzare le attività con thread per migliorare la velocità di risposta e il throughput nelle
applicazioni GUI (Graphical User Interface).
■
Annullare le attività dalla lunga esecuzione e gestire le eccezioni generate dalle operazioni
parallele.
Fin qui si è visto come utilizzare Microsoft Visual C# per creare applicazioni in grado di fornire
un'interfaccia grafica utente e di gestire i dati contenuti in un database. Queste caratteristiche
sono comuni alla maggior parte dei sistemi moderni. Tuttavia, assieme alla tecnologia sono
cresciute anche le esigenze degli utenti, e le applicazioni che consentono loro di eseguire le
operazioni quotidiane devono poter fornire soluzioni ancora più sofisticate. La parte finale
di questo libro illustra alcune delle funzionalità avanzate introdotte con .NET Framework 4.0.
In particolare, in questo capitolo è possibile apprendere come migliorare la concorrenza in
un'applicazione mediante l'uso della TPL (Task Parallel Library). Il capitolo successivo mostra come
utilizzare le estensioni parallele fornite con .NET Framework insieme a LINQ (Language Integrated
Query) per migliorare il throughput delle operazioni di accesso ai dati. Infine, l'ultimo capitolo
presenta Windows Communication Foundation e la creazione di soluzioni distribuite in grado di
incorporare servizi eseguiti su più computer. Al termine, l'appendice descrive come impiegare il
Dynamic Language Runtime per creare applicazioni e componenti C# in grado di interagire con
servizi creati in altri linguaggi ed eseguiti fuori dalla struttura fornita da .NET Framework, come ad
esempio Python e Ruby.
Nei capitoli precedenti di questo libro, hai visto come usare C# per scrivere programmi eseguibili
in modalità a thread singolo. Per “thread singolo” si intende che in ogni dato momento, un
programma esegue un'unica istruzione. Non sempre questo è l'approccio più efficiente. Ad
esempio, come si è visto nel capitolo 23, “Raccolta dell’input utente”, se il programma attende che
l'utente faccia clic su un pulsante in un form WPF (Windows Presentation Foundation), potrebbero
esserci altri lavori da svolgere durante tale attesa.
599
600
Parte VI
Creazione di soluzioni professionali con Visual Studio 2010
Tuttavia, quando un programma a thread singolo deve eseguire calcoli lunghi e pesanti per il
processore, esso non può rispondere all'utente che immette dati in un form o fa clic su una voce
di menu. Per l'utente, l'applicazione sembra congelata. In questi casi, l'interfaccia utente risponde
di nuovo solo al termine delle operazioni di calcolo. Le applicazioni in grado di eseguire più
attività contemporaneamente possono fare un uso molto migliore delle risorse disponibili su un
computer, operare più velocemente e essere più rapide nel rispondere. Inoltre, alcune singole
attività possono essere eseguite più rapidamente se è possibile dividerle in percorsi di esecuzione
paralleli eseguiti in modo concorrente. Nel capitolo 23 si è visto come WPF può sfruttare i thread
per migliorare la velocità di risposta di un'interfaccia grafica utente. In questo capitolo è possibile
apprendere come utilizzare la TPL per implementare nei propri programmi una forma più
generica di multitasking utilizzabile in applicazioni pesanti dal punto di vista dell'elaborazione e
non solo in quelle cui è assegnata la gestione delle interfacce utente.
Perché eseguire il multitasking mediante l'elaborazione parallela?
Vi sono due motivi principali per cui può essere utile eseguire il multitasking in un'applicazione:
■ Per migliorare la velocità di risposta Al fine di dare all'utente di un'applicazione
l'impressione che il programma esegua più di un'attività alla volta, è possibile dividere il
programma in thread concorrenti e consentire l'esecuzione di ciascuno di essi a turno per un
breve periodo di tempo. Questo è il comune modello cooperativo con cui hanno familiarità
molti sviluppatori esperti di Windows. Tuttavia non si tratta di un vero multitasking,
perché il processore è condiviso tra i vari thread, e la natura cooperativa di questo
approccio richiede che il codice eseguito da ogni thread si comporti in modo appropriato.
Se un thread prende il controllo della CPU e delle risorse disponibili a spese degli altri,
i vantaggi di questo approccio sono vanificati. Talvolta è difficile creare applicazioni dal
comportamento corretto che seguano questo modello in modo appropriato.
■ Per migliorare la scalabilità La scalabilità può essere migliorata facendo un uso efficiente
delle risorse di elaborazione disponibili e utilizzando queste risorse per ridurre il tempo
necessario per eseguire le varie parti di un'applicazione. Lo sviluppatore può stabilire quali
parti di un'applicazione possono essere eseguite in parallelo, e predisporle quindi per
l'esecuzione simultanea. Con l'aggiunta di ulteriori risorse di calcolo, potrà essere eseguito
in parallelo un maggior numero di attività. Fino a poco tempo fa, questo modello era
adatto solo a sistemi dotati di più CPU o in grado di suddividere l'elaborazione tra computer
differenti collegati a una rete. In entrambi i casi, era necessario utilizzare un modello in
grado di gestire la coordinazione tra attività parallele. Microsoft fornisce una versione
specializzata di Windows chiamata High Performance Compute (HPC) Server 2008, la quale
permette alle organizzazioni di creare cluster di server in grado di distribuire ed eseguire
le attività in parallelo. Gli sviluppatori possono utilizzare l'implementazione Microsoft
dell'interfaccia MPI (Message Passing Interface), per creare applicazioni basate su attività
parallele che si coordinano e collaborano tra loro per mezzo di messaggi.
Capitolo 27
Introduzione alla TPL (Task Parallel Library)
601
Le soluzioni basate su Windows HPC Server 2008 e MPI sono ideali per applicazioni di
grandi dimensioni usate per eseguire elaborazioni di dati ingegneristici e scientifici, ma sono
molto costose per sistemi desktop di dimensioni più ridotte.
Da queste descrizioni è possibile essere indotti a pensare che il modo più conveniente per creare
soluzioni multitasking per le applicazioni desktop consista nell'usare l'approccio multithread
cooperativo. Tuttavia, questo approccio è stato sviluppato semplicemente come meccanismo
da usare per aumentare la velocità di risposta e permettere ai computer con un solo processore
di garantire che ogni attività riceva un'equa quantità di tempo del processore. Non è tuttavia
adatto per le macchine multiprocessore, poiché non è progettato per distribuire il carico tra i vari
processori e, di conseguenza, non gestisce bene la scalabilità. In passato, le macchine desktop
con più processori erano costose e di conseguenza relativamente rare, pertanto tutto ciò non
rappresentava un problema. Tuttavia oggi la situazione sta cambiando, come è possibile vedere di
seguito.
La crescita dei processori multicore
Dieci anni fa, il costo di un personal computer decoroso era compreso tra 500 e 1000 euro. Oggi,
un personal computer dignitoso ha ancora lo stesso prezzo anche dopo dieci anni di aumento
dell'inflazione. Tra le caratteristiche di un PC tipico odierno vi sono probabilmente un processore
operante a una velocità compresa tra 2 e 3 GHz, un disco fisso da 500 GB, 4 GB di memoria RAM,
una scheda grafica ad alta velocità e alta risoluzione e un'unità DVD riscrivibile. Dieci anni fa, la
velocità del processore di una macchina tipica era compresa tra 500 MHz e 1 GHz, un disco rigido
da 80 GB era considerato molto grande, Windows funzionava felicemente con 256 MB o meno di
memoria RAM e le unità CD riscrivibili costavano ben oltre 100 dollari US, mentre le unità DVD
riscrivibili erano rare e estremamente costose. Queste sono le gioie del progresso tecnologico:
hardware sempre più veloce e potente a prezzi sempre più bassi.
Questa tendenza non è nuova. Nel 1965, Gordon E. Moore, cofondatore di Intel, scrisse un
articolo intitolato “Cramming more components onto integrated circuits” (Come inserire più
componenti nei circuiti integrati), in cui discuteva la crescente miniaturizzazione dei componenti
e la conseguente possibilità di incorporare più semiconduttori in un chip di silicio, e come i costi
di produzione in calo e la maggiore accessibilità della tecnologia avrebbero portato l'economia
a imporre la compressione di anche 65.000 componenti su un singolo chip entro il 1975. Le
osservazioni di Moore furono alla base della famosa “Legge di Moore”, la quale in sostanza
afferma che il numero di semiconduttori che è possibile incorporare in un circuito integrato di
costo conveniente aumenta esponenzialmente, raddoppiando circa ogni due anni. In realtà,
Gordon Moore inizialmente era più ottimista e prevedeva che il volume dei semiconduttori
fosse destinato a raddoppiare ogni anno, ma dovette in seguito modificare i suoi calcoli. La
capacità di compattare i semiconduttori aprì la strada alla possibilità di scambiare dati tra loro
più velocemente. Ciò permise di prevedere che i produttori di chip sarebbero stati in grado di
produrre microprocessori più veloci e potenti con un ritmo pressoché continuo, permettendo
così agli sviluppatori di software di creare programmi sempre più complessi da eseguire più
velocemente.
602
Parte VI
Creazione di soluzioni professionali con Visual Studio 2010
La Legge di Moore sulla miniaturizzazione dei componenti elettronici è ancora valida, anche
dopo oltre 40 anni. Tuttavia, la fisica inizia a farsi sentire in materia. Esiste un limite di velocità
oltre cui non è possibile andare nel trasmettere segnali tra i semiconduttori di un singolo chip,
indipendentemente da quanto piccoli o ammassati essi siano. Per uno sviluppatore di software,
la conseguenza più evidente di questa limitazione è che si è interrotta la corsa verso velocità dei
processori sempre più elevate. Sei anni fa, un processore veloce funzionava a 3 GHz. Oggi, un
processore veloce funziona ancora a 3 GHz.
Il limite alla velocità con cui i processori possono trasmettere i dati tra componenti ha fatto sì che
le società produttrici di chip cercassero metodi alternativi per aumentare la quantità di lavoro
svolto da un processore. Il risultato è che la maggior parte dei processori moderni hanno ora
due o più core. In effetti, i produttori di chip hanno inserito più processori su uno stesso chip e
aggiunta la logica necessaria per consentire loro di comunicare e di coordinarsi. I processori dualcore (con due core) e quad-core (con quattro core) si sono diffusi. Inoltre sono disponibili anche
chip con 8, 16, 32 e 64 core il cui prezzo si prevede sia destinato a scendere rapidamente nel
prossimo futuro. Pertanto, nonostante i processori abbiano smesso di accelerare, ora è possibile
averne più di uno su un unico chip.
Che cosa significa tutto questo per uno sviluppatore che crea applicazioni in C#?
Prima dei processori multicore, un'applicazione a thread singolo poteva essere velocizzata
semplicemente eseguendola su un processore più veloce. Con i processori multicore, ciò non è
più vero. Un'applicazione a thread singolo viene eseguita alla stessa velocità su processori a core
singolo, dual-core o quad-core con la stessa frequenza di clock. La differenza consiste nel fatto
che, su un processore dual-core, uno di questi rimarrà inutilizzato, mentre su un processore quadcore a rimanere in attesa di un compito da svolgere saranno tre dei core disponibili. Per utilizzare
al meglio i processori multicore, è necessario creare applicazioni in grado di sfruttare i vantaggi
offerti dal multitasking.
Implementazione del multitasking in un'applicazione desktop
Il termine multitasking indica la capacità di svolgere più operazioni contemporaneamente. Questo
concetto è facile da descrivere, ma finora si è dimostrato difficile da implementare.
In uno scenario ottimale, un'applicazione eseguita su un processore multicore funziona come
molte attività concorrenti in presenza di più core di elaborazione disponibili, in modo da
impegnare tutti i core presenti. Tuttavia, vi sono numerosi problemi da tenere in considerazione
per implementare la concorrenza, tra cui i seguenti:
■ Come è possibile dividere un'applicazione in un insieme di operazioni concorrenti?
■ Come è possibile gestire l'esecuzione concorrente di un insieme di operazioni su più
processori?
■ Come è possibile essere sicuri di avviare un numero di operazioni concorrenti pari al
numero di processori disponibili?
Capitolo 27
Introduzione alla TPL (Task Parallel Library)
603
■ Se un'operazione risulta bloccata, ad esempio quando è in attesa del completamento di
un'operazione di I/O, come è possibile rilevarne tale stato e fare in modo che il processore
esegua un'altra operazione invece di rimanere inattivo?
■ Come è possibile stabilire quando sono state completate una o più operazioni concorrenti?
■ Come è possibile sincronizzare l'accesso a dati condivisi per garantire che due o più
operazioni concorrenti non danneggino involontariamente i dati delle altre?
Per uno sviluppatore di applicazioni, la prima domanda dipende dal progetto dell'applicazione. Le
risposte alle altre domande dipendono dall'infrastruttura programmatica; per aiutare a risolvere
questi problemi, Microsoft offre la Task Parallel Library (TPL).
Il capitolo 28, “Esecuzione di accessi simultanei ai dati”, illustra come alcuni problemi legati alle
query abbiano soluzioni parallele e come utilizzare il tipo ParallelEnumerable di PLINQ per gestire
le operazioni di query in parallelo. Tuttavia, talvolta è necessario un approccio più autoritario per
situazioni più generali. La TPL contiene una serie di tipi e operazioni che permettono di specificare
in modo più esplicito come si desidera dividere un'applicazione in un insieme di attività parallele.
Attività, thread e ThreadPool
Il tipo più importante nella TPL è la classe Task. La classe Task è un'astrazione di un'operazione
concorrente. Dopo essere stato creato, un oggetto Task consente di eseguire un blocco di
codice. È possibile creare le istanze di più oggetti Task e avviarli in parallelo, a condizione che sia
disponibile un numero sufficiente di processori o core di processori.
Nota Da ora in poi, il termine “processore” sarà usato per fare riferimento ai processori singlecore, ai singoli core di un processore e ai processori multicore.
Internamente, la TPL implementa le attività e le pianifica in modo da eseguirle mediante
oggetti Thread e la classe ThreadPool. Il multithreading e i pool di thread sono stati disponibili
in .NET Framework fin dalla versione 1.0, ed è possibile utilizzare la classe Thread nello spazio
dei nomi System.Threading direttamente nel codice. Tuttavia, la TPL fornisce un ulteriore grado
di astrazione che permette di distinguere facilmente tra il grado di esecuzione in parallelo
in un'applicazione (le attività) e le unità di esecuzione in parallelo (i thread). Solitamente, su
computer con un unico processore questi elementi coincidono. Tuttavia essi differiscono sui
computer con più processori o con un processore multicore. Se si progetta un programma basato
direttamente sui thread, l'applicazione che ne deriva avrà una scalabilità insufficiente; una volta
avviato, il programma utilizzerà il numero di thread specificato e il sistema operativo pianificherà
solo tale numero di thread. Ciò può condurre a un overload e a tempi di risposta scadenti qualora
il numero di thread superi di molto quello dei processori disponibili, oppure a inefficienze e scarso
throughput se il numero di thread è inferiore a quello dei processori.
La TPL ottimizza il numero di thread necessari per implementare un insieme di attività concorrenti
e li pianifica efficientemente in base al numero di processori disponibili. La TPL impiega un
604
Parte VI
Creazione di soluzioni professionali con Visual Studio 2010
insieme di thread forniti da .NET Framework chiamato ThreadPool e implementa un meccanismo
di accodamento per distribuire il carico di lavoro tra tali thread.
Quando un programma crea un oggetto Task, l'attività viene aggiunta a una coda globale.
Quando un thread diviene disponibile, l'attività viene rimossa dalla coda globale ed eseguita dal
thread in questione. Il ThreadPool implementa numerose ottimizzazioni e utilizza un algoritmo di
sottrazione del lavoro per assicurare che i thread siano pianificati in modo efficiente.
Nota Il ThreadPool è disponibile anche nelle edizioni precedenti di .NET Framework, ma è stato
migliorato in modo significativo in .NET Framework 4.0 per supportare i Tasks.
È utile notare che il numero di thread creati da .NET Framework per gestire le attività non è
necessariamente uguale al numero di processori. A seconda della natura del carico di lavoro, uno
o più processori possono essere impiegati per eseguire lavori ad alta priorità per altre applicazioni
e servizi. Di conseguenza, il numero ottimale di thread per un'applicazione può essere inferiore a
quello dei processori presenti nella macchina. In alternativa, uno o più thread di un'applicazione
possono rimanere in attesa del completamento di lunghe operazioni di accesso alla memoria,
di I/O o di rete, lasciando liberi i processori corrispondenti. In questo caso, il numero ottimale
di thread può essere superiore a quello dei processori disponibili. .NET Framework segue una
strategia iterativa nota come algoritmo hill-climbing, per stabilire in modo dinamico il numero
ideale di thread per il carico di lavoro corrente.
Il punto saliente è dato dal fatto che tutto ciò che deve fare il codice è dividere l'applicazione in
attività che possano essere eseguite in parallelo. .NET Framework si prende la responsabilità di
creare il numero appropriato di thread in base all'architettura del processore e al carico di lavoro
del computer, associando le attività ai thread e gestendoli in modo da poterli eseguire in modo
efficiente. Non è importante se il lavoro viene diviso in troppe attività, poiché .NET Framework
tenta di eseguire solo un numero accettabile di thread concorrenti; infatti, è consigliabile
sovrapartizionare il lavoro da svolgere, perché ciò aiuta a fare in modo che l'applicazione sia
scalabile quando si passa a computer con un maggior numero di processori disponibili.
Creazione, esecuzione e controllo delle attività
L'oggetto Task e gli altri tipi presenti nella TPL sono ubicati nello spazio dei nomi System.
Threading.Tasks. Gli oggetti Task possono essere creati mediante il costruttore Task. Questo
costruttore è in overload, ma tutte le versioni presuppongono che il programmatore fornisca un
delegato Action come parametro. Come si è visto nel capitolo 23, il delegato Action fa riferimento
a un metodo che non restituisce alcun valore. L'oggetto task utilizza questo delegato per eseguire
il metodo quando pianificato. L'esempio che segue crea un oggetto Task che utilizza un delegato
per eseguire il metodo chiamato doWork; in alternativa è anche possibile usare un metodo
anonimo o un espressione lambda, come mostrato dal codice nei commenti:
Capitolo 27
Introduzione alla TPL (Task Parallel Library)
605
Task task = new Task(new Action(doWork));
// Task task = new Task(delegate { this.doWork(); });
// Task task = new Task(() => { this.doWork(); });
...
private void doWork()
{
// The task runs this code when it is started
...
}
Nota In molti casi, è possibile lasciare che il compilatore deduca autonomamente il tipo di dele-
gato Action e specificare semplicemente il metodo da eseguire. Ad esempio, è possibile riformulare il primo esempio appena vista come segue:
Task task = new Task(doWork);
Le regole di deduzione del delegato implementate dal compilatore vengono applicate non soltanto al tipo Action, ma ovunque sia possibile utilizzare un delegato. Nel resto di questo manuale
sono riportati molti altri esempi.
Il tipo Action predefinito fa riferimento a un metodo che non accetta alcun parametro. Altri
overload del costruttore Task accettano un parametro Action<object> che rappresenta un
delegato che fa riferimento a un metodo che accetta un unico parametro object. Questi overload
consentono di passare i dati all'interno del metodo eseguito dall'attività. Il codice che segue
mostra un esempio:
Action<object> action;
action = doWorkWithObject;
object parameterData = ...;
Task task = new Task(action, parameterData);
...
private void doWorkWithObject(object o)
{
...
}
Dopo aver creato un oggetto Task, è possibile avviarne l'esecuzione mediante il metodo Start,
come mostrato di seguito:
Task task = new Task(...);
task.Start();
Anche il metodo Start è un overload, ed è possibile specificare facoltativamente un oggetto
TaskScheduler da usare per controllare il livello di concorrenza e altre opzioni di pianificazione. Si
raccomanda di utilizzare l'oggetto TaskScheduler predefinito e incorporato in .NET Framework,
oppure di definire una propria classe TaskScheduler personalizzata qualora si desideri avere
un maggiore controllo sul modo in cui le attività vengono accodate e pianificate. Informazioni
dettagliate su come fare tutto ciò esulano dallo scopo di questo manuale, ma per ulteriori
informazioni è possibile consultare la descrizione della classe astratta TaskScheduler nella
documentazione di .NET Framework Class Library fornita con Visual Studio.
606
Parte VI
Creazione di soluzioni professionali con Visual Studio 2010
Riferimenti all'oggetto TaskScheduler predefinito sono ottenibili tramite la proprietà statica Default
della classe TaskScheduler. La classe TaskScheduler fornisce anche la proprietà statica Current, la
quale restituisce un riferimento all'oggetto TaskScheduler attualmente utilizzato. (Questo oggetto
TaskScheduler viene usato ove non sia specificato esplicitamente un pianificatore.) Un'attività può
fornire suggerimenti per l'oggetto TaskScheduler predefinito su come pianificare ed eseguire
l'attività se si specifica un valore dall'enumerazione TaskCreationOptions nel costruttore Task. Per
ulteriori informazioni sull'enumerazione TaskCreationOptions, consultare la documentazione che
descrive la .NET Framework Class Library fornita con Visual Studio.
Una volta completato il metodo eseguito dall'attività, l'attività termina e il thread usato per
eseguire l'attività può essere riciclato per eseguire un'altra attività.
Normalmente, ove possibile il pianificatore si predispone per eseguire le attività in parallelo,
ma è anche possibile impostarlo in modo da pianificare le attività in modo seriale mediante la
creazione di una condizione di continuazione. Questa può essere creata richiamando il metodo
ContinueWith di un oggetto Task. Una volta completata l'azione eseguita dall'oggetto Task, il
pianificatore crea automaticamente un nuovo oggetto Task per eseguire l'azione specificata dal
metodo ContinueWith. Il metodo specificato dalla condizione di continuazione presuppone un
parametro Task, e il pianificatore passa al metodo un riferimento all'attività completata. Il valore
restituito da ContinueWith è un riferimento al nuovo oggetto Task. L'esempio di codice che segue
crea un oggetto Task che eseguire il metodo doWork e specifica una condizione di continuazione
che esegue il metodo doMoreWork in una nuova attività non appena viene completata la prima
attività:
Task task = new Task(doWork);
task.Start();
Task newTask = task.ContinueWith(doMoreWork);
...
private void doWork()
{
// The task runs this code when it is started
...
}
...
private void doMoreWork(Task task)
{
// The continuation runs this code when doWork completes
...
}
Il metodo ContinueWith è molto sovraccarico ed è possibile fornire numerosi parametri che
specificano elementi aggiuntivi, come ad esempio l'oggetto TaskScheduler da usare e un valore
TaskContinuationOptions. Il tipo TaskContinuationOptions è un'enumerazione contenente un
sovrainsieme dei valori presenti nell'enumerazione TaskCreationOptions. I valori aggiuntivi
disponibili comprendono:
Capitolo 27
■
Introduzione alla TPL (Task Parallel Library)
607
NotOnCanceled e OnlyOnCanceled L'opzione NotOnCanceled specifica che la condizione
di continuazione deve essere eseguita esclusivamente se l'azione precedente è stata
completata e non eliminata, mentre l'opzione OnlyOnCanceled indica che la condizione di
continuazione deve essere eseguita soltanto in caso di annullamento dell'azione precedente.
La sezione “Annullamento delle attività e gestione delle eccezioni” più avanti in questo
capitolo descrive come annullare un'attività.
■ NotOnFaulted e OnlyOnFaulted L'opzione NotOnFaulted indica che la condizione di
continuazione dovrebbe essere eseguita solamente al termine dell'azione precedente e
senza generare eccezioni non gestite. L'opzione OnlyOnFaulted fa sì che la condizione di
continuazione sia eseguita solo se l'azione precedente genera un'eccezione non gestita.
La sezione “Annullamento delle attività e gestione delle eccezioni” contiene ulteriori
informazioni su come gestire le eccezioni in un'attività.
■ NotOnRanToCompletion e OnlyOnRanToCompletion L'opzione NotOnRanToCompletion
specifica che la condizione di continuazione deve essere eseguita solamente se l'azione
precedente non viene completata con successo; in questo caso è necessario annullarla
o generare un'eccezione. OnlyOnRanToCompletion fa in modo che la condizione di
continuazione sia eseguita solo se l'azione precedente viene completata con successo.
L'esempio di codice che segue mostra come aggiungere una condizione di continuazione a
un'attività da eseguire solo se l'azione iniziale non genera un'eccezione non gestita:
Task task = new Task(doWork);
task.ContinueWith(doMoreWork, TaskContinuationOptions.NotOnFaulted);
task.Start();
Se si utilizza comunemente lo stesso insieme di valori TaskCreationOptions e lo stesso oggetto
TaskScheduler, è possibile utilizzare un oggetto TaskFactory per creare ed eseguire un'attività in
un'unica operazione. Il costruttore per la classe TaskFactory consente di specificare il pianificatore
delle attività, le opzioni di creazione dell'attività e le opzioni della condizione di continuazione
dell'attività che devono essere usate dalle attività costruite da questa factory. La classe
TaskFactory fornisce il metodo StartNew per creare ed eseguire un oggetto Task. Come il metodo
Start della classe Task, il metodo StartNew è di overload, ma tutti presuppongono un riferimento a
un metodo che dovrebbe essere eseguito dall'attività.
Il codice che segue mostra un esempio che crea ed esegue due attività mediante la stessa task
factory:
TaskScheduler scheduler = TaskScheduler.Current;
TaskFactory taskFactory = new TaskFactory(scheduler, TaskCreationOptions.None,
TaskContinuationOptions.NotOnFaulted);
Task task = taskFactory.StartNew(doWork);
Task task2 = taskFactory.StartNew(doMoreWork);
608
Parte VI
Creazione di soluzioni professionali con Visual Studio 2010
Anche non specificando alcuna particolare opzione di creazione delle attività e utilizzando il
pianificatore di attività predefinito, è consigliabile valutare l'utilizzo di un oggetto TaskFactory;
questo assicura uniformità e comporta la modifica di una minore quantità di codice per garantire
che tutte le attività siano eseguite nello stesso modo qualora risulti necessario modificare questo
processo in futuro. La classe Task espone il TaskFactory predefinito utilizzato dalla TPL tramite la
proprietà statica Factory. Puoi utilizzarlo come segue:
Task task = Task.Factory.StartNew(doWork);
Un requisito comune delle applicazioni che richiamano operazioni in parallelo consiste nel
sincronizzare le attività. La classe Task fornisce il metodo Wait, il quale implementa un semplice
metodo di coordinamento dell'attività. Esso permette di sospendere l'esecuzione del thread
corrente fino al completamento dell'attività specificata, come mostrato di seguito:
task2.Wait(); // Wait at this point until task2 completes
È possibile attendere un insieme di attività utilizzando i metodi statici WaitAll e WaitAny della
classe Task. Entrambi questi metodi accettano un array params contenente un insieme di oggetti
Task. Il metodo WaitAll attende il completamento di tutte le attività specificate, mentre WaitAny
si interrompe fino a quando non viene completata almeno una delle attività specificate. L’esempio
che segue mostra come utilizzarli:
Task.WaitAll(task, task2); // Wait for both task and task2 to complete
Task.WaitAny(task, task2); // Wait for either of task or task2 to complete
Utilizzo della classe Task per implementare l'esecuzione
in parallelo
Nell'esercizio che segue viene mostrato come utilizzare la classe Task per eseguire in parallelo
codice di un'applicazione che comporta un elevato carico di lavoro per il processore, ed è
possibile vedere come questa pratica riduca il tempo necessario per eseguire l'applicazione
mediante la suddivisione dei calcoli su più core del processore.
L'applicazione, chiamata GraphDemo, comprende un form WPF che utilizza un controllo Image
per visualizzare un grafico. L'applicazione traccia i punti di un grafico eseguendo un calcolo
complesso.
Nota Gli esercizi di questo capitolo sono concepiti per essere eseguiti su computer con processore multicore. Utilizzando una CPU single-core, non è possibile notare gli stessi effetti. Inoltre,
è consigliabile non avviare programmi o servizi aggiuntivi tra i vari esercizi, perché ciò potrebbe
avere effetto sui risultati visualizzati.
Capitolo 27
Introduzione alla TPL (Task Parallel Library)
609
Esame ed esecuzione dell'applicazione GraphDemo a thread singolo
1. Avvia Microsoft Visual Studio 2010 se non è già in esecuzione.
2. Apri la soluzione GraphDemo situata nella cartella \Microsoft Press\Visual CSharp Step By
Step\Chapter 27\GraphDemo della cartella Documenti.
3. In Esplora soluzioni, nel progetto GraphDemo, fai doppio clic sul file GraphWindow.xaml per
visualizzare il form nella finestra Progettazione.
Il form contiene i seguenti controlli:
■
Un controllo Image chiamato graphImage che mostra il grafico fornito
dall'applicazione.
■
Un controllo Button chiamato plotButton. L'utente può fare clic su questo pulsante per
generare i dati del grafico e vederlo nel controllo graphImage.
■
Un controllo Label chiamato duration. L'applicazione visualizza il tempo impiegato per
generare e visualizzare i dati per il grafico in questa etichetta.
4. In Esplora soluzioni, espandi GraphWindow.xaml e fai doppio clic su GraphWindow.xaml.cs
per visualizzare il codice per il form nella finestra dell’editor di codice e di testo.
Per visualizzare il grafico, il form utilizza un oggetto System.Windows.Media.Imaging.
WriteableBitmap chiamato graphBitmap. Le variabili pixelWidth e pixelHeight specificano
rispettivamente la risoluzione orizzontale e verticale per l'oggetto WriteableBitmap;
le variabili dpiX e dpiY specificano rispettivamente la densità orizzontale e verticale
dell'immagine in punti per pollice:
public partial class GraphWindow : Window
{
private static long availableMemorySize = 0;
private int pixelWidth = 0;
private int pixelHeight = 0;
private double dpiX = 96.0;
private double dpiY = 96.0;
private WriteableBitmap graphBitmap = null;
…
}
5. Esamina il costruttore GraphWindow. Sarà simile al seguente:
public GraphWindow()
{
InitializeComponent();
PerformanceCounter memCounter = new PerformanceCounter("Memory", "Available
Bytes");
availableMemorySize = Convert.ToUInt64(memCounter.NextValue());
this.pixelWidth = (int)availablePhysicalMemory / 20000;
if (this.pixelWidth < 0 || this.pixelWidth > 15000)
this.pixelWidth = 15000;
610
Parte VI
Creazione di soluzioni professionali con Visual Studio 2010
this.pixelHeight = (int)availablePhysicalMemory / 40000;
if (this.pixelHeight < 0 || this.pixelHeight > 7500)
this.pixelHeight = 7500;
}
Per evitare di presentare codice che esaurisce la memoria disponibile sul computer e genera
eccezioni OutOfMemory, questa applicazione crea un oggetto PerformanceCounter per
interrogare la quantità di memoria fisica disponibile sul computer. Fatto ciò impiega queste
informazioni per stabilire valori appropriati per le variabili pixelWidth e pixelHeight. Più
memoria è disponibile sul computer, maggiori saranno i valori generati per pixelWidth e
pixelHeight (limitati rispettivamente a 15.000 e 7.500 per le due variabili) e maggiori saranno
i vantaggi derivanti dall'uso della TPL con il progredire degli esercizi di questo capitolo.
Tuttavia, se l'applicazione continua a generare eccezioni OutOfMemory, aumentare i divisori
(20.000 e 40.000) usati per generare i valori di pixelWidth e pixelHeight.
Se si dispone di molta memoria, i valori calcolati per pixelWidth e pixelHeight possono
causare un overflow. In questo caso, tali valori conterranno valori negativi e l'applicazione
causerà un errore con conseguente eccezione. Il codice nel costruttore controlla il verificarsi
di questo caso e imposta i campi pixelWidth e pixelHeight su una coppia di valori utili che
permettono la corretta esecuzione dell'applicazione in questa situazione.
6. Esamina il codice del metodo plotButton_Click:
private void plotButton_Click(object sender, RoutedEventArgs e)
{
if (graphBitmap == null)
{
graphBitmap = new WriteableBitmap(pixelWidth, pixelHeight, dpiX, dpiY,
PixelFormats.Gray8, null);
}
int bytesPerPixel = (graphBitmap.Format.BitsPerPixel + 7) / 8;
int stride = bytesPerPixel * graphBitmap.PixelWidth;
int dataSize = stride * graphBitmap.PixelHeight;
byte [] data = new byte[dataSize];
Stopwatch watch = Stopwatch.StartNew();
generateGraphData(data);
duration.Content = string.Format("Duration (ms): {0}", watch.ElapsedMilliseconds);
graphBitmap.WritePixels(
new Int32Rect(0, 0, graphBitmap.PixelWidth, graphBitmap.PixelHeight),
data, stride, 0);
graphImage.Source = graphBitmap;
}
Questo metodo viene eseguito quando l'utente fa clic sul pulsante plotButton. Il codice crea
l'istanza dell'oggetto graphBitmap (se non creato in precedenza dall'utente facendo clic sul
pulsante plotButton), quindi specifica che ogni pixel rappresenta un tono di grigio con 8 bit
per pixel. Questo metodo utilizza le seguenti variabili e metodi:
Capitolo 27
Introduzione alla TPL (Task Parallel Library)
611
■
La variabile bytesPerPixel valuta il numero di byte necessari per contenere ciascun
pixel. (Il tipo WriteableBitmap supporta un'ampia gamma di formati di pixel, fino a 128
bit per pixel per le immagini a colori.)
■
La variabile stride contiene la distanza verticale, in byte, tra pixel adiacenti
nell'oggetto WriteableBitmap.
■
La variabile dataSize valuta il numero di byte necessari per contenere i dati per
l'oggetto WriteableBitmap. Questa variabile è utilizzata per inizializzare l'array data
con le dimensioni appropriate.
■
L'array byte data contiene i dati del grafico.
■
La variabile watch è un oggetto System.Diagnostics.Stopwatch.
Il tipo StopWatch
è utile per le operazioni di sincronizzazione. Il metodo statico StartNew del tipo
StopWatch crea una nuova istanza di un oggetto StopWatch e ne avvia l'esecuzione.
Per ottenere il tempo di esecuzione di un oggetto StopWatch, è possibile esaminare la
proprietà ElapsedMilliseconds.
■
Il metodo generateGraphData popola l'array data con i dati del grafico da visualizzare
tramite l'oggetto WriteableBitmap. Questo metodo sarà esaminato nel passaggio
successivo.
■
Il metodo WritePixels della classe WriteableBitmap copia i dati da un array di byte a
un'immagine bitmap per la visualizzazione. Questo metodo accetta un parametro
Int32Rect che specifica l'area da popolare nell'oggetto WriteableBitmap, i dati da
utilizzare per la copia nell'oggetto WriteableBitmap, la distanza verticale tra pixel
adiacenti nell'oggetto WriteableBitmap e lo scostamento con cui iniziare a scrivere i
dati nell'oggetto WriteableBitmap.
Nota Il metodo WritePixels può essere usato per sovrascrivere in modo selettivo le
informazioni presenti in un oggetto WriteableBitmap. In questo esempio, il codice
sovrascrive l'intero contenuto. Per ulteriori informazioni sulla classe WriteableBitmap,
consultare la documentazione di .NET Framework Class Library installata con Visual Studio
2010.
■
La proprietà Source di un controllo Image indica i dati che il controllo Image
deve visualizzare. Questo esempio imposta la proprietà Source sull'oggetto
WriteableBitmap.
7. Esamina il codice del metodo generateGraphData:
612
Parte VI
Creazione di soluzioni professionali con Visual Studio 2010
private
{
int
int
int
void generateGraphData(byte[] data)
a = pixelWidth / 2;
b = a * a;
c = pixelHeight / 2;
for (int x = 0; x < a; x ++)
{
int s = x * x;
double p = Math.Sqrt(b - s);
for (double i = -p; i < p; i += 3)
{
double r = Math.Sqrt(s + i * i) / a;
double q = (r - 1) * Math.Sin(24 * r);
double y = i / 3 + (q * c);
plotXY(data, (int)(-x + (pixelWidth / 2)), (int)(y + (pixelHeight / 2)));
plotXY(data, (int)(x + (pixelWidth / 2)), (int)(y + (pixelHeight / 2)));
}
}
}
Questo metodo esegue una serie di calcoli per visualizzare i punti di un grafico piuttosto
complesso. (Il calcolo effettivo non è importante e serve solo a generare un grafico
piacevole alla vista.) Durante il calcolo di ciascun punto, il programma richiama il metodo
plotXY per impostare i byte necessari nell'array data che corrispondono a tali punti. I punti
del grafico appaiono riflessi specularmente attorno all'asse X, pertanto il metodo plotXY
viene chiamato due volte per ogni calcolo: una volta per il valore positivo della coordinata
X, e una volta per il valore negativo.
8. Esamina il metodo plotXY:
private void plotXY(byte[] data, int x, int y)
{
data[x + y * pixelWidth] = 0xFF;
}
Questo semplice metodo imposta il byte appropriato nell'array data che corrisponde
alle coordinate X e Y passate come parametri. Il valore 0xFF indica che, al momento della
visualizzazione del grafico, il pixel corrispondente dovrebbe essere impostato su bianco.
Eventuali pixel lasciati incustoditi appaiono di colore nero.
9. Nel menu Debug, fai clic su Avvia senza eseguire debug per costruire ed eseguire
l’applicazione.
10. Una volta apparsa la finestra Graph Demo, fai clic su Plot Graph e attendi.
Abbi pazienza. L'applicazione impiega alcuni secondi per generare e visualizzare il grafico.
L'immagine che segue mostra il grafico. Nota il valore nell'etichetta Duration (ms) della
seguente figura. In questo caso, per visualizzare il grafico l'applicazione ha impiegato 4478
millisecondi (ms).
Capitolo 27
Introduzione alla TPL (Task Parallel Library)
613
Nota
L'applicazione è stata eseguita su un computer con 2 GB di memoria e un processore Intel®Core 2 Duo Desktop E6600 a 2,40 GHz. I tempi ottenuti possono variare in
caso di utilizzo di processori o di quantità di memoria differenti. Inoltre, è possibile notare
che inizialmente sembra essere necessario più tempo del previsto per visualizzare il grafico. Ciò avviene a causa del tempo necessario per inizializzare le strutture dei dati richieste
per la reale visualizzazione del grafico come parte del metodo WritePixels del controllo
graphBitmap, oltre al tempo necessario per calcolare i dati per il grafico. Esecuzioni successive non sono soggette a questo overhead.
11. Fai nuovamente clic su Plot Graph, quindi prendi nota del tempo impiegato. Ripeti questa
azione più volte per ottenere un valore medio.
12. Sul desktop, fai clic con il pulsante destro del mouse in una zona vuota della barra delle
applicazioni, quindi fai clic su Avvia Gestione attività nel menu di scelta rapida.
Nota In Windows Vista, il comando nel menu di scelta rapida è chiamato Task Manager.
13. In Gestione attività Windows, fai clic sulla scheda Prestazioni.
14. Torna alla finestra Graph Demo e fai clic su Plot Graph.
15. In Gestione attività Windows, prendi nota del valore massimo di utilizzo della CPU mentre
viene generato il grafico. I risultati ottenuti possono variare, ma su un processore dual-core
l'utilizzo della CPU dovrebbe attestarsi attorno al 50–55%, come mostrato nella seguente
immagine. Su una macchina quad-core, l'utilizzo della CPU sarà probabilmente inferiore al
30%.
614
Parte VI
Creazione di soluzioni professionali con Visual Studio 2010
16. Torna alla finestra Graph Demo e fai nuovamente clic su Plot Graph. Annota il valore di
utilizzo della CPU in Gestione attività Windows. Ripeti questa azione più volte per ottenere
un valore medio.
17. Chiudi la finestra Graph Demo e riduci Gestione attività Windows a icona.
A questo punto disponi di una linea base relativa al tempo necessario all'applicazione per eseguire
i calcoli. Tuttavia, è evidente dall'utilizzo della CPU mostrato da Gestione attività Windows che
l'applicazione non sta facendo pieno uso delle risorse di elaborazione disponibili. Su una macchina
dual-core, viene usata poco più della metà della potenza della CPU, mentre su una macchina
quad-core sarebbe impiegato poco più di un quarto di tale potenza CPU. Questo fenomeno si
verifica perché l'applicazione è di tipo a thread singolo, e questa in un'applicazione Windows può
occupare solo un singolo core di un processore multicore. Per suddividere il carico tra tutti i core
disponibili, è necessario dividere l'applicazione in attività e fare in modo che queste siano eseguite
da un thread separato eseguito su un core differente.
Modifica dell'applicazione GraphDemo per utilizzare thread paralleli
1. Torna a Visual Studio 2010 e visualizza il file GraphWindow.xaml.cs nella finestra dell’editor
di codice e di testo (se non già aperto).
2. Esamina il metodo generateGraphData.
Lo scopo di questo metodo è di popolare gli elementi dell'array data. Questo esegue
un'iterazione attraverso l'array utilizzando il ciclo for più esterno basato sulla variabile di
controllo del loop x, come qui evidenziato in grassetto:
Capitolo 27
private
{
int
int
int
Introduzione alla TPL (Task Parallel Library)
615
void generateGraphData(byte[] data)
a = pixelWidth / 2;
b = a * a;
c = pixelHeight / 2;
for (int x = 0; x < a; x ++)
{
int s = x * x;
double p = Math.Sqrt(b - s);
for (double i = -p; i < p; i += 3)
{
double r = Math.Sqrt(s + i * i) / a;
double q = (r - 1) * Math.Sin(24 * r);
double y = i / 3 + (q * c);
plotXY(data, (int)(-x + (pixelWidth / 2)), (int)(y + (pixelHeight / 2)));
plotXY(data, (int)(x + (pixelWidth / 2)), (int)(y + (pixelHeight / 2)));
}
}
}
Il calcolo eseguito da un'iterazione di questo ciclo è indipendente dai calcoli eseguiti dalle
altre iterazioni. Pertanto, è logico partizionare il lavoro eseguito da questo ciclo ed eseguire
iterazioni differenti su un processore separato.
3. Modifica la definizione del metodo generateGraphData in modo da accettare due ulteriori
parametri int chiamati partitionStart e partitionEnd, come qui evidenziato in grassetto:
private void generateGraphData(byte[] data, int partitionStart, int partitionEnd)
{
...
}
4. Nel metodo generateGraphData, modifica il ciclo for più esterno in modo da consentirgli
l'iterazione tra i valori di partitionStart e di partitionEnd, come qui evidenziato in grassetto:
private void generateGraphData(byte[] data, int partitionStart, int partitionEnd)
{
...
for (int x = partitionStart; x < partitionEnd; x ++)
{
...
}
}
5. Nella finestra dell’editor di codice e di testo, aggiungi la seguente istruzione using all'elenco
nella parte iniziale del file GraphWindow.xaml.cs:
using System.Threading.Tasks;
6. Nel metodo plotButton_Click, contrassegna come commento l'istruzione che richiama il
metodo generateGraphData e aggiungi l'istruzione mostrata di seguito in grassetto che
crea l'oggetto Task mediante l'oggetto TaskFactory predefinito e ne avvia l'esecuzione:
616
Parte VI
Creazione di soluzioni professionali con Visual Studio 2010
...
Stopwatch watch = Stopwatch.StartNew();
// generateGraphData(data);
Task first = Task.Factory.StartNew(() => generateGraphData(data, 0, pixelWidth / 4));
...
L'attività esegue il codice specificato nell'espressione lambda. I valori dei parametri
partitionStart e partitionEnd indicano che l'oggetto Task calcola i dati per la prima metà del
grafico. (I dati dell'intero grafico consistono in una serie di punti tracciati per valori compresi
tra 0 e pixelWidth/2.)
7. Aggiungi un'altra istruzione che crei ed esegua un secondo oggetto Task su un altro thread,
come qui evidenziato in grassetto:
...
Task first = Task.Factory.StartNew(() => generateGraphData(data, 0, pixelWidth / 4));
Task second = Task.Factory.StartNew(() => generateGraphData(data, pixelWidth / 4,
pixelWidth / 2));
...
Questo oggetto Task invoca il metodo generateGraph e calcola i dati per i valori compresi
tra pixelWidth/4 e pixelWidth/2.
8. Aggiungi la seguente istruzione che attende il completamento di entrambi gli oggetti Task
prima di proseguire:
Task.WaitAll(first, second);
9. Nel menu Debug, fai clic su Avvia senza eseguire debug per costruire ed eseguire
l’applicazione.
10. Visualizza Gestione attività Windows e fai clic sulla scheda Performance (se non già
visualizzata).
11. Torna alla finestra Graph Demo e fai clic su Plot Graph. In Gestione attività Windows, prendi
nota del valore massimo di utilizzo della CPU mentre viene generato il grafico. Quando il
grafico appare nella finestra Graph Demo, annota il tempo impiegato per generarlo. Ripeti
questa azione più volte per ottenere un valore medio.
12. Chiudi la finestra Graph Demo e riduci Gestione attività Windows a icona.
Questa volta dovresti rilevare che l'applicazione viene eseguita in modo sensibilmente più
veloce che in precedenza. Sul mio computer, il tempo è sceso fino a 2682 millisecondi,
con una riduzione di circa il 40%. Inoltre, è possibile vedere che l'applicazione utilizza
un numero maggiore di core della CPU. Su una macchina dual-core, l'utilizzo della CPU
ha raggiunto un picco del 100%. Provando a utilizzare un computer quad-core, l'utilizzo
della CPU non sarebbe così elevato, poiché due dei core non sarebbero impegnati.
Per correggere questo stato e ridurre ulteriormente il tempo, aggiungere due ulteriori
oggetti Task e dividere il lavoro in quattro sezioni nel metodo plotButton_Click, come qui
evidenziato in grassetto:
Capitolo 27
Introduzione alla TPL (Task Parallel Library)
617
...
Task first = Task.Factory.StartNew(() => generateGraphData(data, 0, pixelWidth / 8));
Task second = Task.Factory.StartNew(() => generateGraphData(data, pixelWidth / 8,
pixelWidth / 4));
Task third = Task.Factory.StartNew(() => generateGraphData(data, pixelWidth / 4,
pixelWidth * 3 / 8));
Task fourth = Task.Factory.StartNew(() => generateGraphData(data, pixelWidth * 3 / 8,
pixelWidth / 2));
Task.WaitAll(first, second, third, fourth);
...
Anche se si dispone solo di un processore dual-core, è comunque possibile provare a
introdurre questa modifica, in seguito alla quale si dovrebbe notare un'ulteriore riduzione
del tempo. Ciò è dovuto principalmente all'efficienza della TPL e agli algoritmi di .NET
Framework che ottimizzano il modo in cui vengono pianificati i thread di ciascuna attività.
Astrazione delle attività mediante la classe Parallel
Grazie all'uso della classe Task, è possibile avere il controllo completo del numero di attività
create da un'applicazione. Tuttavia, per fare ciò è stato necessario modificare il progetto
dell'applicazione in modo da gestire l'uso di oggetti Task. È stato necessario anche aggiungere il
codice che sincronizza le operazioni; l'applicazione può quindi visualizzare il grafico solo una volta
completate tutte le attività. In un'applicazione complessa, la sincronizzazione delle attività può
diventare un processo complesso e causa di facili errori.
La classe Parallel della TPL permette di eseguire in parallelo alcuni comuni costrutti di
programmazione senza richiedere la riprogettazione dell'applicazione. Internamente, la classe
Parallel crea un proprio insieme di oggetti Task, quindi sincronizza automaticamente tali attività
una volta completate. La classe Parallel è situata nello spazio dei nomi System.Threading.Tasks e
fornisce un piccolo insieme di metodi statici che possono essere usati per indicare che il codice
deve essere eseguito in parallelo quando possibile. Questi metodi sono i seguenti:
■ Parallel.For Può essere utilizzato al posto di un'istruzione C# for. Definisce un ciclo in cui
le iterazioni possono procedere in parallelo tramite l'uso di attività. Questo metodo è molto
sovraccarico (con nove variazioni), ma il principio generale è lo stesso per ognuna di esse;
per procedere, specificare un valore iniziale, un valore finale e un riferimento a un metodo
che accetti un parametro intero. Il metodo viene eseguito per ogni valore compreso
tra il valore iniziale e il valore finale meno 1, con il parametro popolato con un intero
corrispondente al valore corrente. Ad esempio, si consideri il seguente semplice ciclo for che
esegue ogni iterazione in sequenza:
for (int x = 0; x < 100; x++)
{
// Perform loop processing
}
618
Parte VI
Creazione di soluzioni professionali con Visual Studio 2010
A seconda dell'elaborazione eseguita dal corpo del ciclo, è possibile riuscire a sostituirlo
con un costrutto Parallel.For in grado di eseguire le iterazioni in parallelo, come mostrato di
seguito:
Parallel.For(0, 100, performLoopProcessing);
...
private void performLoopProcessing(int x)
{
// Perform loop processing
}
Gli overload del metodo Parallel.For consentono di provvedere dati locali riservati per
ciascun thread, specificare più opzioni per la creazione delle attività eseguite dal metodo
For e creare un oggetto ParallelLoopState utilizzabile per passare informazioni di stato ad
altre iterazioni concorrenti del ciclo. (L'uso dell'oggetto ParallelLoopState è descritto più
avanti in questo capitolo.)
■ Parallel. Questo metodo può essere usato al posto di un'istruzione C# foreach. Come il
metodo For, anche ForEach definisce un ciclo in cui le iterazioni possono essere eseguite
in parallelo. In questo caso è possibile specificare una raccolta che implementi l'interfaccia
generica IEnumerable<T> e un riferimento a un metodo in grado di accettare un singolo
parametro di tipo T. Il metodo viene eseguito per ogni elemento della raccolta, e l'elemento
viene passato al metodo come parametro. Sono disponibili overload che permettono di
provvedere dati di thread locali privati e di specificare le opzioni da usare per creare le
attività eseguite dal metodo ForEach.
■ Parallel. Questo metodo può essere impiegato per eseguire un insieme di chiamate a
metodi senza parametri come attività parallele. In questo caso è possibile specificare un
elenco di chiamate a metodi delegati (o espressioni lambda) che non accettano alcun
parametro e non restituiscono valori. Ogni chiamata a un metodo può essere eseguita in
un thread separato e in qualsiasi ordine. Ad esempio, il seguente codice esegue una serie di
chiamate a metodi:
doWork();
doMoreWork();
doYetMoreWork();
Queste istruzioni possono essere sostituite con il codice che segue, il quale richiama questi
metodi mediante una serie di attività:
Parallel.Invoke(
doWork,
doMoreWork,
doYetMoreWork
);
È utile ricordare che .NET Framework stabilisce il grado effettivo di esecuzione in parallelo più
adatto all'ambiente e al carico di lavoro del computer. Ad esempio, se si utilizza Parallel.For per
implementare un ciclo che esegue 1000 iterazioni, .NET Framework non deve necessariamente
creare 1000 attività concorrenti, tranne qualora si disponga di un processore eccezionalmente
Capitolo 27
Introduzione alla TPL (Task Parallel Library)
619
potente con 1000 core. .NET Framework crea invece ciò che considera il numero ottimale di
attività che permette di bilanciare le risorse disponibili rispetto al requisito di tenere occupati
i processori. Una singola attività può eseguire più iterazioni, e le attività si coordinano tra
loro per stabilire quali iterazioni dovranno essere eseguite da ciascuna attività. Un'importante
conseguenza di ciò è l'impossibilità di garantire l'ordine in cui vengono eseguite le iterazioni,
pertanto è necessario assicurarsi che non vi siano dipendenze tra di loro; in caso contrario è
possibile ottenere risultati imprevisti, come illustrato più avanti in questo capitolo.
Nell'esercizio successivo, si tornerà alla versione originale dell'applicazione GraphData per
utilizzare la classe Parallel ed eseguire operazioni concorrenti.
Utilizzo della classe Parallell per eseguire operazioni in parallelo nell'applicazione
GraphData
1. Utilizzando Visual Studio 2010, apri la soluzione GraphDemo situata nella cartella \Microsoft
Press\Visual CSharp Step By Step\Chapter 27\GraphDemo Using the Parallel Class della
cartella Documenti.
Questa è una copia dell'applicazione originale GraphDemo. Non utilizza ancora alcuna
attività.
2. In Esplora soluzioni, espandi il nodo GraphWindow.xaml nel progetto GraphDemo, quindi
fai doppio clic su GraphWindow.xaml.cs per visualizzare il codice del form nella finestra
dell’editor di codice e di testo.
3. All’inizio dell'elenco, aggiungi le seguenti istruzioni using:
using System.Threading.Tasks;
4. Localizza il metodo generateGraphData. Sarà simile al seguente:
private
{
int
int
int
void generateGraphData(byte[] data)
a = pixelWidth / 2;
b = a * a;
c = pixelHeight / 2;
for (int x = 0; x < a; x++)
{
int s = x * x;
double p = Math.Sqrt(b - s);
for (double i = -p; i < p; i += 3)
{
double r = Math.Sqrt(s + i * i) / a;
double q = (r - 1) * Math.Sin(24 * r);
double y = i / 3 + (q * c);
plotXY(data, (int)(-x + (pixelWidth / 2)), (int)(y + (pixelHeight / 2)));
plotXY(data, (int)(x + (pixelWidth / 2)), (int)(y + (pixelHeight / 2)));
}
}
}
620
Parte VI
Creazione di soluzioni professionali con Visual Studio 2010
Il principale candidato per l'esecuzione in parallelo è il ciclo for più esterno che esegue
le iterazioni attraverso valori della variabile intera x. È possibile anche considerare il ciclo
più interno basato sulla variabile i, ma questo richiede più lavoro per essere eseguito in
parallelo a causa del tipo di i. (I metodi della classe Parallel presuppongono che la variabile
di controllo sia un intero.) Inoltre, in presenza di cicli nidificati come avviene in questo
codice, è buona norma prima eseguire in parallelo i cicli più esterni, quindi verificare se le
prestazioni dell'applicazione sono sufficienti. In caso contrario, localizzare i cicli nidificati
ed eseguirli in parallelo procedendo dai cicli più esterni a quelli più interni, valutando le
prestazioni dopo ogni modifica. Spesso, è possibile riscontrare che l'esecuzione in parallelo
dei cicli più esterni ha il massimo impatto sulle prestazioni, mentre gli effetti della modifica
dei cicli più interni diviene più marginale.
5. Sposta il codice nel corpo del ciclo for, quindi crea un nuovo metodo privato void chiamato
calculateData con questo codice. Il metodo calculateData dovrebbe accettare un parametro
intero chiamato x e un array di byte chiamato data. Inoltre, sposta le istruzioni che
dichiarano le variabili locali a, b e c dal metodo generateGraphData all'inizio del metodo
calculateData. Il codice che segue mostra il metodo generateGraphData dopo la rimozione
di questo codice, e il metodo calculateData (non provare ancora a compilare questo codice):
private void generateGraphData(byte[] data)
{
for (int x = 0; x < a; x++)
{
}
}
private
{
int
int
int
void calculateData(int x, byte[] data)
a = pixelWidth / 2;
b = a * a;
c = pixelHeight / 2;
int s = x * x;
double p = Math.Sqrt(b - s);
for (double i = -p; i < p; i += 3)
{
double r = Math.Sqrt(s + i * i) / a;
double q = (r - 1) * Math.Sin(24 * r);
double y = i / 3 + (q * c);
plotXY(data, (int)(-x + (pixelWidth / 2)), (int)(y + (pixelHeight / 2)));
plotXY(data, (int)(x + (pixelWidth / 2)), (int)(y + (pixelHeight / 2)));
}
}
6. Nel metodo generateGraphData, modifica il ciclo for in un'istruzione in grado di richiamare
il metodo statico Parallel.For, come qui evidenziato in grassetto:
private void generateGraphData(byte[] data)
{
Parallel.For (0, pixelWidth / 2, (int x) => { calculateData(x, data); });
}
Capitolo 27
Introduzione alla TPL (Task Parallel Library)
621
Questo codice è l'equivalente parallelo dell'originale ciclo for ed esegue iterazioni attraverso
i valori compresi tra 0 e pixelWidth/2 – 1 compresi. Ogni chiamata viene eseguita mediante
un'attività. (Ogni attività può eseguire più di una iterazione.) Il metodo Parallel.For termina
solo quando tutte le attività create completano il proprio lavoro. Ricorda che il metodo
Parallel.For si aspetta che il parametro finale sia un metodo in grado di accettare un unico
parametro intero. Esso richiama questo metodo passando l'indice del ciclo in corso come
parametro. In questo esempio, il metodo calculateData non soddisfa i requisiti posti poiché
accetta due parametri: un intero e un array di byte. Per questo motivo, il codice utilizza
un'espressione lambda per definire un metodo anonimo con le caratteristiche appropriate e
in grado di agire come un adattatore che richiama il metodo calculateData con i parametri
corretti.
7. Nel menu Debug, fai clic su Avvia senza eseguire debug per costruire ed eseguire
l’applicazione.
8. Visualizza Gestione attività Windows e fai clic sulla scheda Performance (se non già
visualizzata).
9. Torna alla finestra Graph Demo e fai clic su Plot Graph. In Gestione attività Windows, prendi
nota del valore massimo di utilizzo della CPU mentre viene generato il grafico. Quando il
grafico appare nella finestra Graph Demo, annota il tempo impiegato per generarlo. Ripeti
questa azione più volte per ottenere un valore medio.
10. Chiudi la finestra Graph Demo e riduci Gestione attività Windows a icona.
Come puoi notare, l'applicazione viene eseguita a una velocità paragonabile a quella della
versione precedente che utilizzava gli oggetti Task (forse leggermente più veloce a seconda
del numero di CPU disponibili), e che l'utilizzo della CPU risulta pari al 100%.
Quando non utilizzare la classe Parallel
È utile tenere presente che, nonostante le false impressioni e gli sforzi del team di sviluppo di
Microsoft Visual Studio, la classe Parallel non è in grado di fare magie; non è pertanto possibile
utilizzarla senza prestare la necessaria attenzione o per avere la certezza di ottenere applicazioni
improvvisamente più veloci e in grado di fornire gli stessi risultati. Lo scopo della classe Parallel è
di eseguire in parallelo parti indipendenti di operazioni di elaborazione del codice.
Le frasi chiave del paragrafo precedente sono operazioni di elaborazione e indipendenti. Se
il codice non contiene operazioni di elaborazione, l'esecuzione in parallelo potrebbe non
migliorarne le prestazioni. L'esercizio successivo mostra come fare attenzione nel decidere quando
utilizzare il costrutto Parallel.Invoke per eseguire chiamate ai metodi in parallelo.
Come stabilire quando utilizzare Parallel.Invoke
1. Torna a Visual Studio 2010 e visualizza il file GraphWindow.xaml.cs nella finestra dell’editor
di codice e di testo (se non è già aperto).
2. Esamina il metodo calculateData.
622
Parte VI
Creazione di soluzioni professionali con Visual Studio 2010
Il ciclo for più interno contiene le seguenti istruzioni:
plotXY(data, (int)(-x + (pixelWidth / 2)), (int)(y + (pixelWidth / 2)));
plotXY(data, (int)(x + (pixelWidth / 2)), (int)(y + (pixelWidth / 2)));
Queste due istruzioni impostano i byte nell'array data corrispondenti ai punti specificati
dai due parametri passati. Ricorda che i punti del grafico sono riflessi attorno all'asse X,
pertanto il metodo plotXY viene richiamato per il valore positivo della coordinata X, ma
anche per il valore negativo. Queste due istruzioni sembrano essere eccellenti candidati per
l'esecuzione in parallelo, poiché non è importante quale delle due viene eseguita per prima
e hanno il compito di impostare byte differenti nell'array data.
3. Modifica queste due istruzioni, quindi racchiudile in una chiamata a un metodo Parallel.
Invoke, come mostrato di seguito. Nota che entrambe le chiamate sono ora racchiuse in
espressioni lambda, e che il carattere punto e virgola alla fine della prima chiamata a plotXY
è stato sostituito con una virgola, mentre il punto e virgola alla fine della seconda chiamata
a plotXY è stato rimosso poiché queste istruzioni ora sono una lista di parametri:
Parallel.Invoke(
() => plotXY(data, (int)(-x + (pixelWidth / 2)), (int)(y + (pixelWidth / 2))),
() => plotXY(data, (int)(x + (pixelWidth / 2)), (int)(y + (pixelWidth / 2)))
);
4. Nel menu Debug, fai clic su Avvia senza eseguire debug per costruire ed eseguire
l’applicazione.
5. Nella finestra Graph Demo, fai clic su Plot Graph. Prendi nota del tempo impiegato per
generare il grafico. Ripeti questa azione più volte per ottenere un valore medio.
Come puoi vedere, forse inaspettatamente, l'applicazione impiega un tempo sensibilmente
più lungo. Può infatti arrivare a essere fino a 20 volte più lenta che in precedenza.
6. Chiudi la finestra Graph Demo.
Le domande più frequenti a questo punto in genere sono “Cosa è andato storto? Perché
l'applicazione ha rallentato tanto?” La risposta sta nel metodo plotXY. Osservando nuovamente
questo metodo, è possibile vedere che è molto semplice:
private void plotXY(byte[] data, int x, int y)
{
data[x + y * pixelWidth] = 0xFF;
}
Al suo interno, solo una piccola parte richiede tempo per l'esecuzione, ed è evidente che
non si tratta di codice riguardante operazioni di elaborazione. In effetti, è così semplice che
l'overhead per la creazione di un'attività, la sua esecuzione in un thread separato e l'attesa del
suo completamento risulta molto maggiore del costo di esecuzione diretta di questo metodo.
L'overhead aggiuntivo può comportare solo alcuni millisecondi di attesa ogni volta che il metodo
viene richiamato, ma è utile ricordare quante volte esso viene eseguito; la chiamata al metodo
è ubicata in un ciclo nidificato e viene eseguita migliaia di volte, causando l'accumulo di questi
piccoli costi di overhead. La regola generale dice di utilizzare Parallel.Invoke solo quando ne vale
la pena, riservandolo per le operazioni che richiedono grandi quantità di lavoro di elaborazione.
Capitolo 27
Introduzione alla TPL (Task Parallel Library)
623
Come discusso più indietro in questo capitolo, l'altra considerazione chiave sull'impiego della
classe Parallel è che le operazioni devono essere indipendenti. Ad esempio, se si tenta di
utilizzare Parallel.For per eseguire in parallelo un ciclo in cui le iterazioni non sono indipendenti,
i risultati saranno imprevedibili. Per comprendere meglio cosa ciò significhi, osservare il seguente
programma:
using System;
using System.Threading;
using System.Threading.Tasks;
namespace ParallelLoop
{
class Program
{
private static int accumulator = 0;
static void Main(string[] args)
{
for (int i = 0; i < 100; i++)
{
AddToAccumulator(i);
}
Console.WriteLine("Accumulator is {0}", accumulator);
}
private static void AddToAccumulator(int data)
{
if ((accumulator % 2) == 0)
{
accumulator += data;
}
else
{
accumulator -= data;
}
}
}
}
Questo programma esegue un'iterazione dei valori compresi tra 0 e 99 e richiama il metodo
AddToAccumulator a turno per ognuno di essi. Il metodo AddToAccumulator esamina il valore
corrente della variabile accumulator, quindi lo aggiunge alla variabile accumulator se è pari,
oppure lo sottrae se è dispari. Alla fine del programma viene visualizzato il risultato. Questa
applicazione è disponibile nella soluzione ParallelLoop, nella cartella \Microsoft Press\Visual
CSharp Step By Step\Chapter 27\ParallelLoop della cartella Documenti. Se si esegue questo
programma, il valore visualizzato al termine dovrebbe essere –100.
Per aumentare il livello di esecuzione in parallelo all'interno di questa semplice applicazione, si
può essere tentati di sostituire il ciclo for nel metodo Main con Parallel.For, come mostrato di
seguito:
624
Parte VI
Creazione di soluzioni professionali con Visual Studio 2010
static void Main(string[] args)
{
Parallel.For (0, 100, AddToAccumulator);
Console.WriteLine("Accumulator is {0}", accumulator);
}
Tuttavia, non vi è alcuna garanzia che le attività create per eseguire le varie chiamate del
metodo AddToAccumulator possano essere eseguite in una qualsiasi sequenza specifica. Il
codice non è corretto per l'uso con i thread (thread-safe), poiché più thread che eseguono
contemporaneamente attività diverse possono tentare di modificare la variabile accumulator in
modo concorrente. Il valore calcolato dal metodo AddToAccumulator dipende dalla sequenza
seguita, pertanto il risultato di questa modifica fa sì che l'applicazione possa ora generare valori
differenti a ogni esecuzione. In questo semplice caso, è possibile non notare alcuna differenza
nel valore calcolato, poiché il metodo AddToAccumulator viene eseguito molto velocemente e
.NET Framework può decidere di eseguire le diverse chiamate in ordine sequenziale mediante
lo stesso thread. Tuttavia, effettuando la seguente modifica evidenziata in grassetto al metodo
AddToAccumulator, è possibile ottenere risultati differenti:
private static void AddToAccumulator(int data)
{
if ((accumulator % 2) == 0)
{
accumulator += data;
Thread.Sleep(10); // wait for 10 milliseconds
}
else
{
accumulator -= data;
}
}
Il metodo Thread.Sleep fa semplicemente in modo che il thread corrente debba attendere il
tempo specificato. Questa modifica simula un thread che esegue operazioni di elaborazione
aggiuntive e ha effetto sul modo in cui .NET Framework pianifica le attività, le quali vengono ora
eseguite su thread differenti e danno così origine a una sequenza differente.
La regola generale consiste nell'usare Parallel.For e Parallel.ForEach solo se si è in grado di
garantire che ciascuna iterazione del ciclo sia indipendente, impegnandosi inoltre a testare a
fondo il funzionamento del codice. Una considerazione simile vale per Parallel.Invoke; questo
costrutto può essere utilizzato per eseguire chiamate a metodi solo se questi sono indipendenti e
se l'applicazione non dipende da loro per essere eseguita in una particolare sequenza.
Restituzione di un valore da un'attività
Tutti gli esempi visti finora impiegano un oggetto Task per eseguire codice che elabora una parte
di un lavoro ma senza restituire alcun valore. Tuttavia, può essere necessario anche eseguire
un metodo che calcola un risultato. La TPL comprende una variante generica della classe Task,
Task<TResult>, che può essere impiegata per questo scopo.
Capitolo 27
Introduzione alla TPL (Task Parallel Library)
625
Per fare ciò, è possibile creare ed eseguire un oggetto Task<TResult> in modo simile a quanto
avviene per gli oggetti Task. La differenza principale consiste nel fatto che il metodo eseguito
dall'oggetto Task<TResult> restituisce un valore ed è possibile specificare il tipo di tale valore
come parametro tipo, T, dell'oggetto Task. Ad esempio, il metodo calculateValue mostrato nel
seguente esempio di codice restituisce un valore intero. Per richiamare questo metodo mediante
un'attività, creare un oggetto Task<int> e richiamare il metodo Start. Così facendo si ottiene il
valore restituito dal metodo mediante la query della proprietà Result dell'oggetto Task<int>. Se
l'attività non ha terminato di eseguire il metodo e il risultato non è ancora disponibile, la proprietà
Result blocca il chiamante. Ciò fa sì che non sia necessario eseguire personalmente alcuna
sincronizzazione, poiché è noto che quando la proprietà Result restituirà un valore, l'attività avrà
completato il suo lavoro.
Task<int> calculateValueTask = new Task<int>(() => calculateValue(...));
calculateValueTask.Start(); // Invoke the calculateValue method
...
int calculatedData = calculateValueTask.Result; // Block until calculateValueTask completes
...
private int calculateValue(...)
{
int someValue;
// Perform calculation and populate someValue
...
return someValue;
}
Naturalmente, per creare un oggetto Task<TResult> e avviarne l'esecuzione è possibile anche
utilizzare il metodo StartNew di un oggetto TaskFactory. L'esempio di codice successivo mostra
come usare il TaskFactory predefinito per un oggetto Task<int> per creare ed eseguire un'attività
che richiama il metodo calculateValue:
Task<int> calculateValueTask = Task<int>.Factory.StartNew(() => calculateValue(...));
...
Per semplificare un po' il codice e supportare le attività che restituiscono tipi anonimi, la classe
TaskFactory dispone di overload generici del metodo StartNew e può dedurre il tipo restituito dal
metodo eseguito mediante un'attività. Inoltre, la classe Task<TResult> eredita dalla classe Task. Ciò
significa che è possibile riscrivere l'esempio precedente come segue:
Task calculateValueTask = Task.Factory.StartNew(() => calculateValue(...));
...
L'esercizio successivo contiene un esempio più dettagliato. In questo caso è possibile vedere come
ristrutturare l'applicazione GraphDemo per utilizzare un oggetto Task<TResult>. Nonostante
possa sembrare troppo teorico, la tecnica qui dimostrata può rivelarsi unica in molte situazioni del
mondo reale.
626
Parte VI
Creazione di soluzioni professionali con Visual Studio 2010
Modifica dell'applicazione GraphDemo per utilizzare un oggetto Task<TResult>
1. Utilizzando Visual Studio 2010, apri la soluzione GraphDemo situata nella cartella \Microsoft
Press\Visual CSharp Step By Step\Chapter 27\GraphDemo Using Tasks that Return Results
della cartella Documenti.
Questa è una copia dell'applicazione GraphDemo che crea un insieme di quattro attività già
viste in un esercizio precedente.
2. In Esplora soluzioni, espandi il nodo GraphWindow.xaml nel progetto GraphDemo, quindi
fai doppio clic su GraphWindow.xaml.cs per visualizzare il codice del form nella finestra
dell’editor di codice e di testo.
3. Localizza il metodo plotButton_Click. Questo è il metodo che viene eseguito quando
l'utente fa clic sul pulsante Plot Graph nel form. Attualmente esso crea un insieme di oggetti
Task per eseguire i vari calcoli richiesti e generare i dati per il grafico, quindi attende il
completamento di questi oggetti Task prima di visualizzare i risultati nel controllo Image sul
form.
4. Sotto al metodo plotButton_Click, aggiungi un nuovo metodo chiamato getDataForGraph.
Questo metodo deve accettare un parametro intero chiamato dataSize e restituire un array
byte, come mostrato nel seguente codice:
private byte[] getDataForGraph(int dataSize)
{
}
A questo metodo verrà aggiunto il codice necessario per generare i dati del grafico in un
array byte e restituire tale array al chiamante. Il parametro dataSize specifica la dimensione
dell'array.
5. Sposta l'istruzione che crea l'array di dati dal metodo plotButton_Click al metodo
getDataForGraph, come mostrato di seguito in grassetto:
private byte[] getDataForGraph(int dataSize)
{
byte[] data = new byte[dataSize];
}
6. Sposta il codice che crea, esegue e attende gli oggetti Task che popolano l'array data dal
metodo plotButton_Click al metodo getDataForGraph, quindi aggiungi un'istruzione return
alla fine del metodo che restituisce l'array data al chiamante. +Il codice completo per il
metodo getDataForGraph dovrebbe essere simile a questo:
private byte[] getDataForGraph(int dataSize)
{
byte[] data = new byte[dataSize];
Task first = Task.Factory.StartNew(() => generateGraphData(data, 0, pixelWidth /
8));
Task second = Task.Factory.StartNew(() => generateGraphData(data, pixelWidth / 8,
pixelWidth / 4));
Capitolo 27
Introduzione alla TPL (Task Parallel Library)
627
Task third = Task.Factory.StartNew(() => generateGraphData(data, pixelWidth / 4,
pixelWidth * 3 / 8));
Task fourth = Task.Factory.StartNew(() => generateGraphData(data, pixelWidth * 3 /
8, pixelWidth / 2));
Task.WaitAll(first, second, third, fourth);
return data;
}
Suggerimento Il codice che crea le attività e ne attende il completamento può essere
sostituito con il seguente costrutto Parallel.Invoke:
Parallel.Invoke(
() => Task.Factory.StartNew(()
() => Task.Factory.StartNew(()
pixelWidth / 4)),
() => Task.Factory.StartNew(()
pixelWidth * 3 / 8)),
() => Task.Factory.StartNew(()
pixelWidth / 2))
);
=> generateGraphData(data, 0, pixelWidth / 8))
=> generateGraphData(data, pixelWidth / 8,
=> generateGraphData(data, pixelWidth / 4,
=> generateGraphData(data, pixelWidth * 3 / 8,
7. Nel metodo plotButton_Click, dopo l'istruzione che crea la variabile Stopwatch usata per
temporizzare le attività, aggiungi l'istruzione mostrata di seguito in grassetto e che crea
un oggetto Task<byte[]> chiamato getDataTask e utilizza questo oggetto per eseguire
il metodo getDataForGraph. Questo metodo restituisce un array byte, pertanto il tipo
dell'attività è Task<byte []>. La chiamata al metodo StartNew fa riferimento a un'espressione
lambda che richiama il metodo getDataForGraph e passa a questo metodo la variabile
dataSize come parametro.
private void plotButton_Click(object sender, RoutedEventArgs e)
{
...
Stopwatch watch = Stopwatch.StartNew();
Task<byte[]> getDataTask = Task<byte[]>.Factory.StartNew(() =>
getDataForGraph(dataSize));
...
}
8. Dopo aver creato e avviato l'oggetto Task<byte []>, aggiungi le istruzioni mostrate di
seguito in grassetto e che esaminano la proprietà Result per recuperare l'array di dati
restituito dal metodo getDataForGraph nella variabile array byte locale chiamata data.
Ricorda che la proprietà Result blocca il chiamante fino al completamento dell'attività,
pertanto non è necessario attenderne il termine in modo esplicito.
private void plotButton_Click(object sender, RoutedEventArgs e)
{
...
Task<byte[]> getDataTask = Task<byte[]>.Factory.StartNew(() =>
getDataForGraph(dataSize));
byte[] data = getDataTask.Result;
...
}
628
Parte VI
Creazione di soluzioni professionali con Visual Studio 2010
Nota Può sembrare piuttosto strano dover creare un'attività e quindi attendere immedi-
atamente che sia completata prima di fare altro, poiché essa si limita ad aggiungere overhead all'applicazione. Tuttavia, nella sezione successiva è possibile vedere perché è stato
adottato questo approccio.
9. Verifica che il codice completato del metodo plotButton_Click sia simile a questo:
private void plotButton_Click(object sender, RoutedEventArgs e)
{
if (graphBitmap == null)
{
graphBitmap = new WriteableBitmap(pixelWidth, pixelHeight, dpiX, dpiY,
PixelFormats.Gray8, null);
}
int bytesPerPixel = (graphBitmap.Format.BitsPerPixel + 7) / 8;
int stride = bytesPerPixel * pixelWidth;
int dataSize = stride * pixelHeight;
Stopwatch watch = Stopwatch.StartNew();
Task<byte[]> getDataTask = Task<byte[]>.Factory.StartNew(() =>
getDataForGraph(dataSize));
byte[] data = getDataTask.Result;
duration.Content = string.Format("Duration (ms): {0}", watch.ElapsedMilliseconds);
graphBitmap.WritePixels(new Int32Rect(0, 0, pixelWidth, pixelHeight), data,
stride, 0);
graphImage.Source = graphBitmap;
}
10. Nel menu Debug, fai clic su Avvia senza eseguire debug per costruire ed eseguire
l’applicazione.
11. Nella finestra Graph Demo, fai clic su Plot Graph. Verifica che il grafico venga generato come
in precedenza e che il tempo necessario per questa operazione sia simile agli altri casi già
visti. (Il tempo rilevato può essere leggermente superiore perché l'array di dati ora viene
creato dall'attività, mentre prima veniva creato prima dell'esecuzione dell'attività.)
12. Chiudi la finestra Graph Demo.
Utilizzo di attività e di thread di interfaccia utente insieme
La sezione “Perché eseguire il multitasking mediante l'elaborazione parallela?” all'inizio di
questo capitolo evidenzia i due motivi principali per utilizzare il multitasking in un'applicazione:
migliorare il throughput e aumentare la velocità di risposta. La TPL può sicuramente aiutare a
migliorare il throughput, ma è necessario tenere presente che l'uso della sola TPL non rappresenta
una soluzione completa per migliorare la velocità di risposta, specialmente in un'applicazione
che fornisce un'interfaccia utente grafica. Nonostante il tempo necessario per generare i dati per
il grafico sia ridotto dall'efficace uso delle attività, l'applicazione GraphDemo usata come base
per gli esercizi di questo capitolo mostra i sintomi classici di molte GUI che eseguono lavori di
elaborazione pesanti per il processore: non è veloce a rispondere all'input dell'utente mentre
Capitolo 27
Introduzione alla TPL (Task Parallel Library)
629
sono in corso tali elaborazioni. Ad esempio, se si esegue l'applicazione GraphDemo dell'esercizio
precedente, fai clic su Plot Graph, quindi provare a spostare la finestra Graph Demo facendo clic e
trascinando la barra del titolo; così facendo, è possibile accorgersi che la finestra non potrà essere
spostata fino a quando non saranno state terminate le varie attività utilizzate per generare il
grafico e questo non sarà apparso sullo schermo.
Nelle applicazioni professionali, è necessario accertarsi che gli utenti possano utilizzare le proprie
applicazioni anche quando parti di queste sono impegnate in altri compiti. Questo è il campo in
cui è necessario utilizzare i thread e le attività.
Nel capitolo 23 si è visto come gli elementi che costituiscono l'interfaccia grafica utente in
un'applicazione WPF vengono eseguiti tutti insieme sullo stesso thread di interfaccia utente (UI).
Ciò permette di garantire uniformità e sicurezza e impedisce che due o più thread danneggino
involontariamente le strutture di dati interne usate da WPF per visualizzare l'interfaccia utente.
È anche utile ricordare che l'oggetto Dispatcher di WPF può essere impiegato per accodare le
richieste per il thread UI, e che tali richieste possono aggiornare l'interfaccia utente. L'esercizio
successivo torna sull'oggetto Dispatcher e mostra come utilizzarlo per implementare una
soluzione reattiva insieme ad attività in grado di garantire il miglior throughput possibile.
Miglioramento della velocità di risposta nell'applicazione GraphDemo
1. Torna a Visual Studio 2010 e visualizza il file GraphWindow.xaml.cs nella finestra dell’editor
di codice e di testo (se non è già aperto).
2. Aggiungi un nuovo metodo chiamato doPlotButtonWork sotto al metodo plotButton_Click.
Questo metodo non dovrebbe accettare alcun parametro né restituire risultati. Nei prossimi
passi, il codice che crea ed esegue le attività che generano i dati per il grafico verrà spostato
in questo metodo, quindi tale metodo sarà eseguito in un thread separato per lasciare il
thread UI libero di gestire l'input dell'utente.
private void doPlotButtonWork()
{
}
3. Sposta tutto il codice dal metodo plotButton_Click al metodo doPlotButtonWork method,
tranne l'istruzione if che crea l'oggetto graphBitmap. Nota che alcune di queste istruzioni
tentano di accedere a elementi dell'interfaccia utente; queste istruzioni saranno modificate
per utilizzare l'oggetto Dispatcher più avanti in questo esercizio. I metodi plotButton_Click e
doPlotButtonWork dovrebbero avere un aspetto simile a quello che segue:
private void plotButton_Click(object sender, RoutedEventArgs e)
{
if (graphBitmap == null)
{
graphBitmap = new WriteableBitmap(pixelWidth, pixelHeight, dpiX, dpiY,
PixelFormats.Gray8, null);
}
}
630
Parte VI
Creazione di soluzioni professionali con Visual Studio 2010
private
{
int
int
int
void doPlotButtonWork()
bytesPerPixel = (graphBitmap.Format.BitsPerPixel + 7) / 8;
stride = bytesPerPixel * pixelWidth;
dataSize = stride * pixelHeight;
Stopwatch watch = Stopwatch.StartNew();
Task<byte[]> getDataTask = Task<byte[]>.Factory.StartNew(() =>
getDataForGraph(dataSize));
byte[] data = getDataTask.Result;
duration.Content = string.Format("Duration (ms): {0}", watch.ElapsedMilliseconds);
graphBitmap.WritePixels(new Int32Rect(0, 0, pixelWidth, pixelHeight), data,
stride, 0);
graphImage.Source = graphBitmap;
}
4. Nel metodo plotButton_Click, dopo il blocco if, crea un delegato Action chiamato
doPlotButtonWorkAction che faccia riferimento al metodo doPlotButtonWork, come
mostrato di seguito in grassetto:
private void plotButton_Click(object sender, RoutedEventArgs e)
{
...
Action doPlotButtonWorkAction = new Action(doPlotButtonWork);
}
5. Richiama il metodo BeginInvoke sul delegato doPlotButtonWorkAction. Il metodo
BeginInvoke del tipo Action esegue il metodo associato al delegato (in questo caso il
metodo doPlotButtonWork) in un nuovo thread.
Nota Il tipo Action fornisce anche il metodo Invoke, il quale esegue il metodo delegato
sul thread corrente. Questo comportamento non è ciò che si desidera ottenere in questo
caso, poiché blocca l'interfaccia utente e gli impedisce di poter rispondere mentre il metodo è in esecuzione.
Il metodo BeginInvoke accetta parametri che possono essere usati per gestire le notifiche al
termine del metodo, oltre a qualsiasi altro dato da passare al metodo delegato. In questo
esempio, non è necessario ricevere una notifica quando il metodo viene completato, e il
metodo non accetta alcun parametro; pertanto per questi parametri è possibile specificare
un valore null, come qui evidenziato in grassetto:
private void plotButton_Click(object sender, RoutedEventArgs e)
{
...
Action doPlotButtonWorkAction = new Action(doPlotButtonWork);
doPlotButtonWorkAction.BeginInvoke(null, null);
}
Capitolo 27
Introduzione alla TPL (Task Parallel Library)
631
A questo punto è possibile procedere alla compilazione del codice, ma questo non può
essere eseguito correttamente facendo clic su Plot Graph. Ciò è dovuto a numerose
istruzioni nel metodo doPlotButtonWork che tentano di accedere a elementi dell'interfaccia
utente, mentre questo metodo non è in esecuzione sul thread della UI. Questo problema
è già stato incontrato nel capitolo 23, dove è stata illustrata anche la relativa soluzione:
utilizzare l'oggetto Dispatcher per il thread UI e per l'accesso agli elementi della UI. Le
seguenti operazioni modificano queste istruzioni in modo da utilizzare l'oggetto Dispatcher
per accedere agli elementi dell'interfaccia utente dal thread corretto.
6. All’inizio dell'elenco, aggiungi le seguenti istruzioni using:
using System.Windows.Threading;
L'enumerazione DispatcherPriority è situata in questo spazio dei nomi. Questa
enumerazione può essere usata durante la pianificazione del codice da eseguire sul thread
UI mediante l'oggetto Dispatcher.
7. All'inizio del metodo doPlotButtonWork, esamina l'istruzione che inizializza la variabile
bytesPerPixel:
private void doPlotButtonWork()
{
int bytesPerPixel = (graphBitmap.Format.BitsPerPixel + 7) / 8;
...
}
Questa istruzione fa riferimento all'oggetto graphBitmap, il quale appartiene al thread UI. A
questo oggetto è possibile accedere solo dal codice in esecuzione nel thread UI. Modificare
l'istruzione in modo da inizializzare la variabile bytesPerPixel con il valore zero, quindi
aggiungere un'istruzione per chiamare il metodo Invoke dell'oggetto Dispatcher, come qui
evidenziato in grassetto:
private void doPlotButtonWork()
{
int bytesPerPixel = 0;
plotButton.Dispatcher.Invoke(new Action(() =>
{ bytesPerPixel = (graphBitmap.Format.BitsPerPixel + 7) / 8; }),
DispatcherPriority.ApplicationIdle);
...
}
Come visto nel capitolo 23, all'oggetto Dispatcher è possibile accedere tramite la proprietà
Dispatcher di qualsiasi elemento della UI. Questo codice impiega il pulsante plotButton. Il
metodo Invoke presuppone un delegato e una priorità di spedizione facoltativa. In questo
caso, il delegato fa riferimento a un'espressione lambda. Il codice in questa espressione
viene eseguito sul thread UI. Il parametro DispatcherPriority indica che questa istruzione
dovrebbe essere eseguita esclusivamente quando l'applicazione è inattiva e non vi sono altri
compiti importanti in esecuzione nell'interfaccia utente, ad esempio l'utente che fa clic su un
pulsante, digita del testo o sposta le finestre.
632
Parte VI
Creazione di soluzioni professionali con Visual Studio 2010
8. Esamina le ultime tre istruzioni del metodo doPlotButtonWork. Saranno simili alle seguenti:
private void doPlotButtonWork()
{
...
duration.Content = string.Format("Duration (ms): {0}", watch.ElapsedMilliseconds);
graphBitmap.WritePixels(new Int32Rect(0, 0, pixelWidth, pixelHeight), data,
stride, 0);
graphImage.Source = graphBitmap;
}
Queste istruzioni fanno riferimento agli oggetti duration, graphBitmap e graphImage, tutti
parte dell'interfaccia utente. Di conseguenza, è necessario modificare queste istruzioni
perché vengano eseguite nel thread UI.
9. Modifica le istruzioni ed eseguile mediante il metodo Dispatcher.Invoke, come qui
evidenziato in grassetto:
private void doPlotButtonWork()
{
...
plotButton.Dispatcher.Invoke(new Action(() =>
{
duration.Content = string.Format("Duration (ms): {0}", watch.
ElapsedMilliseconds);
graphBitmap.WritePixels(new Int32Rect(0, 0, pixelWidth, pixelHeight), data,
stride, 0);
graphImage.Source = graphBitmap;
}), DispatcherPriority.ApplicationIdle);
}
Questo codice converte le istruzioni in un'espressione lambda racchiusa in un delegato
Action, quindi richiama tale delegato mediante l'oggetto Dispatcher.
10. Nel menu Debug, fai clic su Avvia senza eseguire debug per costruire ed eseguire
l’applicazione.
11. Nella finestra Graph Demo, fai clic su Plot Graph quindi, prima che il grafico appaia,
trascinare velocemente la finestra in un'altra posizione sullo schermo. In questo caso la
finestra dovrebbe reagire immediatamente senza attendere la comparsa del grafico.
12. Chiudi la finestra Graph Demo.
Annullamento delle attività e gestione delle eccezioni
Un altro requisito comune delle applicazioni che eseguono lunghe operazioni di elaborazione
è la possibilità di interromperle quando necessario. Tuttavia, è necessario non limitarsi
ad abbandonare un'attività, poiché ciò può lasciare i dati nell'applicazione in uno stato
indeterminato. Per fare ciò, la TPL implementa una strategia di annullamento cooperativo.
L'annullamento cooperativo permette a un'attività di selezionare il punto più adatto in cui
interrompere l'elaborazione, e se necessario consente anche di annullare il lavoro svolto.
Capitolo 27
Introduzione alla TPL (Task Parallel Library)
633
Funzionamento dell'annullamento cooperativo
L'annullamento cooperativo si basa sul concetto di cancellation token(token di annullamento).
Il token di annullamento è una struttura che rappresenta una richiesta di annullare una o più
attività. Il metodo eseguito da un'attività dovrebbe includere un parametro System.Threading.
CancellationToken. Un'applicazione che desideri annullare un'attività deve impostare su true
la proprietà booleana IsCancellationRequested di questo parametro. Un metodo eseguito in
un'attività può eseguire una query su questa proprietà in vari punti della sua elaborazione.
Se questa proprietà risulta impostata su true in qualsiasi punto, il metodo è al corrente che
l'applicazione ha chiesto l'annullamento dell'attività. Inoltre, il metodo sa quale lavoro è stato
svolto fino a quel momento, e può quindi annullare qualsiasi modifica se necessario per poi finire.
In alternativa, il metodo può semplicemente ignorare la richiesta e continuare l'elaborazione
senza annullare l'attività.
Suggerimento Si consiglia di esaminare frequentemente il token di annullamento in un'attività,
ma non tanto frequentemente da avere un effetto negativo sulle prestazioni dell'attività. Se possibile, provare a controllare gli annullamenti almeno ogni 10 millisecondi, ma non più frequentemente di ogni millisecondo.
L'applicazione ottiene un CancellationToken creando un oggetto System.Threading.
CancellationTokenSource ed eseguendo una query sulla proprietà Token di questo oggetto.
L'applicazione può quindi passare questo CancellationToken come parametro a qualsiasi metodo
avviato da attività create ed eseguite dall'applicazione. Se deve annullare l'attività, l'applicazione
deve chiamare il metodo Cancel dell'oggetto CancellationTokenSource. Questo metodo imposta la
proprietà IsCancellationRequested del CancellationToken passato a tutte le attività.
Il seguente esempio di codice mostra come creare un token di annullamento e utilizzarlo per
annullare un'attività. Il metodo initiateTasks crea un'istanza della variabile cancellationTokenSource
e ottiene un riferimento all'oggetto CancellationToken disponibile tramite questa variabile.
Il codice quindi crea ed esegue un'attività che avvia il metodo doWork. In seguito, il codice
richiama il metodo Cancel dell'origine del token di annullamento, la quale imposta il token di
annullamento. Il metodo doWork esegue una query sulla proprietà IsCancellationRequested del
token di annullamento. Se la proprietà è impostata, il metodo viene terminato; in caso contrario, il
metodo prosegue nell'esecuzione.
public class MyApplication
{
...
// Method that creates and manages a task
private void initiateTasks()
{
// Create the cancellation token source and obtain a cancellation token
CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
CancellationToken cancellationToken = cancellationToken.Token;
634
Parte VI
Creazione di soluzioni professionali con Visual Studio 2010
// Create a task and start it running the doWork method
Task myTask = Task.Factory.StartNew(() => doWork(cancellationToken));
...
if (...)
{
// Cancel the task
cancellationTokenSource.Cancel();
}
...
}
// Method run by the task
private void doWork(CancellationToken token)
{
...
// If the application has set the cancellation token, finish processing
if (token.IsCancellationRequested)
{
// Tidy up and finish
...
return;
}
// If the task has not been canceled, continue running as normal
...
}
}
Oltre a fornire un elevato livello di controllo sull'elaborazione degli annullamenti, questo
approccio è scalabile tra qualsiasi numero di attività. È possibile avviare più attività e passare
lo stesso oggetto CancellationToken a ognuno di loro. Richiamando Cancel sull'oggetto
CancellationTokenSource, ogni attività vede che la proprietà IsCancellationRequested è stata
impostata e può quindi reagire di conseguenza.
Inoltre, è possibile utilizzare il metodo Register per registrare un metodo di callback con il token di
annullamento. Questo callback viene quindi eseguito quando un'applicazione richiama il metodo
Cancel del corrispondente oggetto CancellationTokenSource. Tuttavia, non è possibile garantire
quando viene eseguito questo metodo; ciò può infatti avvenire prima o dopo che le attività
abbiano eseguito la propria elaborazione dell'annullamento, o persino durante tale processo.
...
cancellationToken,Register(doAdditionalWork);
...
private void doAdditionalWork()
{
// Perform additional cancellation processing
}
Nell'esercizio successivo, la funzionalità di annullamento viene aggiunta all'applicazione
GraphDemo.
Capitolo 27
Introduzione alla TPL (Task Parallel Library)
635
Aggiunta della funzionalità di annullamento all'applicazione GraphDemo
1. Utilizzando Visual Studio 2010, apri la soluzione GraphDemo situata nella cartella \Microsoft
Press\Visual CSharp Step By Step\Chapter 27\GraphDemo Canceling Tasks della cartella
Documenti.
Questa è una copia completata dell'applicazione GraphDemo dall'esercizio precedente che
utilizza attività e thread per migliorare la velocità di risposta.
2. In Esplora soluzioni, nel progetto GraphDemo, fai doppio clic su GraphWindow.xaml per
visualizzare il form nella finestra Progettazizone.
3. Da Toolbox, aggiungi al form un controllo Button sotto l'etichetta duration. Allinea il
pulsante orizzontalmente con il pulsante plotButton. Nella finestra Proprietà, modifica la
proprietà Name del nuovo pulsante in cancelButton, quindi modifica la proprietà Content in
Cancel.
Il form modificato dovrebbe apparire simile a quanto mostrato nell’immagine che segue.
4. Fai doppio clic sul pulsante Cancel per creare un metodo di gestione evento Click chiamato
cancelButton_Click.
5. Nel file GraphWindow.xaml.cs, localizza il metodo getDataForGraph. Questo metodo crea
le attività utilizzate dall'applicazione e ne attende il completamento. Sposta la dichiarazione
delle variabili Task a livello di classe per la classe GraphWindow, come mostrato in grassetto
nel seguente codice, quindi modifica il metodo getDataForGraph per creare le istanze di
queste variabili:
public partial class GraphWindow : Window
{
...
private Task first, second, third, fourth;
...
private byte[] getDataForGraph(int dataSize)
636
Parte VI
Creazione di soluzioni professionali con Visual Studio 2010
{
byte[] data = new byte[dataSize];
first = Task.Factory.StartNew(() => generateGraphData(data, 0, pixelWidth /
8));
second = Task.Factory.StartNew(() => generateGraphData(data, pixelWidth / 8,
pixelWidth / 4));
third = Task.Factory.StartNew(() => generateGraphData(data, pixelWidth / 4,
pixelWidth * 3 / 8));
fourth = Task.Factory.StartNew(() => generateGraphData(data, pixelWidth * 3 /
8, pixelWidth / 2));
Task.WaitAll(first, second, third, fourth);
return data;
}
}
6. All’inizio dell'elenco, aggiungi le seguenti istruzioni using:
using System.Threading;
I tipi utilizzati dall'annullamento cooperativo si trovano in questo spazio dei nomi.
7. Aggiungi un membro CancellationTokenSource chiamato tokenSource alla classe
GraphWindow e inizializzalo con il valore null, come mostrato di seguito in grassetto:
public class GraphWindow : Window
{
...
private Task first, second, third, fourth;
private CancellationTokenSource tokenSource = null;
...
}
8. Localizza il metodo generateGraphData e aggiungi un parametro CancellationToken
chiamato token alla definizione del metodo:
private void generateGraphData(byte[] data, int partitionStart, int partitionEnd,
CancellationToken token)
{
...
}
9. Nel metodo generateGraphData, all'inizio del ciclo for più interno, aggiungi il codice
mostrato di seguito in grassetto per verificare che l'annullamento sia stato richiesto. Se sì,
esci dal metodo; in caso contrario, continua a elaborare i valori e a visualizzare il grafico.
private void generateGraphData(byte[] data, int partitionStart, int partitionEnd,
CancellationToken token)
{
int a = pixelWidth / 2;
int b = a * a;
int c = pixelHeight / 2;
for (int x = partitionStart; x < partitionEnd; x ++)
{
int s = x * x;
double p = Math.Sqrt(b - s);
Capitolo 27
Introduzione alla TPL (Task Parallel Library)
637
for (double i = -p; i < p; i += 3)
{
if (token.IsCancellationRequested)
{
return;
}
double r = Math.Sqrt(s + i * i) / a;
double q = (r - 1) * Math.Sin(24 * r);
double y = i / 3 + (q * c);
plotXY(data, (int)(-x + (pixelWidth / 2)), (int)(y + (pixelHeight / 2)));
plotXY(data, (int)(x + (pixelWidth / 2)), (int)(y + (pixelHeight / 2)));
}
}
}
10. Nel metodo getDataForGraph, aggiungi le seguenti istruzioni mostrate in grassetto, le quali
creano l'istanza di una variabile tokenSource e recuperano l'oggetto CancellationToken in
una variabile chiamata token:
private byte[] getDataForGraph(int dataSize)
{
byte[] data = new byte[dataSize];
tokenSource = new CancellationTokenSource();
CancellationToken token = tokenSource.Token;
...
}
11. Modifica le istruzioni che creano ed eseguono le quattro attività, quindi passa la variabile
token come parametro finale al metodo generateGraphData:
first = Task.Factory.StartNew(() => generateGraphData(data, 0, pixelWidth / 8,
token));
second = Task.Factory.StartNew(() => generateGraphData(data, pixelWidth / 8,
pixelWidth / 4, token));
third = Task.Factory.StartNew(() => generateGraphData(data, pixelWidth / 4, pixelWidth
* 3 / 8, token));
fourth = Task.Factory.StartNew(() => generateGraphData(data, pixelWidth * 3 / 8,
pixelWidth / 2, token));
12. Nel metodo cancelButton_Click, aggiungi il codice qui mostrato in grassetto:
private void cancelButton_Click(object sender, RoutedEventArgs e)
{
if (tokenSource != null)
{
tokenSource.Cancel();
}
}
Questo codice verifica che sia stata creata l'istanza della variabile tokenSource; se sì, il codice
richiama il metodo Cancel su questa variabile.
13. Nel menu Debug, fai clic su Avvia senza eseguire debug per costruire ed eseguire
l’applicazione.
638
Parte VI
Creazione di soluzioni professionali con Visual Studio 2010
14. Nella finestra GraphDemo, fai clic su Plot Graph e verifica che il grafico appaia come in
precedenza.
15. Fai nuovamente clic su Plot Graph, quindi fai rapidamente clic su Cancel.
Se riesci a fare clic su Cancel prima che vengano generati i dati del grafico, questa azione
causa il ritorno dai metodi in esecuzione da parte delle attività. I dati non sono completi,
pertanto il grafico appare con spazi vuoti, come mostrato nella seguente figura. (Le
dimensioni degli spazi vuoti dipendono dalla rapidità con cui si è fatto clic su Cancel.)
16. Chiudi la finestra GraphDemo e torna a Visual Studio.
Per determinare se un'attività è stata completata o annullata, è possibile esaminare la proprietà
Status dell'oggetto Task. La proprietà Status contiene un valore proveniente dall'enumerazione
System.Threading.Tasks.TaskStatus. L'elenco che segue descrive alcuni dei valori di stato che è
possibile incontrare più comunemente; è tuttavia utile ricordare che ve ne sono altri non riportati
qui:
■ Created Stato iniziale di un'attività. L'attività è stata creata, ma non ne è stata pianificata
l'esecuzione.
■ WaitingToRun
■ Running
L'attività è stata pianificata, ma non ancora eseguita.
L'attività è attualmente in esecuzione da parte di un thread.
■ RanToCompletion
gestita.
L'attività è stata completata con successo senza alcuna eccezione non
■ Canceled L'attività è stata annullata prima dell'esecuzione, o ha riconosciuto
l'annullamento ed è stata completata senza generare eccezioni.
■ Faulted L'attività è stata terminata a causa di un'eccezione.
Capitolo 27
Introduzione alla TPL (Task Parallel Library)
639
Nell'esercizio successivo, è possibile provare a creare un report dello stato di ogni attività, in
modo da sapere quando esse vengono completate o annullate.
Annullamento di un ciclo For o ForEach parallelo
I metodi Parallel.For e Parallel.ForEach non forniscono alcun accesso diretto agli oggetti
Task che sono stati creati. In realtà, non è nemmeno possibile sapere quante attività sono
in esecuzione: .NET Framework utilizza le proprie regole euristiche per gestire il numero
ottimale da utilizzare in base alle risorse disponibili e al carico di lavoro attuale del
computer.
Per interrompere anzitempo i metodi Parallel.For o Parallel.ForEach, è necessario utilizzare
un oggetto ParallelLoopState. Il metodo specificato come corpo del ciclo deve includere un
parametro ParallelLoopState aggiuntivo. La TPL crea un oggetto ParallelLoopState e lo passa
come parametro al metodo. La TPL impiega questo oggetto per contenere informazioni su
ciascun richiamo del metodo. Il metodo può richiamare il metodo Stop di questo oggetto
al fine di indicare che la TPL non deve tentare di eseguire alcuna iterazione oltre a quelle
già avviate e terminate. Il seguente esempio mostra il metodo Parallel.For che chiama il
metodo doLoopWork per ciascuna iterazione. Il metodo doLoopWork esamina la variabile di
iterazione; se questa è maggiore di 600, il metodo richiama il metodo Stop del parametro
ParallelLoopState. Ciò fa sì che il metodo Parallel.For interrompa ogni ulteriore iterazione del
ciclo. (Le iterazioni attualmente in corso possono proseguire fino a completamento.)
Nota Ricorda che le iterazioni in un ciclo Parallel.For non vengono eseguite in una spe-
cifica sequenza. Di conseguenza, l'annullamento del ciclo mentre la variabile di iterazione
contiene il valore 600 non assicura che le precedenti 599 iterazioni siano già stata eseguite.
Allo stesso modo, possono essere già state completate iterazioni con valori maggiori di
600.
Parallel.For(0, 1000, doLoopWork);
...
private void doLoopWork(int i, ParallelLoopState p)
{
...
if (i > 600)
{
p.Stop();
}
}
640
Parte VI
Creazione di soluzioni professionali con Visual Studio 2010
Visualizzazione dello stato di ciascuna attività
1. In Visual Studio, localizza il metodo getDataForGraph nella finestra dell’editor di codice e di
testo.
2. Aggiungi il seguente codice illustrato di seguito in grassetto: Questi istruzioni generano
una stringa contenente lo stato di ciascuna attività dopo il suo completamento, oltre a
visualizzare un messaggio con la stringa corrispondente.
private byte[] getDataForGraph(int dataSize)
{
...
Task.WaitAll(first, second, third, fourth);
String message = String.Format("Status of tasks is {0}, {1}, {2}, {3}",
first.Status, second.Status, third.Status, fourth.Status);
MessageBox.Show(message);
return data;
}
3. Nel menu Debug, fai clic su Avvia senza eseguire debug.
4. Nella finestra GraphDemo, fai clic su Plot Graph ma non su Cancel. Verifica che appaia la
seguente finestra di messaggio con il report dello stato delle attività RanToCompletion
(quattro volte), quindi fai clic su OK. Nota che il grafico appare solo dopo che hai fatto clic
su OK.
5. Nella finestra GraphDemo, fai nuovamente clic su Plot Graph, quindi fai rapidamente clic su
Cancel.
Sorprendentemente, nonostante il grafico appaia con degli spazi vuoti, la finestra di
messaggio che appare contiene ancora lo stato di ciascuna attività come RanToCompletion.
Ciò accade perché nonostante la richiesta di annullamento inviata alle varie attività
mediante il token di annullamento, i metodi che erano in esecuzione sono stati restituiti.
Il runtime di .NET Framework non sa se le attività sono state realmente annullate o se è
stato loro consentito di procedere fino a completamento, e ha semplicemente ignorato le
richieste di annullamento.
6. Chiudi la finestra GraphDemo e torna a Visual Studio.
In che modo è possibile indicare che un'attività è stata annullata invece di essere eseguita fino
al suo completamento? La risposta sta nell'oggetto CancellationToken passato come parametro
al metodo eseguito dall'attività. La classe CancellationToken fornisce un metodo chiamato
ThrowIfCancellationRequested. Questo metodo testa la proprietà IsCancellationRequested di un
Capitolo 27
Introduzione alla TPL (Task Parallel Library)
641
token di annullamento; se è true, il metodo genera un'eccezione OperationCanceledException e
interrompe il metodo eseguito dall'attività.
L'applicazione che ha avviato il thread deve essere pronta a rilevare e gestire questa eccezione,
ma ciò conduce a un'altra domanda. Se un'attività viene terminata con la generazione di
un'eccezione, in realtà essa torna allo stato Faulted. Ciò è vero anche se l'eccezione generata
è OperationCanceledException. Un'attività passa allo stato Canceled solo se è stata annullata
senza generare un'eccezione. Pertanto, in che modo un'attività può generare un'eccezione
OperationCanceledException senza essere trattata come tale?
La risposta sta nell'attività stessa. Per poter riconoscere che un'eccezione
OperationCanceledException è il risultato di un annullamento controllato dell'attività e non
solamente un'eccezione causata da altre circostanze, un'attività deve sapere che l'operazione
è stata annullata realmente. Ciò è possibile solo se essa è in grado di esaminare il token di
annullamento. Questo token è stato passato come parametro al metodo eseguito dall'attività,
ma questa non considera realmente alcuno di questi parametri. (L'attività li considera come dati
riguardanti il metodo e non se ne preoccupa ulteriormente.) Il token di annullamento può invece
essere specificato durante la creazione dell'attività, sia come parametro del costruttore Task, sia
come parametro del metodo StartNew dell'oggetto TaskFactory usato per creare ed eseguire le
attività. Il seguente codice mostra un esempio basato sull'applicazione GraphDemo. Nota come
il parametro token viene passato al metodo generateGraphData (come prima), ma anche come
parametro separato al metodo StartNew:
Task first = null;
tokenSource = new CancellationTokenSource();
CancellationToken token = tokenSource.Token;
...
first = Task.Factory.StartNew(() => generateGraphData(data, 0, pixelWidth / 8, token),
token);
Ora quando il metodo in esecuzione da parte dell'attività genera un'eccezione
OperationCanceledException, l'infrastruttura alla base dell'attività esamina il CancellationToken.
Se questo indica che l'attività è stata annullata, l'infrastruttura gestisce l'eccezione
OperationCanceledException, riconosce l'annullamento e imposta lo stato dell'attività su Canceled.
L'infrastruttura genera quindi una TaskCanceledException, che la tua applicazione deve essere
pronta a rilevare. Questo è ciò che viene illustrato nel prossimo esercizio, ma prima di iniziare è
necessario sapere un po' di più su come le attività generano le eccezioni e su come è necessario
procedere per gestirle.
Gestione delle eccezioni delle attività mediante l'uso della
classe AggregateException
In questo manuale si è visto che la gestione delle eccezioni è un elemento importante di ogni
applicazione commerciale. I costrutti per la gestione delle eccezioni visti finora sono di uso
semplice, e utilizzandoli attentamente è facile riuscire a rilevare un'eccezione e determinare
quale sezione di codice l'ha generata. Tuttavia, quando si inizia a dividere il lavoro in più attività
642
Parte VI
Creazione di soluzioni professionali con Visual Studio 2010
concorrenti, tracciare e gestire le eccezioni diviene rapidamente un problema più complesso. La
difficoltà consiste nella presenza di attività differenti che possono generare eccezioni proprie,
e nella necessità di trovare un modo per rilevare e gestire più eccezioni che possono essere
generate anche contemporaneamente. A tal fine entra in gioco la classe AggregateException.
Questa classe opera come una sorta di involucro di una raccolta di eccezioni. All'interno, ogni
eccezione può essere generata da attività differenti. Nell'applicazione, puoi rilevare l'eccezione
AggregateException e quindi procedere con un'iterazione nel contenuto della raccolta, eseguendo
se necessario le operazioni più opportune. Per aiutarti, la classe AggregateException fornisce il
metodo Handle. Questo metodo prende un delegato Func<Exception, bool> che fa riferimento
a un metodo. Il metodo referenziato prende un oggetto Exception come parametro e restituisce
un valore booleano. Quando richiami Handle, il metodo referenziato viene eseguito nell'oggetto
AggregateException per ogni eccezione presente nella raccolta. Il metodo referenziato può così
esaminare l'eccezione ed eseguire l'azione più appropriata. Se è in grado di gestire l'eccezione, il
metodo referenziato restituisce il valore true. In caso contrario, restituisce il valore false. Una volta
completato il metodo Handle, le eventuali eccezioni non gestite vengono riunite insieme in una
nuova AggregateException e viene generata questa eccezione; un successivo gestore di eccezioni
più esterno può così rilevare ed elaborare questa eccezione.
Nell'esercizio successivo viene illustrato come rilevare una AggregateException e come utilizzarla
per gestire l'eccezione TaskCanceledException generata quando viene annullata un'attività.
Riconoscimento dell'annullamento e gestione dell'eccezione AggregateException
1. In Visual Studio, visualizza il file GraphWindow.xaml nella finestra Progettazione.
2. Dalla Casella degli strumenti, aggiungi un controllo Label al form sotto il pulsante
cancelButton. Allinea il bordo sinistro del controllo Label con il bordo sinistro del pulsante
cancelButton.
3. Nella finestra Proprietà, modifica la proprietà Name del controllo Label in status, quindi
rimuovi il valore presente nella proprietà Content.
4. Torna alla finestra dell’editor di codice e di testo che mostra il file GraphWindow.xaml.cs e
aggiungi il seguente metodo sotto il metodo getDataForGraph:
private bool handleException(Exception e)
{
if (e is TaskCanceledException)
{
plotButton.Dispatcher.Invoke(new Action(() =>
{
status.Content = "Tasks Canceled";
}), DispatcherPriority.ApplicationIdle);
return true;
}
else
{
return false;
}
}
Capitolo 27
Introduzione alla TPL (Task Parallel Library)
643
Questo metodo esamina l'oggetto Exception passato come parametro; se si tratta di un
oggetto TaskCanceledException, il metodo mostra il testo “Tasks Canceled” nella scheda
status del form e restituisce true per indicare di aver gestito l'eccezione; in caso contrario,
restituisce false.
5. Nel metodo getDataForGraph, modifica le istruzioni su come creare ed eseguire le attività e
specifica l'oggetto CancellationToken come secondo parametro del metodo StartNew, come
mostrato in grassetto nel seguente codice:
private byte[] getDataForGraph(int dataSize)
{
byte[] data = new byte[dataSize];
tokenSource = new CancellationTokenSource();
CancellationToken token = tokenSource.Token;
...
first = Task.Factory.StartNew(() => generateGraphData(data, 0, pixelWidth / 8,
token), token);
second = Task.Factory.StartNew(() => generateGraphData(data, pixelWidth / 8,
pixelWidth / 4, token), token);
third = Task.Factory.StartNew(() => generateGraphData(data, pixelWidth / 4,
pixelWidth * 3 / 8, token), token);
fourth = Task.Factory.StartNew(() => generateGraphData(data, pixelWidth * 3 / 8,
pixelWidth / 2, token), token);
Task.WaitAll(first, second, third, fourth);
...
}
6. Aggiungi un blocco try attorno alle istruzioni che creano ed eseguono le attività, quindi
attendi il loro completamento. Se l'attesa ha successo, il metodo Dispatcher.Invoke
visualizza il testo “Tasks Completed” nella scheda status del form. Aggiungi un blocco
catch per gestire l'eccezione AggregateException. In questo gestore di eccezioni, richiama
il metodo Handle dell'oggetto AggregateException e passa il riferimento al metodo
handleException. Il codice mostrato di seguito in grassetto mostra le modifiche necessarie:
private byte[] getDataForGraph(int dataSize)
{
byte[] data = new byte[dataSize];
tokenSource = new CancellationTokenSource();
CancellationToken token = tokenSource.Token;
try
{
first = Task.Factory.StartNew(() => generateGraphData(data, 0, pixelWidth / 8,
token), token);
second = Task.Factory.StartNew(() => generateGraphData(data, pixelWidth / 8,
pixelWidth / 4, token), token);
third = Task.Factory.StartNew(() => generateGraphData(data, pixelWidth / 4,
pixelWidth * 3 / 8, token), token);
fourth = Task.Factory.StartNew(() => generateGraphData(data, pixelWidth * 3 /
8, pixelWidth / 2, token), token);
644
Parte VI
Creazione di soluzioni professionali con Visual Studio 2010
Task.WaitAll(first, second, third, fourth);
plotButton.Dispatcher.Invoke(new Action(() =>
{
status.Content = "Tasks Completed";
}), DispatcherPriority.ApplicationIdle);
}
catch (AggregateException ae)
{
ae.Handle(handleException);
}
String message = String.Format("Status of tasks is {0}, {1}, {2}, {3}",
first.Status, second.Status, third.Status, fourth.Status);
MessageBox.Show(message);
return data;
}
7. Nel metodo generateDataForGraph, sostituisci l'istruzione if che esamina la
IsCancellationProperty dell'oggetto CancellationToken con codice che richiama il metodo
ThrowIfCancellationRequested, come mostrato di seguito in grassetto:
private void generateDataForGraph(byte[] data, int partitionStart, int partitionEnd,
CancellationToken token)
{
...
for (int x = partitionStart; x < partitionEnd; x++);
{
...
for (double i = -p; I < p; i += 3)
{
token.ThrowIfCancellationRequested();
...
}
}
...
}
8. Nel menu Debug, fai clic su Avvia senza eseguire debug.
9. Nella finestra Graph Demo, fai clic su Plot Graph e verifica che lo stato di ciascuna attività
sia segnalato come RanToCompletion, che il grafico venga generato e che la scheda status
mostri il messaggio “Tasks Completed”.
10. Fai nuovamente clic su Plot Graph, quindi fai rapidamente clic su Cancel. Se sei veloce,
lo stato di una o più attività può essere segnalato come Canceled, la scheda status può
riportare il testo “Tasks Canceled” e il grafico può apparire con spazi vuoti al suo interno. Se
non sei abbastanza rapido, prova a ripetere l'operazione.
11. Chiudi la finestra Graph Demo e torna a Visual Studio.
Capitolo 27
Introduzione alla TPL (Task Parallel Library)
645
Utilizzo delle condizioni di proseguimento con attività
annullate e fallite
Se hai la necessità di svolgere lavoro aggiuntivo quando un'attività viene annullata o genera
un'eccezione non gestita, ricorda che è possibile utilizzare il metodo ContinueWith con il valore
TaskContinuationOptions appropriato. Ad esempio, il seguente codice crea un'attività che esegue
il metodo doWork. Se l'attività viene annullata, il metodo ContinueWith specifica che deve
essere creata un'altra attività che esegua il metodo doCancellationWork. Questo metodo può
eseguire alcune semplici registrazioni o pulizie. Se l'attività non viene annullata, la condizione di
continuazione non viene eseguita.
Task task = new Task(doWork);
task.ContinueWith(doCancellationWork, TaskContinuationOptions.OnlyOnCanceled);
task.Start();
...
private void doWork()
{
// The task runs this code when it is started
...
}
...
private void doCancellationWork(Task task)
{
// The task runs this code when doWork completes
...
}
Similmente, è possibile specificare il valore TaskContinuationOptions.OnlyOnFaulted per indicare
una condizione di continuazione da eseguire se il metodo originale eseguito dall'attività genera
un'eccezione non gestita.
Questo capitolo illustra perché è importante scrivere applicazioni in grado di essere scalabili tra
più processori e core di processori. Si è visto inoltre come utilizzare la Task Parallel Library per
eseguire operazioni in parallelo, e come sincronizzare le operazioni concorrenti e attenderne
il completamento. Hai appreso come usare la classe Parallel per eseguire in parallelo alcuni
comuni costrutti di programmazione, e hai anche visto quando è inappropriato eseguire il
codice in parallelo. Hai utilizzato attività e thread insieme per migliorare la velocità di risposta e
il throughput di un'interfaccia grafica utente, e hai scoperto come annullare le attività in modo
pulito e controllato.
■ Se desideri continuare con il capitolo successivo
Continua a eseguire Visual Studio 2010 e passa al capitolo 28.
■ Se desideri uscire ora da Visual Studio 2010
Nel menu File, fai clic su Esci. Se sullo schermo è visualizzata una finestra di dialogo Salva, fai
clic su Sì per salvare il progetto.
646
Parte VI
Creazione di soluzioni professionali con Visual Studio 2010
Riferimenti rapidi del capitolo 27
Obiettivo
Azione
Creare un'attività ed eseguirla
Utilizza il metodo StartNew di un oggetto TaskFactory per creare ed eseguire l'attività in un'unica fase:
Task task = taskFactory.StartNew(doWork());
...
private void doWork()
{
// The task runs this code when it is started
...
}
In alternativa crea un nuovo oggetto Task che faccia riferimento al metodo da eseguire e richiama il metodo Start:
Task task = new Task(doWork);
task.Start();
Attendere il completamento di
un'attività
Attendere il completamento di più
attività
Richiama il metodo Wait dell'oggetto Task:
Task task = ...;
...
task.Wait();
Richiama il metodo statico WaitAll della classe Task e specifica le attività
per cui desideri attendere:
Task task1 = ...;
Task task2 = ...;
Task task3 = ...;
Task task4 = ...;
...
Task.WaitAll(task1, task2, task3, task4);
Specificare un metodo da eseguire
in una nuova attività quando
questa viene completata
Restituire un valore da un'attività
Richiama il metodo ContinueWith dell'attività e specifica il metodo come
condizione di continuazione:
Task task = new Task(doWork);
task.ContinueWith(doMoreWork,
TaskContinuationOptions.NotOnFaulted);
Utilizza un oggetto Task<TResult> per eseguire un metodo, dove il parametro di tipo T specifica il tipo del valore di ritorno del metodo. Usa la
proprietà Result dell'attività per attenderne il completamento e la restituzione del valore:
Task<int> calculateValueTask = new Task<int>(() =>
calculateValue(...));
calculateValueTask.Start(); // Invoke the calculateValue method
...
int calculatedData = calculateValueTask.Result; // Block until
calculateValueTask completes
Capitolo 27
Introduzione alla TPL (Task Parallel Library)
647
Obiettivo
Azione
Eseguire iterazioni di cicli e sequenze di istruzione mediante attività parallele
Utilizza i metodi Parallel.For e Parallel.ForEach per eseguire iterazioni di
ciclo tramite le attività:
Parallel.For(0, 100, performLoopProcessing);
...
private void performLoopProcessing(int x)
{
// Perform loop processing
}
Usa il metodo Parallel.Invoke per eseguire chiamate concorrenti ai metodi
mediante attività separate:
Parallel.Invoke(
doWork,
doMoreWork,
doYetMoreWork
);
Gestire le eccezioni generate da
una o più attività
Rileva l'eccezione AggregateException. Utilizza il metodo Handle per
specificare un metodo in grado di gestire ogni eccezione nell'oggetto
AggregateException. Se è in grado di portare al termine il compito, il metodo di gestione delle eccezioni restituisce true; in caso contrario, restituisce false:
try
{
Task task = Task.Factory.StartNew(...);
...
}
catch (AggregateException ae)
{
ae.Handle(new Func<Exception, bool> (handleException));
}
...
private bool handleException(Exception e)
{
if (e is TaskCanceledException)
{
...
return true;
}
else
{
return false;
}
}
648
Parte VI
Creazione di soluzioni professionali con Visual Studio 2010
Obiettivo
Azione
Supportare l'annullamento di
un'attività
Implementa l'annullamento cooperativo creando un oggetto
CancellationTokenSource e utilizzando un parametro CancellationToken
nel metodo eseguito dall'attività. Nel metodo dell'attività, richiama il metodo ThrowIfCancellationRequested del parametro CancellationToken per
generare un'eccezione OperationCanceledException e terminare l'attività:
private void generateGraphData(..., CancellationToken token)
{
...
token.ThrowIfCancellationRequested();
...
}
Capitolo 28
Esecuzione di accessi simultanei
ai dati
Gli argomenti trattati in questo capitolo sono:
■
Usare PLINQ per eseguire query LINQ simultanee
■
Usare classi di insiemi simultanee per mantenere gli insiemi di dati in modo corretto
(thread-safe).
■
Usare la sincronizzazione simultanea dei primitivi per coordinare l'accesso ai dati che sono
stati manipolati.
Nel capitolo 27 hai visto come sfruttare le nuove funzionalità di NET Framework per eseguire
operazioni simultanee. Anche nei primi capitoli di questo libro ti ho mostrato come accedere
ai dati in modo dichiarativo con il linguaggio LINQ (Language Integrated Query). Una tipica
query LINQ genera un set di risultati enumerabile e la puoi ripetere in sequenza all'interno
del set per recuperare i dati. Se i dati di origine usati per generare i risultati è grande, il tempo
di elaborazione di una query LINQ può essere lungo. Molti sistemi di gestione di database,
incontrando il problema di ottimizzare le query, privilegiano gli algoritmi per spezzare il
processo di identificazione dei dati in più passi, e quindi fanno partire l'elaborazione simultanea e
combinano, alla fine dell'operazione, i dati per ottenere l'insieme completo di risultati. I progettisti
di TPL (Task Parallel Library) hanno deciso di dare a LINQ una caratteristica simile e il risultato è
PLINQ (Parallel LINQ). Nella prima parte del capitolo studierai PLINQ.
Ricorda che PLINQ non è sempre la tecnologia più appropriata per un'applicazione. Se hai
creato la tua operazione manualmente assicurati che i thread che accompagnano le operazioni
si coordinino correttamente tra di loro. TPL prevede dei metodi attraverso i quali puoi attendere
che le operazioni si completino e puoi utilizzare questi metodi per coordinare le operazioni ad
ogni singolo livello. Ma ora fermati a pensare a che cosa succede se due elaborazioni cercano di
accedere e modificare gli stessi dati. Se le elaborazioni vengono eseguite nello stesso momento,
le loro operazioni potrebbero corrompere i dati. In questo modo potrebbero crearsi bug difficili
da correggere, principalmente per la loro imprevedibilità. Sin dalla versione 1.0 di Microsoft NET.
Framework ci sono dei primitivi che si possono usare per bloccare i dati e coordinare i thread,
ma per usarli in maniera efficace devi conoscere bene il modo in cui i thread interagiscono. TPL
include alcune varianti a questi primitivi. Include anche delle classi specializzate nel recupero dei
dati che possono sincronizzare l'accesso ai dati durante le elaborazioni. Sono queste le classi che
di fatto nascondono la complessità intrinseca del coordinamento dell'accesso ai dati. Vedrai come
usare alcuni di questi primitivi di sincronizzazione e di classi di insiemi nella seconda parte del
capitolo.
649
650
Parte VI
Creazione di soluzioni professionali con Visual Studio 2010
Utilizzo di PLINQ per l’accesso ai dati in modo dichiarativo e
parallelo
Nei capitoli precedenti hai visto la potenza di LINQ nel recuperare dati da diverse strutture di dati.
In NET.Framework 4.0, LINQ è stato esteso usando parte della tecnologia disponibile nella TPL per
migliorarne notevolmente la performance e per elaborare in parallelo alcune operazioni di query.
Questo ampliamento è PLINQ.
PLINQ divide un set di dati in partizioni e, in parallelo in ogni partizione, elabora i dati per reperire
quelli che concordano con quelli specificati nei criteri della query. Al termine delle elaborazioni,
i risultati reperiti da ogni partizione vengono poi combinati in un singolo set di risultati
enumerabile. PLINQ è ideale in quegli scenari che prevedono set di dati con un gran numero di
elementi, o in quei casi in cui il criterio specificato prevede operazioni complesse e dispendiose.
Uno degli scopi principali di PLINQ è di essere il meno invadente possibile. Se hai già molte
query LINQ di certo non hai voglia di modificare il codice perché queste girino sull'ultima
versione di NET.Framework. E per fare questo, NET.Framework include il metodo AsParallel che
puoi usare con oggetti enumerabili. Il metodo AsParallel ritorna un oggetto ParallelQuery che
agisce in maniera simile all'oggetto enumerabile originario tranne che per il fatto che fornisce
delle implementazioni in parallelo di molti operatori LINQ, come join e where. Queste nuove
implementazioni di operatori LINQ sono basate sulla TPL e usano diversi algoritmi per cercare e
far girare parti delle tue query LINQ in parallelo, ove possibile.
Come sempre nel mondo dell'elaborazione simultanea, il metodo AsParallel non è magico. Non è
garantito che il tuo codice sia più veloce perché tutto dipende dalla natura delle tue query LINQ
e se le elaborazioni che devono essere eseguite possono essere eseguite in parallelo. Per capire
meglio come lavora PLINQ, e in quali situazioni, forse è meglio portare qualche esempio. Gli
esercizi che incontrerai nelle prossime sezioni descrivono un paio di scenari.
Utilizzo di PLINQ per migliorare la performance durante
un'iterazione all'interno di un insieme
Il primo scenario è semplice. Prendi in considerazione una query LINQ che esegue una iterazione
all'interno di un insieme e recupera elementi dall'insieme basandosi su calcoli complessi. Questo
genere di query di sicuro può beneficiare di elaborazioni in parallelo, sempre che i calcoli siano
tra loro indipendenti. Gli elementi contenuti nell'insieme possono essere suddivisi in un numero
di partizioni, il numero esatto dipende dall'attuale carico del computer e del numero di CPU a
disposizione. Gli elementi in ogni partizione possono essere processati da thread separati. Una
volta che tutte le partizione sono state elaborate, i risultati possono essere uniti. Un insieme
che supporta l'accesso agli elementi attraverso un indice, come una matrice o un insieme che
implementi l'interfaccia IList<T>, può essere gestito in questa maniera.
Capitolo 28
Esecuzione di accessi simultanei ai dati
651
Nota Se i calcoli richiedono l'accesso a dati condivisi, bisogna sincronizzare i thread. Questo
può comportare un sovraccarico e quindi annullare i benefici di una query in parallelo.
Creazione di una query LINQ parallela all'interno di un insieme semplice
1. Utilizzando Microsoft Visual Studio 2010, apri la soluzione PLINQ, che si trova nella cartella \
Microsoft Press\Visual CSharp Step by Step\Chapter 28\PLINQ della cartella Documenti.
2. In Esplora soluzioni, fai doppio clic sul file Program.cs per visualizzarlo nella finestra
dell’editor di codice e testo.
Questa è un'applicazione console. La struttura di base dell'applicazione è già stata creata
per te. La classe Program contiene due metodi chiamati Test1 e Test2 e illustrano un paio di
scenari comuni tipici. Il metodo Main richiama ogni metodo di questi test a turno.
Entrambe i metodi hanno la stessa struttura generale, creano una query LINQ e la
eseguono, e mostrano poi il tempo impiegato. Il codice di ognuno di questi metodi è quasi
del tutto separato dalle dichiarazioni che di fatto creano e eseguono le query. Queste
dichiarazioni le aggiungerai a mano a mano che procedi lungo gli esercizi.
3. Individua il metodo Test1. Il metodo crea una grande matrice di numeri interi e la popola
in modo casuale con numeri da 0 a 200. Ora aggiungi una query LINQ che recupera tutti i
numeri presenti nella matrice che hanno valore maggiore di 100.
4. Dopo il il primo commento TO DO di questo metodo, aggiungi la query LINQ mostrata di
seguito in grassetto:
// TO DO: Crea una query LINQ che recupera tutti numeri maggiori di 100
var over100 = from n in numbers
where TestIfTrue(n > 100)
select n;
Il test n > 100 non è sufficientemente impegnativo, a livello di calcolo, per rendere al
meglio i benefici di una query simultanea, quindi il codice richiama un metodo chiamato
TestIfTrue che lo rallenta eseguendo l'operazione SpinWait. Il metodo SpinWait fa in modo
che il processore continui a eseguire un loop di istruzioni speciali "no operation" per un
breve arco di tempo, tenendolo occupato ma di fatto dandogli nulla da fare. (Questo
comportamento è noto come spinning). Il metodo TestIfTrue sarà simile al seguente:
public static bool TestIfTrue(bool expr)
{
Thread.SpinWait(1000);
return expr;
}
652
Parte VI
Creazione di soluzioni professionali con Visual Studio 2010
5. Dopo il secondo commento TO DO nel metodo Test1, aggiungi il codice mostrato di seguito
in grassetto:
// TO DO: Esegui una query LINQ e salva i risultati in un oggetto List<int>
List<int> numbersOver100 = new List<int>(over100);
Ricordati che le query LINQ vengono eseguite in differita, quindi non partono fino a
quando non hai raccolto tutti i loro risultati. Questa dichiarazione crea un oggetto List<int>
e lo popola con i risultati della query over100.
6. Dopo il terzo commento TO DO nel metodo Test1, aggiungi il codice mostrato di seguito in
grassetto:
// TO DO: Mostra i risultati
Console.WriteLine("There are {0} numbers over 100.", numbersOver100.Count);
7. Nel menu Debug, fai clic su Avvia senza eseguire debug. Prendi nota del tempo utilizzato
per il Test 1 e del numero di elementi presenti nella matrice maggiori di 100.
8. Esegui l'applicazione più volte a prendi nota della media del tempo utilizzato per eseguirla.
Verifica che il numero degli elementi maggiore di 100 sia lo stesso ogni volta. Quando hai
terminato torna a Microsoft Visual Studio.
9. Ogni elemento che la query LINQ ha prodotto è indipendente da ogni riga presente,
e questo rende la query una candidata ideale per il partizionamento. Modifica la
dichiarazione che definisce la query LINQ e specifica il metodo AsParallel nella matrice
numbers come mostrato in grassetto di seguito:
var over100 = from n in numbers.AsParallel()
where TestIfTrue(n > 100)
select n;
10. Nel menu Debug, fai clic su Avvia senza eseguire debug. Controlla che il numero degli
elementi riportati dal Test 1 sia lo stesso di prima, ma che il tempo impiegato sia diminuito
in modo significativo. Esegui il test più volte a prendi nota della media del tempo utilizzato
per eseguirlo. Sei hai un computer con processore dual-core o un computer con doppio
processore dovresti vedere diminuito il tempo del 40-45%. Se hai a disposizione una
macchina ancora più potente, la diminuzione dovrebbe essere più evidente.
11. Chiudi l’applicazione e ritorna a Visual Studio.
L'esercizio precedente ti mostra come ottenere miglioramenti nella performance apportando
delle piccole modifiche a una query LINQ. Comunque, tieni presente che risultati di questo
genere li puoi ottenere in query in cui i calcoli prevedono un grande utilizzo di tempo CPU. Io
ho imbrogliato un pochino facendo fare dello spinning al processore. Senza questo sovraccarico,
la versione simultanea della query LINQ è decisamente più lenta di quella seriale. Nel prossimo
esercizio vedrai una query LINQ che unisce due matrici in memoria. Questa volta usiamo volumi di
dati più realistici, in modo da non dover rallentare la query artificialmente.
Capitolo 28
Esecuzione di accessi simultanei ai dati
653
Creazione di query LINQ parallela che unisce due insiemi
1. Nella finestra dell’editor di codice e di testo individua la classe CustomersInMemory.
Questa classe contiene una matrice pubblica string chiamata Customers. Ogni stringa
nella matrice Customers contiene dati relativi a un singolo cliente e i campi sono separati
da virgole; questo è un tipico formato di dati che un'applicazione può leggere da un file
di testo che utilizza i campi separati da virgola. Il primo campo contiene l'ID del cliente,
il secondo campo è il nome dell'azienda che il cliente rappresenta e i restanti campi
contengono l'indirizzo, città, paese e CAP.
2. Cerca la classe OrdersInMemory.
Questa classe è simile a quella CustomersInMemory tranne che per il fatto che contiene una
stringa chiamata Orders. Il primo campo in ogni stringa è il numero d'ordine, il secondo
campo è l'ID del cliente e il terzo campo è la data dell'ordine.
3. Cerca la classe OrderInfo. Questa classe contiene quattro campi che contengono l'ID del
cliente, il nome dell'azienda, l'ID e la data dell'ordine, per ogni ordine. Userai una query
LINQ per popolare l'insieme di oggetti OrderInfo dai dati contenuti nelle matrici Customers
e Orders.
4. Individua il metodo Test2 nella classe Program. Con questo metodo creerai una query LINQ
che unisce le matrici Customers e Orders all'ID del cliente. La query archivia ogni riga dei
risultati in un oggetto OrderInfo.
5. Nel blocco try di questo metodo aggiungi il codice il grassetto riportato qui sotto dopo il
primo commento TO DO.
// TO DO: Crea una query LINQ che recupera i clienti e ordini dalle matrici
// Archivia ogni riga di risultati in un oggetto OrderInfo
var orderInfoQuery = from c in CustomersInMemory.Customers
join o in OrdersInMemory.Orders
on c.Split(',')[0] equals o.Split(',')[1]
select new OrderInfo
{
CustomerID = c.Split(',')[0],
CompanyName = c.Split(',')[1],
OrderID = Convert.ToInt32(o.Split(',')[0]),
OrderDate = Convert.ToDateTime(o.Split(',')[2], new
CultureInfo("en-US"))
};
Questa dichiarazione definisce la query LINQ. Nota che viene usato il metodo Split della
classe String per dividere ogni stringa in una matrice di stringhe. Le stringhe sono divise
dal segno della virgola. (Le virgole sono state tolte.) C'è una complicazione, il formato delle
date è quello americano, quindi il codice che le converte in oggetti DateTime, all'interno
dell'oggetto OrderInfo, specifica la formattazione americana. Se usi la formattazione locale
di default, le date potrebbero non essere lette correttamente.
654
Parte VI
Creazione di soluzioni professionali con Visual Studio 2010
6. Nel metodo Test2 aggiungi il seguente codice che trovi in grassetto dopo la seconda
dichiarazione TO DO:
// TO DO: Esegui una query LINQ e salva i risultati in un oggetto List<int>
List<OrderInfo> orderInfo = new List<OrderInfo>(orderInfoQuery);
Questa dichiarazione fa partire la query e popola l'insieme orderInfo.
7. Aggiungi la dichiarazione che vedi in grassetto dopo TO DO:
// TO DO: Mostra i risultati
Console.WriteLine("There are {0} orders", orderInfo.Count);
8. Nel menu Debug, fai clic su Avvia senza eseguire debug.
Verifica che il Test 2 recuperi 830 ordini e segnati il tempo che ci ha impiegato per farlo.
Avvia l'applicazione più volte per ottenere una media della durata del test e ritorna poi a
Visual Studio.
9. Modifica la query LINQ specificata nel metodo Test2e aggiungi l'estensione del metodo alle
matrici Customerse Orders come mostrato in grassetto di seguito:
var orderInfoQuery = from c in CustomersInMemory.Customers.AsParallel()
join o in OrdersInMemory.Orders.AsParallel()
on c.Split(',')[0] equals o.Split(',')[1]
select new OrderInfo
{
CustomerID = c.Split(',')[0],
CompanyName = c.Split(',')[1],
OrderID = Convert.ToInt32(o.Split(',')[0]),
OrderDate = Convert.ToDateTime(o.Split(',')[2], new
CultureInfo("en-US"))
};
Nota Quando unisci due sorgenti di dati in questo modo, entrambi devono essere og-
getti IEnumerable o ParallelQuery. Questo significa che se tu specifichi che il metodo
AsParallel vale per una sorgente, devi farlo anche per l'altra. Se non lo fai, l'esecuzione non
sarà in grado di gestire in parallelo la query e tu non otterrai alcun beneficio.
10. Esegui più volte l’applicazione. Prendi nota del tempo impiegato dal Test 2 e verifica che
sia notevalmente inferiore alla volta precedente. PLINQ è in grado di utilizzare molti thread
per ottimizzare le operazioni di unione cercando in parallelo i dati all'interno di ogni parte
dell'unione.
11. Chiudi l’applicazione e ritorna a Visual Studio.
Questi due semplici esercizi ti hanno dimostrato la forza dell'estensione del metodo AsParallel e
di PLINQ. Tieni presente che PLINQ è una tecnologia in evoluzione e di tanto in tanto ci potranno
essere delle novità sostanziali. In più, i volumi di dati e la quantità di processi che fai eseguire a
una query ha un grande effetto sull'efficacia di PLINQ. Quindi non prendere questi due esempi
come regole sempre valide per ogni caso. Piuttosto questi esercizi mirano a mostrarti come sia
Capitolo 28
Esecuzione di accessi simultanei ai dati
655
necessario misurare e valutare con attenzione quella che può essere una performance usando
PLINQ nel tuo ambiente e con i tuoi dati.
Definizione delle opzioni per una query PLINQ
L'oggetto ParallelEnumerable risultante dal metodo AsParallel mette in luce una serie di metodi
che possono essere usati per influenzare il modo in cui una query viene messa in parallelo. Per
esempio, puoi specificare il numero di elaborazioni che pensi essere ottimale e scavalcare ogni
decisione fatta dal runtime utilizzando il metodo WithDegreeOfParallelism, come vedi qui:
var orderInfoQuery =
from c in CustomersInMemory.Customers.AsParallel().WithDegreeOfParallelism(4)
join o in OrdersInMemory.Orders.AsParallel()
on ...
Il valore che specifichi si applica a tutta la query. Di conseguenza dovrai specificare solo una volta
il valore di WithDegreeOfParallelism. Nell'esempio appena fatto il grado di parallelismo si applica
agli oggetti Customers e Orders.
A volte ci possono essere casi in cui il runtime decide, con le sue regole euristiche, che eseguire
in parallelo la query può essere di scarso beneficio. Se sei sicuro che questo non sia il caso, puoi
usare il metodo WithExecutionMode della classe ParallelQuery e forzare il runtime a andare in
parallelo. Il codice seguente illustra un esempio:
var orderInfoQuery =
from c in CustomersInMemory.Customers.AsParallel().WithExecutionMode(ParallelExecutionMo
de.ForceParallelism)
join o in OrdersInMemory.Orders.AsParallel()
on ...
Ricorda che, nella query, puoi usare solo una volta WithExecutionMode.
Quando fai andare andare in parallelo le query, l'ordine dei dati risultanti può venirne influenzato.
Se l'ordinamento è importante allora devi specificare l'estensione del metodo AsOrdered nella
classe ParallelQuery. Per esempio, se vuoi avere solo i primi risultati della query, potrebbe essere
importante mantenere l'ordinamento al fine di assicurarti che la query ti restituisca dati coerenti
ogni volta che la esegui, come ti mostro nell'esempio che segue dove uso il metodo Take che
restituisce le prime 10 corrispondenze dal set di dati:
var over100 = from n in numbers.AsParallel().AsOrdered().Take(10)
where ...
select n;
Di sicuro la query in questo modo si rallenta, devi quindi considerare i pro e i contro tra l'avere
una performance e un ordinamento ogni volta che vuoi implementare una query come ti ho
appena descritto.
656
Parte VI
Creazione di soluzioni professionali con Visual Studio 2010
Annullamento di una query PLINQ
A differenza di una query LINQ, una query PLINQ può essere annullata. Per fare questo
specifica l'oggetto CancellationToken da CancellationTokenSource e usa l'estensione del metodo
WithCancellation in ParallelQuery.
CancellationToken tok = ...;
...
var orderInfoQuery =
from c in CustomersInMemory.Customers.AsParallel().WithCancellation(tok)
join o in OrdersInMemory.Orders.AsParallel()
on ...
Dovrai specificare, nella query, solo una volta WithCancellation. L'annullamento si applica a tutte le
sorgenti della query. Se l'oggetto CancellationTokenSource usato per generare CancellationToken
viene annullato, la query si ferma con l'eccezione OperationCanceledException.
Sincronizzazione degli accessi simultanei imperativi ai dati
La TPL fornisce un ambito di lavoro così potente da permetterti di strutturare e creare applicazioni
che sfruttino più CPU per eseguire applicazioni in parallelo. Comunque, come ho già accennato
all'inizio del capitolo, devi prestare attenzione quando crei soluzioni che eseguono elaborazioni
simultaneamente, specialmente quelle che accedono agli stessi dati.
Il punto è che tu hai un controllo minimo su come le elaborazioni in parallelo si organizzano
tra di loro, come anche il grado di parallelismo che il sistema operativo è in grado di dare a
un'applicazione costruita con la TPL. Queste sono decisioni lasciate al runtime e dipendono
dal carico di lavoro e dalle caratteristiche hardware a disposizione sul computer in cui gira
l'applicazione. Questo livello di astrazione è il frutto di decisioni degli sviluppatori Microsoft,
che in questo modo hanno eliminato la necessità per te di conoscere i dettagli più intimi dei
thread e della loro cadenza mentre creai un'applicazione con elaborazioni simultanee. Ma questa
astrazione ha un costo. Anche se tutto sembra funzionare come per magia, devi comunque
capire come gira il tuo codice, altrimenti potresti trovarti con delle applicazioni che mostrano
comportamenti non prevedibili (ed errati), come ti mostro nell'esempio qui sotto:
using System;
using System.Threading;
class Program
{
private const int NUMELEMENTS = 10;
static void Main(string[] args)
{
SerialTest();
}
Capitolo 28
Esecuzione di accessi simultanei ai dati
657
static void SerialTest()
{
int[] data = new int[NUMELEMENTS];
int j = 0;
for (int i = 0; i < NUMELEMENTS; i++)
{
j = i;
doAdditionalProcessing();
data[i] = j;
doMoreAdditionalProcessing();
}
for (int i = 0; i < NUMELEMENTS; i++)
{
Console.WriteLine("Element {0} has value {1}", i, data[i]);
}
}
static void doAdditionalProcessing()
{
Thread.Sleep(10);
}
static void doMoreAdditionalProcessing()
{
Thread.Sleep(10);
}
}
Il metodo SerialTest popola una matrice di numeri interi con dei valori predefiniti (in un
modo piuttosto lungo), e scorre lungo la lista, stampando l'indice di ogni elemento presente
nella matrice assieme al valore che gli corrisponde. I metodi doAdditionalProcessing e
doMoreAdditionalProcessing simulano semplicemente delle operazioni lunghe come parte
dell'elaborazione in modo da poter causare al runtime l'eventuale perdita di controllo del
processore. L’output di questo programma viene visualizzato qui:
Element
Element
Element
Element
Element
Element
Element
Element
Element
Element
0
1
2
3
4
5
6
7
8
9
has
has
has
has
has
has
has
has
has
has
value
value
value
value
value
value
value
value
value
value
0
1
2
3
4
5
6
7
8
9
Ora considera il metodo ParallelTest mostrato qui: questo metodo è simile al SerialTest tranne per
il fatto che il costrutto Parallel.For popola la matrice di dati attraverso elaborazioni simultanee.
Il codice contenuto nell'espressione lambda eseguito da ogni elaborazione è identico all'iniziale
loop for nel metodo SerialTest.
658
Parte VI
Creazione di soluzioni professionali con Visual Studio 2010
using System.Threading.Tasks;
...
static void ParallelTest()
{
int[] data = new int[NUMELEMENTS];
int j = 0;
Parallel.For (0, NUMELEMENTS, (i) =>
{
j = i;
doAdditionalProcessing();
data[i] = j;
doMoreAdditionalProcessing();
});
for (int i = 0; i < NUMELEMENTS; i++)
{
Console.WriteLine("Element {0} has value {1}", i, data[i]);
}
}
L'idea è che il metodo ParallelTest esegua le stesse operazioni del metodo SerialTest, tranne per il
fatto di utilizzare elaborazioni simultanee e (magari) impiegando molto meno tempo. Il problema
è che non sempre funziona come ti aspetti. Qui sotto ti mostro alcuni esempi dell'output generato
dal metodo ParallelTest:
Element
Element
Element
Element
Element
Element
Element
Element
Element
Element
0
1
2
3
4
5
6
7
8
9
has
has
has
has
has
has
has
has
has
has
value
value
value
value
value
value
value
value
value
value
1
1
4
8
4
1
4
8
8
9
I valori assegnati a ogni elemento nella matrice di dati non sono sempre gli stessi generati dal
metodo SerialTest. Ti aggiungo anche che più si usa il metodo ParallelTest, maggiore è il numero
di risultati differenti che si ottengono.
Se esamini la logica del costrutto Paralell.For puoi scoprire dove risiede il problema. L'espressione
lambda contiene le seguenti dichiarazioni:
j = i;
doAdditionalProcessing();
data[i] = j;
doMoreAdditionalProcessing();
Capitolo 28
Esecuzione di accessi simultanei ai dati
659
Il codice sembra del tutto innocuo. Di fatto copia l'attuale valore della variabile i (la variabile
indice che identifica quale dei loop sta eseguendo) nella variabile j, per poi archiviare il valore di
j tra gli elementi della matrice di dati indicizzata da i. Se i contiene 5, allora a j viene assegnato
il valore 5 per poi archiviare il valore j in data[5]. Qui il problema è che tra l'assegnare il valore
a j e il leggerlo, il codice fa del lavoro in più; richiama il metodo doAdditionalProcessing. Se
l'esecuzione di questo metodo impiega troppo tempo, il runtime potrebbe sospendere il thread
e schedulare una nuova elaborazione. Se un'elaborazione simultanea è in esecuzione seguendo
il costrutto Parallel.For, questa potrebbe assegnare un nuovo valore a j. Di conseguenza,
quando l’applicazione iniziale ricomincia, il valore j che assegna a data[5] non è più il valore
archiviato, dando così luogo a dati corrotti. Ancora più problematico è il caso in cui il codice si
comporti come ci si aspetta e produca i risultati corretti, e altre volte invece non lo faccia; questo
dipende da quanto il computer è impegnato e da quante diverse elaborazioni sono schedulate.
Di conseguenza questo genere di bug possono rimanere dormienti durante la fase di test e
d'improvviso manifestarsi durante il normale ciclo di vita nel proprio ambiente.
La variabile j è condivisa da elaborazioni simultanee. Se un'elaborazione archivia un valore in j
per poi leggerlo, è necessario assicurarsi che nessun'altra elaborazione l'abbia modificato. Ciò
comporta la sincronizzazione degli accessi alla variabile tra tutte le elaborazioni che vi devono
accedere. Un modo per fare questo è di bloccare i dati.
Blocco dei dati
Il linguaggio C# fornisce semantica di blocco attraverso la parola chiave lock, che puoi usare per
garantire un accesso esclusivo alle risorse. Puoi utilizzare le parole chiave lock così:
object myLockObject = new object();
...
lock (myLockObject)
{
// Codice che richiede un accesso esclusivo a delle risorse condivise
...
}
La dichiarazione di lock cerca di ottenere una mutua esclusione su specifici oggetti (puoi di
fatto fare riferimento a qualsiasi cosa, non solo agli oggetti) e si blocca se lo stesso oggetto è al
momento bloccato da un altro thread. Quando il thread ottiene il blocco, il codice successivo alla
dichiarazione di blocco si avvia. Al suo termine il blocco viene rilasciato. Se c'è un altro thread è in
attesa questo può acquisire il blocco e quindi proseguire nella sua esecuzione.
Se un oggetto è bloccato, cosa dovrebbe fare un thread mentre aspetta che si sblocchi? Ci sono
almeno due possibili risposte. Il thread può essere messo in attesa fino a quando il blocco torna
accessibile. Questo prevede che il runtime lavori molto, incluso il salvare lo stato del thread e
l'accodarlo per l'esecuzione in un momento successivo, quando il blocco diventa accessibile. Se il
660
Parte VI
Creazione di soluzioni professionali con Visual Studio 2010
blocco è di breve durata, il sovraccarico dato dal sospendere, ri-pianificare e riprendere il thread
può superare il tempo necessario al blocco per essere di nuovo accessibile, dando così luogo a
una bassa performance dell'applicazione .
Una strategia alternativa è di fare in modo che il thread faccia dello spinning sul processore
(alla stessa maniera del metodo Thread.SpinWait che hai visto all'inizio del capitolo) fino a che il
blocco non viene rilasciato, a questo punto il thread può ottenere velocemente il blocco e quindi
continuare.
Questo meccanismo evita il sovraccarico dato dalla sospensione e la ripresa del thread; però se
l'attesa è lunga, quest'azione potrebbe occupare il processore consumandone risorse e facendo
del lavoro inutile, impattando anche sulla performance della tua applicazione.
Internamente la dichiarazione lock usa la classe System.Threading.Monitor per bloccare un
oggetto specifico. La classe Monitor segue un algoritmo intelligente che minimizza il potenziale
sovraccarico bloccando l'oggetto rimanendo in attesa che il blocco sia rilasciato.
Quando un thread richiama un blocco, se lo stesso oggetto è al momento bloccato da un altro
thread, il thread in attesa impegna con lo spinning il processore solo per un numero limitato di
volte. Se il blocco non viene rilasciato durante questo periodo, il thread viene messo in attesa e
sospeso.
Il thread viene ripreso solo al momento del rilascio del blocco. In questo modo brevi attese hanno
una risposta immediata mentre attese più lunghe non vanno a impattare la performance del
thread in maniera drastica.
La parola chiave lock va bene in molti scenari, ma ci sono situazioni in cui hai delle necessità
più complesse. La TPL include una serie aggiuntiva di primitivi di sincronizzazione che possono
tornare utili in queste situazioni. Di seguito ti riassumo alcuni di questi primitivi. I primitivi sono
stati progettati per operare non solo con la TPL ma anche in qualunque codice multi-thread. Si
trovano nello spazio dei nomi System.Threading.
Nota Sin dalla sua prima release NET.Framework include un buon numero di primitivi di
sincronizzazione. La prossima parte del capitolo ti descrive solo i primitivi aggiunti dalla TPL. Ci
sono delle sovrapposizioni tra i nuovi primitivi e i precedenti. Dove ci sono sovrapposizioni di
funzionalità, dovresti sempre usare i primitivi della TPL poiché sono stati progettati e ottimizzati
per computer con più CPU.
Una discussione dettagliata sulla teoria di possibili meccanismi di sincronizzazione disponibili per
creare applicazioni multi-thread è al di fuori dello scopo del libro. Per maggiori informazioni sulla
teoria generale della sincronizzazione di più thread leggete l'argomento trattato nella guida di
NET.Framework che parla della sincronizzazione di dati in multi-thread.
Capitolo 28
Esecuzione di accessi simultanei ai dati
661
Importante Non devi utilizzare mai i primitivi di sincronizzazione e di blocco come sostituti di
una buona pratica di progettazione o di programmazione. NET.Framework fornisce moltri altri
meccanismi che puoi usare per massimizzare il parallelismo riducendo contemporaneamente il
potenziale sovraccarico derivante dal blocco dei dati. Per esempio, se molte elaborazioni devono
aggiornare informazioni comuni all'interno di un insieme, puoi usare il thread local storage (TLS)
per trattenere i dati usati dagli altri thread che eseguono ogni elaborazione. Per creare una
variabile in TLS definisci la variabile come membro di una classe statica e poi mettere un prefisso
con l'attributo ThreadStatic, come ti mostro qui:
[ThreadStatic]
static Hashtable privateDataForThread;
Benché la variabile Hashtable viene dichiarata statica, non viene condivisa da tutte le istanze
della classe in cui essa è definita. Invece una copia della variabile statica è mantenuta nella TLS.
Se due elaborazioni simultanee accedono a questa variabile, riceveranno ognuna la propria copia
TLS. Quando le elaborazioni sono completate, possono ritornare la propria copia dei dati statici e
tu puoi così aggregarne i risultati. Maggiori dettagli sui TLS sono al di fuori dello scopo di questo
libro, ma per più informazioni consulta la documentazione fornita con Visual Studio 2010.
Primitivi di sincronizzazione nella TPL
Molti primitivi di sincronizzazione hanno la forma di un meccanismo di blocco che limita gli
accessi alla risorsa mentre un thread ne mantiene il blocco. La TPL supporta diversi tipi di
tecniche di blocco che puoi usare per implementare differenti stili di accesso simultanei, dal
semplice blocco esclusivo (dove un singolo thread ha un unico accesso alla risorsa) ai semafori
(dove più thread possono accedere alla risorsa simultaneamente; ma in maniera controllata) ai
blocchi lettura/scrittura che abilitano differenti thread a condividere una risorsa in sola lettura
garantendo, nel frattempo, un accesso esclusivo al thread che necessita di modificare la risorsa.
La classe ManualResetEventSlim
La classe ManualResetEventSlim fornisce funzionalità che permettono a uno o più thread di
attendere fino al verificarsi di un evento. Un oggetto ManualResetEventSlim può trovarsi in
uno di questi due stati: segnalato (true) e non-segnalato (false). Un thread crea un oggetto
ManualResetEventSlim e ne specifica lo stato iniziale. Altri thread possono aspettare che
l'oggetto ManualResetEventSlim venga segnalato attraverso il metodo Wait. Se l'oggetto
ManualResetEventSlim è nello stato di non segnalato, il metodo Wait blocca i thread. Un altro
thread può cambiare lo stato dell'oggetto ManualResetEventSlim richiamando il metodo Set.
Quest'azione rilascia tutti i thread in attesa dell'oggetto ManualResetEventSlim, che possono così
riprendere l'esecuzione. Il metodo Reset cambia lo stato dell'oggetto ManualResetEventSlim in
non-segnalato.
Quando un thread è in attesa di un evento, inizia a fare spinning. Comunque, se l'attesa eccede
il numero di cicli di spinning, il thread viene sospeso e rilascia il processore in maniera simile a
quella della classe Monitor, descritta prima. Puoi specificare il numero di cicli di spinning prima
662
Parte VI
Creazione di soluzioni professionali con Visual Studio 2010
che avvenga la sospensione del thread nel costruttore per un oggetto ManualResetEventSlim, e
puoi determinare quanti cicli di spinning devono essere fatti prima che il thread sia sospeso nelle
proprietà SpinCount. Ti puoi accertare dello stato dell'oggetto ManualResetEventSlim esaminando
le proprietà Booleane di IsSet.
Il prossimo esempio crea due elaborazioni che devono accedere alla variabile intera i. La prima
elaborazione usa un loop do/while che mostra continuamente il valore di i e lo incrementa
fino a raggiungere il valore 10. La seconda elaborazione attende fino a quando la prima
elaborazione finisce di aggiornare il valore di i e lo mostra. Le elaborazioni usano un oggetto
ManualResetEventSlim per coordinare le proprie azioni. L'oggetto ManualResetEventSlim viene
inizializzato con lo stato non-segnalato (false), e specifica che il thread che è in attesa di questo
oggetto deve fare 100 cicli di spinning prima di essere sospeso. La prima elaborazione segnala
l'oggetto ManualResetEventSlim al termine del loop. La seconda elaborazione attende che
l'oggetto ManualResetEventSlim venga segnalato. In questo modo la seconda elaborazione legge i
solo quando ha raggiunto il valore 10.
ManualResetEventSlim resetEvent = new ManualResetEventSlim(false, 100);
int i = 0;
Task t1 = Task.Factory.StartNew(() =>
{
do
{
Console.WriteLine("Task t1. i is {0}", i);
i++;
} while (i < 10);
resetEvent.Set();
});
Task t2 = Task.Factory.StartNew(() =>
{
resetEvent.Wait();
Console.WriteLine("Task t2. i is {0}", i);
});
Task.WaitAll(t1, t2);
L'out put di questo codice è simeile a questo:
Task
Task
Task
Task
Task
Task
Task
Task
Task
Task
Task
t1.
t1.
t1.
t1.
t1.
t1.
t1.
t1.
t1.
t1.
t2.
i
i
i
i
i
i
i
i
i
i
i
is
is
is
is
is
is
is
is
is
is
is
0
1
2
3
4
5
6
7
8
9
10
Capitolo 28
Esecuzione di accessi simultanei ai dati
663
La classe SemaphoreSlim
Puoi usare una classe SemaphoreSlim per controllare gli accessi a un pool di risorse. Un oggetto
SemaphoreSlim ha come valore iniziale (un numero intero non-negativo) e valore massimo
facoltativo. Il tipico valore iniziale di un oggetto SemaphoreSlim è il numero delle risorse nel pool.
I thread che accedono alle risorse del pool richiamano prima il metodo Wait. Questo metodo
cerca di diminuire il valore dell'oggetto SemaphoreSlim , e se il risultato è un non-zero allora
il thread può continuare e attingere alle risorse del pool. Quando ha finito, il thread dovrebbe
richiamare il metodo Release nell'oggetto SemaphoreSlim. Quest'azione incrementa il valore di
Semaphore.
Se un thread richiama il metodo Wait e il risultato del decremento del valore dell'oggetto
SemaphoreSlim è negativo, il thread aspetta fino a quando un altro thread richiama il metodo
Release. Il thread inizialmente parte con lo spinning, ma il thread viene sospeso se il tempo di
attesa è troppo lungo.
La classe SemaphoreSlim fornisce anche le proprietà CurrentCount che puoi usare per determinare
se un'operazione Wait è probabile che si verifichi immediatamente o se ci sarà un blocco.
L'esempio che segue ti mostra come creare un oggetto SemaphoreSlim che protegge un pool
di tre risorse condivise. Elaborazioni simultanee possono richiamare il metodo Wait prima di
accedere alle risorse di questo pool e possono richiamare il metodo Release quando hanno
terminato. In questo modo non più di tre elaborazioni possono usare una risorsa alla volta, la
quarta elaborazione viene bloccata fino a quando le prime tre non richiamano Release.
// Oggetto SemaphoreSlim condiviso da più thread
SemaphoreSlim semaphore = new SemaphoreSlim(3);
...
Task t1 = Task.Factory.StartNew(() =>
{
semaphore.Wait();
// Accedi alla risosrsa del pool
semaphore.Release();
});
Task t2 = Task.Factory.StartNew(() =>
{
semaphore.Wait();
// Accedi alla risosrsa del pool
semaphore.Release();
});
Task t3 = Task.Factory.StartNew(() =>
{
semaphore.Wait();
// Accedi alla risosrsa del pool
semaphore.Release();
});
664
Parte VI
Creazione di soluzioni professionali con Visual Studio 2010
Task t4 = Task.Factory.StartNew(() =>
{
// Questa elaborazione viene bloccata fino a che le tre precedenti chiamano Release
semaphore.Wait();
// Accedi alla risosrsa del pool
semaphore.Release();
});
Task.WaitAll(t1, t2, t3, t4);
La classe CountdownEvent
Puoi pensare alla classe CountdownEvent come un incrocio tra l'opposto di un semaforo e un
evento di reset manuale. Quando un thread crea un oggetto CountdownEvent ne specifica il valore
iniziale (un numero intero non negativo). Uno o più thread possono richiamare il metodo Wait
dell'oggetto CountdownEvent, e se il valore è non-zero i thread vengono bloccati. (Inizialmente
il thread inizia lo spinning e poi viene sospeso.) Wait non diminuisce il valore dell'oggetto
CountdownEvent, invece altri thread possono richiamare il metodo Signal per ridurne il valore.
Quando il valore dell'oggetto CountdownEvent raggiunge lo zero, tutti i thread bloccati vengono
segnalati e possono ripartire.
Un thread può impostare il valore dell'oggetto CountdownEvent a quello specificato dal
costruttore utilizzando il metodo Reset, e un thread può incrementare il valore richiamando il
metodo AddCount. Puoi verificare il valore di un oggetto CountdownEvent e determinare se una
chiamata al metodo Wait può bloccare, esaminando le proprietà CurrentCount. Il codice che
segue crea un oggetto CountdownEvent che deve essere segnalato cinque volte prima che i thread
bloccati nella sua attesa possano procedere. Il countDownWaitTask è in attesa dell'oggetto, e
l'elaborazione countDownSignalTask lo segnala cinque volte.
CountdownEvent countDown = new CountdownEvent(5);
Task countDownWaitTask = Task.Factory.StartNew(() =>
{
countDown.Wait();
Console.WriteLine("CountdownEvent has been signaled 5 times");
});
Task countDownSignalTask = Task.Factory.StartNew(() =>
{
for (int i = 0; i < 5; i++)
{
Console.WriteLine("Signaling CountdownEvent");
countDown.Signal();
}
});
Task.WaitAll(countDownWaitTask, countDownSignalTask);
Capitolo 28
Esecuzione di accessi simultanei ai dati
665
L’output di questo codice viene visualizzato qui:
Signaling CountdownEvent
Signaling CountdownEvent
Signaling CountdownEvent
Signaling CountdownEvent
Signaling CountdownEvent
CountdownEvent has been signaled 5 times
La classe ReaderWriterLockSlim
La classe ReaderWriterLockSlim è un primitivo di sincronizzazione avanzato che supporta una
singola scrittura e letture multiple. L'idea è che modificare (scrivere) all'interno di una risorsa
richieda un accesso esclusivo, ma per leggere una risorsa no; più letture possono essere fatte sulla
stessa risorsa allo stesso tempo.
Un thread che vuole leggere una risorsa richiama il metodo EnterReadLock di un oggetto
ReaderWriterLockSlim. Questa azione permette di prendere un blocco di lettura dell'oggetto.
Una volta che il thread ha terminato con la risorsa, richiama il metodo ExitReadLock, che rilascia
il blocco di lettura. Più thread possono leggere la stessa risorsa allo stesso tempo e ogni thread
ottine il suo blocco di lettura.
Se un thread vuole modificare la risorsa per ottenere il blocco di scrittura , richiama il metodo
EnterWriteLock dello stesso oggetto ReaderWriterLockSlim. Se uno o più thread hanno
simultaneamente un blocco di lettura per l'oggetto, il metodo EnterWriteLock si blocca fino a
quando non vengono tutti rilasciati. Il thread può poi modificare la risorsa e richiamare il metodo
ExitWriteLock per rilasciare il blocco di scrittura. Un oggetto ReaderWriterLockSlim ha solo un
blocco di scrittura. Se un altro thread tenta di ottenere il blocco di scrittura, viene bloccato fino a
quando il primo thread non lo rilascia.
Per assicurarsi che i thread di scrittura non vengano bloccati all'infinito, appena il thread richiede il
blocco di scrittura tutte le successive chiamate a EnterReadLock vengono bloccate fino a quando il
blocco di scrittura non viene acquisito e rilasciato.
Il meccanismo di blocco è simile a quello usato da altri primitivi qui descritti; fa si che il processore
vada in spinning per un numero di cicli prima di sospendere il thread se è ancora bloccato.
Il codice che segue crea un oggetto ReaderWriterLockSlim che protegge le risorse condivise e
crea tre elaborazioni. Due delle elaborazioni ottengono un blocco di lettura e la terza quello di
scrittura. Le due elaborazioni readerTask1 e readerTask2 possono accedere alle risorse condivise
contemporaneamente, ma l'elaborazione writerTask può accedere solo quando readerTask1 e
readerTask2 hanno entrambe rilasciato i blocchi.
666
Parte VI
Creazione di soluzioni professionali con Visual Studio 2010
ReaderWriterLockSlim readerWriterLock = new ReaderWriterLockSlim();
Task readerTask1 = Task.Factory.StartNew(() =>
{
readerWriterLock.EnterReadLock();
// Leggi la risorsa condivisa
readerWriterLock.ExitReadLock();
});
Task readerTask2 = Task.Factory.StartNew(() =>
{
readerWriterLock.EnterReadLock();
// Leggi la risorsa condivisa
readerWriterLock.ExitReadLock();
});
Task writerTask = Task.Factory.StartNew(() =>
{
readerWriterLock.EnterWriteLock();
// Scrivi la risorsa condivisa
readerWriterLock.ExitWriteLock();
});
Task.WaitAll(readerTask1, readerTask2, writerTask);
La classe Barrier
La classe Barrier ti permette di fermare temporaneamente l'esecuzione di una serie di thread in un
punto preciso in un'applicazione e riprendere solo quando tutti i thread hanno raggiunto questo
punto. Questa classe è utile quando devi sincronizzare thread che devono eseguire una serie di
operazioni concorrenti una dietro l'altra.
Quando un thread crea un oggetto Barrier, specifica il numero di thread che deve essere
sincronizzato. Puoi pensare a questo valore come un contatore di thread inserito all'interno della
classe Barrier. Successivamente, sei sempre in grado di modificare questo valore richiamando
i metodi AddParticipant e RemoveParticipant. Quando un thread raggiunge il punto di
sincronizzazione richiama il metodo SignalAndWait dell'oggetto Barrier e nel contempo va a
diminuire il valore del contatore all'interno dell'oggetto stesso. Se il contatore è maggiore di zero,
il thread viene bloccato. Solo quando il valore del contatore raggiunge lo zero i thread che sono
in attesa vengono rilasciati e solo allora posso riprendere la loro esecuzione.
La classe Barrier fornisce la proprietà ParticipantCount, che specifica il numero di thread che
sincronizza, e la proprietà ParticipantsRemaining, che indica quanti thread devono richiamare
SignalAndWait prima che la barriera venga alzata e i thread bloccati possano continuare la loro
esecuzione.
Capitolo 28
Esecuzione di accessi simultanei ai dati
667
All'interno del costruttore Barrier puoi anche definire un delegato. Questo delegato può riferirsi
a un metodo in esecuzione quando i tutti i thread hanno raggiunto la barriera. L'oggetto Barrier
è di fatto un parametro all'interno di questo metodo. La barriera non viene alzata e i thread non
vengono rilasciati fino a quando il metodo non è concluso.
L'esempio che segue crea un oggetto Barrier che sincronizza tre thread e fornisce un'espressione
lambda che viene eseguita quando Barrier viene segnalato. Le elaborazioni t1, t2 e t3 richiamano
tutte insieme alla barriera SignalAndWait. Le elaborazioni posso proseguire sono quando tutte e
tre hanno segnalato la barriera e il delegato specificato dall'oggetto Barrier è completo.
Barrier barrier = new Barrier(3, (x) =>
{
Console.WriteLine("All tasks have reached the barrier");
});
Task t1 = Task.Factory.StartNew(() =>
{
Console.WriteLine("Task t1 starting");
barrier.SignalAndWait();
Console.WriteLine("Task t1 continuing after the barrier");
});
Task t2 = Task.Factory.StartNew(() =>
{
Console.WriteLine("Task t2 starting");
barrier.SignalAndWait();
Console.WriteLine("Task t2 continuing after the barrier");
});
Task t3 = Task.Factory.StartNew(() =>
{
Console.WriteLine("Task t3 starting");
barrier.SignalAndWait();
Console.WriteLine("Task t3 continuing after the barrier");
});
Task.WaitAll(t1, t2, t3);
L'output di questo codice assomiglia a questo (l'ordine in cui le elaborazioni iniziano e riprendono
dopo che la barriera viene alzata può variare):
Task t1 starting
Task t2 starting
Task t3 starting
All tasks have reached the barrier
Task t3 continuing after the barrier
Task t1 continuing after the barrier
Task t2 continuing after the barrier
668
Parte VI
Creazione di soluzioni professionali con Visual Studio 2010
Annullamento e sincronizzazione di primitivi
Le classi ManualResetEventSlim, SemaphoreSlim, CountdownEvent e Barrier supportano tutte
l'annullamento secondo il modello descritto nel capitolo 27. Le operazioni di attesa per ognuna di
queste classi possono avere un parametro facoltativo CancellationToken, recuperato da un oggetto
CancellationTokenSource. Se richiami il metodo Cancel dell'oggetto CancellationTokenSource,
ogni operazione di attesa che si riferisce a un CancellationToken generato da questa risorsa viene
annullata con un'eccezione OperationCanceledException.
Nota Se l'operazione di attesa viene eseguita da un'elaborazione, OperationCanceledException
viene incapsulata all'interno di una AggregateException, come ti ho descritto nel capitolo 27.
Il codice che segue mostra come richiamare il metodo Wait di un oggetto SemaphoreSlim e
specificarne un token di annullamento. Se l'operazione di attesa viene annullata il gestore catch
OperationCanceledException si avvia.
CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
CancellationToken cancellationToken = cancellationTokenSource.Token;
...
// Semaforo che protegge un pool di 3 risorse
SemaphoreSlim semaphoreSlim = new SemaphoreSlim(3);
...
// Aspetta al semaforo, e prendi OperationCanceledException se
// un altro thread richiama Cancel su cancellationTokenSource
try
{
semaphoreSlim.Wait(cancellationToken);
}
catch (OperationCanceledException e)
{
...
}
Classi d'insieme concorrenti
Un requisito comune di molte applicazioni con thread multipli è quello di archiviare e recuperare
dati all'interno di un insieme. Le classi d'insieme standard fornite con NET.Framework non sono
thread-safe come default, anche se tu puoi comunque usare i primitivi di sincronizzazione descritti
in precedenza per incapsulare codice che aggiunge, fa query e rimuove elementi da un insieme.
Purtroppo questo metodo è soggetto a errori e non è neanche molto scalabile, ecco perchè la
libreria di classi di NET.Framework 4.0 include un piccolo gruppo di interfacce e classi d'insieme
thread-safe nello spazio dei nomi System.Collections.Concurrent progettato specificatamente per
essere usato all'interno della TPL. La tabella che segue riassume brevemente questi tipi.
Capitolo 28
Esecuzione di accessi simultanei ai dati
669
Classe
Descrizione
ConcurrentBag<T>
Questa è una classe generale che raccoglie in modo non
ordinato l'insieme degli elementi. Include i metodi per inserire
(Add), rimuovere (TryTake) e esaminare (TryPeek) elementi
nell'insieme. Questi metodi sono thread-safe. L'insieme è
anche enumerabile, quindi puoi iterare all'interno dei suoi
contenuti usando la dichiarazione foreach.
ConcurrentDictionary<TKey, TValue>
Questa classe implementa una versione thread-safe della
classe d'insieme generica Dictionary<TKey, TValue> descritta
nel capitolo 18. Fornisce i metodi TryAdd, ContainsKey,
TryGetValue, TryRemove, e TryUpdate, che puoi usare per
aggiungere query, rimuovere e modificare gli elementi del
dizionario.
ConcurrentQueue<T>
Questa classe fornisce una versione thread-safe della classe
generica Queue<T>, descritta nel capitolo 18. Include i metodi
Enqueue, TryDequeue, e TryPeek, che puoi utilizzare per
aggiungere, rimuovere e fare query su elementi in coda.
ConcurrentStack<T>
Questa è un'implementazione della classe generica Stack<T>
descritta nel capitolo 18. Fornisce metodi quali Push, TryPop, e
TryPeek,che puoi utilizzare per fare operazioni push. pop e di
query sugli elementi nello stack.
IProducerConsumerCollection<T>
Questa interfaccia definisce metodi per implementare classi
che mostrano comportamenti tipici produttore/consumatore.
Un produttore aggiunge elementi all'insieme e il consumatore
legge (e probabilmente rimuove) elementi dall'insieme stesso.
Spesso viene utilizzata per definire tipi che lavorano con
classe BlockingCollection<T>, che descrivo più avanti in questa
tabella.
L'interfaccia IProducerConsumerCollection definisce i metodi
per aggiungere elementi (TryAdd) a un insieme, rimuoverli
(TryTake) e ottenere un enumeratore (GetEnumerator)
che permette a un'applicazione di iterare in un insieme.
(L'interfaccia definisce anche altri metodi e proprietà.) Puoi
implementare questa interfaccia e creare classi d'insieme
concorrenti a tua misura. Le classi ConcurrentBag<T>,
ConcurrentQueue<T>, e ConcurrentStack<T> implementano
tutte questa interfaccia.
BlockingCollection<T>
Questa classe è utile nella creazione di applicazioni basate
su produttori e consumatori che accedono allo stesso
insieme. Il parametro type riferisce a un tipo che implementa
l'interfaccia IProducerConsumerCollection<T> e ne aggiunge
caratteristiche di blocco.
Riesce in questo perché agisce come un adattatore threadsafe; fornisce metodi come Add e Take che incapsulano
chiamate ai metodi TryAdd e TryTake nell'insieme sottostante
usando codice che crea e utilizza primitivi di sincronizzazione
descritti in precedenza. La classe BlockingCollection<T> può
anche limitare il numero di elementi mantenuti all'interno
dell'insieme sottostante.
670
Parte VI
Creazione di soluzioni professionali con Visual Studio 2010
Il codice che segue mostra come usare un oggetto BlockingCollection<T> per incapsulare
un'istanza di un tipo definito dall'utente chiamato MyCollection<T> che implementa l'interfaccia
IProducerConsumerCollection. Limita il numero di elementi nell'insieme sottostante a 1000.
class MyCollection<T> : IProducerConsumerCollection<T>
{
// Dettagli dell'implementazione non mostrati
...
}
...
// Crea un'istanza MyCollection<T>,
// e uniscila all'oggetto BlockingCollection<T>
MyCollection<int> intCollection = new MyCollection<int>();
BlockingCollection<int> collection = new BlockingCollection<int>(myCollection, 1000);
Nota Puoi creare anche un'istanza all'oggetto BlockingCollection<T> senza specificare una
classe d'insieme. In questo caso l'oggetto BlockingCollection<T> crea un oggetto interno
ConcurrentQueue<T>.
Rendere thread-safe i metodi all'interno di una classe d'insieme implica un sovraccarico nel
runtime, quindi queste classi non sono così veloci come quelle contenute nelle classi d'insieme
regolari. Devi assolutamente avere ben chiaro questo fatto quando decidi di mettere in parallelo
una serie di operazioni che richiedono accesso a un insieme condiviso.
Utilizzo di insiemi concorrenti e di un blocco per
implementare accessi ai dati in modo thread-safe
Nei prossimi esercizi implementerai un'applicazione che calcola PI usando un'approssimazione
geometrica. Inizialmente farai i calcoli con un solo thread, poi cambierai il codice al fine di fare i
calcoli usando elaborazioni in parallelo. In questo processo incontrerai alcune problematiche di
sincronizzazione di dati che risolverai usando una classe d'insieme concorrente e un blocco per
assicurati che le elaborazioni coordinino correttamente le proprie attività.
L'algoritmo che implementerai calcola PI in base a campionamenti matematici e statistici semplici.
Se disegni un cerchio con raggio r e attorno un quadrato i cui lati toccano il cerchio, la lunghezza
dei lati del quadrato è 2*r, come ti mostro nella figura che segue.
Capitolo 28
Esecuzione di accessi simultanei ai dati
671
L'area del quadrato, S, viene calcolata come vedi di seguito:
(2 * r) * (2 * r)
oppure
4*r*r
L'area del cerchio, C, viene calcolata come segue:
PI * r * r
Usando queste aree, puoi calcolare PI come vedi di seguito:
4*C/S
Il trucco è di definire il valore del rapporto C/S. Ecco dove si inserisce il campionamento statistico.
Per fare questo, genera una serie di punti casuali che rimangano all'interno del quadrato e conta
quanti di questi punti finisce all'interno del cerchio. Se hai generato un campione sufficientemente
casuale e grande, il rapporto tra i punti all'interno del cerchio e quelli all'interno del quadrato (e
quindi anche nel cerchio) si approssima al rapporto tra le aree delle due forme, C/S. Tutto quello
che devi fare e contarli.
Come puoi stabilire che un punto sia all'interno del cerchio? Per aiutarti a visualizzare la soluzione
disegna, su carta quadrettata, il quadrato il cui centro origina nel punto 0.0. Poi puoi generare
coppie di valori, o coordinate, che rimangano all'interno del range (-r, -r) e (+r, +r). Puoi quindi
determinare se una qualsiasi serie di coordinate (x,y) rientri nel cerchio applicando il teorema di
Pitagora che definisce la distanza d delle coordinate dall'origine.
672
Parte VI
Creazione di soluzioni professionali con Visual Studio 2010
Puoi calcolare d come la radice quadrata di ((x * x) + (y * y)). Se d è minore o uguale a r, il raggio
del cerchio, allora le coordinate (x,y) identificano un punto all'interno del cerchio, come ti mostro
nel seguente diagramma:
Puoi semplificare le cose generando solo coordinate che sono comprese nel quadrante in alto
a destra del grafico, in questo modo devi solo generare coppie di numeri tra 0 e r. Questo è
l’approccio che utilizzerai nei prossimi esercizi.
Nota Gli esercizi di questo capitolo sono pensati per essere fatti su un computer con processore
multi-core. Se hai una CPU con un singolo core non sarai in grado di osservare gli stessi effetti.
Inoltre, non dovresti avviare nessun altro programma o servizio tra gli esercizi perché i risultati ne
potrebbero risentire.
Calcolo del PI con un thread singolo
1. Avvia Microsoft Visual Studio 2010 se non è già in esecuzione.
2. Apri la soluzione CalculatePI che si trova nella cartella \Microsoft Press\Visual CSharp Step
By Step\Chapter 28\CalculatePI della cartella Documenti.
3. In Esplora soluzioni, fai doppio clic sul file Program.cs per visualizzarlo nella finestra
dell’editor di codice e testo.
Questa è un'applicazione console. La struttura di base dell'applicazione è già stata creata
per te.
Capitolo 28
Esecuzione di accessi simultanei ai dati
673
4. Scorri verso la fine del file e esamina il metodo Main. Sarà simile al seguente:
static void Main(string[] args)
{
double pi = SerialPI();
Console.WriteLine("Geometric approximation of PI calculated serially: {0}", pi);
Console.WriteLine();
pi = ParallelPI();
Console.WriteLine("Geometric approximation of PI calculated in parallel: {0}",
pi);
}
Questo codice richiama il metodo SerialPi, che calcolerà PI usando l'algoritmo geometrico
che ti ho descritto prima. Il valore viene restituito come double e viene mostrato. Il codice
poi richiama il metodo ParallelPi, che farà lo stesso calcolo ma usando elaborazioni
concorrenziali. Il risultato mostrato dovrebbe essere uguale a quello restituito dal metodoS
erialPI.
5. Esamina il metodo SerialPI.
static double SerialPI()
{
List<double> pointsList = new List<double>();
Random random = new Random(SEED);
int numPointsInCircle = 0;
Stopwatch timer = new Stopwatch();
timer.Start();
try
{
// TO DO: Implement the geometric approximation of PI
return 0;
}
finally
{
long milliseconds = timer.ElapsedMilliseconds;
Console.WriteLine("SerialPI complete: Duration: {0} ms", milliseconds);
Console.WriteLine("Points in pointsList: {0}. Points within circle: {1}",
pointsList.Count, numPointsInCircle);
}
}
Questo metodo genererà una grande quantità di serie di coordinate e calcola la distanza di
ogni serie di coordinate dell'origine. La numerosità della serie viene definita nella costante
NUMPOINTS all'inizio della classe Program. Maggiore è il valore, più numerosa è la serie
di coordinate e più accurato è il valore di PI calcolato in questo modo. Se hai memoria
sufficiente, puoi aumentare il valore di NUMPOINTS. Alla stessa maniera, se vedi che
l'applicazione, mentre gira, produce eccezioni OutOfMemoryException, poi ridurre il suo
valore.
674
Parte VI
Creazione di soluzioni professionali con Visual Studio 2010
La distanza dall'origine di ogni punto viene archiviata nell'insieme pointsList List<double>. I
dati delle coordinate vengono generati usando una variabile random. Questo è un oggetto
Random, dotato di una costante che genera la stessa serie di numeri random ogni volta che
avvii il programma. (Questo ti aiuta a capire se sta girando correttamente.) Puoi cambiare
la costante SEED all'inizio della classe Program se intendi fornire al generatore di numeri
casuali un altro valore.
Puoi usare la variabile numPointsInCircle per contare il numero di punti nell'insieme
pointsList che si trova all'estremità del cerchio. Il raggio del cerchio è specificato dalla
costante RADIUS all'inizio della classe Program.
Per aiutarti a paragonare le performance di questo metodo e del metodo ParallePI, il codice
crea una variabile Stopwatch chiamata timer e l'avvia. Il blocco finally determina quanto
tempo ci è voluto per fare il calcolo e mostrarne i risultati.
Per ragioni che ti descriverò più avanti, il blocco finally mostra anche il numero di elementi
nell'insieme pointsList e il numero di punti che ha trovato all'interno del cerchio.
Nei prossimi passi inserirai il codice che in realtà fa il calcolo con il blocco try.
6. Nel blocco try, cancella il commento e rimuovi la dichiarazione return. (Questa dichiarazione
è stata inserita solo per verificare che il codice sia corretto.) Aggiungi al blocco try il blocco
for e le istruzioni visualizzate di seguito in grassetto :
try
{
for (int points = 0; points < NUMPOINTS; points++)
{
int xCoord = random.Next(RADIUS);
int yCoord = random.Next(RADIUS);
double distanceFromOrigin = Math.Sqrt(xCoord * xCoord + yCoord * yCoord);
pointsList.Add(distanceFromOrigin);
doAdditionalProcessing();
}
}
Questo parte di codice genera una coppia di valori di coordinate che rientra nel range
0-RADIUS e le archivia nelle variabili xCoord e yCoord. Il codice poi usa il teorema di
Pitagora per calcolare la distanza tra queste coordinate e l'origine per poi aggiungere i
risultati all'insieme pointsList.
Nota In questo esempio di codice c'è davvero poco lavoro di calcolo, nel mondo reale,
soprattutto quello scientifico, è molto più probabile che siano inclusi calcoli ben più complessi che terranno il processore occupato più a lungo. Per simulare una situazione del
genere, questo blocco di codice richiama un altro metodo, doAdditionalProcessing. Tutto
questo metodo non fa altro che occupare un numero di cicli di CPU, come vedi nell'esempio di codice che segue. Ho scelto di seguire questo approccio per mostrarti meglio i
requisiti dei dati di sincronizzazione per elaborazioni multiple piuttosto che scrivere un'applicazione che faccia calcoli complessi come un Fast Fourier Transform per tenere la CPU
occupata:
Capitolo 28
Esecuzione di accessi simultanei ai dati
675
private static void doAdditionalProcessing()
{
Thread.SpinWait(SPINWAITS);
}
SPINWAITS è un'altra costante definita all'inizio della classe Program.
7. Nel metodo SerialPI, nel blocco try aggiungi le dichiarazioni foreach che ti mostro in
grassetto dopo il blocco for:
try
{
for (int points = 0; points < NUMPOINTS; points++)
{
...
}
foreach (double datum in pointsList)
{
if (datum <= RADIUS)
{
numPointsInCircle++;
}
}
}
Questo codice itera lungo l'insieme pointsList ed esamina a turno ogni valore. Se il valore è
minore o uguale al raggio del cerchio, incrementa la variabile numPointsInCircle. Al termine
del loop, numPointsInCircle dovrebbe contenere il numero totale di coordinate che sono
state trovate all'interno dei bordi del cerchio
8. Aggiungi le istruzioni qui mostrate in grassetto al blocco try dopo l’istruzione foreach:
try
{
for (int points = 0; points < NUMPOINTS; points++)
{
...
}
foreach (double datum in pointsList)
{
...
}
double pi = 4.0 * numPointsInCircle / NUMPOINTS;
return pi;
}
Questa dichiarazione calcola PI in base al rapporto tra il numero di punti all'interno del
cerchio e il numero totale dei puni, usando la forma che ti ho spiegato prima. Il valore che
viene restituito è il risultato del metodo.
9. Nel menu Debug, fai clic su Avvia senza eseguire debug.
676
Parte VI
Creazione di soluzioni professionali con Visual Studio 2010
Il programma parte e mostra la sua approssimazione a PI come vedi nell'immagine qui di
seguito. (Ci sono voluti pochi secondi con il mio computer, quindi preparati ad aspettare
un pochino.) Anche il tempo impiegato per il calcolo ti viene mostrato. (Puoi ignorare il
risultato del metodo ParallelPI perché non hai ancora scritto il codice per questo metodo.)
Nota A parte il tempo impiegato, i tuoi risultati dovrebbero essere uguali a meno che tu
non abbia cambiato le costanti RADIUS, SEED o NUMPOINTS.
10. Chiudi la finestra della console e torna a Visual Studio.
Nel metodo SerialPI è evidente che il codice nel loop for,che genera i punti e calcola la distanza
dall'origine, può essere tranquillamente messo in parallelo. Questo è l’argomento dell’esercizio
che segue.
Calcolo del PI con elaborazioni parallele
1. In Esplora soluzioni, fai doppio clic sul file Program.cs per visualizzare il file nella finestra
dell’editor di codice e testo.
2. Individua il metodo ParallelPI. Questo contiene esattamente lo stesso codice della versione
iniziale del metodo SerialPI prima di aver aggiunto il codice al blocco try per calcolare PI.
3. Nel blocco try, cancella il commento e rimuovi la dichiarazione return. Aggiungi al blocco
try la dichiarazione Parallel.For illustrata di seguito in grassetto.
try
{
Parallel.For (0, NUMPOINTS, (x) =>
{
int xCoord = random.Next(RADIUS);
int yCoord = random.Next(RADIUS);
double distanceFromOrigin = Math.Sqrt(xCoord * xCoord + yCoord * yCoord);
Capitolo 28
Esecuzione di accessi simultanei ai dati
677
pointsList.Add(distanceFromOrigin);
doAdditionalProcessing();
});
}
Questa istruzione è il parallelo analogo del codice del loop for del metodo SerialPI. Il corpo
del loop originario for è incapsulato in un'espressione lambda.
4. Aggiungi il seguente codice mostrato in grassetto al blocco try dopo la dichiarazione
Parallel.For: Questo codice è esattamente uguale alle dichiarazioni contenute nel metodo
SerialPI.
try
{
Parallel.For (...
);
foreach (double datum in pointsList)
{
if (datum <= RADIUS)
{
numPointsInCircle++;
}
}
double pi = 4.0 * numPointsInCircle / NUMPOINTS;
return pi;
}
5. Nel menu Debug, fai clic su Avvia senza eseguire debug.
Il programma viene avviato. L'immagine che segue mostra un output tipico:
Il valore calcolato dal metodo SerialPI dovrebbe essere uguale a quello di prima. In qualche
misura, però, il risultato del metodo ParallelPI sembra sospetto. Il generatore di numeri
casuali è alimentato con lo stesso valore usato nel metodo SerialPI, quindi dovrebbe
produrre la stessa sequenza di numeri casuali con lo stesso risultato e lo stesso numero di
punti compresi nel cerchio.
678
Parte VI
Creazione di soluzioni professionali con Visual Studio 2010
Un'altra cosa curiosa è che l'insieme pointsList nel metodo ParallelPI sembra contenere un
numero inferiore di punti paragonato all'insieme del metodo SerialPI.
Nota Se l'insieme pointsList contiene veramente il numero previsto di elementi, fai girare
l'applicazione di nuovo. Di fatto dovresti vedere che contiene un numero inferiore di elementi da quello atteso in quasi tutte le volte che fai girare l'applicazione.
6. Chiudi la finestra della console e torna a Visual Studio.
Allora cos'è andato storto nel calcolo in parallelo? Un buon punto di partenza è il numero di
elementi all'interno dell'insieme pointsList. Questo insieme è un oggetto generico List<double>.
Tuttavia, questo tipo non è thread-safe. Il codice nella dichiarazione Parallel.For richiama il
metodo Add per aggiungere il valore all'insieme, ma ricordati che questo codice viene eseguito da
elaborazioni che lavorano come thread concorrenti. Di conseguenza, dato un numero di elementi
aggiunti all'insieme, è altamente probabile che alcune delle chiamate a Add interferiscano tra
di loro causando quindi una corruzione. Una soluzione è quella di usare uno degli insiemi dallo
spazio dei nomi System.Collections.Concurrent visto che si tratta di insiemi thread-safe. La classe
generica ConcurrentBag<T> nello spazio dei nomi è probabilmente l'insieme più adatto per
l'esempio appena fatto.
Utilizzo di un insieme thread-safe
1. In Esplora soluzioni, fai doppio clic sul file Program.cs per visualizzare il file nella finestra
dell’editor di codice e testo.
2. Individua il metodo ParallelPI. All'inizio di questo metodo sostituisci la dichiarazione
che crea un'istanza nell'insieme List<double> con il codice che crea un insieme
ConcurrentBag<double>, come ti mostro qui in grassetto:
static double ParallelPI()
{
ConcurrentBag<double> pointsList = new ConcurrentBag <double>();
Random random = ...;
...
}
3. Nel menu Debug, fai clic su Avvia senza eseguire debug.
Il programma si avvia e mostra la sua approssimazione a PI usando i metodi SerialPI e
ParallelPI. L'immagine che segue mostra il tipico output.
Capitolo 28
Esecuzione di accessi simultanei ai dati
679
Questa volta, l'insieme pointsList nel metodo ParallelPI contiene il numero corretto di punti,
ma il numero di punti all'interno del cerchio appare ancora troppo alto; dovrebbe essere lo
stesso di quello riportato dal metodo SerialPI.
Dovresti anche vedere che il tempo impiegato dal metodo ParallelPI è aumentato in
maniera significativa. Questo accade perché i metodi nella classe ConcurrentBag<T>
devono bloccare e sbloccare i dati al fine di garantire la thread-safety, e questo processo si
aggiunge al sovraccarico causato dal richiamo di questi metodi. Devi tenere a mente tutto
questo quando consideri se è appropriato mettere in parallelo un'operazione.
4. Chiudi la finestra della console e torna a Visual Studio.
Ora hai il numero corretto di punti nell'insieme pointsList, ma i valori di questi punti sembra
sospetto. Il codice nell'istruzione Parallel.For richiama il metodo Next di un oggetto Random, ma
come i metodi nella classe generica List<T>, questo metodo non è thread-safe.
Purtroppo non c'è una versione concorrente della classe Random, quindi dovrai ingegnarti a
usare tecniche alternative per serializzare le chiamate al metodo Next. Poiché ogni chiamata è
relativamente breve, ha senso usare un blocco che controlli le chiamate a questo metodo.
Utilizzo di un blocco per serializzare le chiamate al metodo
1. In Esplora soluzioni, fai doppio clic sul file Program.cs per visualizzare il file nella finestra
dell’editor di codice e testo.
2. Individua il metodo ParallelPI. Modifica il codice dell'espressione lambda nella dichiarazione
Parallel.For al fine di proteggere le chiamate a random.Next attraverso la dichiarazione lock.
Specifica l'insieme pointsList, come il soggetto blocco come ti mostro in grassetto di seguito:
static double ParallelPI()
{
...
Parallel.For(0, NUMPOINTS, (x) =>
{
int xCoord;
int yCoord;
680
Parte VI
Creazione di soluzioni professionali con Visual Studio 2010
lock(pointsList)
{
xCoord = random.Next(RADIUS);
yCoord = random.Next(RADIUS);
}
double distanceFromOrigin = Math.Sqrt(xCoord * xCoord + yCoord * yCoord);
pointsList.Add(distanceFromOrigin);
doAdditionalProcessing();
});
...
}
3. Nel menu Debug, fai clic su Avvia senza eseguire debug.
Questa volta i valori di PI calcolati dai metodi SerialPI e ParallelPI sono uguali. L'unica
differenza è che il metodo ParallelPI è più veloce.
4. Chiudi la finestra della console e torna a Visual Studio.
In questo capitolo hai imparato qualcosa su PLINQ e come puoi usare le estensioni del metodo
AsParallel per mettere in parallelo alcune query LINQ. Tuttavia, PLINQ è una materia a se stante e
questo capitolo ti ha mostrato solamente come iniziare. Per ulteriori informazioni sul "Paralle LINQ
(PLINQ)", consulta la documentazione fornita con Visual Studio.
Questo capitolo ti ha anche mostrato come sincronizzare l'accesso ai dati in elaborazioni
concorrenti usando i primitivi di sincronizzazione forniti nella TPL. Hai anche visto come usare
insieme di classi concorrenti per mantenere gli insiemi di dati in modo corretto (thread-safe).
■ Se desideri continuare con il capitolo successivo
Continua a eseguire Visual Studio 2010 e passa al capitolo 29.
■ Se desideri uscire ora da Visual Studio 2010
Nel menu File, fai clic su Esci. Se sullo schermo è visualizzata una finestra di dialogo Salva, fai
clic su Sì per salvare il progetto.
Capitolo 28
Esecuzione di accessi simultanei ai dati
681
Riferimenti rapidi del capitolo 28
Obiettivo
Azione
Mettere in parallelo una query
LINQ
Specifica nella query l'estensione del metodo AsParallel con la sorgente dati.
Ad esempio:
var over100 = from n in numbers.AsParallel()
where ...
select n;
Abilitare l'annullamento di una
query PLINQ
Usa il metodo WithCancellation della classe ParallelQuery all'interno della
query PLINQ, e specifica un token di annullamento. Ad esempio:
CancellationToken tok = ...;
...
var orderInfoQuery =
from c in CustomersInMemory.Customers.AsParallel().
WithCancellation(tok)
join o in OrdersInMemory.Orders.AsParallel()
on ...
Sincronizzare una o più
elaborazioni per implementare
un accesso esclusivo e threadsafe a dati condivisi
Usa la dichiarazione lock per garantire l'accesso esclusivo ai dati. Ad esempio:
Sincronizzare thread e fare in
modo che si mettano in attesa
di un evento
Usa un oggetto ManualResetEventSlim per sincrnizzare un numero
indeterminato di thread.
object myLockObject = new object();
...
lock (myLockObject)
{
// Codice che richiede un accesso esclusivo a delle risorse
condivise
...
}
Usa un oggetto CountdownEvent per aspettare che un evento venga
segnalato un numero preciso di volte.
Usa un oggetto Barrier per coordinare un numero specifico di thread e
sincronizzarli in punto particolare dell'operazione.
Sincronizzare l'accesso a un
pool condiviso di risorse
Usa un oggetto SemaphoreSlim. Specifica nel costruttore il numero di
elementi all'interno del pool. Richiama il metodo Wait prima di effettuare
l'accesso a una risorsa del pool condiviso. Richiama il metodo Release quando
hai finito con la risorsa. Ad esempio:
SemaphoreSlim semaphore = new SemaphoreSlim(3);
...
semaphore.Wait();
// Accedi alla risorsa del pool
...
semaphore.Release();
682
Parte VI
Creazione di soluzioni professionali con Visual Studio 2010
Obiettivo
Azione
Fornire un accesso esclusivo in
scrittura a una risorsa, ma un
accesso condiviso in lettura
Usa un oggetto ReaderWriterLockSlim. Prima di leggere la risorsa condivisa,
richiama il metodo EnterReadLock . Richiama il metodo ExitReadLock quando
hai terminato. Prima di scrivere una risorsa condivisa, richiama il metodo
EnterWriteLock. Richiama il metodo ExitWriteLock quando hai completato le
operazioni di scrittura. Ad esempio:
ReaderWriterLockSlim readerWriterLock = new ReaderWriterLockSlim();
Task readerTask = Task.Factory.StartNew(() =>
{
readerWriterLock.EnterReadLock();
// Leggi la risorsa condivisa
readerWriterLock.ExitReadLock();
});
Task writerTask = Task.Factory.StartNew(() =>
{
readerWriterLock.EnterWriteLock();
// Scrivi la risorsa condivisa
readerWriterLock.ExitWriteLock();
});
Annullare un'operazione di
blocco-attesa
Crea un token di annullamento da un oggetto CancellationTokenSource
e specifica che il token è il parametro per l'operazione di attesa. Per
annullare l'operazione di attesa, richiama il metodo Cancel dell'oggetto
CancellationTokenSource. Ad esempio:
CancellationTokenSource cancellationTokenSource = new
CancellationTokenSource();
CancellationToken cancellationToken = cancellationTokenSource.Token;
...
// Semaforo che protegge un pool di 3 riosrse
SemaphoreSlim semaphoreSlim = new SemaphoreSlim(3);
...
// Aspetta al semaforo, e avvia OperationCanceledException se
// un altro thread richiama cancellationTokenSource
semaphore.Wait(cancellationToken);
Creare un oggetto d'insieme
thread-safe
A seconda della funzionalità richiesta dall'insieme, usa una
delle classi all'interno dello spazio dei nomi System.Collections.
Concurrent (ConcurrentBag<T>, ConcurrentDictionary<TKey, TValue>,
ConcurrentQueue<T>, o ConcurrentStack<T>) oppure crea la tua classe
che implementa l'interfaccia IProducerConsumerCollection<T> e incapsula
un'istanza di questo tipo in un oggetto BlockingCollection<T>. Ad esempio:
class MyCollection<T> : IProducerConsumerCollection<T>
{
// Dettagli dell'implementazione non mostrati
...
}
...
// Crea un'istanza MyCollection<T>,
// e incapsulala all'oggetto BlockingCollection<T>
MyCollection<int> intCollection = new MyCollection<int>();
BlockingCollection<int> collection = new BlockingCollection<int>(myCo
llection, 1000);
Capitolo 29
Creazione e utilizzo di un servizio
Web
Gli argomenti trattati in questo capitolo sono:
■
Creazione di servizi Web SOAP e REST che espongono metodi Web semplici.
■
Visualizzazione della descrizione di un servizio Web SOAP utilizzando Internet Explorer.
■
Progettazione di classi che possono essere passate come parametri a un metodo Web e
restituite da un metodo Web.
■
Creazione di proxy per servizi Web REST e SOAP in un'applicazione client.
■
Richiamare un metodo Web REST con Internet Explorer
■
Richiamare metodi Web da un'applicazione client.
I capitoli precedenti ti hanno dimostrato come creare applicazioni desktop che possono
svolgere una serie di operazioni. Tuttavia, al giorno d'oggi, sono veramente pochi i sistemi
che lavorano in "isolamento". Le organizzazioni svolgono il loro business e quindi hanno delle
applicazioni a supporto dell'organizzazione. Uno dei sempre più pressanti requisiti, è creare
nuove soluzioni che possano riutilizzare le funzionalità esistenti in modo da proteggere gli
investimenti che l'organizzazione ha fatto nello sviluppo o nell'acquisto del software. Il software
in questione potrebbe essere sviluppato attraverso l'utilizzo di molte tecnologie e linguaggi di
programmazione, ed essere eseguito su diversi computer connessi in rete locale o geografica. In
più, l'organizzazione potrebbe aver scelto diverse soluzioni che incorporano vari servizi di terze
parti. La sfida è definire come mettere insieme questa eterogeneità di codici in modo che possano
comunicare e cooperare come se fossero un tutt'uno.
I servizi Web offrono una soluzione. Utilizzando i servizi Web, potrai generare sistemi distribuiti
a partire dagli elementi distribuiti in Internet: database, servizi business e altro. I componenti e
servizi risiedono su un server Web che riceve richieste da una applicazione client, le analizza e
inoltra il giusto comando al componente o servizio. La risposta viene quindi reinidirizzata dal
server Web all'applicazione client.
In questo capitolo troverai una descrizione dettagliata su come progettare, creare e testare servizi
Web accessibili da Internet e integrabili nelle applicazioni distribuite. Inoltre imparerai come
strutturare un’applicazione client che usa i metodi esposti da un servizio Web.
683
684
Parte VI
Creazione di soluzioni professionali con Visual Studio 2010
Nota Lo scopo di questo capitolo è fornire un’introduzione di base ai servizi Web e alle appli-
cazioni di Microsoft WCF (Windows Communication Foundation). Se desideri avere informazioni
dettagliate sul funzionamento delle applicazioni WCF e su come creare servizi sicuri utilizzandole,
è opportuno che tu consulti un testo come Microsoft Windows Communication Foundation Step
by Step, pubblicato da Microsoft Press, 2007.
Che cos’è un servizio Web?
Un servizio Web è un componente business che fornisce alcune utili funzioni ai client o consumer.
Si può pensare a un servizio Web come a un componente con accessibilità veramente globale: chi
ha i diritti di accesso appropriati può infatti utilizzare un servizio Web ovunque nel mondo, basta
avere un computer connesso a Internet. I servizi Web utilizzano un protocollo standard accettato
e molto conosciuto: HTTP (Hypertext Transfer Protocol) per trasmettere i dati e un formato di
dati portatile che si basa sul linguaggio XML. Sia HTTP sia XML sono tecnologie standardizzate
che possono essere utilizzate da altri ambienti di programmazione esterni a Microsoft .NET
Framework. Con Visual Studio 2010, puoi creare nuovi servizi Web utilizzando Microsoft Visual
C++, Microsoft Visual C# o Microsoft Visual Basic. Tuttavia, per ciò che concerne esclusivamente
le applicazioni client, non è importante il linguaggio utilizzato per creare il servizio Web e persino
la modalità con cui questo servizio esegue le proprie attività. Le applicazioni client che girano
in ambienti totalmente differenti, come Java, possono utilizzare tali servizi. Ed è anche vero il
contrario: puoi creare i servizi Web utilizzando il linguaggio Java e scrivere le applicazioni client in
C#.
Il ruolo di Windows Communication Foundation
Windows Communication Foundation, o WCF, nasce come parte della versione 3.0 di .NET
Framework. Visual Studio ha una serie di modelli che puoi utilizzare per creare servizi Web usando
WFC. In secondo luogo, i servizi Web sono l’unica tecnologia che è possibile utilizzare per creare
applicazioni distribuite per i sistemi operativi di Windows. Gli altri includono Enterprise Services,
.NET Framework Remoting e Microsoft Message Queue (MSMQ). Se si crea un’applicazione
distribuita per Windows, quale tecnologia dovresti utilizzare e che difficoltà avresti per cambiarla
in un secondo tempo, se fosse necessario? Lo scopo di WCF consiste nel fornire un modello di
programmazione unificato per molte di queste tecnologie, in modo da poter creare applicazioni
il più possibile indipendenti dal meccanismo di base utilizzato per connettere i servizi e le
applicazioni. (Nota che WCF si applica tanto ai servizi che operano in ambienti non-Web quanto
al servizio World Wide Web.) È molto difficile, se non impossibile, separare completamente
la struttura di programmazione di un’applicazione o di un servizio dalla sua infrastruttura di
comunicazione, ma WCF si avvicina di molto a questo obiettivo.
Per riepilogare, se stai pensando di creare delle applicazioni e dei servizi distribuiti per Windows è
consigliabile che tu utilizzi WCF. Gli esercizi di questo capitolo ti mostreranno come procedere.
Capitolo 29
Creazione e utilizzo di un servizio Web
685
Architetture dei servizi WEB
Ci sono due architetture comuni che le organizzazioni utilizzano per implementare servizi
WEB: servizi basati su SOAP (Simple Object Access Protocol) e servizi basati sul modello REST
(Representational State Transfer). Ambedue le architetture fanno affidamento sul protocollo HTTP
e lo schema di indirizzamento utilizzato in Internet. Il punto è che vengono usati in modo diverso
dal solito. Se stai sviluppando soluzioni che incorporano servizi Web in hosting presso differenti
organizzazioni, tieni conto che tali organizzazioni potrebbero implementare questi servizi Web
in uno dei due modi descritti. È importante quindi comprenderli entrambi. La seguente sezione
descrive brevemente le due architetture.
Servizi Web SOAP
Un servizio Web SOAP espone la propria funzionalità attraverso un modello procedurale
trazionale; la differenza principale dalle applicazioni desktop ordinarie è che le procedure
vanno in remoto direttamente sul server Web. L’applicazione client visualizza un servizio Web
come un’interfaccia in cui sono esposti un certo numero di metodi noti, definiti metodi Web.
L’applicazione client non deve fare altro che richiamare questi metodi Web utilizzando i protocolli
Internet standard, passando i parametri in un formato XML e ricevendo le risposte in questo
stesso formato. I metodi Web SOAP possono interrogare e modificare i dati.
Il ruolo di SOAP
Il protocollo SOAP (Simple Object Access Protocol) viene utilizzato dalle applicazioni client per
inviare richieste e ricevere risposte dai servizi Web. SOAP è costruito su HTTP. SOAP definisce una
grammatica XML con cui specificare i nomi dei metodi Web che un consumer può richiamare in
un servizio Web per definire i parametri e restituire i valori e per descrivere i tipi di parametri e
valori restituiti. Un client che intende richiamare un servizio Web è tenuto a specificare il metodo
e i parametri utilizzando questa grammatica XML.
Il protocollo SOAP è uno standard di settore. Ha lo scopo di migliorare l’interoperabilità tra le
piattaforme. La forza di SOAP sta nella sua semplicità e nel fatto che si basa su altre tecnologie
standard come HTTP e XML. La specifica SOAP definisce diversi aspetti. Di seguito sono elencati i
più importanti:
■ Il formato di un messaggio SOAP
■ La modalità di codifica dei dati
■ La modalità di invio dei messaggi
■ La modalità di risposta del processo
686
Parte VI
Creazione di soluzioni professionali con Visual Studio 2010
Lo scopo di questo libro non è descrivere nel dettaglio come funziona SOAP e il formato interno
di un messaggio SOAP. Le probabilità che in futuro tu debba creare e formattare i messaggi SOAP
manualmente sono davvero scarse perché molti strumenti di sviluppo, tra cui Visual Studio 2010,
automatizzano questo processo presentando un’API di semplice programmazione agli sviluppatori
che creano i servizi Web e le applicazioni client.
Che cos’è Web Services Description Language?
Il corpo di un messaggio SOAP è un documento XML. Quando un’applicazione client richiama
un metodo Web, il server Web si aspetta che il client utilizzi un insieme particolare di tag
per codificare i parametri per il metodo. Come fa il client a sapere quali tag, o schema XML,
utilizzare? La risposta è che, quando richiesto, un servizio Web deve essere in grado di fornire
una descrizione di sé stesso. La risposta del servizio Web è un altro documento XML contenente
la descrizione del servizio Web. Non deve quindi sorprendere che questo documento sia
noto come Web Service Description. Lo schema XML utilizzato per questo documento è stato
standardizzato e viene definito WSDL (Web Services Description Language). Questa descrizione
fornisce un volume di informazioni tali da consentire a un’applicazione client di creare una
richiesta SOAP in un formato comprensibile al server Web. Ancora, questo libro non prevede la
trattazione dettagliata di WSDL, ma Visual Studio 2010 contiene degli strumenti che analizzano
meccanicamente WSDL per un servizio Web. Visual Studio 2010 utilizza poi le informazioni per
definire una classe proxy utilizzabile dall’applicazione client per convertire le chiamate di metodo
ordinarie di questa classe proxy in richieste SOAP che il proxy invia attraverso il Web. Questo è
l’approccio che utilizzerai negli esercizi di questo capitolo.
Requisiti non funzionali dei servizi Web
I servizi Web e gli standard ad essi associati avevano inizialmente lo scopo di affrontare gli aspetti
funzionali dell’invio e della ricezione dei messaggi SOAP. Dopo che la tecnologia dei servizi Web
ha iniziato ad essere impiegata su larga scala per l’integrazione dei servizi distribuiti, è risultato
chiaro che c’erano problematiche che i soli protocolli SOAP e HTTP non erano in grado di
risolvere. Tali aspetti riguardano molti requisiti non funzionali che ricoprono un ruolo importante
in qualsiasi ambiente distribuito, ma che sono indispensabili quando si utilizza Internet come base
per una soluzione distribuita. Tali aspetti includono:
■ Sicurezza Come accertarsi che i messaggi SOAP che transitano tra un servizio Web e
un consumer non vengano intercettati e modificati mentre attraversano Internet? Come
verificare che un messaggio SOAP venga effettivamente inviato dal consumer o dal servizio
Web che afferma di aver eseguito l’invio e non da un sito di spoofing che tenta di ottenere
informazioni in modo ingannevole? Come concedere l’accesso a un servizio Web solo a
utenti specifici? Queste domande riguardano l’integrità, la riservatezza e l’autenticazione dei
messaggi e rappresentano temi di fondamentale importanza quando si creano applicazioni
distribuite che utilizzano Internet.
Capitolo 29
Creazione e utilizzo di un servizio Web
687
Agli inizi del 1990, diversi rivenditori che fornivano strumenti per la creazione di sistemi
distribuiti hanno dato vita a un'organizzazione che successivamente si è affermata con il
nome di Organization for the Advancement of Structured Information Standards o OASIS.
Mano a mano che si evidenziavano le carenze della prima infrastruttura di servizi Web, i
membri di OASIS hanno valutato tali aspetti (e altri problemi dei servizi Web) redigendo
ciò che è oggi noto ai più come la specifica WS-Security. Nella specifica WS-Security viene
descritto come proteggere i messaggi inviati dai servizi Web. I fornitori che sottoscrivono
la WS-Security si impegnano a garantire che le proprie implementazioni ne soddisfino le
specifiche, solitamente utilizzando tecnologie come la crittografia e i certificati.
■ Criteri La specifica WS-Security definisce la modalità con cui fornire la protezione migliore,
ma spetta agli sviluppatori scrivere il codice con cui implementarla. I servizi Web creati da
diversi sviluppatori possono spesso presentare delle differenze nel rigore dei meccanismi di
protezione che scelgono di implementare. Ad esempio, un servizio Web potrebbe utilizzare
solo una forma di crittografia relativamente debole, semplice da violare. Un consumer che si
trova ad inviare informazioni molto riservate a questo servizio Web, probabilmente insisterà
per ottenere un livello di protezione maggiore. Questo è un esempio di criterio. Altri esempi
includono la qualità del servizio e l’affidabilità del servizio Web. Un servizio Web potrebbe
implementare diversi gradi di protezione, qualità del servizio e affidabilità e aumentando
di conseguenza il costo dell’applicazione client. L’applicazione client e il servizio Web
possono negoziare sul livello di servizio da utilizzare, in base ai requisiti e al costo. Tuttavia,
per questa negoziazione è necessario che sia il client sia il servizio Web conoscano i criteri
disponibili. La specifica WS-Policy fornisce un modello generale e la corrispondente sintassi
per descrivere e comunicare i criteri implementati da un servizio Web.
■ Reindirizzamento È utile che un server Web sia in grado di reindirizzare la richiesta di
un servizio Web a uno dei diversi computer in cui sono presenti le istanze del servizio.
Ad esempio, molti sistemi scalabili possono voler bilanciare il carico, ovvero eseguire
un’operazione in cui le richieste inviate a un server Web siano in effetti reindirizzate da
questo server ad altri computer, per distribuire il carico di lavoro su questi. Il server può
utilizzare un numero qualsiasi di algoritmi per tentare di bilanciare il carico. L’importante
è che questo reindirizzamento sia trasparente per il client che presenta la richiesta del
servizio Web e che il server che gestisce per ultimo la richiesta sappia dove inviare le
risposte che genera. Il reindirizzamento delle richieste dei servizi Web è utile anche quando
l’amministratore deve spegnere un computer per eseguire operazioni di manutenzione.
Le richieste che normalmente sarebbero inviate a questo computer possono essere
reindirizzate a uno dei suoi peer. Nella specifica WS-Addressing è descritto un framework
per il routing delle richieste dei servizi Web.
Nota Gli sviluppatori fanno riferimento alle specifiche WS-Security, WS-Policy, WS-Addressing
e altre specifiche WS definendole collettivamente specifiche WS-*.
688
Parte VI
Creazione di soluzioni professionali con Visual Studio 2010
Servizi Web REST
A differenza dei servizi Web SOAP, il modello REST utilizza uno schema di navigazione per
rappresentare oggetti e risorse su una rete. Per esempio, una organizzazione potrebbe dare
accesso a informazioni sui dipendenti, rendendo disponibili i dettagli di ogni persona come
singola risorsa attraverso uno schema simile al seguente:
http://northwind.com/employees/7
Attraverso questo URL il servizio Web recupera i dati del dipendente 7. Tali dati possono essere
restituiti in diversi formati; per avere portabilità il formato più comune include XML (a volte
citato come POX - Plain Old XML) e JSON (JavaScript Object Notation). Nel nostro esempio, se
l'organizzazione Northwind Traders sceglie di usare POX, il risultato che ci restituisce l'URL di cui
sopra potrebbe essere simile al seguente:
<Employee>
<EmployeeID>
7
</EmployeeID>
<LastName>
King
</LastName>
<FirstName>
Robert
</FirstName>
<Title>
Sales Representative
</Title>
</Employee>
La chiave per progettare soluzioni REST è comprendere come traslare un business model in un
insieme di risorse. In alcuni casi, come nell'esempio fatto, potrebbe essere molto semplice, ma in
altre situazioni potremmo incontrare delle difficoltà.
REST si affida all'applicazione che ha accesso ai dati, inviando il comando HTTP come parte della
richiesta usata per accedere ai dati. Per esempio, la richiesta sopra descritta invia una richiesta
HTTP GET al servizio Web. HTTP supporta altri verbi come POST, PUT e DELETE che possono
essere usati rispettivamente per creare, modificare e cancellare risorse. Con REST puoi sfruttare
questi verbi e creare servizi Web che possono aggiornare dati.
A differenza di SOAP, i messaggi inviati e ricevuti utilizzando il metodo REST tendono a essere più
compatti. Principalmente perché REST non dà la stessa sicurezza data dalle specifiche WS-* e devi
quindi affidarti alla struttura su cui poggia il server Web per proteggere i servizi REST. Tuttavia,
questo approccio minimalista vuol dire che un servizio Web REST è normalmente più efficiente
dell'equivalente SOAP quando si tratta di trasmettere e ricevere messaggi.
Capitolo 29
Creazione e utilizzo di un servizio Web
689
Creazione di Servizi Web
Per mezzo di WFC puoi sviluppare servizi Web sia sul modello REST, sia su quello SOAP. Il più
semplice dei due schemi è SOAP quando si parla di implementazione con WFC. Ci concentriamo
quindi su questo modello per i prossi esercizi. Più avanti nel capitolo vediamo come creare un
servizio Web REST.
In questo capitolo creerai due servizi Web:
■ Il servizio Web ProductInformation. Questo è un servizio Web SOAP che permette
all'utente di calcolare il costo di acquisto di una specifica quantità di un particolare prodotto
presente nel database Northwind.
■ Il servizio Web ProductDetails. Questo è un servizio Web REST che permette all'utente di
recuperare i dettagli di prodotti presenti nel database Northwind.
Creazione del servizio Web SOAP ProductInformation
Nel primo esercizio, creerai il servizio Web ProductInformation ed esaminerai il codice di esempio
generato da Visual Studio 2010 ogni volta che viene creato un nuovo progetto WCF. Negli
esercizi successivi, definirai e implementerai il metodo Web HowMuchWillItCost e lo testerai per
verificarne il funzionamento.
Importante Microsoft Visual C# 2010 Express Edition non consente di creare applicazio-
ni o servizi Web. Bisogna usare Microsoft Visual Web Developer 2010 Express. Puoi scaricare
Microsoft Visual Web Developer 2010 Express direttamente dal sito Web Microsoft.
Creazione del servizio Web ed esame del codice di esempio
1. Avvia Visual Studio 2010 se non è già in esecuzione, o avvia Visual Web Developer 2010
Express.
2. Se utilizzi Visual Studio 2010 Professional Edition o Enterprise Edition, seleziona Nuovo nel
menu File, quindi fai clic su Sito Web.
3. Se utilizzi Microsoft Visual Web Developer 2010 Express Edition, fai clic su Nuovo sito Web
nel menu File. Assicurati di aver selezionato Visual C# sotto Modelli installati nel riquadro di
sinistra.
4. Nella finestra di dialogo Nuovo Sito Web, fai clic sul modello Servizio WCF. Seleziona
File system nella casella di riepilogo a discesa Percorso e specifica la cartella \Microsoft
Press\Visual CSharp Step By Step\Chapter 29\ProductInformationService nella tua cartella
Documenti.
690
Parte VI
Creazione di soluzioni professionali con Visual Studio 2010
Visual studio genera un sito Web utilizzando il server web incorporato in Visual Studio
e Visual Web Developer. Puoi anche utilizzare IIS se è disponibile. Tieni conto che
le spiegazioni sono limitate all'utilizzo del server Web incorporato nei due succitati
programmi. Il sito contiene delle cartelle denominate App_Code e App_Data, un file
chiamato Service.svc, e un file di configurazione denominato Web.config. Il codice per un
servizio Web di esempio è definito nella classe Service, che trovi nel file Service.cs nella
cartella App_Code e visualizzata nella finestra dell’editor di codice e di testo. La classe
Service implementa un’interfaccia di esempio chiamata IService, memorizzata nel file
IService.cs della cartella App_Code.
Nota Una soluzione per il progetto sito Web si trova nella cartella Visual Studio 2010\
Projects della cartella Documenti. Puoi aprire un progetto esistente sia aprendo il file della
soluzione, sia usando il comando Sito web del menu File - Apri e specificando la cartella
che contiene i file di progetto. Per ragioni di sicurezza non è consigliabile, ma puoi anche
copiare il file della soluzione all'interno della cartella ove si trovano i file del sito Web.
5. Fai clic sul progetto C:\...\ProductInformationService\. Nella finestra Proprietà, imposta la
proprietà Utilizza porte dinamiche su False e imposta la proprietà Numero porta su 4500.
Nota Potresti dover attendere un pochino dopo aver impostato la proprietà Utilizza porte
dinamiche in False prima di poter impostare la proprietà Numero porta.
Una porta indica la posizione in cui il server Web è in ascolto e attesa di ricevere richieste
da applicazioni client. Per impostazione predefinita, il server Web Development sceglie
una porta a caso, limitando così la possibilità di generare errori di incompatibilità con altre
porte utilizzate da altri servizi di rete eseguiti sul computer. Questa funzionalità è utile se
stai creando e testando siti Web in un ambiente di sviluppo prima di copiarli in un server
di produzione come IIS (Microsoft Internet Information Services). Tuttavia, quando si crea
un servizio Web, è più utile utilizzare un numero di porta fisso perché è necessario che le
applicazioni client siano in grado di stabilire una connessione.
Nota Quando chiudi il sito Web e successivamente lo riapri con Visual Studio o Visual
Web Developer, la proprietà Utilizza porte dinamiche spesso ritorna al valore True e la proprietà Numero porta è impostata in modo casuale. In questo caso devi reimpostare i valori
come sopra specificato.
6. In Esplora soluzioni, espandi la cartella App_Code, fai clic con il pulsante destro del
mouse sul file Service.cs e quindi seleziona Rinomina. Modifica il nome del file in
ProductInformation.cs.
7. Utilizzando la stessa tecnica, modifica il nome del file IService.cs in
IProductInformation.cs.
Capitolo 29
Creazione e utilizzo di un servizio Web
691
8. Fai doppio clic sul file IProductInformation.cs per visualizzarlo nella finestra dell’editor di
codice e di testo.
Questo file contiene la definizione di un’interfaccia chiamata IService. Nella parte superiore
del file IProductInformation.cs, troverai le istruzioni using per gli spazi dei nomi System,
System.Collections.Generic e System.Text (che hai già trovato in precedenza) seguiti da
due istruzioni aggiuntive che riguardano gli spazi dei nomi System.ServiceModel e System.
Runtime.Serialization.
Lo spazio dei nomi System.ServiceModel contiene le classi utilizzate da WCF per definire
i servizi e le loro operazioni. WCF usa le classi nello spazio dei nomi System.Runtime.
Serialization per convertire gli oggetti in un flusso di dati per la trasmissione in rete (un
processo noto con il nome di serializzazione) e per riconvertire un flusso di dati ricevuti
dalla rete in oggetti (deserializzazione). La parte successiva di questo capitolo illustra per
sommi capi la modalità con cui WCF serializza e deserializza gli oggetti.
Il contenuto principale del file IProductInformation è rappresentato da un’interfaccia
chiamata IService e una classe chiamata CompositeType. Il prefisso dell’interfaccia IService è
l’attributo ServiceContract mentre la classe CompositeType è contraddistinta dall’attributo
DataContract. Data la struttura di un servizio WCF, per lo sviluppo puoi scegliere un
approccio “contract-first development”. Nell’eseguire lo sviluppo contract-first, definirai le
interfacce o i contracts che il servizio implementerà e quindi compilerai un servizio ad essi
conforme. Questa non è una tecnica nuova: hai già trovato esempi di questa strategia in
questo libro. La scelta dello sviluppo contract-first ti offre la possibilità di concentrarti sul
progetto del tuo servizio. Se necessario, puoi rivederlo rapidamente prima di completare
lo sviluppo per verificare che il tuo progetto non introduca dipendenze da hardware o
software specifici; non dimenticare che in molti casi le applicazioni client potrebbero non
essere state create con WCF e potrebbero non essere eseguite in Windows.
L’attributo ServiceContract contrassegna un’interfaccia definendo i metodi che la classe che
implementa il servizio Web esporrà come metodi Web. I metodi stessi sono contraddistinti
dall’attributo OperationContract. Gli strumenti forniti con Visual Studio 2010 usano questi
attributi come ausili per generare il documento WSDL appropriato per il servizio. Tutti i
metodi dell’interfaccia non contrassegnati con l’attributo OperationContract non saranno
inclusi nel documento WSDL e quindi non saranno accessibili alle applicazioni client che
utilizzano il servizio Web.
Se un metodo Web acquisisce i parametri o restituisce un valore, i dati per questi parametri
e questo valore devono essere convertiti in un formato che possa essere trasmesso in
rete e quindi riconvertito in oggetti: questo è il processo noto come serializzazione e
deserializzazione menzionato in precedenza. I diversi standard dei servizi Web definiscono
i meccanismi con cui specificare il formato serializzato di tipi di dati semplici, come i numeri
e le stringhe, come parte della descrizione WSDL per un servizio Web. Tuttavia, puoi anche
definire tipi di dati complessi sfruttando le classi e le strutture. Se utilizzi questi tipi in un
servizio Web, devi fornire informazioni su come serializzarli e deserializzarli.
692
Parte VI
Creazione di soluzioni professionali con Visual Studio 2010
Guardando la definizione del metodo GetDataUsingDataContract nell’interfaccia IService,
ti sarà facile capire che questa prevede la specifica di un parametro di tipo CompositeType.
La classe CompositeType è contrassegnata con l’attributo DataContract che specifica che la
classe deve definire un tipo che possa essere serializzato e deserializzato come flusso XML
come parte di una richiesta SOAP o come messaggio di risposta.
Ogni membro da includere nel flusso di serializzazione viene inviato in rete e deve essere
contrassegnato con l’attributo DataMember.
9. Fai doppio clic sul file ProductInformation.cs per visualizzarlo nella finestra dell’editor di
codice e di testo.
Questo file contiene una classe chiamata Service che implementa l’interfaccia IService
e fornisce i metodi GetData e GetDataUsingDataContract definiti da questa interfaccia.
Questa classe è il servizio Web. Quando un’applicazione client richiama un metodo Web in
questo servizio Web, genera un messaggio di richiesta SOAP e lo invia al server Web che
contiene il servizio Web. Il server Web crea un’istanza di questa classe ed esegue il metodo
corrispondente. Al completamento del metodo, il server Web genera un messaggio di
risposta SOAP che reinvia all’applicazione client.
10. Fai doppio clic sul file Service.svc per visualizzarlo nella finestra dell’editor di codice e di
testo.
Questo è il file di servizio per il servizio Web; viene utilizzato dall’ambiente host (IIS,
in questo caso) per determinare quale classe caricare quando riceve una richiesta da
un’applicazione client.
La proprietà Service della direttiva @ ServiceHost specifica il nome della classe del servizio
Web mentre la proprietà CodeBehind specifica il percorso del codice sorgente per tale
classe.
Suggerimento Se non vuoi distribuire il codice sorgente per il tuo servizio WCF nel server Web, puoi fornire in alternativa un assembly compilato. Puoi quindi specificare il nome
e il percorso di questo assembly utilizzando la direttiva @ Assembly. Per ulteriori informazioni, cerca “direttiva @ Assembly” nella documentazione di Visual Studio 2010.
Ora che hai visto la struttura di un servizio WCF puoi definire l’interfaccia che specifica il service
contract per il servizio Web ProductInformation e crea la classe che implementa tale service
contract.
Capitolo 29
Creazione e utilizzo di un servizio Web
693
Definizione dei contract per il servizio Web ProductInformation
1. Visualizza il file di codice IProductInformation.cs nella finestra dell’editor di testo e di codice.
2. Nella riga di codice che definisce l'interfaccia IService, fai doppio clic su IService per
evidenziarlo. Nel menu Refactoring fai clic su Rinomina. Nella finestra di dialogo Rinomina,
digita IProductInformation all'interno della casella Nuovo nome. Deseleziona quindi
Anteprima modifiche riferimento e fai clic su OK.
Questa azione modifica il il nome dell'interfaccia da IService a IProductInformation e
modifica anche tutti i riferimenti a IService in IProductInformation in tutti i file del progetto.
La riga che definisce l'interfaccia nella finestra dell’editor di codice e di testo dovrebbe
essere così:
public interface IProductInformation
{
...
}
3. Nell’interfaccia IProductInformation, rimuovi le definizioni dei metodi GetData
e GetDataUsingDataContract e sostituiscile con i metodi HowMuchWillItCost e
GetProductInfo, come mostrato in grassetto nel codice seguente. Ricorda di conservare
l’attributo OperationContract per ogni metodo Web.
[ServiceContract]
public interface IProductInformation
{
[OperationContract]
decimal HowMuchWillItCost(int productID, int howMany);
}
Il metodo HowMuchWillItCost acquisisce un ID prodotto e una quantità e restituisce un
valore decimal in cui è specificato il costo di questa quantità.
4. Togli la classe CompositeType, compreso l'attributo DataContract dal file
IProductInformation.cs. Il file dovrebbe contenere solo la definizione dell'interfaccia
IProductInformation.
La fase successiva consiste nel definire la classe ProductInformation che implementa l’interfaccia
IProductInformation. Il metodo HowMuchWillItCost in questa classe, recupera il prezzo del
prodotto dal database eseguendo una semplice query ADO.NET.
Nota Il servizio Web che crei in questo capitolo richiede l'accesso al database Northwind. Se
non lo hai ancora fatto, puoi creare questo database seguendo i passi della sezione "Creazione
del database" nel capitolo 25.
694
Parte VI
Creazione di soluzioni professionali con Visual Studio 2010
Implementazione dell’interfaccia IProductInformation
1. Visualizza il codice del file ProductInformation.cs nella finestra dell’editor di codice e di
testo.
2. All’inizio dell'elenco, aggiungi le seguenti istruzioni using:
using System.Data;
using System.Data.SqlClient;
Dovresti ricordare, dal capitolo 25, che questi spazi dei nomi contengono i tipi necessari per
interrogare un database di Microsoft SQL Server.
3. Nella riga di codice che definisce l'interfaccia Service, fai doppio clic su Service per
evidenziarlo. Nel menu Refactoring fai clic su Rinomina. Nella finestra di dialogo Rinomina,
digita ProductInformation all'interno della casella Nuovo nome e fai clic su OK.
Questa azione modifica il il nome della classe da Service a ProductInformation e modifica
anche tutti i riferimenti a Service in ProductInformation in tutti i file del progetto. La riga che
definisce la classe nella finestra dell’editor di codice e di testo dovrebbe essere così:
public class ProductInformation : IProductInformation
{
...
}
4. Rimuovi i metodi GetData e GetDataUsingDataContract dalla classe ProductInformation.
5. Aggiungi il metodo HowMuchWillItCost alla classe ProductInformation, come mostrato di
seguito in grassetto:
public class ProductInformation : IProductInformation
{
public decimal HowMuchWillItCost(int productID, int howMany)
{
SqlConnection dataConnection = new SqlConnection();
decimal totalCost = 0;
try
{
SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder();
builder.DataSource = ".\\SQLExpress";
builder.InitialCatalog = "Northwind";
builder.IntegratedSecurity = true;
dataConnection.ConnectionString = builder.ConnectionString;
dataConnection.Open();
SqlCommand dataCommand = new SqlCommand();
dataCommand.Connection = dataConnection;
dataCommand.CommandType = CommandType.Text;
dataCommand.CommandText = "SELECT UnitPrice FROM
= @ProductID";
Products WHERE ProductID
SqlParameter productIDParameter = new SqlParameter("@ProductID",
SqlDbType.Int);
productIDParameter.Value = productID;
Capitolo 29
Creazione e utilizzo di un servizio Web
695
dataCommand.Parameters.Add(productIDParameter);
decimal? price = dataCommand.ExecuteScalar() as decimal?;
if (price.HasValue)
{
totalCost = price.Value * howMany;
}
}
finally
{
dataConnection.Close();
}
return totalCost;
}
}
Questo metodo si connette al database ed esegue una query ADO.NET per recuperare
il costo del prodotto corrispondente all’ID prodotto fornito dal database Northwind. Se
il costo è diverso da zero, il metodo calcola il costo totale della richiesta e lo restituisce;
diversamente, il metodo restituisce il valore 0. Il codice assomiglia a quello già visto nel
capitolo 25 a eccezione del fatto che utilizza il metodo ExecuteScalar per leggere il valore
della colonna UnitPrice dal database. Il metodo ExecuteScalar ci dà un meccanismo molto
efficiente per eseguire query che restituiscono un singolo valore scalare (ed è molto
più efficiente che aprire un cursore e leggere il valore dal cursore stesso). Il valore che
ExecuteScalar ci restituisce è un oggetto e prima di usarlo bisogna eseguire il cast verso il
tipo appropriato prima di usarlo.
Nota Questo metodo non esegue alcuna convalida dei parametri di input. Ad esempio,
è possibile specificare un valore negativo per il parametro howMany. In un servizio Web
di produzione, potresti individuare errori di questo genere, registrarli e restituire un’eccezione. Tuttavia, la ritrasmissione all’applicazione client dei motivi di un’eccezione ha
delle implicazioni a livello di protezione in un servizio WCF. Per ulteriori informazioni sugli
endpoint e gli host personalizzati, consulta il testo Microsoft Windows Communication
Foundation Step by Step.
Prima di utilizzare il servizio Web devi aggiornare la configurazione nel file Service.svc in modo
che faccia riferimento alla classe ProductInformation nel file ProductInformation.cs. Il server Web
utlizza le informazioni contenute nel file Web.config creato con il progetto per sapere come
pubblicare il servizio e renderlo disponibile alle applicazioni client. Devi modificare il file Web.
config aggiungendo i dettagli del servizio Web.
696
Parte VI
Creazione di soluzioni professionali con Visual Studio 2010
Configurazione del servizio Web
1. In Esplora soluzioni, fai doppio clic sul file Service.svc per visualizzarlo nella finestra
dell’editor di codice e di testo. Aggiorna gli attributi Service e CodeBehind della direttiva
ServiceHost, come mostrato di seguito in grassetto:
<%@ ServiceHost Language="C#" Debug="true" Service="ProductInformation"
CodeBehind="~/App_Code/ProductInformation.cs" %>
2. In Esplora soluzioni, fai doppio clic sul file Web.config. Nella finestra dell’editor di codice e di
testo, individua l’elemento <system.serviceModel>. Userai questo elemento per specificare la
configurazione di un servizio WFC. Qui è contenuto un elemento Behaviors che al momento
ignoreremo.
3. Nel file Web.config, aggiungi l'elemento <services> e gli elementi figlio illustrati di seguito
in grassetto, all'elemento <system.serviceModel>, prima dell'elemento <behaviors>:
<system.serviceModel>
<services>
<service name="ProductInformation">
<endpoint address="" binding="wsHttpBinding" contract="IProductInformation"/>
</service>
</services>
<behaviors>
...
</behaviors>
</system.serviceModel>
Questa configurazione specifica il nome della classe che implementa il servizio Web
(ProductInformation). WCF usa la nozione degli endpoint per associare un indirizzo di rete
a un servizio Web specifico. Se ospiti un servizio Web utilizzando IIS o Development Server,
dovresti lasciare in bianco la proprietà address del tuo endpoint perché IIS riceve le richieste
in entrata su un indirizzo specificato dalle sue informazioni di configurazione. Nota Puoi
compilare delle applicazioni host personalizzate se non vuoi utilizzare IIS o Development
Server. In tal caso, devi specificare un indirizzo per il servizio durante la definizione
dell’endpoint. Il parametro Binding indica il protocollo di rete che il server usa per ricevere
le richieste e trasmettere le risposte.
Per ulteriori informazioni sugli endpoint e gli host personalizzati, consulta il testo Microsoft
Windows Communication Foundation Step by Step.
4. Nel menu File, fai clic su Salva tutto.
5. In Esplora soluzioni, fai clic con il pulsante destro su Service.svc e quindi su Visualizza nel
browser.
Capitolo 29
Creazione e utilizzo di un servizio Web
697
Internet Explorer si avvia e mostra la pagina seguente, a conferma che il servizio Web
è stato creato e distribuito in modo corretto. Questa pagina presenta inoltre un’utile
descrizione di come creare una semplice applicazione client che possa accedere al servizio
Web.
Nota Se fai clic sul collegamento mostrato nella pagina Web (http://localhost:4500/
NorthwindServices/Service.svc?wsdl), Internet Explorer apre una pagina contenente la descrizione WSDL del servizio Web. Si tratta di un blocco XML lungo e complesso ma Visual
Studio 2010 può inserire le informazioni nella propria descrizione e utilizzarle per generare
una classe utilizzabile da un’applicazione client per comunicare con il servizio Web.
6. Chiudi Internet Explorer e torna a Visual Studio 2010.
Servizi Web SOAP, client e proxy
Un servizio Web SOAP utilizza il protocollo SOAP per trasmettere dati tra una applicazione client
e un servizio. Per formattare i dati trasmessi SOAP usa il linguaggio XML che viene eseguito in un
protocollo HTTP utilizzato dai server Web e dai browser. Ciò che rende così potenti i servizi Web è
proprio il fatto che SOAP, HTTP e XML sono mezzi di comunicazione interpretabili senza problemi
e soggetti a diversi standard. Qualsiasi applicazione client in grado di utilizzare il linguaggio SOAP
può comunicare con un servizio Web. La domanda che ti devi porre è quindi: come può un client
parlare il linguaggio SOAP? I possibili metodi sono due: uno complesso e uno semplice.
698
Parte VI
Creazione di soluzioni professionali con Visual Studio 2010
Utilizzo di SOAP: metodo complesso
Nel metodo complesso l’applicazione client esegue numerosi passaggi per svolgere le seguenti
operazioni: In particolare, deve poter fare quanto segue:
1. Determinare l’URL del servizio Web che esegue il metodo Web.
2. Eseguire una richiesta WSDL (Web Services Description Language) utilizzando l’URL
per ottenere una descrizione dei metodi Web disponibili, i parametri utilizzati e i valori
restituiti. La descrizione di come svolgere questa operazione utilizzando Internet Explorer è
contenuta nell’esercizio precedente.
3. Analizzare il documento WSDL, convertire ogni operazione in una richiesta Web e
serializzare ogni parametro nel formato descritto dal documento WSDL.
4. Inoltrare la richiesta, insieme ai dati serializzati all’URL utilizzando il protocollo HTTP.
5. Attendere la risposta del servizio Web.
6. Utilizzando i formati specificati dal documento WSDL, deserializzare i dati restituiti dal
servizio Web in valori significativi elaborabili dalla tua applicazione.
Quanto descritto implica numerose operazioni per richiamare un metodo e può facilmente
generare degli errori.
Utilizzo di SOAP: metodo semplice
La cattiva notizia è che il metodo semplice per usare SOAP non è molto diverso da quello
complesso! La buona notizia è che il processo può essere automatizzato perché è per lo più
meccanico. Come detto in precedenza, molti rivenditori, tra cui Microsoft, forniscono strumenti
in grado di generare una classe proxy basata su una descrizione WSDL. Il proxy nasconde la
complessità dell’utilizzo di SOAP ed espone una semplice interfaccia programmatica basata
sui metodi pubblicati dal servizio Web. L’applicazione client richiama i metodi Web utilizzando
i metodi con lo stesso nome nel proxy. Il proxy converte queste chiamate di metodi locali in
richieste SOAP e le invia al servizio Web. Il proxy attende la risposra, deserializza i dati e quindi li
ripassa al client come se restituisse una semplice chiamata di metodo. Questo è l’approccio che
utilizzerai negli esercizi di questa sezione.
Usufruire del servizio Web SOAP ProductInformation
Hai creato un servizio Web SOAP che permette a un metodo Web chiamato HowMuchWillItCost
di determinare il costo di acquisto di n prodotti da Northwind Traders. Negli esercizi successivi,
utilizzerai questo servizio Web e creerai l’applicazione che li utilizza.
Capitolo 29
Creazione e utilizzo di un servizio Web
699
Apri l'applicazione client del servizio Web
1. Avvia un’altra istanza di Visual Studio 2010. Questo è un punto importante.
ASP.NET Development Server si blocca se chiudi il progetto del servizio Web
ProductInformationService e ciò significa che non potrai accedervi dal client. (Un approccio
alternativo utilizzabile se esegui Visual Studio 2010 e non Visual Web Developer 2010
Express Edition, consiste nel creare l’applicazione client come un progetto nella stessa
soluzione del servizio Web.) Se ospiti un servizio Web in un ambiente di produzione
utilizzando IIS, questo problema non sorge perché IIS viene eseguito indipendentemente da
Visual Studio 2010.
Importante Se per gli esercizi di questa parte del libro hai utilizzato Visual Web
Developer 2010 Express Edition, avvia Visual C# 2010 Express Edition invece di una seconda istanza di Visual Web Developer 2010 Express Edition (non interrompere l’esecuzione
di Visual Web Developer 2010 Express Edition).
2. Nella seconda istanza di Microsoft Visual Studio 2010, apri la soluzione ProductClient nella
cartella \Microsoft Press\Visual CSharp Step By Step\Chapter 29\ProductClient della tua
cartella Documenti.
3. In Esplora soluzioni, fai doppio clic sul file ProductClient.xaml per visualizzare il form nella
finestra Progettazione. Il form è simile al seguente:
Il form consente all’utente di specificare un ID prodotto e recuperare i dettagli del prodotto
dal database Northwind. (Implementerai questa funzionalità più avanti in un altro esercizio
attraverso l'utilizzo di un servizio Web REST.) L’utente può anche fornire una quantità e
recuperare un prezzo per l’acquisto di quella precisa quantità di prodotto. Attualmente, i
pulsanti sul form non eseguono alcuna operazione. Nei passaggi successivi, aggiungerai il
codice necessario per richiamare i metodi dal servizio Web ProductInformation in modo da
ottenere i dati e visualizzarli.
700
Parte VI
Creazione di soluzioni professionali con Visual Studio 2010
Aggiungi il codice per chiamare il servizio Web nell’applicazione client.
1. Nel menu Progetto, fai clic su Aggiungi riferimento al servizio.
Si apre la finestra Aggiungi riferimento al servizio. In questa finestra di dialogo, è possibile
cercare i servizi Web ed esaminare i metodi Web che questi forniscono.
2. Nella casella di testo Indirizzo, immetti http://localhost:4500/NorthwindServices/
Service.svc e fai clic su Vai.
Nella casella Servizi è visualizzato il servizio ProductInformation.
3. Espandi il servizio ProductInformation e quindi fai clic sull’interfaccia IProductInformation
visualizzata. Nella casella di riepilogo Operazioni, verifica la presenza di HowMuchWillItCost,
come mostrato nella seguente immagine.
4. Modifica il valore della casella di testo Spazio dei nomi in ProductInformationService e fai
clic su OK.
In Esplora soluzioni si apre una nuova cartella chiamata Service References. Questa cartella
contiene un elemento chiamato ProductInformationService.
5. Fai clic sul pulsante Mostra tutti i file nella barra degli strumenti di Esplora soluzioni. Espandi
la cartella ProductInformationService e quindi espandi Reference.svcmap. Visualizza il file
Reference.cs nella finestra dell’editor di testo e di codice facendo doppio clic su di esso.
Questo file contiene molte classi e interfacce, inclusa una classe chiamata
ProductInformationClient in uno spazio dei nomi chiamato ProductClient.
ProductInformationService. ProductInformationClient è una classe proxy generata dalla
descrizione WSDL del servizio Web ProductInformation da Visual Studio 2010. Contiene
Capitolo 29
Creazione e utilizzo di un servizio Web
701
diversi costruttori, come il metodo chiamato HowMuchWillItCost. Il codice seguente
(formattato per renderlo più fruibile) illustra alcune cose interessanti:
namespace ProductClient.ProductInformationService {
...
[System.ServiceModel.ServiceContractAttribute(...)]
public interface IProductInformation {
[System.ServiceModel.OperationContractAttribute(...)]
decimal HowMuchWillItCost(int productID, int howMany);
}
...
...
public partial class ProductInformationClient :
System.ServiceModel.ClientBase<ProductClient.ProductInformationService.
IProductInformation>,
ProductClient.ProductInformationService.IProductInformation {
public ProductInformationClient() {
}
public ProductInformationClient(string endpointConfigurationName) :
base(endpointConfigurationName) {
}
public ProductInformationClient(string endpointConfigurationName, string
remoteAddress) :
base(endpointConfigurationName, remoteAddress) {
}
public ProductInformationClient(string endpointConfigurationName,
System.ServiceModel.EndpointAddress remoteAddress) :
base(endpointConfigurationName, remoteAddress) {
}
public ProductInformationClient(System.ServiceModel.Channels.Binding binding,
System.ServiceModel.EndpointAddress remoteAddress) :
base(binding, remoteAddress) {
}
public decimal HowMuchWillItCost(int productID, int howMany) {
return base.Channel.HowMuchWillItCost(productID, howMany);
}
}
}
L'interfaccia IProductInformation è simile a quella che hai definito nel servizio Web a
eccezione del fatto che alcuni attributi specificano dei parametri aggiuntivi (La spiegazione
di questi parametri è oltre lo scopo del libro). La classe ProductInformation, oltre a
implementare questa interfaccia, la eredita dalla classe generica ClientBase. La classe
ClientBase nello spazio dei nomi System.ServiceModel dà la funzionalità di comunicazione di
base che una applicazione client richiede per comunicare con un servizio Web. Il parametro
Type specifica l'interfaccia che la classe implementa. La classe ClientBase dà anche la
proprietà Channel, che incapsula la connessione HTTP al servizio Web.
702
Parte VI
Creazione di soluzioni professionali con Visual Studio 2010
I diversi costruttori per la classe ProductInformationClient configurano il canale di
connessione all'endpoint sul quale il servizio Web è in ascolto.
L'applicazione client crea l'istanza nella classe ProductInformationClient specificando
l'endpoint a cui connettersi e quindi chiama il metodo HowMuchWillItCost. Quindi il
canale nella classe ClientBase impacchetta l'informazione sotto forma di parametro in
un messaggio SOAP che trasmette al servizio Web. Quando il servizio Web risponde, le
informazioni restituite vengono estratte dalla risposta SOAP e ripassate all’applicazione
client. In tal modo l’applicazione client può richiamare un metodo in un servizio Web
proprio come se chiamasse un metodo locale.
Nota Dovresti aver notato che l'interfaccia è taggata con l'attributo
ServiceContractAttribute anzichéServiceContract, e l'operazione a sua volta è taggata con
l'attributo OperationContractAttribute anzichéOperationContract. Infatti tutti gli attributi
hanno il suffisso Attribute. Il compilatore C# riconosce questa convenzione e conseguentemente ti permette di omettere il suffisso Attribute dal tuo codice.
6. Visualizza il form ProductClient.xaml nella finestra Progettazione. Fai
doppio clic sul pulsante Calculate Cost per generare il metodo per la gestione eventi
calcCost_Click per questo pulsante.
7. Nella finestra dell’editor di codice e di testo, aggiungi le seguenti istruzioni using all’elenco
nella parte superiore del file ProductClient.xaml.cs:
using ProductClient.ProductInformationService;
using System.ServiceModel;
8. Nel metodo calcCost_Click, aggiungi il codice mostrato di seguito in grassetto:
private void calcCost_Click(object sender, RoutedEventArgs e)
{
ProductInformationClient proxy = new ProductInformationClient();
}
Questa istruzione crea un’istanza della classe ProductInformationClient che il codice
utilizzerà per chiamare il metodo Web HowMuchWillItCost.
9. Aggiungi al metodo calcCost_Click il codice illustrato di seguito in grassetto. Questo codice
estrae il product ID e il numero richiesto dalla casella di testo How Many del form, esegue
il metodo Web HowMuchWillItCost utilizzando l'oggetto proxy proxy, e quindi visualizza il
risultato nell'etichetta totalCost.
private void calcCost_Click(object sender, RoutedEventArgs e)
{
ProductInformationClient proxy = new ProductInformationClient();
try
{
int prodID = Int32.Parse(productID.Text);
int numberRequired = Int32.Parse(howMany.Text);
decimal cost = proxy.HowMuchWillItCost(prodID, numberRequired);
totalCost.Content = String.Format("{0:C}", cost);
}
Capitolo 29
Creazione e utilizzo di un servizio Web
703
catch (Exception ex)
{
MessageBox.Show("Error obtaining cost: " + ex.Message,
"Error", MessageBoxButton.OK, MessageBoxImage.Error);
}
finally
{
if (proxy.State == CommunicationState.Faulted)
proxy.Abort();
else
proxy.Close();
}
}
Probabilmente sai già quanto le reti siano imprevedibili: Internet non fa eccezione! Il blocco
try/catch assicura che l’applicazione client “catturi” ogni eccezione di rete che si verifica.
Potrebbe anche accadere che l’utente non inserisca un valore intero valido nella casella di
testo ProductID del form. Il blocco try/catch gestisce anche questa eccezione.
Il blocco finally esamina lo stato dell’oggetto proxy. Se nel servizio Web si è verificata
un’eccezione (che potrebbe essere causata dall’utente che ha inserito un ID prodotto
inesistente, ad esempio), lo stato del proxy sarà Faulted. In tal caso, il blocco finally
richiama il metodo Abort del proxy per riconoscere l’eccezione e chiudere la connessione,
altrimenti richiama il metodo Close. I metodi Abort e Close chiudono entrambi il canale
di comunicazione con il servizio Web e rilasciano le risorse associate con questa istanza
dell’oggetto ProductInformationClient.
Test dell’applicazione
1. Nel menu Debug, fai clic su Avvia senza eseguire debug.
2. Quando appare il form Product Details.Digita 3 nella casella di testo Product ID, digita 5
nella casella di testo How Many, quindi fai clic su Calculate Cost.
Dopo una breve attesa durante la quale il client crea l’istanza del proxy e genera una
richiesta SOAP contenente l’ID del prodotto, il proxy invia la richiesta al servizio Web. Il
servizio Web deserializza la richiesta SOAP per estrarre il product ID, legge il prezzo unitario
del prodotto dal database, calcola il costo totale, lo impacchetta come XML in un messggio
SOAP di risposta e quindi invia questa risposta al proxy. Il proxy deserializza i dati XML
e passa poi questo oggetto al codice nel metodo calcCost_Click. Il costo di 5 unità del
prodotto 3 appare nel form (50 unità di valuta).
Suggerimento Se ottieni l'eccezione “Error obtaining cost: vuol dire che non c'era un
endpoint in ascolto all'indirizzo http://localhost:4500/ProductInformationService/Service.
svc. (Il server si spegne se rimane inattivo per qualche tempo.) Per riavviarlo, passa
all’istanza Visual Studio 2010 per il servizio Web ProductsService, fai clic con il pulsante destro del mouse su Service.svc in Esplora soluzioni e quindi su Visualizza nel browser. Chiudi
Internet Explorer quando si apre.
704
Parte VI
Creazione di soluzioni professionali con Visual Studio 2010
3. Fai delle prove inserendo gli ID di altri prodotti. Nota che se immetti un ID per un prodotto
che non esiste, il servizio Web restituisce 0.
4. Al termine delle prove, chiudi il form e torna a Visual Studio.
Creazione del servizio Web REST ProductDetails
Nella precedente sezione abbiamo sviluppato in servizio Web SOAP per implementare un piccolo
pezzo di funzionalità. Nel prossimo esercizio, svilupperemo il servizio Web ProductDetails, per
permettere a un utente di recuperare i dettagli dei prodotti. Il form del servizio Web è navigabile,
quindi lo svilupperemo utiizzando il modello REST. Iniziamo creando un data contract per inviare
oggetti Product in rete.
Puoi avere accesso a un servizio Web REST da una applicazione client in maniera simile all'accesso
a un servizio Web SOAP, utilizzando un oggetto proxy che nasconde la complessità dell'invio di un
messaggio in rete da una applicazione client. Dato che Visual Studio non supporta la generazione
automatica di classi proxy per servizi Web REST, dovrai crearla manualmente. In più, non è buona
pratica duplicare codice tipo service contract tra servizi Web e relativi client perché potrebbe
renderne la manutenzione difficile. Per queste ragioni userai un differente approccio nella
creazione del tuo servizio Web.
Creazione dei data contrac per il servizio Web REST
1. Se utilizzi Visual Studio 2010 Standard o Visual Studio 2010 Professional, esegui le
operazioni che seguono per creare un nuovo progetto di libreria di classi:
1.1. In Visual Studio, nel menu File, scegli Nuovo, quindi fai clic su Progetto.
1.2. Nella finestra di dialogo Nuovo progetto, nel riquadro a sinistra, fai clic su Visual C# e
quindi Windows
1.3. Nel riquadro centrale, fai clic sul modello libreria di classi.
1.4. Nella casella di testo Nome, digita ProductDetailsContracts.
1.5. Nel campo Percorso, specifica\Microsoft Press\Visual CSharp Step By Step\Chapter 29
all’interno della cartella Documenti.
1.6. Fai clic su OK.
2. Se utilizzi Microsoft Visual C# 2010 Express, esegui i passaggi che seguono per creare un
nuovo progetto di libreria di classi.
2.1. Avvia Visual C# 2010 Express se non è già in esecuzione.
2.2. Nel menu File, fai clic su Nuovo progetto.
2.3. Nella finestra di dialogo Nuovo progetto, nel riquadro centrale seleziona il modello
Libreria di classi.
2.4. Nella casella di testo Nome, digita ProductDetailsContracts.
Capitolo 29
Creazione e utilizzo di un servizio Web
705
2.5. Fai clic su OK.
2.6. Nel menu File, fai clic su Salva ProductDetailsContracts.
2.7. Nel riquadro a destra della finestra di dialogo Salva progetto, nel campo del percorso,
specifica Microsoft Press\Visual CSharp Step By Step\Chapter 29 che si trova nella
cartella Documenti.
2.8. Fai clic su Salva.
3. Nel menu Progetto, fai clic su Aggiungi riferimento.
4. Nella finestra di dialogo Aggiungi riferimento, fai clic sulla scheda .NET. Seleziona gli insiemi
System.Data.Linq, System.ServiceModel, System.ServiceModel.Web, e System.Runtime.
Serialization e fai clic su OK.
5. In Esplora soluzioni, fai clic su Class1.cs e quindi Rinomina. Modifica il nome del file in
Product.cs. Quando richiesto, permetti a Visual Studio di cambiare tutti i riferimenti da
Class1 a Product
6. Fai doppio clic sul file Product.cs per visualizzarlo nella finestra dell’editor di codice e di
testo.
7. Nel file Product.cs, aggiungi le istruzioni using seguenti all’inizio dell’elenco:
using System.Runtime.Serialization;
using System.Data.Linq.Mapping;
8. Assegna alla classe Products gli attributi Table e DataContract come mostrato di seguito in
grassetto:
[Table (Name="Products")]
[DataContract]
public class Product
{
}
Per fare ciò, utilizzerai LINQ to SQL per recuperare i dati dal database Northwind. Ricordati
che l'attributo Table segna la classe come una classe Entity Nel database Northwind, la
tabella viene chiamata Products .
9. Aggiungi le proprietà qui mostrate in grassetto alla classe Product: Assicurati di anteporre a
ogni proprietà gli attibuti Column e DataMember. Alcune di queste proprietà sono nullable
[DataContract]
public class Product
{
[Column]
[DataMember]
public int ProductID { get; set; }
[Column]
[DataMember]
public string ProductName { get; set; }
[Column]
706
Parte VI
Creazione di soluzioni professionali con Visual Studio 2010
[DataMember]
public int? SupplierID { get; set; }
[Column]
[DataMember]
public int? CategoryID { get; set; }
[Column]
[DataMember]
public string QuantityPerUnit { get; set; }
[Column]
[DataMember]
public decimal? UnitPrice { get; set; }
[Column]
[DataMember]
public short? UnitsInStock { get; set; }
[Column]
[DataMember]
public short? UnitsOnOrder { get; set; }
[Column]
[DataMember]
public short? ReorderLevel { get; set; }
[Column]
[DataMember]
public bool Discontinued { get; set; }
}
La fase successiva consiste nel definire il service contract per il servizio Web IProductDetails.
Creazione del service contract per il servizio Web REST
1. Nel menu Progetto, fai clic su Aggiungi classe.
2. Nella finestra di dialogo Aggiungi classe, nel riquadro centrale seleziona il modello Classe.
Nella casella di testo Nome, digita IProductDetails.cs, e poi fai clic su Aggiungi.
3. Nella finestra dell’editor di codice e di testo relativa al file IProductDetails.cs, aggiungi le
istruzioni using che seguono all’elenco all’inizio del file:
using System.ServiceModel;
using System.ServiceModel.Web;
4. Cambia la classe IProductDetails in interfaccia pubblica e anteponi l'attributo ServiceContract
come mostrato di seguito in grassetto:
[ServiceContract]
public interface IProductDetails
{
}
5. Aggiungi la definizione del metodo GetProduct illustrato di seguito in grassetto,
all’interfaccia IProductsService:
Capitolo 29
Creazione e utilizzo di un servizio Web
707
[ServiceContract]
public interface IProductDetails
{
[OperationContract]
[WebGet(UriTemplate = "products/{productID}")]
Product GetProduct(string productID);
}
Il metodo GetProduct prende un product ID e restituisce un oggetto Product per il prodotto
che ha quell'ID. L'attributo OperationContract indica che questo metodo deve risultare come
metodo Web. (Se ometti l'attributo OperationContract, il metodo non è accessibile dalle
applicazioni client.) L'attributo WebGet indica che è una operazione logica di richiamo e il
parametro UriTemplate specifica il formato dell'URL che devi passare per richiamare questa
operazione, relativo all'indirizzo base del servizio Web. In questo caso specifica il seguente
URL per richiamare il prodotto con productID 7:
http://host/service/products/7
I termini host e service rappresentano, rispettivamente, l'indirizzo del tuo server Web e il
nome del servizio web. L'elemento racchiuso tra parentesi graffe dell'UriTemplate, denota
che il dato è passato come parametro del metodo GetProduct. L'identificatore fra parentesi
graffe deve coincidere con il nome del parametro.
6. Nel menu Compila, fai clic su Compila soluzione e verifica che venga compilato
correttamente. Il progetto crea un assembly chiamato ProductDetailsContracts.dll.
Ora che hai creato un assembly che definisce il data contract e il service contract per il servizio
Web, puoi sviluppare il servizio Web.
Creazione del servizio Web REST
1. Apri una nuova istanza di Visual Studio o Visual Web Developer Express.
Importante Non utilzzare l'istanza che hai usato per creare il servizio Web SOAP in
quanto questa copia di Visual Studio deve rimanere in esecuzione come server Web con il
servizio SOAP aperto.
2. Se utilizzi Visual Studio 2010 Professional Edition o Enterprise Edition, seleziona Nuovo nel
menu File, quindi fai clic su Sito Web.
3. Se utilizzi Microsoft Visual Web Developer 2010 Express Edition, fai clic su Nuovo sito Web
nel menu File.
4. Nella finestra di dialogo Nuovo Sito Web, fai clic sul modello Servizio WCF. Seleziona File
system nella casella di riepilogo a discesa Percorso e specifica la cartella \Microsoft Press\
Visual CSharp Step By Step\Chapter 29\ProductDetailsService nella tua cartella Documenti.
5. Fai clic sul progetto C:\...\ProductDetailsService\. Nella finestra Proprietà, imposta la
proprietà Utilizza porte dinamiche su False e imposta la proprietà Numero porta su 4600.
708
Parte VI
Creazione di soluzioni professionali con Visual Studio 2010
Nota È importante specificare una porta differente dal servizio Web
ProductInformationService; diversamente i due servizi Web vanno in conflitto.
6. Nel menu Sito Web, fai clic su Aggiungi riferimento. Nella finestra di dialogo Aggiungi
riferimento, fai clic sulla scheda Sfoglia. Nella barra degli strumenti clicca l'icona Livello
superiore e vai nella cartella ProductDetailsContracts\ProductDetailsContracts\bin\Debug;
seleziona l'assembly ProductDetailsContracts.dll e fai clic su OK.
7. In Esplora soluzioni, espandi la cartella App_Code, fai clic con il pulsante destro del mouse
sul file Service.cs e quindi seleziona Rinomina. Modifica il nome del file in ProductDetails.cs.
8. Nella cartella App_Code, cancella il file IService.cs. Questo file non serve al servizio Web
9. Fai doppio clic sul file ProductDetails.cs per visualizzarlo nella finestra dell’editor di codice e
di testo.
10. All’inizio del file, aggiungi le istruzioni using seguenti:
using System.Data.Linq;
using System.Data.SqlClient;
using ProductDetailsContracts;
11. Modifica la definizione della classe Service, cambiagli nome in ProductDetails in modo che
implementi l'interfacca IProductDetailse , come illustrato in grassetto di seguito: Rimuovi i
metodi GetData e GetDataUsingDataContract dalla classe ProductDetails.
public class ProductDetails : IProductDetails
{
}
12. Aggiungi il metodo GetProduct illustrato di seguito in grassetto, alla classe ProductDetails:
public class ProductDetails : IProductDetails
{
public Product GetProduct(string productID)
{
int ID = Int32.Parse(productID);
SqlConnectionStringBuilder builder =
new SqlConnectionStringBuilder();
builder.DataSource = ".\\SQLExpress";
builder.InitialCatalog = "Northwind";
builder.IntegratedSecurity = true;
DataContext productsContext =
new DataContext(builder.ConnectionString);
Product product = (from p in productsContext.GetTable<Product>()
where p.ProductID == ID
select p).First();
return product;
}
}
Capitolo 29
Creazione e utilizzo di un servizio Web
709
Il product ID viene passato al metodo come stringa, la prima istruzione lo converte in
integer e archivia il risultato nella variabile ID. Il codice, quindi, crea l'oggetto DataContext
che si connette al database Northwind. La query LINQ recupera tutte le righe the hanno un
product ID che è uguale a quello contenuto nella variabile ID. Dovrebbe esserci almeno un
prodotto. Normalmente bisogna eseguire delle iterazioni nei risultati restituiti da una query
LINQ to SQL per prelevare una riga per volta. Ma se c'è una sola riga puoi usare l'estensione
del metodo First per prelevare i dati immediatamente. L'oggetto Product recuperato dalla
query è restituito come risultato del metodo.
Il prossimo passo consiste nel configurare il servizio Web REST in modo che esponga la stringa
di connessione che l'assembly ProductDetailsContract deve utilizzare per collegarsi al database, e
quindi specificare il protocollo e l'endpoint che l'applicazione client può usare per comunicare con
il servizio Web.
Configurazione del servizio Web
1. In Esplora soluzioni, fai doppio clic sul file web.config per visualizzarlo nella finestra
dell’editor di codice e di testo.
2. Aggiungi l'elemento <services> e gli elementi figlio illustrati di seguito in grassetto,
all'elemento <system.serviceModel>, prima dell'elemento <behaviors>: Aggiungi anche
l'elemento <endpointBehaviors> anche esso illustrato in grassetto come elemento figlio
dell'elemento <behaviors>. Attenzione, devi assegnare il nome completo all'interfaccia che
assegna al service contract lo spazio dei nomi ProductDetailsContracts.
<?xml version="1.0"?>
<configuration>
<system.web>
<compilation debug="false" targetFramework="4.0" />
</system.web>
<system.serviceModel>
<services>
<service name="ProductDetails">
<endpoint address="" binding="webHttpBinding"
contract="ProductDetailsContracts.IProductDetails"
behaviorConfiguration="WebBehavior"/>
</service>
</services>
<behaviors>
<endpointBehaviors>
<behavior name="WebBehavior">
<webHttp/>
</behavior>
</endpointBehaviors>
<serviceBehaviors>
<behavior>
<!-- To avoid disclosing metadata information, set the value below
to false and remove the metadata endpoint above before deployment -->
<serviceMetadata httpGetEnabled="true"/>
<!-- To receive exception details in faults for debugging
purposes, set the value below to true. Set to false before deployment to
avoid disclosing exception information -->
710
Parte VI
Creazione di soluzioni professionali con Visual Studio 2010
<serviceDebug includeExceptionDetailInFaults="false"/>
</behavior>
</serviceBehaviors>
</behaviors>
</system.serviceModel>
</configuration>
Questo servizio Web usa un'associazione, webHttpBinding, diversa rispetto a quella usata
da ProductInformation. L'associazione webHttpBinding e il comportamento WebBehavior
indicano che il servizio Web attende le richieste in formato REST, codificate nell'URL, e che
trasmette le risposte in formato XML (POX).
3. In Esplora soluzioni, fai doppio clic sul file Service.svc per visualizzarlo nella finestra
dell’editor di codice e di testo. Aggiorna gli elementi Service e CodeBehind in modo che
facciano riferimento alla classe ProductDetails nel file ProductDetails.cs, come mostrato di
seguito in grassetto:
<%@ ServiceHost Language="C#" Debug="true" Service="ProductDetails"
CodeBehind="~/App_Code/ProductDetails.cs" %>
4. Nel menu Compila, fai clic su Compila Sita Web.
5. In Esplora soluzioni, fai clic con il pulsante destro su Service.svc e quindi su Visualizza nel
browser.
Si apre Internet Explorer e visualizza la pagine del servizio ProductDetails.
6. Nella barra dell'indirizzo, digita il seguente URL e premi Invio:
http://localhost:4600/ProductDetailsService/Service.svc/products/5
Questo URL richiama il metodo GetProduct nel servizio Web ProductDetails e specifica il
prodotto 5. Il metodo GetProduct preleva i dati del prodotto 5 dal database Northwind
e restituisce l'informazione come oggetto Product, serializzato in XML. Internet Explorer
dovrebbe visualizzare la rappresentazione in formato XML del prodotto.
Capitolo 29
Creazione e utilizzo di un servizio Web
711
7. Chiudi Internet Explorer.
Utilizzo del servizio Web REST ProductDetails
Hai visto che puoi richiamare un servizio Web REST Web abbastanza semplicemente da un
browser Web. Basta mettere l'URL appropriato. Per richiamare metodi in un servizio Web REST
Web da una applicazione, puoi costruire una classe proxy, simile a quella usata dall'applicazione
client che si connette a un servizio Web SOAP. Come detto prima, Visual Studio non supporta la
generazione automatica di classi proxy per servizi Web REST. Dovrai crearne una, e puoi usare la
stessa classe generica ClientBase che una classe proxy SOAP usa.
Nel corso dell'ultimo esercizio aggiungerai all'applicazione ProductClient la funzionalità che
permette la chiamata al metodo GetProduct nel servizio Web REST.
Chiamata al servizio Web REST dall'applicazione client
1. Ritorna all'istanza di Visual Studio o Visual C# Express utilizzata per creare il service contract
per il servizio Web REST.
2. Apri la soluzione ProductClient che si trova nella cartella \Microsoft Press\Visual CSharp Step
By Step\Chapter 29\ProductClient della cartella Documenti. È l'applicazione client creata più
indietro in questo capitolo per verificare il funzionamento del servizio Web SOAP.
3. Nel menu Progetto, fai clic su Aggiungi riferimento. Nella finestra di dialogo Aggiungi
riferimento, fai clic sulla scheda Sfoglia. Nella barra degli strumenti clicca due volte l'icona
Livello superiore e vai nella cartella ProductDetailsContracts\ProductDetailsContracts\bin\
Debug; seleziona l'assembly ProductDetailsContracts e fai clic su OK.
4. Nel menu Progetto, fai ancora clic su Aggiungi riferimento. Nella finestra di dialogo Aggiungi
riferimento, fai clic sulla scheda .NET. Seleziona l'insieme System.Data.Linq e fai clic su OK.
5. Nel menu Progetto, fai clic su Aggiungi classe. Nella finestra di dialogo Aggiungi nuovo
elemento -ProductClient, nel riquadro centrale seleziona il modello Classe. Nella casella di
testo Nome, digita ProductClientProxy.cs, e poi fai clic su Aggiungi.
6. Nella finestra dell’editor di codice e di testo relativa al file ProductClientProxy.cs, aggiungi le
istruzioni using che seguono all’elenco all’inizio del file:
using System.ServiceModel;
using ProductDetailsContracts;
7. Modifica la definizione della classe ProductClientProxy in modo che erediti dalla classe
generica ClientBase e implementi l'interfaccia IProductDetails. Definisci l'interfaccia
IProductDetails come parametro per la classe ClientBase. La classe ProductClientProxy
dovrebbe essere simile a quanto segue:
class ProductClientProxy : ClientBase<IProductDetails>, IProductDetails
{
}
712
Parte VI
Creazione di soluzioni professionali con Visual Studio 2010
8. Aggiungi il metodo GetProduct illustrato di seguito in grassetto, alla classe
ProductClientProxy: Questo metodo segue quanto detto per il proxy SOAP; inoltra le
richieste dal client al canale di comunicazione.
class ProductClientProxy : ClientBase<IProductDetails>, IProductDetails
{
public Product GetProduct(string productID)
{
return this.Channel.GetProduct(productID);
}
}
9. Visualizza il file ProductClient.xaml nella finestra Progettazione.
10. Fai doppio clic sul pulsante Get Product per generare il metodo del gestore evento
getProduct_Click per questo pulsante.
11. Nella finestra dell’editor di codice e di testo, aggiungi la seguente istruzione using all’elenco
nella parte superiore del file ProductClient.xaml.cs:
using ProductDetailsContracts;
12. Nel metodo getProduct_Click, aggiungi il codice mostrato di seguito in grassetto:
private void getProduct_Click(object sender, RoutedEventArgs e)
{
ProductClientProxy proxy = new ProductClientProxy();
try
{
Product product = proxy.GetProduct(productID.Text);
productName.Content = product.ProductName;
supplierID.Content = product.SupplierID.Value;
categoryID.Content = product.CategoryID.Value;
quantityPerUnit.Content = product.QuantityPerUnit;
unitPrice.Content = String.Format("{0:C}", product.UnitPrice.Value);
unitsInStock.Content = product.UnitsInStock.Value;
unitsOnOrder.Content = product.UnitsOnOrder.Value;
reorderLevel.Content = product.ReorderLevel.Value;
discontinued.IsChecked = product.Discontinued;
}
catch (Exception ex)
{
MessageBox.Show("Error fetching product details: " + ex.Message,
"Error", MessageBoxButton.OK, MessageBoxImage.Error);
}
finally
{
if (proxy.State == CommunicationState.Faulted)
{
proxy.Abort();
}
else
{
proxy.Close();
}
}
}
Capitolo 29
Creazione e utilizzo di un servizio Web
713
Questo codice crea una istanza della classe ProductClientProxy e la usa per chiamare
il metodo GetProduct nel servizio Web REST. I dati restituiti dall'oggetto Product sono
visualizzati nelle etichette nel form.
13. In Esplora soluzioni, fai doppio clic sul file app.config. Questo è il file di configurazione per
l’applicazione. È stato generato automaticamente quando hai creato il proxy del servizio
Web SOAP in un esercizio precedente. Contiene un elemento <system.serviceModel> che
descrive l'endpoint per il servizio Web SOAP, compreso l'URL al quale l'applicazione si deve
connettere.
14. Trova l'elemento <Client> e aggiungi l'elemento <endpoint>qui mostrato in grassetto
inserendolo prima della sezione <endpoint> già presente.
<client>
<endpoint address="http://localhost:4600/ProductDetailsService/Service.svc"
binding="webHttpBinding" contract="ProductDetailsContracts.IProductDetails"
behaviorConfiguration="WebBehavior">
</endpoint>
<endpoint address="http://localhost:4500/ProductInformationService/Service.svc"
binding="wsHttpBinding" bindingConfiguration="WSHttpBinding_
IProductInformation"
contract="ProductInformationService.IProductInformation"
name="WSHttpBinding_IProductInformation">
<identity>
<userPrincipalName value="YourComputer\YourName" />
</identity>
</endpoint>
</client>
15. Dopo il tag di chiusura </client> aggiungi la sezione <behaviors> qui mostrata in grassetto:
<client>
...
</client>
<behaviors>
<endpointBehaviors>
<behavior name="WebBehavior">
<webHttp />
</behavior>
</endpointBehaviors>
</behaviors>
Questo pezzo di codice definisce il comportamento WebBehavior. Specifica che il client
deve connettersi al servizio Web usando il comportamento webHttp atteso dal servizio Web
REST.
16. Nel menu Debug, fai clic su Avvia senza eseguire debug.
17. Appare il form Product Details. Digita 10 nella casella di testo Product ID, digita 5 nella
casella di testo How Many, quindi fai clic su Calculate Cost. Il costo totale dovrebbe essere
visualizzato (155 unità di valuta). Questo conferma che il servizio Web SOAP è in esecuzione.
18. Fai Clic su Get Product. A questo punto nel form sono visualizzati i dettagli su Ikura, come
mostrato nella seguente figura:
714
Parte VI
Creazione di soluzioni professionali con Visual Studio 2010
19. Fai delle prove con altri ID. Nota che se immetti un ID per un prodotto che non esiste, il
servizio Web restituisce un’eccezione.
20. Al termine, chiudi il form.
In questo capitolo, hai appreso come utilizzare Visual Studio 2010 per creare due differenti tipi di
servizio Web: SOAP e REST. Hai inotre visto come creare applicazioni che usufruiscono di questi
servizi Web.
Hai inoltre completato tutti gli esercizi di questo libro. Ora il linguaggio C# non dovrebbe
avere più segreti per te e dovresti saper utilizzare senza problemi Visual Studio 2010 per creare
applicazioni professionali. Tuttavia la tua avventura non termina qui! Hai superato il primo
ostacolo, ma i migliori programmatori C# si perfezionano esercitandosi continuamente e puoi fare
esperienza solo creando applicazioni C#. Nello sperimentare scoprirai nuovi metodi per utilizzare
il linguaggio C# e le numerosi funzionalità disponibili in Visual Studio 2010 che non è stato
possibile inserire in questo libero per ragioni di spazio. Ricorda inoltre che C# è un linguaggio in
evoluzione. Nel 2001, quando è stata redatta la prima edizione di questo libro, C# introduceva la
sintassi e la semantica necessaria per creare le applicazioni che utilizzavano .NET Framework 1.0.
Sono stati aggiunti alcuni miglioramenti a Visual Studio e a .NET Framework 1.1 nel 2003 poi, nel
2005, C# 2.0 si è affermato come supporto di applicazioni generiche e di .NET Framework 2.0.
C# 3.0 aggiunse numerose funzionalità come i tipi anonimi espressioni lambda e, segnatamente,
LINQ. Ora C# 4.0 ha esteso ulteriormente il linguaggio, supportando gli argomenti denominati,
parametri facoltativi, interfacce covarianti o controvarianti e l'integrazione con i linguaggi
dinamici. Che cosa porterà la nuova versione di C#? Lo scopriremo fra qualche tempo!
Capitolo 29
Creazione e utilizzo di un servizio Web
715
Riferimenti rapidi del capitolo 29
Obiettivo
Azione
Creare un servizio Web
Utilizza il modello del servizio WCF. Definisci un servizio contract che specifica i metodi Web esposti dal servizio Web creando un’interfaccia con
l’attributo ServiceContract. Contrassegna ogni metodo con l’attributo
OperationContract. Crea una classe che implementi questa interfaccia.
Creare un servizio Web
Utilizza il modello del servizio WCF. Definisci un servizio contract che specifica
i metodi Web esposti dal servizio Web creando un’interfaccia con l’attributo ServiceContract. Tagga ogni metodo con l'attributo OperationContract e
WebGet, che specifica il modello URI per richiamare il metodo. Crea una classe che implementi questa interfaccia.
Configura il servizio per usare l'associazione wsHttpBinding.
Configura il servizio per usare webHttpBinding, e specifica il comportamento
webHttp per l'endpoint del servizio.
Visualizzare la descrizione di un In Esplora soluzioni fai clic con il pulsante destro del mouse su il file .svc
servizio Web SOAP
e quindi fai clic su Visualizza nel browser. Internet Explorer si avvia, passa
all’URL del servizio Web e mostra una pagina in cui è descritto come creare
un’applicazione client che può accedere al servizio Web. Fai clic sul collegamento WSDL per visualizzare la descrizione WSDL del servizio Web.
Passare dati complessi come
parametri del metodo Web e
restituire i valori
Definisci una classe in cui inserire i dati e contrassegnali con l’attributo
DataContract. Assicurati che ogni elemento dei dati sia accessibile come campo public o attraverso una proprietà public che fornisce l’accesso get e set.
Assicurati che la classe abbia un costruttore predefinito (che potrebbe essere
vuoto).
Creazione di una classe proxy
per un servizio Web SOAP in
un’applicazione client.
Nel menu Progetto, fai clic su Aggiungi riferimento al servizio. Immetti l’URL
del servizio Web nella casella di testo Indirizzo nella parte superiore della finestra, quindi fai clic su Vai. Specifica lo spazio dei nomi per la classe proxy e
fai clic su OK.
Creazione di una classe proxy
per un servizio Web REST in
un’applicazione client.
Crea una classe che eredita dalla classe generica ClientBase, e specifica l'interfaccia che definisce service contract come parametro. Implementa questa
interfaccia e usa la proprietà Channel ereditata dalla classe ClientBase per
inviare richieste al servizio Web.
Richiamare un metodo Web.
Crea un’istanza della classe proxy. Richiama il metodo Web utilizzando la classe proxy.
Appendice
Interazione con i linguaggi dinamici
Gli argomenti trattati in questo capitolo sono:
■
Spiegazione delle funzione del Dynamic Language Runtime.
■
Utilizzo della parola chiave dynamic per fare riferimento a oggetti implementati utilizzando
i linguaggi dinamici e richiamare metodi di tali oggetti.
L’interoperabilità tra il codice scritto utilizzando i linguaggi gestiti ha rappresentato una
funzionalità chiave di Microsoft .NET Framework sin dagli inizi. L’idea è poter creare un
componente utilizzando il linguaggio che si preferisce, compilarlo in un assembly, fare riferimento
all’assembly dall’applicazione e poi accedere al componente dal codice nell’applicazione.
L’applicazione può essere costruita utilizzando un linguaggio diverso dal componente, non è un
problema. I compilatori di ogni linguaggio gestito (Visual C#, Visual Basic, Visual C++, Visual F#
e così via) convertono tutti il codice scritto utilizzando questi linguaggi in un altro linguaggio
chiamato MSIL, o Microsoft Intermediate Language. Quando l’applicazione viene eseguita,
il runtime .NET Framework converte il codice MSIL in istruzioni macchina e poi le esegue. Il
risultato è che .NET Framework di fatto non sa, o non si preoccupa, del linguaggio utilizzato
originariamente. Se lo desideri, puoi scrivere le applicazioni utilizzando MSIL invece che C#, anche
se sarebbe un vero peccato!
Tuttavia, non tutti i moderni linguaggi informatici vengono compilati. Esiste un esteso numero
di linguaggi di scripting interpretati attualmente in uso. I due esempi più comuni apparsi al di
fuori del dominio Microsoft sono Ruby e Python. Nelle precedenti versioni di .NET Framework, è
stato però sempre un problema incorporare codice scritto utilizzando linguaggi come questi nelle
applicazioni gestite, il risultato era spesso applicazioni difficili da comprendere e gestire. In .NET
Framework 4.0 il problema è stato risolto con il Dynamic Language Runtime, che verrà illustrato in
questa breve appendice.
Nota In questa appendice si presume che si conoscano Ruby o Python. L’intento non è
insegnare l’utilizzo di questi linguaggi. Inoltre, non sono presenti esercizi. Se desideri eseguire il
codice illustrato in questo capitolo, devi scaricare e installare le versioni più recenti di IronRuby o
IronPython dal sito Web CodePlex all’indirizzo http://www.codeplex.com (informazioni in lingua
inglese). IronPython e IronRuby sono le implementazioni complete dei linguaggi Python e Ruby
in cui sono incluse le estensioni che consentono di creare le istanze degli oggetti definiti in .NET
Framework. Sono completamente compatibili con le versioni open-source più recenti di questi
linguaggi, e puoi utilizzarli per eseguire script Python e Ruby esistenti non modificati.
717
718
Appendice
Introduzione a Dynamic Language Runtime
C# è un linguaggio fortemente tipizzato. Quando crei una variabile, specifichi il tipo di tale
variabile e puoi richiamare solo i metodi e accedere ai membri definiti da tale tipo. Se provi a
richiamare un metodo non implementato dal tipo, il codice non eseguirà la compilazione. Questo
è positivo perché individua un notevole numero di possibili errori sin dall’inizio, prima ancora di
eseguire il codice.
Tuttavia, questa tipizzazione forte diventa un problema se vuoi creare oggetti definiti da
linguaggi come Ruby e Python che vengono interpretati e non compilati. È molto difficile, se
non impossibile, che il compilatore C# riesca a verificare che tutti i membri a cui accedi nel
codice C# di fatto esistano in questi oggetti. Inoltre, se esegui una chiamata a un metodo su un
oggetto Ruby o Python, il compilatore C# non può verificare che hai passato il numero corretto di
parametri e che ogni parametro ha il tipo appropriato.
C’è un altro problema. I tipi definiti da C# e .NET Framework hanno una rappresentazione interna
diversa da quella utilizzata da Ruby e Python. Quindi, se richiami un metodo Ruby che restituisce
un numero intero, ad esempio, in qualche modo questo numero deve essere convertito dalla
rappresentazione utilizzata da Ruby a quella che si aspetta C#. Un problema simile si verifica se
passi un numero intero come parametro da un’applicazione C# a un metodo Ruby; il numero
intero deve essere convertito dalla rappresentazione C# a quella di Ruby.
Il processo di conversione dei dati da un formato all’altro è noto come marshaling; si tratta di un
vecchio problema noto agli sviluppatori che hanno dovuto creare applicazioni che richiamano
componenti COM. Una soluzione sarebbe l’utilizzo di livello intermedio. In .NET Framework 4.0,
tale livello viene chiamato Dynamic Language Runtime, o DLR.
Oltre al marshaling dei dati tra linguaggi, il DLR fornisce anche molti servizi forniti dal compilatore
quando si utilizza un linguaggio fortemente tipizzato. Ad esempio, quando richiami un metodo su
un oggetto Ruby o Python, il DLR verifica che tale chiamata al metodo sia valida.
Il DLR non è legato a un insieme specifico di linguaggio; implementa un’architettura basata su
associazioni del linguaggio, come illustrato nell’immagine seguente:
IronPython
IronRuby
C#
VB.NET
Altri
Dynamic Language Runtime
Python
Binder
Ruby
Binder
Object
Binder
JScript
Binder
COM
Binder
Python
Ruby
Microsoft
.NET
Microsoft
Silverlight
Microsoft
Office
Appendice
719
Un gestore di associazioni è un componente che si inserisce nel DLR e capisce come richiamare
i metodi in un linguaggio specifico e come eseguire il marshaling o meno dei dati tra il formato
atteso dal linguaggio e .NET Framework. Il gestore di associazioni esegue inoltre una certa
quantità di verifiche, ad esempio verifica che un oggetto di fatto esponga un metodo che è stato
richiamato e che i parametri e i tipi restituiti siano validi.
.NET Framework 4.0 fornisce gestore di associazioni per IronPython, IronRuby, COM (che puoi
utilizzare per accedere ai componenti COM, ad esempio quelli di Microsoft Office) e Jscript, oltre
che per .NET Framework stesso. Inoltre, .NET Framework 4.0 consente di scrivere dei propri gestori
di associazioni per altri linguaggi utilizzando i tipi e le interfacce dello spazio dei nomi System.
Dynamic (i dettagli in merito esulano dallo scopo di questa appendice). Inoltre, IronPython e
IronRuby possono utilizzare il DLR per accedere agli oggetti creati utilizzando altre tecnologie e
altri linguaggi.
Il DLR esegue le sue operazioni in fase di esecuzione. Ciò significa che ogni verifica del tipo per gli
oggetti che fa riferimento al DLR viene ritardata fino a quando non viene eseguita l’applicazione.
Come si può indicare in un’applicazione C# che la verifica del tipo per un oggetto deve essere
ritardata in questo modo? La risposta è la parola chiave dynamic.
La parola chiave dynamic
La parola chiave dynamic è nuova in C# 4.0. Viene utilizzata esattamente come si usa un tipo.
Ad esempio, la seguente istruzione crea una variabile chiamata rubyObject utilizzando un tipo
dynamic:
dynamic rubyObject;
Di fatto non esiste una cosa come il tipo dynamic in C#. Tutto ciò che fa questa istruzione è
creare una variabile di tipo object, ma con la verifica del tipo ritardata fino all’esecuzione. Puoi
assegnare un valore a questa variabile e utilizzarla per richiamare metodi. In fase di esecuzione,
il DLR utilizza il gestore di associazioni appropriato per convalidare il codice, creare un’istanza
degli oggetti e richiamare i metodi. I dettagli interni del DLR sono soggetti a modifica, quindi
discuterne il funzionamento va oltre lo scopo di questa appendice. Basta dire che il DLR sa come
richiamare un gestore di associazioni per creare oggetti, richiamare metodi ed eseguire o meno il
marshaling dei dati.
In questo caso è necessario un piccolo avvertimento. Dal momento che la verifica del tipo è
ritardata fino all’esecuzione, Visual Studio IntelliSense non può aiutarti fornendo i nomi dei
membri esposti tramite un riferimento a un oggetto dinamico. Se tenti di richiamare un metodo
non valido o di fare riferimento a un campo inesistente in un oggetto dinamico, non lo saprai fino
all’esecuzione, quando verrà generata un'eccezione RuntimeBinderException.
720
Appendice
Esempio: IronPython
Nel seguente esempio viene mostrato uno script Python chiamato CustomerDB.py. Questa classe
contiene quattro elementi:
■ Una classe denominata Customer. Questa classe contiene tre campi, contenenti l’ID, il nome
e il numero di telefono del cliente. Il costruttore inizializza questi campi con i valori passati
come parametri. Il metodo __str__ formatta i dati nella classe come stringa in modo che
possa essere generato un output.
■ Una classe denominata CustomerDB. Questa classe contiene un dizionario chiamato
customerDatabase. Il metodo storeCustomer aggiunge un cliente a questo dizionario,
mentre il metodo getCustomer recupera un cliente quando viene fornito l’ID cliente. Il
metodo __str__ esegue un’iterazione tra i clienti nel dizionario e li formatta come stringa.
Per semplicità, nessuno di questi metodi include un form di verifica degli errori.
■ Una funzione chiamata GetNewCustomer. È un metodo di fabbrica che costruisce un
oggetto Customer utilizzando i parametri passati e poi restituisce questo oggetto.
■ Una funzione chiamata GetCustomerDB. Si tratta di un altro metodo di fabbrica che
costruisce un oggetto CustomerDB e lo restituisce.
class Customer:
def __init__(self, id, name, telephone):
self.custID = id
self.custName = name
self.custTelephone = telephone
def __str__(self):
return str.format("ID: {0}\tName: {1}\tTelephone: {2}",
self.custID, self.custName, self.custTelephone)
class CustomerDB:
def __init__(self):
self.customerDatabase = {}
def storeCustomer(self, customer):
self.customerDatabase[customer.custID] = customer
def getCustomer(self, id):
return self.customerDatabase[id]
def __str__(self):
list = "Customers\n"
for id, cust in self.customerDatabase.iteritems():
list += str.format("{0}", cust) + "\n"
return list
def GetNewCustomer(id, name, telephone):
return Customer(id, name, telephone)
Appendice
721
def GetCustomerDB():
return CustomerDB()
Il seguente esempio di codice mostra una semplice applicazione console C# che testa questi
elementi. Puoi trovare questa applicazione nella cartella \Microsoft Press\Visual CSharp Step By
Step\Appendix\PythonInteroperability all’interno della tua cartella Documenti. Fa riferimento agli
assembly IronPython, che forniscono il binding del linguaggio per Python. Questi assembly sono
inclusi con il download di IronPython e non fanno parte della libreria di classi .NET Framework.
Nota Questa applicazione di esempio è stata creata utilizzando la versione di IronPython
più aggiornata nel momento in cui è stato scritto il libro. Se hai una versione di IronPython
successiva, devi sostituire i riferimenti agli assembly IronPython e Microsoft.Scripting in questa
applicazione con quelli forniti con la nuova installazione di IronPython.
Il metodo statico CreateRuntime della classe Python crea un’istanza del runtime Python. Il metodo
UseFile del runtime Python apre uno script contenente codice Python e rende accessibili gli
elementi presenti in tale script.
Nota In questo esempio, lo script CustomerDB.py si trova nella cartella Appendix,
ma l’eseguibile viene generato nella cartella Appendix\PythonInteroperability\
PythonInteroperability\bin\Debug, che considera il percorso allo script CustomerDB.py mostrato
nel parametro per il metodo UseFile.
In questo codice, nota che le variabili pythonCustomer e pythonCustomerDB fanno riferimento ai
tipi Python, quindi vengono dichiarate come dynamic. La variabile python utilizzata per richiamare
le funzioni GetNewCustomer e GetCustomerDB viene dichiarata anche come dynamic. In realtà, il
tipo restituito dal metodo UseFile è un oggetto Microsoft.Scriping.Hosting.ScriptScope. Tuttavia,
se dichiari la variabile python utilizzando il tipo ScriptScope, il codice non verrà generato perché il
compilatore, correttamente, rileva che il tipo ScriptScope non contiene le definizioni per i metodi
GetNewCustomer e GetCustomerDB. Specificando dynamic il compilatore ritarda la verifica
all’esecuzione del DLR, e in questo lasso di tempo la variabile python fa riferimento a un’istanza di
uno script Python, che include queste funzioni.
Il codice richiama la funzione Python GetNewCustomer per creare un nuovo oggetto Customer
con i dettagli per Fred. Poi chiama GetCustomerDB per creare un oggetto CustomerDB e richiama
il metodo storeCustomer per aggiungere Fred al dizionario nell’oggetto CustomerDB. Il codice
crea un altro oggetto Customer per un cliente chiamato Sid, e aggiunge anche tale cliente
all’oggetto CustomerDB. Infine, il codice visualizza l’oggetto CustomerDB. Il metodo Console.
WriteLine si aspetta una rappresentazione stringa dell’oggetto CustomerDB.
Di conseguenza, il runtime Python richiama il metodo __str__ per generare tale rappresentazione,
mentre l’istruzione WriteLine visualizza un elenco dei clienti trovati nel dizionario Python.
722
Appendice
using System;
using IronPython.Hosting;
namespace PythonInteroperability
{
class Program
{
static void Main(string[] args)
{
// Creating IronPython objects
Console.WriteLine(“Testing Python”);
dynamic python =
Python.CreateRuntime().UseFile(@”..\..\..\..\CustomerDB.py”);
dynamic pythonCustomer = python.GetNewCustomer(100, “Fred”, “888”);
dynamic pythonCustomerDB = python.GetCustomerDB();
pythonCustomerDB.storeCustomer(pythonCustomer);
pythonCustomer = python.GetNewCustomer(101, “Sid”, “999”);
pythonCustomerDB.storeCustomer(pythonCustomer);
Console.WriteLine(“{0}”, pythonCustomerDB);
}
}
}
L’immagine che segue mostra i risultati generati da questa applicazione.
Esempio: IronRuby
Per completezza, il codice seguente mostra uno script Ruby chiamato CustomerDB.rb, contenente
le classi e le funzioni che esibiscono funzionalità simili a quelle dello script Python mostrato
precedentemente.
Nota Il metodo to_s in una classe Ruby restituisce una rappresentazione stringa di un oggetto,
come il metodo __str__ in una classe Python.
Appendice
class Customer
attr_reader :custID
attr_accessor :custName
attr_accessor :custTelephone
def initialize(id, name, telephone)
@custID = id
@custName = name
@custTelephone = telephone
end
def to_s
return “ID: #{custID}\tName: #{custName}\tTelephone: #{custTelephone}”
end
end
class CustomerDB
attr_reader :customerDatabase
def initialize
@customerDatabase ={}
end
def storeCustomer(customer)
@customerDatabase[customer.custID] = customer
end
def getCustomer(id)
return @customerDatabase[id]
end
def to_s
list = “Customers\n”
@customerDatabase.each {
|key, value|
list = list + “#{value}” + “\n”
}
return list
end
end
def GetNewCustomer(id, name, telephone)
return Customer.new(id, name, telephone)
end
def GetCustomerDB
return CustomerDB.new
end
723
724
Appendice
Il seguente programma C# utilizza questo script Ruby per creare due oggetti Customer, salvarli
in un oggetto CustomerDB e stampare i contenuti dell'oggetto CustomerDB. Funziona come
l’applicazione di interoperabilità Python descritta nella sezione precedente, inoltre utilizza
il tipo dynamic per definire le variabili per lo script Ruby e gli oggetti Ruby. Puoi trovare
questa applicazione nella cartella \Microsoft Press\Visual CSharp Step By Step\Appendix\
RubyInteroperability all’interno della tua cartella Documenti. Fa riferimento agli assembly
IronRuby, che forniscono il binding del linguaggio per Ruby. Tali assembly fanno parte del
download di IronRuby.
Nota Questa applicazione di esempio è stata creata utilizzando la versione di IronRuby più
aggiornata nel momento in cui è stato scritto il libro. Se hai una versione di IronRuby successiva,
devi sostituire i riferimenti agli assembly IronRuby, IronRuby.Libraries e Microsoft.Scripting in
questa applicazione con quelli forniti con la nuova installazione di IronRuby.
using System;
using IronRuby;
namespace RubyInteroperability
{
class Program
{
static void Main(string[] args)
{
// Creating IronRuby objects
Console.WriteLine(“Testing Ruby”);
dynamic ruby =
Ruby.CreateRuntime().UseFile(@”..\..\..\..\CustomerDB.rb”);
dynamic rubyCustomer = ruby.GetNewCustomer(100, “Fred”, “888”);
dynamic rubyCustomerDB = ruby.GetCustomerDB();
rubyCustomerDB.storeCustomer(rubyCustomer);
rubyCustomer = ruby.GetNewCustomer(101, “Sid”, “999”);
rubyCustomerDB.storeCustomer(rubyCustomer);
Console.WriteLine(“{0}”, rubyCustomerDB);
Console.WriteLine();
}
}
}
La seguente immagine visualizza l’output di questo programma:
Appendice
725
Riepilogo
Questa appendice ha fornito una breve introduzione all’utilizzo del DLR per integrare codice
scritto con linguaggi di scripting come Ruby e Python in un’applicazione C#. Il DLR fornisce un
modello estensibile in grado di supportare ogni linguaggio o tecnologia che abbia un gestore
di associazioni. Puoi scrivere un gestore di associazioni utilizzando i tipi nello spazio dei nomi
System.Dynamic.
Il linguaggio C# include il tipo dinamico. Quando dichiari una variabile come dinamica, la verifica
del tipo di C# viene disattivata per tale variabile. Il DLR esegue la verifica del tipo in fase di
esecuzione, invia le chiamate ai metodi ed esegue il marshaling dei dati.
Indice analitico
Simboli
–– operatore, 44, 425
%= operatore, 37, 92
* operatore, 36, 170
*= operatore, 92
.csproj, suffisso, 33
.dll, estensione del nome del file, 16
/= operatore, 92
? modificatore, 157, 171, 174
++ operatore, 43, 425
+= operatore di assegnazione
composto, 92, 331, 343
<Add New Event>, comando, 476
<New Event Handler>, comando, 472,
485, 492
<Style.Triggers>, elemento, 456
–= operatore di assegnazione
composto, 92, 332, 344
A
about metodi evento, 488–489
abstract, parola chiave, 270, 276, 277
accedere ai dati
thread-safe, 670–682
accessibilità
vecchi campi e metodi, 132–133
accesso protetto, 242
accesso sincronizzato, 659
accesso, protetto, 242
accessori, get e set, 298
Action, 630
Action, delegati, 604–605
creazione, 508
richiamare, 632
adattatori per metodo, 339
Add Window,comando, 457
Add, metodo, 208, 214, 217, 587
add_Click, metodo, 472
AddCount, metodo, 664
AddExtension, proprietà, 496
AddObject, metodo, 596
AddParticipant, metodo, 666
addValues, metodo, 50, 52
ADO.NET Entity Data Model, 566,
569, 596
ADO.NET Entity Framework, 566–583
ADO.NET, 535
connessione a un database, 564
LINQ to SQL, 549
query di un database, 535–548, 564
ADO.NET, libreria di classi, 535
AggregateException, classe, 642–644
Handle, metodo, 642
AggregateException, eccezioni, 647
AggregateException, gestore,
642–644
algoritmo geometrico, 670–672
ambito
applicazione, 53–56
definizione, 54
for, istruzioni, 98–99
risorse statiche, 454
stili, 453
ambito locale, definizione, 54
AND (&), operatore, 316
annullamento cooperativo, 632–637
annullamento, 632–645
di query PLINQ, 656
primitivi di sincronizzazione e, 668
API, 300
App.config (application configuration)
file, 8, 573
stringhe di connessione, 572, 573
App.xaml files, codice, 24
application programming interfaces,
API, 330
Application, oggetti, 457
Application.xaml.cs, file, 24
ApplicationException, eccezioni, 517
Applicazione console, icona, 5, 6
Applicazione console, modello, 7
applicazione Ticket Ordering
associazione del controllo casella di
testo alla proprietà della classe,
515–518
convertitore di classi e metodi,
creazione, 523–525
esaminare, 511–512
livello di privilegi e numero di
ticket, convalida, 520–522
TicketOrder, classe con logica di
convalida, 514–515
visualizzazione del numero di
ticket, 512–514
applicazioni
build, 26
esecuzione, 26
multitasking, 602–628
parallelizzazione, 603
risposta, 498–507
Applicazioni console
assembly, riferimenti, 16
creazione, 8–14, 26
definizione di, 3
file creati con Visual Studio, 8–9
applicazioni database
associazione dei dati, stabilire,
579–582
interfaccia utente per, 574–579
recupero delle informazioni,
579–582
applicazioni desktop Vedere anche
applicazioni
multitasking, 602–628
applicazioni grafiche
creazione, 17–26
visualizzazioni di, 17
approccio mutlithreaded, 600–601
Apri file, finestra di dialogo, 498
Apri, finestra di dialogo, 94
Architetture dei servizi WEB, 684
argomenti
denominati, ambiguità, 66–71
di posizione, 66
modifica, 159–162
nei metodi, 52
omissione, 66
passaggio ai metodi, 159
argomenti array, 220–226
argomenti denominati, 66
ambiguità con, 66–71
argomenti int, somma, 224–226
ArgumentException, classe, 220
727
728
ArgumentException, eccezione
ArgumentException, eccezione, 205,
225
argumentList, 51
ArgumentOutOfRangeException,
classe, 121
aritmetica a virgola mobile, 119
array associativi, 212
array valori, 212
array, 191–206
applicazione card playing, 199–206
array a lunghezza nulla, 223
array params, 219–220
associativa, 212
celle, 198
copia, 197–198
di oggetti, 207
di variabili int, 207
dimensione di, 192–193
inizializzazione elementi di, 218
inserimento di elementi, 208
iterazione, 195–197, 218
lunghezza, 218
matrici di chiavi, 212, 213
multidimensionalità, 198–199
ridimensionamento, 208
rimozione di elementi da, 208
tipizzato in modo implicito,
194–195
vs. insiemi, 214
ArrayList, classe, 208–209, 217
numero di elementi, 209
AsOrdered, metodo, 655
AsParallel, metodo, 650, 681
specificazione, 652
AssemblyInfo.cs, file, 8
Association, attributo, 555
associatività, 42
di operatori booleani, 76–77
di operatori di assegnazione, 42
associazione dei dati
associazione controlli a proprietà
classe, 515
associazione controlli WPF a origini
dati, 580
associazione proprietà controllo
a proprietà controllo, 513,
525–526, 531
associazione proprietà controllo a
proprietà oggetto, 531
classi convertitrici, 522–525
dati esistenti, aggiornamento con,
583–584
Entity Framework, utilizzo con,
579–582
interrogazione e visualizzazione dei
dati con, 579–583
modifica dei dati con, 583–596
origini di associazione, 518, 531
per convalida, 511–527
asterisco (*), operatore, 36
at (@), simbolo, 542
attività a lunghe operazioni
esecuzione in thread multipli,
499–502
simulazione, 498–499
attività concorrenti
prestazioni non prevedibili di,
656–659
sincronizzare l’accesso alle risorse
659
attività parallele, 600, 647
attività, 603–604
annullamento, 632–645
astrazione, 617–624
attesa, 616, 646
continuazione di, 606–608, 645,
646
coordinamento, 649
creazione, esecuzione e controllo,
604–608, 646
gestione delle eccezioni, 641–644,
647
parallelo, 600, 647
pianificazione, 606–607
restituzione dei valori da, 624–628,
646
sincronizzazione, 608, 615–617
stato di, 638, 640, 644
thread interfaccia utente e,
628–632
token di annullamento, 633
attributi del progetto, aggiunta, 8
attributi, classe, 523
autenticazione di Windows per
accedere ai database, 540, 541
Avvia debug, comando, 13
Avvia senza eseguire debug, comando,
13
azioni a cascata associate alle
istruzioni if, 79–80, 84
B
BackgroundWorker, classe, 504
barra (/), operatore, 36
barra degli strumenti Debug, 61
visualizzazione, 61, 72
barra di stato, visualizzazione dello
stato delle operazioni di
salvataggio, 505–507
barra rovesciata (\), 88
Barrier, classe, 666–667
Barrier, costruttore, specifica dei
delegati, 667
Barrier, oggetti, 681
base, parola chiave, 234, 239
BeginInvoke, metodo, 630
BellRingers, progetto, 444–476
GUI applicazione, 444
bin, cartella, 13
Binding, oggetti, 526
Binding.ValidationRules, elementi, 532
elementi figlio, 516
BindingExpression, classe
HasError, proprietà, 529–530, 532
UpdateSource, metodo, 529
BindingExpression, oggetti, 532
creazione, 529
BindingOperations, classe, GetBinding,
metodo, 526
Black.Hole, metodo, 223
blocchi di istruzioni unchecked, 119
blocchi di istruzioni, 78–79
virgolette, 98
blocchi di lettura, 661, 665
blocchi di scrittura, 661, 665
blocchi esclusivi, 661
blocco, 659–661. Vedere
anche primitivi di
sincronizzazione
overhead di, 661
serializzazione delle chiamate del
metodo con, 679–680
BlockingCollection<T>, classe, 669
BlockingCollection<T>, oggetti, 670
bool, parola chiave, 89
bool, tipo di dati, 32, 74
boxing, 165–166
break, istruzioni, 85
nelle istruzioni switch, 87
per interrompere loop, 99
prevenzione del passaggio, 86
Button, classe, 345
Button.Resources, proprietà, 451–452
ButtonBase, classe, 345
C
C#
COM, interoperabilità, 64
compilazione del codice, 11
classi
coppie di caratteri, 10
distinzione tra maiuscole e
minuscole, 9
IntelliSense e, 9
ruolo in .NET, 3
stile, 28
C#, file di progetto, 8
C#, file sorgente (Program.cs ), 8
C#, parole chiave. Vedere anche
parole chiave
equivalenti .NET, 179
IntelliSense, 9
calcolo dei valori integer, 118–120,
126
calcolo dei valori integer, algoritmo,
102
calcolo lambda, 340
calculateClick, metodo, 52–53
camelCase, notazione, 30, 133
per nomi di metodi, 48
campi condivisi, 143–144
campi privati, 132–133, 242, 298
aggiunta, 139
campi pubblici, 132–133, 242
campi readonly, 200
campi statici privati, scrittura, 145
campi statici, 143–144
const, parola chiave, 144
dichiarazione, 149
scrittura, 145
campi, 54–55, 129
campi condivisi, 143–144
convezioni di denominazione, 133
definizione di, 131
ereditarietà, 234–235
inizializzazione, 133, 139, 140
statici e non statici, 143–144. Vedere
anche campi statici
CanBeNull, parametro, 550
Canceled, attività nello stato di, 638,
641
CancelEventArgs, classe, 475
CancellationToken, oggetti, 633
specificazione, 643, 656
ThrowIfCancellationRequested,
metodo, 640–641
CancellationTokenSource, oggetti,
Cancel metodo, 668
carattere di escape (\), 88
carattere di nuova riga (‘\n’), 95
caratteri, lettura di un flusso di, 95
cartella Debug, 13
cartella Documenti, 6
cartella Proprietà, 8
case, etichette, 85
passaggio e, 86
regole di utilizzo, 86
case, parola chiave, 85
case, utilizzo nei nomi identificatori,
30
Casella degli strumenti
Controlli WPF comuni, sezione, 19
sezione Tutti i Controlli, 19
visualizzazione, 19
caselle di testo
pulizia, 101
visualizzazione elementi in, 34–35
cast dei dati, 167–171, 175
catch, gestori, 110
multipli, 112–113
ordine di precedenza, 114
scrittura, 117, 126
sintassi, 111
catch, parola chiave, 110
celle negli array, 198
char, tipo di dati, 32, 542
checked, espressioni, 119–120
checked, istruzioni, 118
checked, parola chiave
chiamata del metodo completata,
notifica della, 630
chiamate dei metodi
esaminare, 52–53
memoria necessaria per, 163
parallelizzazione, 618
parametri facoltativi vs. elenchi
parametri, 227–229
parentesi in, 51
serializzazione, 679–680
sintassi, 51–53
cicli
iterazioni indipendenti, 624
parallelizzazione, 618–621, 623, 647
Circle, classe, 130–131
NumCircles, campo, 143–144
Class, attributo, 445
class, metodi, 144
class, parola chiave, 130, 149
classe generalizzata, 357
Classi Abstract, 232, 253, 269–271
creazione, 272–274, 277
classi anonime, 147–148
classi base, 232–234. Vedere
anche ereditarietà
membri protetti della classe di, 242
prevenire l’utilizzo di classi come,
271–272, 277
729
classi Common Dialog, 94
classi convertitrici, 578
creazione, 523–525
per l’associazione dei dati, 522
classi derivate, 232–234. Vedere
anche ereditarietà
costruttori di classe base, chiamata,
234–235, 251
creazione, 251
classi di insiemi thread-safe, 668–670
classi di insiemi, 206–217
applicazione card playing,
implementazione, 214–217
ArrayList, classe, 208–209
Queue, classe, 210
SortedList, classe, 213
Stack, classe, 210–211
thread-safe, 668–670
classi entità
codice per, 572
convezioni di denominazione, 556
creazione con Entity Framework,
596
definizione, 549–551, 560–562, 564
EntityCollection<Product>,
proprietà, 577
ereditarietà da, 572
generazione di, 571
modifica delle proprietà di, 571–572
relazioni tabelle, definizione in,
554–555
tabelle del database, relazioni tra,
551–552
classi generiche, 358–370
classi gerarchiche, definizione,
242–247
classi parziali, 136
classi provider di dati per ADO.NET,
539
classi sigillate, 232, 271–277
creazione, 277
classi statiche, 144–145, 248
classi, 129
accessibilità di campi e metodi,
132–142
attributi di, 523
Classi Abstract, 232, 269–274
classi anonime, 147–148
classi base, 232–234
classi derivate, 232–234
classi di insiemi, 206–217, 668–670
classi generiche, 358–370
classi parziali, 136
classi sigillate, 232, 271–277
730
classificazione
classi statiche, 144–145
classificazione e, 129–130
con più interfacce, 257
convezioni di denominazione, 133
corpo di, 131
costruttori per, 133–134. Vedere
anche costruttori
definizione di, 132
definizione, 130–132
dichiarazione, 149
ereditarietà dalle interfacce,
255–256
incapsulamento in, 130
interfacce, implementazione,
261–266
istanze di, assegnazione, 131
metodo delle combinazioni di
parole chiave, 276
modellazione di entità con,
513–515
negli insiemi, 16
nuovo, aggiunta, 243
riferimento attraverso le interfacce,
256–257
testing, 266–269
vs. strutture, 181–182, 188–190
classificazione, 129–130
ereditarietà e, 231–232
clear_Click, metodo, 471
clearName_Click, metodo, 492
Click, eventi, 25, 345
Clone, metodo, 198
Closing, gestore evento, 474–476
CLS (Common Language
Specification), 30
code, 353
creazione, 669
codice compilato, 14
riferimento a, 8
codice esente da eccezioni, 289–292
codice non sicuro, 170
codice per .MathsOperators, 39–41
codice sorgente, 8
codice. Vedere anche flusso di
esecuzione
compilato, 8, 14
compilazione, 11
esente da eccezioni, 289–292
gestione degli errori, separazione,
110
nei form WPF, visualizzazione, 476
operazioni di elaborazione,
621–623
refactoring, 60, 270
verificare, 110–117
codici caratteri, 102
Collect, metodo, 283
Colors, enumerazione, 268
Column, attributo, 550, 564
Command, classe, 542
Command, oggetti, 542
CommandText, proprietà, 542, 564
commentare un blocco di codice, 414
commenti su più righe, 11
commenti, 11
commenti su più righe, 11
Common Language Specification
(CLS), 30
Compare, metodo, 84, 377
CompareTo, metodo, 362
Compila soluzione, comando, 11, 12
compilatore
commenti e, 11
risoluzione di chiamata del metodo,
66–71, 226–228
compilazione del codice, 11, 14
Complex, oggetti, 432
Component Object Model (COM) e
interoperabilità C#, 64
componente protetto della classe,
accesso a, 242
CompositeType, classe, 691
concorrenza ottimistica, 584–585
concorrenza pessimistica, 585
Concurrency Mode, proprietà, 585
ConcurrentBag<T>, classe, 669,
678–679
overhead di, 679
ConcurrentDictionary<TKey, TValue>,
classe, 669
ConcurrentQueue<T>, classe, 669
ConcurrentStack<T>, classe, 669
condizioni di proseguimento, 606–
608, 645, 646
confrontare due oggetti, 377
confronto tra stringhe, 378
Connection, classe, 539
Connection, proprietà, 564
ConnectionString, proprietà, 540, 564
connessioni database
apertura, 540
chiusura, 545–547, 553
creazione, 569–570
logica per, 572
pool di connessione, 547
Console, classe, 9
Console.Write, metodo, 58
Console.WriteLine, metodo, 186, 219,
224
chiamate, 137–138
const, parola chiave, 144
consumer, 669
Content, proprietà, 20, 459, 469
ContextMenu, elementi, 491
aggiunta, 508
ContextMenu, proprietà, 494
continue, istruzioni, 100
ContinueWith, metodo, 606, 645, 646
Control, classe, Setter, elementi, 456
controlli
aggiunta ai form, 459–461, 476
allineamento, 460
Content, proprietà, 469
convalida e, 509, 518, 527
impostazione ai valori predefiniti,
466–470
indicatori di allineamento, 21
IsChecked, proprietà, 476
Name, proprietà, 452
ordine z di, 451
proprietà, impostazioni, 449, 476
proprietà, modifica, 20
punti di ancoraggio, 447
Resources, elementi, 452
ridimensionamento, 21
rimozione dai form, 459
riposizionamento, 19
stile di, 451–457, 464–466
TargetType, attributo, 454–456
testo, proprietà, 457
ToolTip, proprietà di, 532
visualizzazione, 40
WPF, controlli, 458–459
controlli casella di testo
aggiunta, 21, 455, 459–460
associazione a proprietà classe,
515–518
scelta rapida per, 491–492
scelta rapida predefinita per, 491
controlli check box, 458
aggiunta, 460
inizializzazione, 476
IsChecked, proprietà, 473
controlli combo box, 458
aggiunta, 460
popolare, 476
controlli immagine, aggiunta,
449–451
controlli list box, 459
aggiunta, 461
dati
popolare, 476
controlli pulsante
aggiunta, 21
ancoraggio, 447
Gestori di evento Click, 471–474
passaggio del mouse,
comportamenti, 456–457
proprietà Width e Height, 449
controllo incrociato dei dati, 509–510
controllo pannello
DockPanel, controlli, 479, 481, 508
Grid, pannelli, 446, 447
ordine z, 451
riquadri di layout, 446
StackPanels, 446, 460, 476
WrapPanels, 446
controvarianza, 377
convalida
con l’associazione dei dati, 511–532
controllo programmatico di, 532
convalida dell’input, 509–510
explicit, 528–531
tempismo durante l’esecuzione,
518, 527–531
testing, 526–527, 530–531
convalida dei dati, 509–532
convalida dell’input, 509–510
convenzioni di denominazione
pubblico/privato, 298
conversione narrowing, 435
conversione widening, 434
Convert, metodo, 578
ConvertBack, metodo, 523, 524
convertitori, metodi, 522–523
creazione, 523–525
convezioni di denominazione
per campi e metodi, 133
per classi entità, 556
per identificatori non pubblici, 133
per identificatori, 237–238
per interfacce, 255
per variabili di array, 192
copia
variabili di struttura, 187
variabili tipo valore e riferimento,
189
coppie chiave/valore, 356
come tipi anonimi, 214
in Hashtables, 212
in SortedLists, 213
coppie di caratteri, 10
Copy, metodo, 198
CopyTo, metodo, 197
corto circuito, 76
costruttori di classe base, chiamata,
234–235, 251
costruttori non predefiniti, 134
costruttori predefiniti, 133–135
in classi statiche, 144
scrittura, 138
strutture e, 181, 184–185
costruttori struttura, 183
costruttori, 133–134
chiamate, 149
codice per abilitare il menu di scelta
rapida in, 493–494
costruttori di classe base, 234–235
definizione di, 23
dichiarazione, 149
inizializzazione di campi con, 139
inizializzazione di oggetti con, 279
ordine di definizione, 135
overload, 134–135
per strutture, 183, 185
predefinito, 133–134, 135
privato, 134
scrittura, 137–140
Count, metodo, 403
Count, proprietà, 209, 218
CountdownEvent, oggetti, 681
covarianza, 376
CREATE TABLE, istruzioni, 549
Created, attività nello stato di, 638
CreateDatabase, metodo, 551
CreateXXX, metodo, 587, 596
CurrentCount, proprietà, 663
cursori firehose, 544
cursori, (set di righe), 544
D
database
accesso con autenticazione di
Windows, 540, 541
aggiornamento dati, 583–596
aggiunta ed eliminazione dei dati,
587–594, 596
concessione dell’accesso a, 567–568
connessione a, 538–540, 564
connessioni concorrenti, 547
creazione, 536–538
dati bloccati, 544
disconnessione da, 545–547
entity data model, 565
errori di accesso, 539
errori di integrità referenziale, 588
731
interrogazione e visualizzazione dei
dati, 543–544, 551–553
mappatura del tipo di dati, 550
mappatura layer, 565
nuovo, creazione, 551
query con ADO.NET, 535–548
query con LINQ to SQL, 549–564
query, 541–543
richiesta di informazioni all’utente,
541–543, 562
salvataggio informazioni utente,
562
salvataggio modifiche a, 584–586,
594–596
valori null in, 547–548, 550, 564
database relazionali Vedere
anche database
valori null in, 547
DataContext, classi, 551–552
accesso tabelle del database con,
562–564
personalizzato, 559–560
DataContext, oggetti, 551–553
creazione, 564
DataContext, proprietà, 577, 596
di controlli padre, 580
DataContract, attributo, 691
DataLoadOptions, classe, LoadWith,
metodo, 558
DataMember, attributo, 691
DataTemplate, 576
date, confronto, 80–83, 84
dateCompare, metodo, 81, 82
DatePicker, controlli
aggiunta, 460
scelta rapida predefinita per, 492
DateTime, tipo di dati, 81, 84
DateTimePicker, controlli, 458
SelectedDate, proprietà, 81
dati
aggregazione, 401
blocco, 659–661
conteggio del numero di righe, 406
convalida di, 509–532
Count, metodo, 403
Distinct, metodo, 406
filtro, 400
group, operatore, 406
GroupBy, metodo, 402
incapsulamento di, 130
Max, metodo, 403
Min, metodo, 403
OrderBy, 402
732
dati utente, convalida
orderby, operatore, 406
OrderByDescending, 402
query, 395–417
raggruppamento, 401
selezione, 398
ThenByDescending, 402
unione, 404
dati utente, convalida, 509–532
DbType, parametro, 550
debugger
analisi passo per passo con, 61–63
variabili, controllo valori in, 62–63
Debugger JIT di Visual Studio, finestra
di dialogo, 115
decimal, tipo di dati, 31
default, parola chiave, 85
DefaultExt, proprietà, 496
definizione di coppie di operatori, 426
Delegate, classe, 506
delegati, 329
chiamata automatica, 342
collegamento agli eventi, 345
definizione, 331
dichiarazione, 331
DoWorkEventHandler, delegato, 504
espressioni lambda, 338
forme corrispondenti, 331
inizializzazione con un singolo
metodo specifico, 331
richiamare, 332
scenario per utilizzo, 330
utilizzo, 333
vantaggi, 332
DeleteObject, metodo, 589, 596
delimitatori, 88
Dequeue, metodo, 354
deserializzazione, 691
detach.sql script, 567
dichiarazione di metodi in override,
239–240
Dictionary, classe, 356
Dictionary<TKey, TValue>, classe di
insiemi, versione thread-safe,
669
DictionaryEntry, classe, 212, 213
Dispatcher, oggetti, 505–507
Invoke, metodo, 506
risposta, migliorare la, 629, 631–632
Dispatcher.Invoke, metodo, 632
DispatcherPriority, enumerazione,
507, 631
Dispose, metodo, 287
chiamata dai distruttori, 288–289
Distinct, metodo, 406
distruttori
chiamate, 292
Dispose, metodo, chiamata da,
288–289
raccomandazioni su, 284
scrittura, 280–282, 292
tempismo durante l’esecuzione, 283
disuguaglianza (!=), operatore, 74
DivideByZeroException, eccezione,
123
divisione di integer, 39
dizionari, creazione, 669
do, istruzioni, 99–108
passo per passo, 103–107
sintassi, 99
DockPanel, controlli, aggiunta, 479,
508
documentazione del codice, 11
documentazione, definizione di, 477
double, tipo di dati, 31
double.Parse, metodo, 58
DoWork, evento, sottoscrizione a, 504
DoWorkEventHandler, delegato, 504
Drawing, progetto, 260–262
drawingCanvas_
MouseLeftButtonDown,
metodo, 267
drawingCanvas_
MouseRightButtonDown,
metodo, 268
DrawingPadWindow, classe, 267
dual-core, processori, 602
duplicazione del codice, 269–270
duplicazioni nel codice, 269–270
DynamicResource, parola chiave, 454
E
eccezioni non gestite, 111–112, 123
reporting, 115
rilevamento, 124–125
eccezioni., 109
AggregateException, eccezioni, 647
alle regole di convalida,
rilevamento, 516–519
ApplicationException, eccezioni, 517
ArgumentException, eccezioni, 205,
225
DivideByZeroException, eccezione,
123
esaminare, 111–112
flusso di esecuzione e, 111, 113, 124
FormatException, 110, 111
generazione di eccezioni, 121–126
gerarchie di ereditarietà, 113
gestione, 110
InvalidCastException, eccezioni, 167
InvalidOperationException,
eccezioni, 122, 501, 553
non gestite, 111–112, 115, 124–125
NotImplementedException,
eccezioni, 202, 263
NullReferenceException, eccezioni,
344
OperationCanceledException,
eccezioni, 641, 668
OptimisticConcurrency-Exception,
eccezioni, 586, 587
ordine di esecuzione, 114
OutOfMemoryException, eccezioni,
164, 199
OverflowException, eccezioni, 111,
118, 120
rilevamento di tutte, 123, 124, 126
rilevamento, 110–117, 126
SqlException, eccezioni, 539–540,
562
UpdateException, eccezioni, 588
visualizzazione del codice che
causa, 116
Editor dell’insieme: Elementi, finestra
di dialogo, 480, 481
editor di codice e di testo, riquadro, 7
parole chiave, 29
elaborazione in parallelo, 676–678
benefici, 600–601
implementazione con la classe Task,
608–617
elementi array
accesso, 195, 218
tipi di, 192
ElementName, tag, 531
Elenco errori, finestra, 12
else, parola chiave, 77, 78
Enqueue, metodo, 354
Enterprise Services, 684
EnterReadLock, metodo, 665
EnterWriteLock, metodo, 665
entità
aggiunta, 587
eliminazione, 588
modellazione, 129
entity data model, 565
generare, 568–572
Entity Framework, 565
aggiornamento dei dati con,
583–596
finally, blocchi
aggiunta ed eliminazione dei dati
con, 587–588, 596
associazione dei dati, utilizzo con,
566–583
classi entità, creazione, 596
concorrenza ottimistica, 584–585
interfacce utente applicazione,
creazione, 574–579
interrogazione e visualizzazione dei
dati, 579–582
LoadProperty<T>, metodo, 581
mappatura layer e, 565
recuperare dati da tabelle con, 581
entity, oggetti
associazione proprietà dei controlli
con, 566–583
modifica, 583
visualizzare i dati da, 596
EntityCollection<Product>, proprietà,
577, 580
EntityObject, classe Concurrency
Mode, proprietà, 585
EntityRef<TEntity>, tipi, 555–556
EntitySet<Product>, tipi, 556
EntitySet<TEntity>, tipi, 555–556
enum, parola chiave, 173, 190
enum, tipi, 173–178
enumerator, oggetti, 382
enumeratori
Current, proprietà, 382
implementazione manuale, 383
MoveNext, metodo, 382
operatori di iterazione, 389
Reset, metodo, 382
yield, parola chiave, 390
enumerazione degli insiemi, 381
enumerazioni, 173–178
conversione in stringhe, 522
dichiarazione, 173–174, 176–178,
190
nomi letterali, 174
sintassi, 173–174
tipo sottostante di, 176
utilizzo, 174–175
valori interi per, 175
valori letterali, 175
versioni nullable di, 174
Equal, metodo, 431
Equals, metodo, 432
ereditarietà, 231–232
accesso protetto, 242
classi astratte e, 269–271
classi gerarchiche, creazione,
242–247
classi, assegnazione, 235–236
costruttori di classe base, chiamata,
234–235, 251
implementazione, 274–276
implicitamente pubblico, 233
menu, elementi., 483
metodi di override, dichiarazione,
239–240
metodi virtuali, dichiarazione,
238–239, 251
new, metodo, dichiarazione,
237–238
utilizzo, 232–247
errori
contrassegnare le, 12
descrizione delle, 111
eccezioni. Vedere eccezioni
gestione degli, 109
errori del compilatore, 12–13
errori di integrità referenziale, 588
errorStyle, stile, 525
esecuzione
elaborazione in parallelo, 600–601,
608–617, 676–678
multitasking, 602–628
single-thread, 599
esecuzione, 647
Esegui come amministratore,
comando, 536
Esegui fino al cursore, comando, 61,
103
Esplora soluzioni, accesso al codice,
34
Esplora soluzioni, riquadro, 7
espressioni booleane
creazione, 89
dichiarazione, 73–74
nelle istruzioni if, 78
nelle istruzioni while, 93
espressioni lambda
come adattatori, 339
e delegati, 338–342
form, 340
metodi anonimi, 341
per delegati anonimi, 501, 507
sintassi, 340
specificate come parametri metodo,
553
variabili, 341
espressioni unchecked, 119
espressioni, confrontare i valori di, 89
EventArgs, argomento, 346
eventi di menu, gestione, 484–491
eventi, 342–344
733
annullamento della sottoscrizione,
344
argomento sender, 346
attesa, 661, 681
collegare delegati, 345
controlli null, 344
creazione, 344
dichiarazione, 342
EventArgs, argomento, 346
eventi di menu, gestione, 484–491
origini, 342
sottoscrittori, 342
sottoscrizione, 343
utilizzando unico metodo, 346
vs. trigger, 456
WPF, interfaccia utente, 345
Example, classe, 289
Exception, gerarchia di ereditarietà,
113
ExceptionValidationRule, elementi,
516
ExecuteReader, metodo, 543
chiamate, 564
overload di, 548
Exit, comando, Click, gestore eventi
per, 486
ExitReadLock, metodo, 665
ExitWriteLock, metodo, 665
Extensible Application Markup
Language (XAML), 19–20, 445
Extract Method, comando, 60
F
F, tipo di suffisso, 34
F10, tasto funzione, 62
F11, tasto funzione, 62
F5, tasto funzione, 63
Faulted, stato dell’attività, 638, 641
file di progetto, 8
file di sorgente, visualizzazione, 7
file, chiusura, 96
FileInfo, classe, 94
OpenText, metodo, 95
fileName, parametro, 500
FileName, proprietà, 496
Finalize, metodo, generato dal
compilatore, 282
finalizzazione, 284
ordine di, 283, 284
finally, blocchi, 124–126
flusso di esecuzione e, 125
istruzioni di chiusura della
connessione del database, 545
734
finestra di comando, apertura
metodi disposal in, 285–286
finestra di comando, apertura, 538
finestra di dialogo Apri si Windows,
visualizzazione, 94
finestra Progettazione, 19
informazioni in memoria, 579
lavorare in, 19–22
WPF, form in, 445
finestre di dialogo comuni di
Windows, 495–498
finestre di dialogo comuni, 495–498
modale, 496
SaveFileDialog, classe, 495–498
finestre di dialogo modali, 496
finestre di dialogo, comuni, 495–498
firme dei metodi, 237
first-in, first-out (FIFO), meccanismi,
210
float, tipo di dati, 31
flusso di esecuzione, eccezioni e, 111,
113, 124
focus dei controlli, convalida e, 509,
518, 527
FontFamily, proprietà, 457
FontSize, proprietà, 20, 457
FontWeight, proprietà, 457
for, istruzioni, 97–99, 108
ambito di, 98–99
iterazione di array con, 196
omissione di parti di, 97–98
sintassi, 97
foreach, istruzioni, 196, 381
iterazione di array a lunghezza
nulla con, 223
iterazione di array con, 204
iterazione di array param con, 225
iterazione di query di database con,
552
iterazione di tabelle di database
con, 563
per query di database, 553,
556–558
form. Vedere anche form WPF
punti di ridimensionamento, 21
Format, metodo, 186
FormatException, eccezioni, 110, 111
FormatException, gestore, 110–113,
120
formattazione di stringhe, 60
freachable queue, 284
fully qualified names (FQN), 15
Func<T>, tipo generico, 506
G
garbage collection, 156–157, 280
distruttori, 280–282
garanzia di, 282–283
richiamare, 283, 292
tempismo durante l’esecuzione, 283
garbage collector, funzionalità di,
283–284
GC, classe
Collect, metodo, 283
SuppressFinalize, metodo, 289
Genera stub metodo, procedura
guidata, 57–60, 72
generics, 355–380
creazione, 358–370
e classi generalizzate, 357
funzione, 353
parametri di tipo multipli, 356
parametri di tipo, 356
strutture binarie, 358
strutture binarie, creazione, 361
vincoli, 358
gestione delle eccezioni, 110
per attività, 641–644
gestione risorse, 284–289
connessioni database, 545
multitasking, 600
rilascio delle risorse, 292
Gestori di evento Click, 471–474
per elementi di menu, 485–487
gestori eventi save, 487
gestori evento
esecuzioni lunghe, simulazione,
498–499
in applicazioni WPF, 470–476
per azioni di menu, 508
per eventi about, 488
per eventi Closing, 474–476
per eventi di salvataggio, 487–488
per nuovi eventi, 485–486
testing, 489–490
get, accessori, 299
per query di database, 555–556
get, blocchi, 298
GetBinding, metodo, 526
GetEnumerator, metodi, 382
GetInt32, metodo, 544
GetPosition, metodo, 267
GetString, metodo, 544
GetTable<TEntity>, metodo, 552
GetXXX, metodo, 544, 564
goto, istruzioni, 87
Grid, controlli, in form WPF, 40
Grid, pannelli, 446
controlli, posizionamento, 447
in applicazioni WPF, 446
GridView,controlli, visualizzazione
delle caratteristiche, 578
group, operatore, 406
GroupBox, controlli, 469
aggiunta, 460
GroupBy, metodo, 402
H
Handle, metodo, 40, 110, 116
Handle, metodo, 642, 647
HasError, proprietà, 529
testing, 530
Hashtable, classe, 215
Hashtable, oggetto, SortedList,
oggetto di insieme, 215
HasValue, proprietà, 158
Header, attributo, 480
High Performance Compute (HPC)
Server 2008, 600
hill-climbing, algoritmo, 604
HorizontalAlignment, proprietà,
447–448
Hypertext Transfer Protocol (HTTP),
684
I
IColor, interfaccia, 260–261
implementazione, 261–266
iComparable, interfaccia, 255, 362
identificatori non pubblici, 133
identificatori pubblici, convenzioni di
denominazione, 133
identificatori, 28–29
ambito di, 54
denominazione, 237–238
overload, 55–65
riservata, 28
IDisposable, interfaccia, 287
IDraw, interfaccia, 260–261
implementazione, 261–266
IEnumerable, interfaccia, 382, 549
implementazione, 387
IEnumerable, oggetti, unione, 654
If, istruzioni, 77–84, 89
a cascata, 79–80, 84
blocchi di istruzioni, 78–79
espressioni booleane in, 78
scrittura, 80–83
istruzioni
sintassi, 77–78
“Il nome ’Console’ non esiste nel
contesto corrente”, 15
Image.Source, proprietà, 450
Implementa interfaccia in modo
esplicito, comando, 262–263
implementazioni originali (dei
metodi), 239–241
implementazioni proprietà virtuali,
304
incapsulamento, 130, 146
regola d’oro, 296
incremento (++), operatore, 44, 92,
425
indici array, 195, 218
interi tipi per, 201
indicizzatori, 315–322
accessori, 319
chiamate, 326
definizione, 318
esempio con e senza, 315
implementazioni virtuali, 322
in contesto read/write, 319
in una applicazione Windows, 323
nelle interfacce, 322
operatori con ints, 316
scrittura, 324
sintassi dell’implementazione
esplicita dell’interfaccia, 323
sintassi, 315
vs. array, 320
informazioni immesse dall’utente
pressione dei tasti, 588–589
risposta a, 628–629
informazioni relative all’errore,
visualizzazione, 518–519, 532
Informazioni su, modello delle
finestre, 488
InitialDirectory, proprietà, 496
InitializeComponent, metodo, 23
inizializzatori degli insiemi, 214
inizializzatori di oggetto, 310
inizializzazione
di campi, 133, 139, 140
di classi derivate, 234–235
di strutture, 183–184
di variabili array, 193–194
INotifyPropertyChanged, interfaccia,
572
INotifyPropertyChanging, interfaccia,
572
Insert, metodo, 208, 366
insiemi
conteggio del numero di righe, 406
di elementi non ordinati, 669
enumerabile, 381
enumerazione degli elementi,
381–389
GetEnumerator, metodi, 382
IEnumerable, interfaccia, 382
iterazione, 218, 650–655
join, operatore, 407
limitare il numero di elementi in,
669–670
numero di elementi in, 218
operatori di iterazione, 389
procedure di, 669
thread-safe, 678–679
vs. array, 214
insiemi enumerabili, 381
insiemi, 361
definizione di, 16
spazi dei nomi e, 16
uso di, 8
instnwnd.sql script, 538, 549, 567
int, parametri, passare, 154
int, tipo variabile, 31
int, tipo, 31
dimensione fissa di, 118
int, valori
minimo, trovare, 220–221
operazioni aritmetiche su, 38–41
int.MaxValue, proprietà, 118
int.MinValue, proprietà, 118
int?, tipo, 158
Int32.Parse, metodo, 37
integer, conversione di valori string
in, 40, 46
IntelliSense, 9–10
icone, 10, 11
suggerimenti, 10
Interfacce covarianti, 375
interfacce generiche
controvariante, 377
covariante, 375
varianza, 373–379
Interfacce invarianti, 375
interfacce parziali, 136
interfacce, 253–269
convezioni di denominazione, 255
definizione, 254–255, 260–261
dichiarazione, 277
ereditarietà, 253, 255–256
implementata esplicitamente,
257–259
implementazione, 255–256, 261–
266, 277
735
metodo delle combinazioni di
parole chiave, 276
multipli, 257
regole di utilizzo, 255
restrizioni di, 259
riferimento a classi tramite, 256–257
interfaccia utente, linee guida
Microsoft per, 478
interface, parola chiave, 254, 272, 277
interface, proprietà, 304
interoperabilità cross-platform, 685
interrogazione differita, 553–554,
558–559
interrogazione, 543–545
con oggetti SqlDataReader, 564
immediato, 558
ritardato, 553–554, 558
InvalidCastException, eccezioni, 167
InvalidOperationException, eccezioni,
122, 501, 553
InvalidOperationException, gestore,
123
Invoke, metodo, 506–508
chiamate, 505
IProducerConsumerCollection<T>,
classe, 669
is, operatore, 168–169
IsChecked, proprietà, 473, 476
valori null, 504
IsDBNull, metodo, 548, 564
IsSynchronizedWithCurrentItem,
parametro, 550
istanza utente di SQL Server Express,
567
scollegarsi, 567–568
istanze
di classi, assegnazione, 131
di form WPF, 489
istanze array
copia, 197–198
creazione, 192–193, 218
IsThreeState, proprietà (controllo
CheckBox), 458
istruzioni di assegnazione, 91
per classi anonime, 148
istruzioni di iterazione, 91
istruzioni dichiarative, 30–31
istruzioni in loop, 108
continuazione, 100
do, istruzioni, 99–107
for, istruzioni, 97–99
uscita, 99
while, istruzioni, 92–96, 97–99
istruzioni, 27–28
736
ItemsSource, proprietà
esecuzione di iterazioni di, 108.
Vedere anche istruzioni in loop
semantica, 27
sintassi, 27
ItemsSource, proprietà, 577
ItemTemplate, proprietà, 577
IValueConverter, interfaccia, 522–523
Convert, metodo, 524
ConvertBack, metodo, 524–525
J
JavaScript Object Notation (JSON),
688
join, 404, 554–558
di origini dati, 654
Join, metodo, 404
parametri, 404
JSON (JavaScript Object Notation),
688
K
Key, proprietà, 213
Knuth, Donald E., 358
L
Label, controlli
aggiunta, 19, 459
proprietà, modifica, 20
Language Integrated Query (LINQ),
395
All, metodo, 407
Any, metodo, 407
BinaryTree, oggetti, 407
definizione di un insieme
enumerabile, 412
equi-join, 407
filtro dei dati, 400
Intersect, metodo, 407
Join, metodo, 404
metodi di estensione, 412
metodi generici vs. non generici,
415
operatori query, 405
OrderBy, metodo, 401
Select, metodo, 398
selezione dei dati, 398
Skip, metodo, 407
Take, metodo, 407
Union, metodo, 407
unione dei dati, 404
utilizzo, 396
valutazione differita, 412
Where, metodo, 401
last-in, first-out (LIFO), meccanismi,
210–211
legge di Moore, 601
Length, proprietà, 195–196, 218
lettura risorse, 665–666
librerie di classi, 361
LINQ parallelo, 649–655
LINQ to Entities, 566
query dei dati con, 573–574
LINQ to SQL, 535, 549–564
creazione di nuovi database e
tabelle, 551
DataContext, classe, personalizzata,
559–560
estensione a, 565
interrogazione differita, 553–554,
558–559
join, 554–558
mappatura del tipo di dati, 561
query di un database, 551–553,
560–564
relazioni di tabella, 554–558
LINQ, query, 649
parallelizzazione, 651–655, 681
LINQ. Vedere Language Integrated
Query (LINQ)
List<Object>, oggetti, 378
List<T>, classe generica di insiemi, 378
ListView, controlli
per accettare input dell’utente, 590
View, elemento, 577
visualizzazione opzioni, 578
LoadProperty<T>, metodo, 581
LoadWith, metodo, 558–559
Locals, finestra, 104–106
lock, istruzioni, 659–661, 681
lock, parola chiave, 659
long, tipo di dati, 31
LostFocus, evento, 509
M
Main, metodo, 8
per applicazioni console, 9
per applicazioni grafiche, 23–24
MainWindow, classe, 23
MainWindow, costruttori, codice per
il menu di scelta rapida in,
493–494
MainWindow.xaml.cs, file, codice per,
22–23
ManualResetEventSlim, classe,
661–682
ManualResetEventSlim, oggetti, 681
Margin, proprietà, 20, 447–448, 479
MARS (multiple active result sets), 545
Math, classe, 131
Sqrt, metodo, 141, 143
Math.PI, campo, 131
matrici di chiavi, 212
ordinate, 213
matrici di parametri, 219, 221–222
dichiarazione, 221–222
oggetto type, 223, 229
scrittura, 224–226
vs. parametri facoltativi, 226–229
matrici multidimensionali, 198–199
params, parola chiave e, 221
meccanismi di blocco dei primitivi di
sincronizzazione, 663–665
membri delle classi, casella di
riepilogo a discesa, 34
memoria
allocazione per nuovi oggetti,
279–280
memoria heap, 163–164, 279, 280
memoria stack, 163–164, 178, 669
organizzazione della, 162–164
per array, 191
per Hashtables, 212
per tipi di classe, 151
per tipi valore, 151
per variabili di tipi valore, 131
recupero, 279. Vedere
anche garbage collection
memoria del computer Vedere
memoria
memoria fisica Vedere anche memoria
interrogazione della quantità di,
610
memoria heap, 163–164
allocazione da, 279
restituire memoria a, 280
memoria stack, 163–164
strutture, 178
menu contestuali Vedere menu di
scelta rapida
menu di scelta rapida, 491–494
aggiunta nel codice, 493–494
associazione con form e controlli,
493–494, 508
creazione dinamica, 508
creazione, 491–495, 508
deassociazione da form WPF, 494
.NET Framework
per controlli casella di testo,
491–492
per controlli DatePicker, 492
menu, 477–478
a cascata, 483
barre di separazione, 481, 508
creazione, 478–484, 508
DockPanel, controlli, aggiunta a,
479
menu di scelta rapida, 491–494
Menu, controlli, 477, 478
aggiunta, 479, 508
MenuItem, elementi, 480
menu, elementi.
Click, eventi, 485–487
denominazione, 481, 485
elementi about, 488–489
elementi figlio, 481
stile del testo, 483
tasti di scelta per, 480
tipi di, 483–484
WPF, controlli, 484
MenuItem, elementi, 480, 481
Header, attributo, 480
nidificato, 483
MenuItem, oggetti, 508
MenuItem_Click, metodi, 485
MergeOption, proprietà, 584
Message Passing Interface (MPI), 600
Message, proprietà, 111, 117
MessageBox.Show, istruzione, 25
methodName, 48, 51
metodi, 129
ambito di, 54–55
argomenti, 52. Vedere
anche argomenti
chiamate, 51, 53, 72
combinazioni di parole chiave per,
276
condivisione delle informazioni tra,
54–55
corpi di, 47
costruttori, 134–135
creazione, 47–53
denominazione, 47, 133
dichiarazione, 48–49, 72
esaminare, 50–51
esistenza, 49
globale, 48
implementazioni di, 239–241
incapsulamento di, 130
istruzioni in, 27
lunghezza, 51
matrici di parametri e, 227, 229
metodi anonimi, 341
metodi astratti, 270–271
metodi di estensione, 247–251
metodi evento, 471
metodi sigillati, 271–272
metodi statici, 61–63, 72
metodi virtuali, 238–239, 240–241
nascondere, 237–238
nelle interfacce, 254–255
overload dei metodi, 9
overload, 55–56, 219
override dei metodi, 239–240
override, 272
parametri facoltativi per, 65–66,
68–72, 226
procedura guidata, generazione di,
57–60
restituzione dei dati da, 49–51, 72
scrittura, 56–63
tipi restituiti, 48, 72
valori hard-coded per, 468
metodi anonimi, 341
Metodi astratti, 270–271, 277
metodi di callback, registrazione, 634
metodi di estensione, 247–251
creazione, 248–250
Single, metodo, 553
sintassi, 248
metodi di istanza
definizione di, 140
scrittura e chiamata, 140–142
metodi disposal, 285
esente da eccezioni, 285–286,
289–292
scrittura, 292
metodi evento, 471
denominazione, 472
per elementi di menu, 508
rimozione, 472
scrittura, 476
metodi generici, 370–373
parametri, 371
vincoli, 371
metodi globali, 48
metodi polimorfi, regole di utilizzo,
240
metodi sigillati, 271–272
metodi statici pubblici, scrittura, 146
metodi statici, 142–148
chiamate, 149
dichiarazione, 149
metodi di estensione, 248
737
scrittura, 146
metodi variadic, 219
metodi virtuali
dichiarazione, 238–239, 251
polimorfismo e, 240–241
metodi Web, 685
metodi, stepping into, 61–63
metodi, stepping out, 61–63
Microsoft .NET Framework. Vedere
.NET Framework
Microsoft Message Queue (MSMQ),
684
Microsoft SQL Server 2008 Express,
535. Vedere anche SQL Server
Microsoft Visual C#. Vedere C#
Microsoft Windows Presentation
Foundation. Vedere applicazioni
WPF
Microsoft.Win32, spazio dei nomi, 495
Min, metodo, 220, 221
modello Applicazione Windows Form,
17
modello REST, 684, 688
modifica testo. scelta rapida per, 491
Modifica/Seleziona origine dati,
finestra di dialogo, 569–570
Modifica/Seleziona origine dati,
finestra di dialogo, 569–570
Monitor, classe, 660
Moore, Gordon E., 601
MouseButtonEventArgs, parametro,
267
MoveNext, metodo, 382
MPI (Message Passing Interface), 600
MSMQ (Microsoft Message Queue),
684
multiple active result sets (MARS), 545
multitasking
considerazioni per, 602–603
definizione di, 602
implementazione, 602–628
ragioni per, 600–601
N
Name, parametro, 550
Name, proprietà, 21, 452
Name, proprietà, 576, 577
nascondere metodi, 237–238
.NET common language runtime, 330
.NET Framework Remoting, 684
.NET Framework, 330
hill-climbing, algoritmo, 604
LINQ, estensioni, 650
738
.NET Framework, libreria di classi
mutlithreading, 603
parallelismo, determinazione, 604,
617–619, 624, 639
pool di thread, 603–604
primitivi di sincronizzazione, 660
TaskScheduler, oggetti, 605
.NET Framework, libreria di classi
classi in, 16
spazi dei nomi in, 15
new, metodo, dichiarazione, 237–238
new, operatore, funzionalità di,
279–280
new, parola chiave, 131, 218, 238, 276
per classi anonime, 147
per costruttori, 149
per istanze di array, 192
Next, metodo di SystemRandom, 193
nomi dei file, asterisco, 12
Northwind Traders, 536
Northwind, database, 536
creazione, 536–538
Orders, tabella, 560–562
reset, 538
scollegare dall’istanza utente,
567–568
Suppliers, applicazione, 575, 582–
584, 595–596
Suppliers, tabella, 554
NOT (~), operatore, 316
NOT, operatore (!), 74
notazione dot, 134
notazione ottale, conversione dei
numeri in, 100–103
notazione ungara, 30
notifica della chiamata del metodo
completata, 630
NotImplementedException, eccezioni,
202, 263
NotOnCanceled, opzione, 607
NotOnFaulted, opzione, 607
NotOnRanToCompletion, opzione, 607
null, valori, 156–159, 171
nei database, 547, 548, 564
nelle colonne delle tabelle di un
database, 550
NullReferenceException, eccezioni,
344
NumCircles, campo, 143–144
numeri complessi, 428
numeri, conversione in stringhe,
100–103
Nuovo progetto, finestra di dialogo,
5, 6
O
OASIS (Organization for the
Advancement of Structured
Information Standards), 686
obj, cartella, 13
object, parola chiave, 165
object, tipo, 59, 207
Object.Finalize, metodo, override,
281–282
ObjectContext, classe, 572
Refresh, metodo, 584
ObjectContext, oggetti
cache dei dati, 583
traccia delle modifiche, 584
objectCount, campo, 145
ObjectQuery<T>, oggetti, query
database basate su, 582–583
ObjectSet, insiemi
AddObject, metodo, 596
DeleteObject, metodo, 596
eliminazione delle entità da, 588
ObjectSet<T>, classe di insiemi, 576
ObjectStateEntry, classe, 586
oggetti
accesso membro, 280
assegnazione, 235–236
associazione delle proprietà di, 531
blocco, 659–661
creazione, 131, 137–140, 279–280
definizione di, 132
distruzione di, 280, 282–283
in memoria, aggiornamento,
583–584
inizializzazione con le proprietà,
308
memoria per, 163
raggiungibile e non raggiungibile,
284
riferimento a, 280
riferimento attraverso le interfacce,
256
svantaggi, 353
vita di, 279–284
oggetti associati, riferimento a, 526
oggetti eccezione, 121
esaminare, 111–112, 642–643
ok_Click, metodo, 25
okayClick, metodo, 345
OnlyOnCanceled, opzione, 607
OnlyOnFaulted, opzione, 607
OnlyOnRanToCompletion, opzione,
607
Open, metodo, chiamata, 564
OpenFileDialog, classe, 94, 495
openFileDialog, oggetti, 94
openFileDialogFileOk, metodo, 94
OpenText, metodo, 95
operandi, 36
OperationCanceledException,
eccezioni, 641, 668
OperationContract, attributo, 691
operatore as, 169, 236
operatore condizionale AND,
precedenza e associatività di,
77
operatore condizionale OR,
precedenza e associatività di,
77
operatore di addizione composto,
108
operatore di addizione, 36
precedenza, 41, 77
operatore di assegnazione composto,
91–92, 424
operatore di assegnazione(=), 31,
74, 91
precedenza e associatività di, 42, 77
operatore di divisione, 36
precedenza, 41
operatore di moltiplicazione, 36
precedenza, 41, 77
operatore di sottrazione, 36
precedenza, 41
operatore di sottrazione composto,
108
operatore dot (.), 280
operatori aritmetici
precedenza, 41–42
selezionato e non selezionato, 119
utilizzo, 38–41
operatori binari, 419
operatori booleani, 74–77
corto circuito, 76
precedenza e associatività, 76–77
operatori di assegnazione, composto,
91–98
operatori di conversione, 434, 435
scrittura, 437
operatori di decremento, 425
–– operatore, 44
++ operatore, 92
operatori di iterazione, 389
operatori logici
corto circuito, 76
operatore logico AND (&), 75, 89
operatore OR(||, 75, 89
operatori logici condizionali, 75
OverwritePrompt, proprietà
corto circuito, 76
operatori primari, precedenza e
associatività degli, 76
operatori query, 405
operatori relazionali, 74
precedenza e associatività di, 77
operatori simmetrici, 422, 436
operatori statici, 421
operatori unari, 44, 419
precedenza e associatività di, 76
operatori, 419–440
-- operatore, 44, 425
*= operatore, 92
/= operatore, 92
+ operatore, 419
++ operatore, 43, 44, 92, 425
+= operatore, 92, 331, 343
-= operatore, 92, 332, 344
AND (&), operatore, 316
associatività e, 42, 419
asterisco (*), operatore, 36, 170
barra (/), operatore, 36
basi, 419–424
bitwise, 317
confronto in strutture e classi, 426
conversioni definite dall’utente, 435
coppie di operatori, 426
corto circuito, 76
disuguaglianza (!=), operatore, 74
forme prefisse, 44–45
forme suffisse, 44–45
group, operatore, 406
implementazione, 427–433
incremento (++), operatore, 43, 44,
92, 425
interoperabilità, 424
is, operatore, 168–169
join, operatore, 407
molteplicità, 420
new, operatore, 279–280
NOT (~), operatore, 316
NOT, operatore (!), 74
numeri complessi, 428
operandi, 420
operatore +, 423
operatore as, 169, 236
operatore di addizione composto,
108
operatore di addizione, 77
operatore di assegnazione
composto, 91–92, 424
operatore di assegnazione(=), 31,
42, 74, 77, 91–98
operatore di divisione, 36, 41
operatore di moltiplicazione, 36, 41,
77, 419
operatore di sottrazione composto,
108
operatore dot (.), 280, 420
operatore logico AND (&), 75, 89
operatore OR(||, 75, 89
operatori aritmetici, 38–42, 119
operatori binari, 419
operatori booleani, 74–77
operatori di conversione, 434, 435,
437
operatori di decremento, 44, 92, 425
operatori logici condizionali, 75–77
operatori primari, 76
operatori pubblici, 421
operatori query, 405
operatori relazionali, 74, 77
operatori simmetrici, 422, 436
operatori statici, 421
operatori unari, 44, 76, 419
OR (|), operatore, 316
orderby, operatore, 406
overload, 420
precedenza, 41–42, 419
resto (modulo), operatore, 37
segno percento (%), operatore.,
37, 92
shift a sinistra(<<), operatore, 316
simulazione [], 420
tipi di dati e, 37–38
uguaglianza (==), operatore, 74,
77, 431
vincoli, 420
XOR (^), operatore, 316
operazioni aritmetiche, 36–43
tipi di risultati, 37
operazioni di aggiornamento,
database
aggiornamenti in conflitto, 584–
587, 596
esecuzione, 583–584
operazioni di salvataggio,
aggiornamenti sulla barra di
stato, 505–507
operazioni in attesa
token di annullamento per, 668
CurrentCount, proprietà, 663
operazioni parallele
pianificazione, 656
prestazioni non prevedibili di,
656–659
operazioni relazionali, 401
739
operazioni, a lunga esecuzione
annullamento, 632–645
divisione in attività parallele,
614–617
misurazione del tempo di
elaborazione, 612–614
parallelizzazione con la classe
Parallel, 619–621
risposta, migliorare con l’oggetto
Dispatcher, 629–632
operazioni, indipendenti, 623–624
OptimisticConcurrencyException,
eccezioni, 586, 587
OptimisticConcurrencyException,
gestore, 594, 596
OR (|), operatore, 316
OrderBy, metodo, 401
orderby, operatore, 406
OrderByDescending, metodo, 402
ordinamento dei dati con strutture
binarie, 359
ordine z dei controlli, 451
Organization for the Advancement
of Structured Information
Standards (OASIS), 686
origini dati, unione, 654
origini di associazione, 518
specificazione, 531, 577
origini evento, 342
OtherKey, parametro, 555
out, modificatore, e matrici params,
222
out, parametro, 159–162
out, parola chiave, 160–161, 376
OutOfMemoryException, eccezioni,
164
matrici multidimensionali e, 199
Output, finestra (Visual Studio 2010),
11
Output, icona, 103, 104
overflow, controllo, 118, 119
OverflowException, eccezioni, 111,
118, 120
OverflowException, gestore, 120
overload dei metodi, 9, 55–56
overload, 219
ambiguità, 222
costruttori, 134–135
parametri facoltativi e, 64–65
override dei metodi, 239
metodi sigillati, 271–272
override, parola chiave, 239, 240, 272,
276
OverwritePrompt, proprietà, 496
740
Parallel, classe
P
Parallel, classe
astrazione di attività con, 617–624
Parallel.For, metodo, 617, 618,
620–621, 624, 639, 647
Parallel.ForEach<T>, metodo, 618
Parallel.Invoke, metodo, 618, 621–
624, 627
per operazioni indipendenti, 621,
623–624
utilizzo, 621
Parallel.For, costrutto, 657–658
Parallel.For, metodo, 617, 618, 620–
621, 647
annullamento, 639
utilizzo, 624
Parallel.ForEach, metodo, 647
annullamento, 639
utilizzo, 624
Parallel.ForEach<T>, metodo, 618
Parallel.Invoke, metodo, 618, 627
utilizzo, 621–624
ParallelEnumerable, oggetti, 655
parallelizzazione di query LINQ,
650–656
ParallelLoopState, oggetti, 618, 639
ParallelQuery, classe
AsOrdered, metodo, 655
WithCancellation, metodo, 656, 681
WithExecutionMode, metodo, 655
ParallelQuery, oggetti, 650, 654
parameterList, 48
parametri
alias agli argomenti, 159–160
denominato, 72
denominazione, 59
facoltativo, 64–65, 72
facoltativo, ambiguità, 66–71
facoltativo, definizione, 65–66
passaggio, 66
tipi di metodo, 152
tipi di riferimento, 152–156, 159
tipi di, specificazione, 48
valori predefiniti per, 65–66
parametri denominati, 72
passaggio, 66
parametri di riferimento
out e ref, modificatori, 162
utilizzo, 153–156
parametri di tipo, 356
out, parola chiave, 376
parametri facoltativi, 64–65
ambiguità con, 66–71
definizione, 65–66, 68–69, 72
vs. matrici di parametri, 226–229
parametri ref, 159–162
passaggio di argomenti a, 171
parametri valore
out e ref, modificatori, 162
utilizzo, 153–156
params, array Vedere matrici di
parametri
params, metodi, priorità, 222
params, oggetto [ ], 223
params, parola chiave, 219, 221
overload dei metodi e, 222
parentesi
nelle espressioni booleane, 75, 93
nelle istruzioni if, 78
precedenza, 41
parentesi quadre nelle dichiarazioni
di array, 191
parole chiave, 28–29
abstract, parola chiave, 270, 276,
277
base, parola chiave, 234, 239
bool, parola chiave, 89
case, parola chiave, 85
catch, parola chiave, 110
checked, parola chiave, 126
class, parola chiave, 130, 149
const, parola chiave, 144
default, parola chiave, 85
DynamicResource, parola chiave,
454
else, parola chiave, 77, 78
enum, parola chiave, 173, 190
equivalenti .NET, 179
get e set, parole chiave, 298
IntelliSense, 9
interface, parola chiave, 254, 272,
277
lock, parola chiave, 659
metodo delle combinazioni di
parole chiave, 276
new, parola chiave, 131, 218, 238,
276
object, parola chiave, 165
out, parola chiave, 160–161, 376
override, parola chiave, 239, 240,
272, 276
params, parola chiave, 219, 221, 222
partial, parola chiave, 136
private, parola chiave, 132, 145,
242, 276
protected, parola chiave, 242, 276
public, parola chiave, 132, 242, 276
ref, parola chiave, 159
return, parola chiave, 49
sealed, parola chiave, 271, 272, 276,
277
set, parola chiave, 298
static, parola chiave, 143, 145, 149
StaticResource, parola chiave, 454
string, parola chiave, 152
struct, parola chiave, 180, 190
this, parola chiave, 139–140, 146,
248
try, parola chiave, 110
unchecked, parola chiave, 118–119
unsafe, parola chiave, 170
var, parola chiave, 45, 148
virtual, parola chiave, 239, 240, 251,
272, 276
void, parola chiave, 48, 49, 51
yield, parola chiave, 390
Parse, metodo, 53, 101
partial, parola chiave, 136
partial, strutture, 136
ParticipantCount, proprietà, 666
ParticipantsRemaining, proprietà, 666
partizionamento dei dati, 650
PascalCase, schema di
denominazione, 133
Pass.Value, metodo, 154–155
Password, parametro, 541
Path, tag, 531
per lunghe operazioni
annullamento, 632–645
divisione in attività parallele,
614–617
misurazione del tempo di
elaborazione, 612–614
parallelizzazione con la classe
Parallel, 619–621
risposta, migliorare con l’oggetto
Dispatcher, 629–632
percorsi di associazione, 519
performance
di classi di insiemi concorrenti, 670
miglioramento con PLINQ, 650–655
sospensione e ripresa dei thread,
660
Plain Old XML (POX), 688
PLINQ (Parallel LINQ), 649
miglioramento delle prestazioni
con, 650–655
PLINQ, query
annullamento delle, 681
parallelismo, opzioni per, 655–656
polimorfismo
RefreshMode, enumerazione
testing, 246
metodi virtuali e, 240–241
pool di connessione, 547
pool di risorse, controllo accesso,
663–664
pool di thread, 603
POX (Plain Old XML), 688
precedenza, 419
di operatori booleani, 76–77
controllare, 41–42
override, 46
prefisso dell’operatore, 44–45
Premere un tasto per continuare,
messaggio, 13
pressione dei tasti, esaminare,
588–589
prevenzione del passaggio, 86
primarie, chiavi tabelle del database,
550
primitivi di sincronizzazione
annullamento e, 668
in TPL, 661–667
private, metodi, 132–133
private, parola chiave, 132, 145, 242,
276
private, qualificatore, 58
PrivilegeLevel, enumerazione, 521
aggiunta, 520
privilegi amministrativi, per le
esercitazioni, 535–537
problemi di incompatibilità tra i nomi,
14
Procedura guidata Entity Data Model,
569–571
processori
multicore, 601–602
quad-core, 602
spinning, 651
processori multicore, 601–602
produttori, 669
progetti, ricerca negli, 34
Program, classe, 8
Program.cs file, 8
ProgressChanged, evento, 504
proprietà associate, oggetto
BindingExpression, 532
proprietà automatiche, 307, 310
Proprietà connessione, finestra di
dialogo, 569–570
proprietà del progetto, impostazioni,
118
proprietà di sola lettura, 300
proprietà di sola scrittura, 300
proprietà statiche, 300
proprietà Text, impostazioni, 34–35
proprietà ToolTip, messaggi di errore,
518–519, 532
proprietà, 297–314
accessibilità, 301
applicazioni Windows, 305
associazione alle proprietà del
controllo, 525–526, 531
associazione alle proprietà
dell’oggetto, 531
automatico, 307, 310
contesto di lettura, 299
contesto di lettura/scrittura, 299
contesto di scrittura, 299
dichiarazioni, 298
get e set, parole chiave, 298
get, blocco, 297
implementazioni esplicite, 305
implementazioni virtuali, 304
inizializzatori di oggetto, 310
inizializzazione di oggetti, 308
interfaccia, 304
privato, 301
protetto, 301
pubblico, 298, 301
ragioni per definire, 307
restrizioni, 302
set, blocco, 297
sicurezza, 301
sintassi delle dichiarazioni, 297
sola lettura, 300
sola scrittura, 300
statico, 300
utilizzo appropriato, 303
utilizzo, 299
Proprietà, finestra, 449
visualizzazione, 20
protected, parola chiave, 242, 276
provider di dati, 535–536
pseudo-casuale, generatore di
numeri, 193
public, metodi, 132–133
public, operatori, 421
public, parola chiave, 132, 242, 276
public, proprietà, 298
pulsante Continua (barra degli
strumenti Debug), 63
pulsante Esci da istruzione/routine
(barra degli strumenti Debug),
62–63
pulsante Esegui istruzione (barra
degli strumenti Debug), 61–63
741
pulsante Esegui istruzione/routine
(barra degli strumenti Debug),
62–63
puntatori, 169–170
punti di ancoraggio dei controlli,
447–448
punti di ingresso nel programma, 8
punti di ridimensionamento, 21
punto di domanda (?), modificatore
per valori nullable, 157
Punto di interruzione, icona, 103
punto e virgola
nelle istruzioni do, 99
nelle istruzioni for, 98
regole di sintassi, 27
Q
quad-core, processori, 602
query dei dati, 395–417
query di database
ADO.NET per, 564
iterazione, 552
LINQ to Entities per, 582–583
LINQ to SQL per, 564
ritardato, 557–558
valutazione immediata per,
553–554
Queue, classe, 210
Queue, tipo di dati, 354
Queue<T>, classe, versione threadsafe, 669
Quick Find, comando, 34
R
radio button, controlli, 469
aggiunta, 461
inizializzazione, 476
mutuamente esclusivi, 461, 476
RanToCompletion, stato attività, 638
Read, metodo, 543
reader.ReadLine, metodo, 95
ReaderWriterLockSlim, classe,
665–682
ReadLine, metodo, 58
ref, modificatore, e matrici params,
222
ref, parola chiave, 159
refactoring del codice, 60, 270
References, cartella, 8
Refresh, metodo, 584, 596
chiamate, 586
RefreshMode, enumerazione, 586
742
Register, metodo
Register, metodo, 634
regola dell’assegnazione definita, 32
regole di convalida, 510
aggiunta, 511–518
eccezioni a, rilevamento, 518–519
specificazione, 516
regole di sintassi, 27
for, istruzioni, 27
per identificatori, 28
Release, cartella, 14
Release, metodo, 681
Remove, metodo, 208
RemoveParticipant, metodo, 666
Representational State Transfer
(REST), 684, 688
Reset, metodo, 466–470
chiamate, 486
Resources, elementi, 452
resto (modulo), operatore, 37
validità, 38
result =, clausola, 51
Result, proprietà, 646
return, istruzioni, 49–50, 141
prevenzione del passaggio, 86
return, parola chiave, 49
returnType, 48
richiesta all’oggetto Dispatcher,
505–507
riferimenti, aggiunta, 16
riferimento traballante, 282
riquadri di layout, 446
ordine z dei controlli, 451
risorse
lettura, 665–666
scrittura, 665–666
risorse condivise, accesso esclusivo
alle, 681
risorse finestra, aggiunta di scelta
rapida al menu, 491
risorse statiche, regole di ambito, 454
risposta dell’applicazione
miglioramento, 600. Vedere
anche multitasking
migliorare con l’oggetto Dispatcher,
629–632
thread, 498–507, 507
risultati, ordine di restituzione dei,
655
RoutedEventArgs, oggetto, 345
RoutedEventHandler, 345
run, metodo, 56
runtime, parallelizzazione delle query,
655
RunWorkerAsync, metodo, 504
RunWorkerCompleted, evento, 504
S
Salva file, finestra di dialogo, 496, 497
SaveChanges, metodo, 587
chiamate, 583–584, 596
saveChanges_Click, metodo, 594
SaveFileDialog, classe, 495–498, 508
scalabilità, 600
scelta rapida, menu. Vedere menu di
scelta rapida
scopo della classe, definizione, 54–55
scrittura nelle risorse, 665–666
sealed, parola chiave, 271, 272, 276,
277
segnalazione dei problemi,
configurazione, 115
segno meno (–), operatore, 36
segno percento (%), operatore., 37
segno più(+), operatore, 36
segno uguale (=), operatore, 42.
Vedere anche operatore di
assegnazione (=)
Select, metodo, 398
parametri di tipo, 399
SelectedDate, proprietà, 81
valori null, 504
semafori, 661
semantica, 27
SemaphoreSlim, classe, 663–682
SemaphoreSlim, oggetti, 681
Separator, elementi, 481, 508
serializzazione, 691
delle chiamate dei metodi, 679–680
ServiceContract, attributo, 691
servizi Web, 683–716
architetture, 684
bilanciamento del carico, 687
build, 688
consumo, 711
creazione con REST, 704
creazione con SOAP, 689
definito, 684
indirizzamento, 687
metodi Web, 685
policy, 687
Representational State Transfer
(REST), 684, 687
requisiti non funzionali, 686
richiamare, 711
routing, 687
Service.svc, file, 695
sicurezza, 686
Simple Object Access Protocol
(SOAP), 684, 685
SOAP vs. REST, 688
Web Services Description Language
(WSDL), 686
Web.config, file, 695
Windows Communication
Foundation, 684
WS-Addressing, specifica, 687
WS-Policy, specifica, 687
WS-Security, specifica, 686
set di dati, partizionamento, 650
set, accessori, 298, 299
per query di database, 555–556
set, parola chiave, 298
Setter, elementi, 456
Shape, classe, 273
shift a sinistra(<<), operatore, 316
ShowDialog, metodo, 489, 496, 592
showDoubleValue, metodo, 36
showFloatValue, metodo, 34
showIntValue, metodo, 35
showResult, metodo, 50, 53
sicurezza, hard-code di nomi utente e
password, 541
SignalAndWait, metodo, 666
Simple Object Access Protocol. Vedere
SOAP (Simple Object Access
Protocol), 684
sincronizzazione dei thread, 666, 681
Single, metodo, 553
single-thread, esecuzione, 599. Vedere
anche multithreading
single-thread, operazioni, 672–676
sintassi delle dichiarazioni delle
proprietà, 297
Sleep, metodo, 499
SOAP (Simple Object Access
Protocol), 684–688
metodi, 697
ruolo, 685
servizi Web, 685
sicurezza, 686
SortedList, classe, 213
SortedList, oggetti di insieme nelle
Hashtables, 215
sottoscrittori, 342
Source, proprietà, 611
spazi dei nomi livello superiore, 15
spazi dei nomi, 14–17
assembly e, 16
inserimento nell’ambito, 15
spazio bianco, 28
System.Threading.CancellationToken, parametro
spinning, 651, 660
thread, 661
SpinWait, operazioni, 651
Split, metodo, 653
SQL injection, attacchi, 543
SQL SELECT, istruzioni, 546, 553
SQL Server
avvio, 537
login, 537
multiple active result sets (MARS),
545
SQL Server Express, istanza utente,
567
SQL Server, autenticazione, 541
SQL Server, database. Vedere
anche database
concessione dell’accesso a, 567–568
SQL UPDATE, comandi, 583–584
SQL, strumento di configurazione,
537
sqlcmd, utility, 537
SqlCommand, oggetti, creazione, 542,
564
SqlConnection, oggetti, creazione,
539, 564
SqlConnectionStringBuilder, classe,
540
SqlConnectionStringBuilder, oggetti,
540, 562
SqlDataReader, classe, 543, 544
SqlDataReader, oggetti, 543
chiusura, 545
creazione, 564
lettura dei dati con, 544
query dei dati con, 564
SqlException, eccezioni, 539–540, 562
SqlParameter, oggetti, 542–543
Sqrt, metodo, 141, 142
dichiarazione, 143
Stack, classe, 210–211
Stack<T>, classe, versione thread-safe,
669
StackPanel, controlli, 446, 476
aggiunta, 460
Start, metodo, 501, 605
StartNew, metodo, 625, 646
StartupUri, proprietà, 24–25, 457
StateEntries, proprietà, 586
static, parola chiave, 143, 145, 149
StaticResource, parola chiave, 454
Status property, 638
StatusBar, controlli, aggiunta, 505
stili
ambito di, 453
di controlli WPF, 451–457, 464–466
StopWatch, tipo, 611
Storage, parametro, 555
StreamWriter, oggetti, creazione, 487
String, classe, Split, metodo, 653
string, parola chiave, 152
String.Format, metodo, 473, 578
StringBuilder, oggetti, 473, 474
stringhe
aggiungere ad altre stringhe, 92
conversione di enumerazioni in,
174–175
conversione in enumerazioni, 522
definizione di, 34
formattazione di argomenti come,
186
formattazione di stringhe, 60
suddivisione in array, 653
stringhe di connessione, 559, 562, 564
build, 540
per il costruttore DataContext,
551–552
archiviazione, 572
stringhe di testo Vedere anche stringhe
conversione in interi, 40
struct, parola chiave, 180, 190
StructsAndEnums, spazio dei nomi,
176
Struttura documento, finestra, 39–40
strutture binarie
accesso, 384
build utilizzando i generics, 361
creazione di classi generiche, 371
datum, 358
enumeratori, 383
iComparable, interfaccia, 362
inserimento di un nodo, 362
nodo, 358
ordinamento dei dati, 359
sottostrutture, 358
teoria delle, 358
TreeEnumerator, classe, 383
strutture di dati ricorsive, 358
strutture, 178–190
array di, 194
campi istanza in, 181–182
campi privati in, 180
dichiarazione, 180
e classi, 181–182, 188–190
ereditarietà dalle interfacce,
255–256
gerarchia di ereditarietà per, 232
inizializzazione di, 183–187
743
natura di, 271
operatori per, 180
tipi di, 178–179
utilizzo, 184–187
Style, proprietà, 452
Style, tag, TargetType attributo,
454–456
suffisso .csproj, 33
suffisso dell’operatore, 44–45
suggerimenti
in debug, 62
per variabili, 31
switch, istruzioni, 84–89
break, istruzioni, 87
regole di passaggio, 86–87
regole di utilizzo, 86–87
scrittura, 87–89
sintassi, 85
System.Array, classe, 195
System.Collections, spazio dei nomi,
206
System.Collections.Concurrent,spazio
dei nomi, 668
System.Collections.Generic, spazio dei
nomi, 377
System.Collections.IEnumerable,
interfaccia, 381
System.ComponentModel, spazio dei
nomi, 504
System.Data, spazio dei nomi, 539
System.Data.Linq insieme, 560
System.Data.Objects.DataClasses.
EntityObject, classe, 572
System.Data.Objects.DataClasses.
StructuralObject, classe, 572
System.Data.SqlClient, spazio dei
nomi, 539
System.GC.Collect, metodo, 283, 292
System.IComparable, interfaccia, 362
System.Math class Sqrt, metodo, 141
System.Object, classe, 165
classi derivate da, 233–234
System.Random, classe, 193
System.Runtime.Serialization, spazio
dei nomi, 691
System.ServiceModel, spazio dei nomi,
691
System.ServiceModel.Web, spazio dei
nomi, 691
System.Threading, spazio dei nomi,
603
primitivi di sincronizzazione in, 660
System.Threading.CancellationToken,
parametro, 633
744
System.Threading.Monitor, classe
System.Threading.Monitor, classe, 660
System.Threading.Tasks, spazio dei
nomi, 604, 617
System.ValueType, classe, 232
System.Windows, spazio dei nomi,
443
System.Windows.Data, spazio dei
nomi, 523
SystemException, gerarchia di
ereditarietà, 113
T
tabelle del database
cancellazione di righe in, 588, 596
chiavi primarie, 550
classi entità, relazioni tra, 551–552
Column, attributo, 550
entity data model per, 568–572
modifica informazioni in, 596
molti-a-uno, relazioni, 555–556
nuovo, creazione, 551
query, 541–543
recuperare dati da, 579–582
recuperare righe singole, 553
Table, attributo, 550
tipo di colonna sottostante, 550
unione, 554–558
uno-a-molti, relazioni, 556–558
valori null in, 550
tabelle. Vedere tabelle del database
Table, attributo, 550, 564
Table, insiemi, 553, 558
creazione, 564
Table<TEntity>, insiemi come membri
pubblici, 559
Table<TEntity>, tipi, 552
TargetType, attributo, 454–456
Task, classe, 603
parallelismo, implementazione con,
608–617
Wait, metodo, 608
WaitAll, metodo, 646
Task Parallel Library. Vedi TPL (Task
Parallel Library)
Task, oggetti
ContinueWith, metodo, 606
creazione, 604–605, 616, 646
esecuzione, 605–606
multipli, 603
Start, metodo, 646
Status, proprietà, 638
Wait, metodo, 646
Task,costruttori, 604–605
overload di, 605
Task<byte[]>, oggetti, creazione, 627
Task<TResult>, oggetti, 625–628, 646
TaskContinuationOptions, tipo, 606–
607, 645
TaskCreationOptions, enumerazione,
606
TaskFactory, classe, 607–608
TaskFactory, oggetti, 607–608
StartNew, metodo, 625, 646
Tasks, 507
TaskScheduler, classe, 606
TaskScheduler, oggetti, 605
tasti di scelta per elementi di menu, 480
TEntity, parametro del tipo, 552
teoria delle strutture binarie, 358
TestIfTrue, metodo, 651
TextReader, classe, 95
metodo disposal, 285
ThenBy, metodo, 402
ThenByDescending, metodo, 402
this, parola chiave, 139–140, 146, 248
con indicizzatori, 318
ThisKey, parametro, 555
thread concorrenti, 600. Vedere
anche multitasking, thread
thread di background
accesso ai controlli, 508
copia dei dati a, 502–504
esecuzione di operazioni, 508
per lunghe operazioni, 499–502
thread interfaccia utente
attività e, 628–632
copia dei dati da, 502–505
esecuzione di metodi per conto di
altri thread, 505–507
thread, 603–604
anabolizzare il processore, 660
attesa di eventi, 661–662
bloccaggio, 663–664
bloccare l’esecuzione di, 666–667
blocco dei dati, 659–661
concorrente, 600
definizione di, 283, 499
lettura risorse, 665
multipli, 499–500
numero ottimale di, 604
parallelo, 614–617
pianificazione, 603–604
pool di risorse, accesso, 663–664
restrizioni accesso oggetto, 502
scrittura nelle risorse, 665
sincronizzazione, 651, 666, 681
sleep, 659–660
sospendere, 661–662
thread di background, 502–504
wrapper per, 504
Thread, classe, 499
Start, metodo, 501
Thread, oggetti, 603
creazione di nuovo, 501
riferimento a metodi in, 508
Thread.Sleep, metodo, 624
Thread.SpinWait, metodo, 660
thread-local storage (TLS), 661
ThreadPool, classe, 603
ThreadStatic, attributo, 661
throw, istruzioni, 126
prevenzione del passaggio, 86
scrittura, 122
ThrowIfCancellationRequested,
metodo, 640–641
tilde (~), modificatore, 281, 292
tipi anonimi negli array, 194–195, 197
tipi creati, 357
tipi di classe, copia, 151–156
tipi di dati
bool, tipo di dati, 32, 74
char, tipo di dati, 32, 542
DateTime, tipo di dati, 81, 84
decimal, tipo di dati, 31
di enumerazioni, 176
double, tipo di dati, 31
float, tipo di dati, 31
IntelliSense, 9
long, tipo di dati, 31
mappatura del tipo di dati, 550
operatori e, 37–38
Queue, tipo di dati, 354
thread-safe, 678
tipi di dati primitivi, 31–36, 86, 118
tipi di dati primitivi, 31–36
dimensione fissa di, 118
switch, istruzioni, 86
uso nel codice, 33–34
visualizzazione del valore di, 32–33
tipi di riferimento, 151
array. Vedere array
distruttori per, 281
heap, creazione in, 163
Object, classe, 165
tipi interi, enumerazioni basate sui
tipi, 176
tipi nullable, 156–159
proprietà, 158–159
Value, proprietà, 158–159
virgolette
tipi stringa, 32, 152, 474
tipi struttura, dichiarazione, 190
tipi, estensione, 248
tipo valore, variabili, 299
copia, 171, 189
Title, proprietà, 21, 496
TKey, 357
ToArray, metodo per il recupero dei
dati, 553–554, 558
token di annullamento, 633
creazione, 633–634
esaminare, 641
ToList, metodo per il recupero dei
dati, 553–554, 558–559
ToString, metodo, 41, 175, 185
di strutture, 178–179
implementazioni di, 238–239
TPL (Task Parallel Library), 603
classi di insiemi thread-safe e
interfacce, 668
Parallel, classe, 617–624
primitivi di sincronizzazione in,
661–667
strategia di annullamento, 632–645
Task, classe, 603. Vedere anche Task,
classe
tecniche di blocco, 661
thread, pianificazione, 603–604
traccia delle modifiche, 584
trattino basso, regole di sintassi, 28, 30
TResult, parametro del tipo, 399
trigger, 456
try, blocchi, 110
scrittura, 116
try, parola chiave, 110
try/catch, blocchi istruzione, scrittura,
114–118
TSource, parametro del tipo, 399
TValue, 357
U
uguaglianza (==), operatore, 74
precedenza e associatività di, 77
unboxing, 166–168
unchecked, parola chiave, 118–119
unsafe, parola chiave, 170
UpdateException, eccezioni, 588
UpdateException, gestore, 594–595
UpdateSource, metodo, 529
chiamate, 532
UpdateSourceTrigger, proprietà, 528
convalida differita con, 532
Use dynamic ports, proprietà., 690
User ID, parametro, 541
using, direttive, 286
using, istruzioni, 15, 16
istruzioni di chiusura della
connessione dati, 546
per la gestione delle risorse,
286–288
scrittura, 289–292
sintassi, 286
V
ValidateNames, proprietà, 496
Validation.HasError, proprietà
rilevamento modifiche a, 532
trigger per, 518
ValidationRules, elementi, 516
valori
boxing, 165–166
confronto, 89
ritorno dalle attività, 625–628
unboxing, 166–168
valori nullable, 122
valori stringa
concatenazione, 37, 40
conversione in interi, 46
conversione in valori int, 101
Value, proprietà, 158, 159
value, tipi, 171
copia, 151–156
distruzione di, 279
nullable, 157–158
stack, creazione in, 163
strutture, 178–190
ValueConversionValueConversion,
attributo, 523
var, parola chiave, 45
per variabili tipizzate in modo
implicito, 148
variabili Booleane, dichiarazione, 89
variabili di array
convezioni di denominazione, 192
dichiarazione, 191–192, 218
inizializzazione, 193–194
variabili di enumerazione, 174
assegnazione ai valori, 190
conversione in stringhe, 174
dichiarazione, 190
operazioni matematiche su,
177–178
variabili di riferimento, 280
copia, 171, 189
745
inizializzazione, 156–157
valori null, 157
variabili di struttura
copia, 187
dichiarazione, 182, 190
inizializzazione, 190
versioni nullable di, 182
variabili locali, 54
visualizzazione informazioni, 104
variabili non assegnate, 32, 73
variabili nullable
aggiornamento, 159
assegnazione di espressioni a, 158
testing, 157
variabili statiche, 144
variabili stringa, salvataggio dati in,
101
variabili tipizzate in modo implicito,
45–46
variabili, 29–31
ambito di, 53
assegnazione di valori, 31
controllo valori nel debugger,
62–63
convezioni di denominazione, 30
copia del contenuto in tipi di
riferimento, 153
decremento, 43–44, 46, 92, 108
denominazione, 29, 30
di tipi di classe, 131
dichiarazione, 46
incremento, 43–44, 46, 92, 108
inizializzazione allo stesso valore, 46
inizializzazione, 53
nei metodi, 53
non assegnate, 32, 73
qualificazione come parametri,
139–140
tipi di, dedotte, 45
tipizzato in modo implicito, 45–46,
148
valore di, modifica, 46
valori assegnati a, 45
value, tipi, 171, 189, 299
Variant, tipo, 45
versioni di tipo specifico di una classe
generica, 357
VerticalAlignment, proprietà, 447–448
View Code, comando, 33, 476
vincoli, con i generics, 358
virgolette
nella definizione di classi, 130
per gruppi di istruzioni, 78–79, 93,
98
746
virgolette doppie (“)
virgolette doppie (“), 88
virgolette singole (‘), 88
virtual, parola chiave, 239, 240, 251,
272, 276
Visual C# 2010 Express, 4. Vedere
anche Visual Studio 2010
applicazioni console, creazione, 6–8
applicazioni grafiche, creazione, 18
avvio, 4
impostazioni dell’ambiente di
sviluppo predefinite, 5
salvataggio, specificare il percorso,
539
Visual Studio 2010
ambiente di programmazione, 3–8
avvio, 4
barra degli strumenti, 7
barra dei menu, 7
codice auto generato, 22–23
Editor di codice e di testo, riquadro, 7
Elenco errori, finestra, 12
Entity Framework, 565. Vedere
anche Entity Framework
Esplora soluzioni, riquadro, 7
file creati da, 8–9
impostazioni dell’ambiente di
sviluppo predefinite, 4
Output, finestra, 11
Visual Studio 2010 Professional, 4
applicazioni console, creazione, 5–6
applicazioni grafiche, creazione, 17
Visual Studio 2010 Standard, 4. Vedere
anche Visual Studio 2010
applicazioni console, creazione, 5–6
applicazioni grafiche, creazione, 17
visualizzazione codice, 17
visualizzazione progettazione, 17
void, parola chiave, 48, 49, 51
W
Wait, metodo, 608, 646, 661, 681
WaitAll, metodo, 608, 646
WaitAny, metodo, 608
WaitingToRun, stato attività, 638
WCF (Windows Communication
Foundation), 684
Web Services Description Language
(WSDL), 686
Where, metodo, 401
while, istruzioni, 92–96, 108
scrittura, 93–96
sintassi, 92–93
termine di, 93
Window.Resources, elemento,
453–454, 515
Window_Closing, metodo, 474–476
Window_Loaded, metodo, 579
Windows Communication Foundation
(WCF), 684
Windows Form, 17
Windows Presentation Foundation
(WPF), 17. Vedere anche WPF,
applicazioni; WPF, controlli;
WPF, form
WithCancellation, metodo, 656, 681
WithDegreeOfParallelism, metodo,
655
WithExecutionMode, metodo, 655
workload, numero ottimale di thread
per, 604
WPF, applicazioni
aggiornamento e ricarica dei form,
471
build, 444–458
Closing, eventi, 474–476
controlli menu, 477–508
controlli, aggiunta, 458–470
controlli, impostazione ai valori
predefiniti, 466–470
creazione, 443–445, 476
eventi, gestione, 470–476, 476
form, aggiunta, 457
funzionalità, 457–460
Grid, pannelli, 446
immagini di background, aggiunta,
449–451
informazioni del database,
visualizzazione in, 574–579
proprietà, impostazioni, 476
proprietà, modifica dinamica,
466–470
punti di ancoraggio dei controlli,
447–448
regole di convalida, 510
riquadri di layout, 446
sicurezza dei thread, 502
stile dei controlli, 451–457
tempi di risposta, miglioramento,
498–507
testo, proprietà, 457
visualizzazione codice, 476
XAML, definizione di, 445
WPF, cache, refresh, 579
WPF, controlli Vedere anche controlli
associazione a origini dati, 580
come elementi di menu, 484
scelta rapida per, 491
WPF, eventi interfaccia utente, 345
WPF, finestre, compilazione, 452
WPF, form, 457
istanze di, 489
menu di scelta rapida,
deassociazione da, 494
Struttura documento, finestra,
39–40
visualizzazione codice, 23
visualizzazione, 489, 592
XAML, 19–20
WPF, modello Applicazione, 17, 445,
476
WrapPanels, 446
WrappedInt, classe, 156
WrappedInt, oggetti, passare come
argomenti, 154–156
WrappedInt, variabili, dichiarazione,
155
WriteableBitmap, classe, 611
WriteableBitmap, tipo, 611
WriteLine, metodo, 9, 219
overload, 55–56, 224
WS-*, specifiche, 687
WS-Addressing, specifica, 687
WSDL (Web Services Description
Language), 686. Vedere
anche SOAP (Simple Object
Access Protocol)
WS-Policy, specifica, 687
WS-Security, specifica, 686
X
XAML (Extensible Application Markup
Language) in WPF forms,
19–20, 445
XML, 684
XML, dichiarazione dello spazio dei
nomi, 515
XML, spazi dei nomi, 445
xmlns, attributi, 445
XOR (^), operatore, 316
Y
yield, parola chiave, 390
Z
ZIndex, proprietà, 451
L’autore
747
L’autore
John Sharp è direttore della divisione tecnologia presso
Content Master (www.contentmaster.com), del gruppo CM,
una società leader del settore nel Regno Unito e si occupa
di ricerca e sviluppo dei contenuti tecnici per corsi di
formazione, seminari e white paper. Inoltre, partecipa
attivamente allo sviluppo e all’interoperabilità tra le
applicazioni di Microsoft .NET Framework. Sharp ha scritto
vari documenti, progettato corsi e tutorial e prodotto
presentazioni per conferenze su sistemi distribuiti, servizi
Web, migrazione delle applicazioni e comunicazione tra
Microsoft Windows e .NET Framework e tra UNIX, Linux e
Java, oltre a documenti di sviluppo basati sui linguaggi C#
e J#. Infine, Sharp è l’autore di molti libri tra cui Microsoft Windows Communication Foundation
Step by Step.
.