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=&quot;Data Source=.\SQLExpress;Initial Catalog=Northwind;Integrated Security=True;MultipleActiveResultSets=True&quot;" 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. .