Variante della (pen)ultim’ora
Mi riferisco al post precedente che ovviamente dovrà essere letto prima di questo.
Nottetempo mi è venuta in mente un possibile modifica volta, perlomeno, a evitare il pasticcio principale, pur mantenendo l'impianto delle tre caselle di testo relative agli indirizzi email, ai nomi & cognomi e al messaggio da spedire.
Si tratta di utilizzare come separatore di nomi e indirizzi il cancelletto (#) nelle prime due. Per cui nella prima si avrà “pippo@x.it#pluto@y.com#paperino@z.com”. Analogamente nella seconda. Quindi nella Sub dell’evento New si provvede a sostituire questi # con:
TextBox1.Text = TextBox1.Text.Replace("#", Chr(13))
TextBox2.Text = TextBox2.Text.Replace("#", Chr(13))
che li sostituisce con degli a-capo (così oltretutto l’utente non li vede). Questo banale artifizio elimina l’inconveniente dei cognomi con trattino, come Rossi-Doria e simili.
Quanto alla terza casella, anziché “#NOMI#” conviene mettere “{NOMI }”.
Se si è capito l’antifona, il coro dei buoni intenditori cui mi rivolgo, all’unisono o quasi, canterà per approvarla la seguente modifica della routine d’evento Click del pulsante Spedisci:
Private Sub Button2_Click(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs) Handles Button2.Click
Indirizzi = TextBox1.Text.Replace(Chr(13), ";")
' NB - non occorre togliere l'eventuale ultimo ; che Outlook ignora automaticamente
' Imposta Outlook e un suo oggetto Mailitem
Dim Outlk As New Outlook.Application
Dim MioMess As Outlook.MailItem
Nomi = "" & vbCrLf & TextBox2.Text & vbCrLf
Dim Corpo = TextBox3.Text.Replace("{NOMI}", Nomi)
MioMess = Outlk.CreateItem(Outlook.OlItemType.olMailItem)
With MioMess
.Subject = "Richiesta chiarimenti"
.BCC = "giannigiac@tin.it"
.To = Indirizzi
.Body = Corpo
MioMess.Save()
End With
MioMess = Nothing
Outlk = Nothing
End Sub
S.E.&O.
E che fare del primo pulsante? Con la semplificazione introdotta la missione espressa da Elenco indirizzi converrà che sia cambiata in qualcosa come Controllo coerenza onde verificare, come minimo, che il numero dei nomi e quello degli indirizzi siano identici. Lascio a chi ci tiene questo compitino.
Gianni Giaccaglini
@giannigiac@tin.it
Circolari Outlook con OLE Automation, primi passi
Preliminari
Questo esempietto didattico sfrutta il meccanismo OLE Automation, da tempo detto Automation tout court, per consentire l’invio di circolari Outlook a una lista di nominativi e relativi indirizzi email.
Per partire si debbono fissare i riferimenti alla specifica libreria. In soldoni:
1. Dal menu Progetti scegliere Aggiungi riferimento...;
2. Cliccare sulla pagina COM e dall’elenco scegliere la libreria Microsoft Outlook 14.0 (12.0 se si dispone di Office XP) concludendo con OK;
3. Nella finestra MainWindow (Window1 in Visual Studio 2008) inserire a livello Dichiarazioni la seguente direttiva:
Imports Microsoft.Office.Interop.
L’ultima mossa ha il duplice scopo di rendere disponibile la libreria di un membro della famiglia Microsoft Office (altrimenti i riferimenti sopra fissati non sarebbero concretamente operativi) nonché di abbreviare la sintassi delle istruzioni specifiche. Ne anticipo un esempio:
Dim Outlk As New Outlook.Application
Che, in termini completi sarebbe la kilometrica
Dim Outlk As New Microsoft.Office.Interop.Outlook.Application
Successivamente si crei una finestra WPF dall’aspetto qui sotto grossolanamente riprodotto.
|
Window1
|
|
pippo@x.it pluto@y.com
paperino@z.com
|
|
GentiliSignori#NOMI#
Distinti saluti
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Pippo Rossi-Pluto Bianchi-Paperino Verdi
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Elenco indirizzi
|
|
Spedisci
|
|
|
|
|
Tale Window racchiude in una Grid tre caselle di testo TextBox1, TextBox2 e TextBox3 più due pulsanti di comando CommandButton1 e CommandButton2 etichettati “Elenco indirizzi” e “Spedisci”, il primo avente lo scopo di monitorare la composizione delle due TextBox di sinistra. Quanto alle tre caselle la prima contiene indirizzi email default, la seconda i corrispettivi nomi e cognomi, la terza il testo base del messaggio da affidare a Microsoft Outlook.
Utilizzo
Regola: gli indirizzi e-mail vanno separati da uno e un solo spazio, mentre i vari nomi e cognomi hanno un trattino (-) come separatore, altrimenti l’intera baracca crolla.
Tralasciando il lavoro (meramente esemplificativo) ottenuto cliccando su Elenco indirizzi, le operazioni da compiere per lanciare una circolare sono le seguenti:
1. Assicurarsi che Microsoft Outlook sia aperto (1);
2. Completare indirizzi e-mail e i nomi e cognomi curando rigorosa corrispondenza fra gli uni e gli altri, quindi il testo del messaggio, rispettivamente nella prima, seconda e terza casella di testo (2);
3. Cliccare sul pulsante Spedisci e passare ad Outlook, constando la presenza della nostra missiva nella cartella delle bozze Draft per eventuali ulteriori modifiche.
Note. (1) Analogo codice macro (VBA) lanciato e.g. da Excel opera pure con Outlook in background, non così in Visual Studio, salvo segreti a me ignoti.
(2) il testo “#NOMI# funge da parametro-segnaposto che il codice sostituisce con un elenco dei destinatari, attinti dalla TextBox2, perciò non va tassativamente toccato!
XAML
Il risultato predetto è frutto del codice XAML qui sotto riportato.
<Window x:Class="Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="300" Width="300">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="210*" />
<RowDefinition Height="52*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="108*" />
<ColumnDefinition Width="108*" />
</Grid.ColumnDefinitions>
<TextBox Grid.Column="0" TextWrapping="Wrap"
VerticalAlignment="Top" Height="100"
VerticalScrollBarVisibility="Visible"
Name="TextBox1">
pippo@x.it pluto@y.com paperino@z.com
</TextBox>
<TextBox Grid.Column="0" TextWrapping="Wrap"
VerticalAlignment="Bottom" Height="100"
VerticalScrollBarVisibility="Visible"
Name="TextBox2">
Pippo Rossi-Pluto Bianchi-Paperino Verdi
</TextBox>
<TextBox Grid.Column="1" TextWrapping="Wrap"
VerticalScrollBarVisibility="Visible"
AcceptsReturn="True" Name="TextBox3">
Gentili Signori#NOMI#
Distinti saluti
</TextBox>
<Button Grid.Row="1" Grid.Column="0"
Margin="17,11,17,9" Name="Button1">
Elenco indirizzi
</Button>
<Button Grid.Row="1" Grid.Column="1"
Margin ="17,11,17,9" Name="Button2">
Spedisci
</Button>
</Grid>
</Window>
Dopo quanto descritto in apertura c’è solo da aggiungere che le direttive seguenti accomunano le tre TextBox, ponendo barre di scorrimento laterali e assicurando che il testo sia contenuto nei margini:
TextWrapping="Wrap"
VerticalScrollBarVisibility="Visible"
Solo nella terza casella è invece presente
AcceptsReturn="True"
Che permette all’utente di inserire dei carriage return premendo Invio, cosa che invece, come si constata, risulta inibita nelle altre due dove, ripeto e insisto, sono solo ammessi un singolo spazio nella prima e, nell’altra, un trattino.
VB
Il codice Visual Basic è riprodotto interamente qui di seguito, a partire dalla direttiva Imports a livello Dichiarazioni necessaria perché proprietà e metodi di un oggetto Outlook.Application si possano richiamare e utilizzare.
Imports Microsoft.Office.Interop
Class Window1
Dim Indirizzi As String, Nomi As String
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs) Handles Button1.Click
Dim TestoIndirizzi = TextBox1.Text
If TestoIndirizzi(TestoIndirizzi.Length - 1) <> " " Then
TestoIndirizzi &= " "
End If
'MessageBox.Show(TestoIndirizzi)
Dim Indirizzo = ""
Dim i = 0
While TestoIndirizzi.Length > 0
While TestoIndirizzi(i) <> " "
Indirizzo &= TestoIndirizzi(i)
i += 1
End While
TestoIndirizzi = TestoIndirizzi.Remove(0, Indirizzo.Length + 1)
MessageBox.Show(Indirizzo & vbLf & TestoIndirizzi, "E-MAIL")
Indirizzo = ""
i = 0
End While
TestoIndirizzi = TextBox2.Text
If TestoIndirizzi(TestoIndirizzi.Length - 1) <> "-" Then
TestoIndirizzi &= "-"
End If
Indirizzo = ""
i = 0
While TestoIndirizzi.Length > 0
While TestoIndirizzi(i) <> "-"
Indirizzo &= TestoIndirizzi(i)
i += 1
End While
TestoIndirizzi = TestoIndirizzi.Remove(0, Indirizzo.Length + 1)
MessageBox.Show(Indirizzo & vbLf & TestoIndirizzi, "Nomi e cognomi")
Indirizzo = ""
i = 0
End While
End Sub
Private Sub Button2_Click(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs) Handles Button2.Click
Indirizzi = TextBox1.Text.Replace(" ", ";")
' Imposta Outlook e un suo oggetto Mailitem
Dim Outlk As New Outlook.Application
Dim MioMess As Outlook.MailItem
Nomi = "" & vbCrLf & TextBox2.Text.Replace("-", vbCrLf) & vbCrLf
Dim Corpo = TextBox3.Text.Replace("#NOMI#", Nomi)
MioMess = Outlk.CreateItem(Outlook.OlItemType.olMailItem)
With MioMess
.Subject = "Richiesta chiarimenti"
.BCC = "giannigiac@tin.it"
.To = Indirizzi
.Body = Corpo
MioMess.Save()
End With
MioMess = Nothing
Outlk = Nothing
End Sub
End Class
Per brevità lascio completamente all’esegesi autogestita il codice relativo al Click sul primo pulsante, avente funzione esplorativa, di controllo e, più che altro, didattica. In un caso reale CommandButton1 e l’intero codice associato si potrebbero eliminare.
Veniamo all’evento Click del secondo pulsante. La prima istruzione pone in Indirizzi il testo Text della TextBox1 sostituendone tutti gli spazi con dei punti e virgola, per cui Indirizzi sarà una stringa del tipo seguente, immaginando un altro email oltre i tre default:
“pippo@x.it;pluto@y.com;paperino@z.com;minnie@boh.org”
Seguono istruzioni per impostare in Outlk l’applicazione Outlook e un oggetto messaggistico (di tipo Outlook.MailItem) in MioMess, quindi nella variabile Nomi si compie l’operazione analoga a quella vista per Indirizzi ma sostituendo i trattini con dei CR. In questo caso va premesso "" & (per motivi un po’ misteriosi, ma indispensabili...) più un CR da porre dopo “Gentili signori”, ottenendo così qualcosa del genere indicando solo i CR intermedi:
“Pippo Rossi
Pluto Bianchi
Paperino Verdi
Minnie Brambilla
. . . "
Tale stringa va poi a rimpiazzare il segnaposto “#NOMI# creando il Corpo del messaggio. Il tutto si esprime con la fissazione delle proprietà di MioMess incastonate fra With MioMess ed EndWith e per pigrizia mi affido alla loro eloquenza. Il tutto farà sì che nella cartella Draft di Oulook compaia un messaggio in bozza grossomodo di questo tipo:
A... pippo@x.it; pluto@y.com; paperino@z.com; minnie@boh.org
Ccn... giannigiac@tin.it
Oggetto: Richiesta chiarimenti.
Gentili Signori,
Pippo Rossi
Pluto Bianchi
Paperino Verdi
Minnie Brambilla
Vi prego di provvedere!
Distinti saluti
Nota. Come si constata Outlook inserisce automaticamente uno spazio dopo i vari punti e virgola.
Considerazioni finali
Questa soluzione è presenta imperfezioni anche spicciole (per esempio con cognomi tipo Rossi-Doria nasce un guaio!) per le quali conto sulla benevolenza di chi legge, che spero vorrà perdonare taluni spunti di discutibile umorismo. Così com’è il modello è afflitto da difficoltà di deployment qualora si ambisse a farne un uso erga omnes.
Il fatto è che avevo una certa fretta di fornire un’introduzione a una tecnica forse un po’ ingiustamente snobbata anche da programmatori pro che potrebbero sfruttarla in modo più razionale.
Successivi sviluppi, ai quali anch’io sto pensando, potrebbero comprendere quantomeno:
· l’utilizzo della proprietà HTMLBody in luogo della semplice Body , nel qual caso il messaggio della nostra terza casella potrebbe essere di tipo HTML, ottenendo font più o meno particolari, immagini inserite (e attenzione! il tag <IMG ...> deve riferirsi a un file posto sul web, altrimenti il destinatario NON la vedrebbe);
· soprattutto elenchi di indirizzi e nominativi “seri”, che potrebbero essere attinti, con opportuni binding, da quelli di cui ogni utente dispone nel suo Outlook e che si lasciano esportare in vari formati, tra cui un tal SentItemsOutlook.csv (nel rude sistema comma separated value, ahimè).
Tempo permettendo ne riparleremo.
(v. anche un mio post in materia su http://blog.shareoffice.it/giannigiaccaglini nel quale si creano circolari richiamando Outlook da un file Excel dotato di macro. Può servire a un raffronto sintattico, notando sintassi identiche o poco difformi fra i mondi Visual Studio e VBA, Visual Basic Application edition).
Un utile manuale didattico sul parallel programming
Parallel Programming with Microsoft® Visual Studio®2010 Step by Step
AUTORE: Donis Marshall
EDITORE: O’Reilly Media, Inc.
http://shop.oreilly.com/product/0790145300706.do
ebook; $27,99 – libro cartaceo: $34,99 – libro + ebook: $38,49
(tutti gli esempi, illustrati, sono on line per gli acquirenti)
Forse non tutti i programmatori anche professionali se ne sono accorti, ma le ultime generazioni di CPU sono multicore, vale a dire composti da almeno due o più processori, ciascuno dei quali può lavorare di conserva. In tal modo il multitasking diventa possibile e attraente. In primo luogo per conseguire performance elevate (che superano il più fervido ottimismo della Legge di Moore) ma anche per agire secondo modalità particolari che solo la simultaneità operativa garantisce. A tale scopo occorre cambiare mentalità ovvero passare dallo stile di programmazione sequenziale, di sole istruzioni che si succedono una dopo l’altra, a una nuova modalità che suddivide le operazioni in compiti (task) separati e cooperanti.
Ben venga dunque questo testo dedicato a un tema sempre più importante, approvato dalla Microsoft. La casa di Redmond con il varo di Visual Studio 2010 ha promosso il parallel programming a tecnologia basilare, supportata dagli specifici namespace Task Parallel Library (TPL) e System.Threading.Tasks, che forniscono tutte le codifiche e gli strumenti necessari per la creazione e manutenzione di applicazioni parallelizzate, aiutando a scomporle in task concorrenti e coordinati che girano su “core” ossia processori distinti.
Il libro fin dal titolo si presenta come un tutorial passo dopo passo, una scelta che lo rende di più agevole lettura e, soprattutto, si propone di guidare il passaggio dal vecchio al nuovo paradigma programmatorio. Il seguente sommario dà un’idea abbastanza chiara dei contenuti e della gradualità espositiva seguita dall’Autore.
Capitolo 1 - Introduction to Parallel Programming, introduces the fundamental concepts of parallel programming.
Capitolo 2 - Task Parallelism, focuses on creating parallel iterations and refactoring sequential loops into parallel tasks.
Capitolo 3 -Data Parallelism, focuses on creating parallel tasks from separate operations.
Capitolo 4 - PLINQ, is an overview of parallel programming using Language-Integrated Query (LINQ).
Capitolo 5 - Concurrent Collections, explains how to use concurrent collections, such as ConcurrentBag and ConcurrentQueue.
Capitolo 6 - Customization, demonstrates techniques for customizing the TPL.
Capitolo 7 - Reports and Debugging, shows how to debug and maintain parallel applications and rounds out the full discussion of parallel programming.
I primi due capitoli sono molto importanti e meritano di essere attentamente letti pure dai più esperti. Stiamo alludendo in primo luogo ai molti programmatori che si sono formati “sul campo” e che, sia detto senza offesa, conoscono il multitasking quasi solo per sentito dire. Ma anche a quanti possiedono un background scolastico in materia (almeno nei corsi di Informatica universitari di task, “semafori” e compagnia bella si parla abbastanza a fondo) converrà esaminare questi richiami, che giudichiamo utili, chiari e, insieme, rigorosi. In particolare l’autore ben chiarisce la differenza fra task e thread, questi ultimi già abbastanza noti a chi ha padronanza del Visual Studio, chiarendo che i secondi sono frazioni dei primi. Fra le due creature si fa confusione? Certo e il libro serve egregiamente a evitarla. Importante, al riguardo, la (limpida e rigorosa) trattazione della sincronizzazione di task e thread.
I successivi capitoli sono ricchi di dettagli e corredati da validi esempi anche pratici. Opportuno ci è parso il passaggio, dopo i concetti introduttivi, al tema della messa in parallelo dei dati per assicurarne la necessaria coerenza negli accessi concorrenti.
Come già detto anche i titoli degli altri capitoli parlano da soli, in particolare è allettante quello dedicato al PLINQ, una versione del già espressivo linguaggio di query LINQ, estesa alle problematiche del nuovo mondo.
Due ultime considerazioni. Anzitutto va detto che l’opera, accattivante nel titolo ed efficace nell’esposizione, non è certo per principianti ma si rivolge a gente dotata di buona competenza dell’ambiente .NET (a proposito: tutti gli esempi sono in C#). Inoltre non c’è la pretesa di esaurire la complessa materia. Tuttavia viene fornito l’indispensabile per conoscere e comprendere abbastanza a fondo le basi, permettendo a ciascuno di proseguire l’avventura anche con le proprie gambe.
Uno scampolo tipico preso dal manuale...
... per dare un’idea (intuitiva, per carità!) sulla materia e sul trattamento che ne dà il libro. A monte viene descritto il meccanismo composto dei passi seguenti: 1) crea un certo task coi suoi metodi ecc.; 2) lo accoda a un pool di thread; 3) ne gestisce lo scheduling; 4) lo suddivide nei processori disponibili sulla macchina.
Ed ecco senza commenti lo scampolo testé annunciato:
The Task Class
In the .NET Framework, the Task class is a logical abstraction of a task. You can use this class to schedule and ultimately execute a parallel task. Remember, tasks are unlike threads in that you do not start a task directly. By default, the thread pool schedules a task, places it on a queue in the thread pool, and eventually executes the task on an available thread. In this book, starting a task implies queuing the task first and later executing the task on an available thread from the thread pool.
Using the Parallel.Invoke Method
You can schedule a task in several ways, the simplest of which is by using the Parallel.Invoke method. The following example executes two parallel tasks—one for MethodA and another for MethodB. This version of Parallel.Invoke accepts an array of Action delegates as the sole parameter. Action delegates have no arguments and return void.
Parallel.Invoke(new Action[] { MethodA, MethodB });
The Parallel.Invoke method is convenient for executing multiple tasks in parallel. However, this method has limitations:
■ Parallel.Invoke creates but does not return task objects.
■ The Action delegate is limited—it has no parameters and no return value.
■ Parallel.Invoke is not as flexible as other solutions and always uses an implied
Task.WaitAll method, described in more detail later in this section.
(continua)
A buon intenditore.
Visore di archivio XML tramite binding
Il progetto qui discusso è tratto, con qualche adattamento, da un buon manuale WPF, già positivamente recensito su questo blog:
WPF in action, di Arlen Feldman e Maxx Daymon
L’applicativo collega un archivio XML a una finestra WPF con tecnica binding, con una soluzione interamente dichiarativa, che sfrutta un servizievole parser dedicato ad archivi XML. Quello utilizzato come cavia è relativo ai cosiddetti CVE (Common Vulnerabilities and Exposures) che espone le molteplici cause di vulnerabilità da attacchi maligni monitorate da MITRE, ente federale USA di ricerca in tale campo. Lo si può scaricare da:
Lo abbiamo salvato in C:\ col nome ElementiCVE.xml dopo aver eliminato nel tag cve tuttii riferimenti a namespace vari. Insomma si dovrà avere un nudo e crudo <cve>. Solo così il parser XML opera correttamente.
ElementiCVE.xml ha una struttura gerarchica col nodo-padre <cve>, figli <item>, formati da nipoti e pronipoti vari. Per farla breve e potando drasticamente descrizioni <desc> e commenti <comment> esso si presenta così:
<?xml version="1.0"?>
<cve>
<item type="CAN" name="CVE-1999-0001" seq="1999-0001">
<status>Candidate</status>
<phase date="20051217">Modified</phase>
<desc>ip_input.c in BSD-derived TCP/IP implementations allows remote attackers ... </desc>
<refs>
<ref source="CERT">CA-98-13-tcp-denial-of-service</ref>
<ref source="BUGTRAQ">19981223 Re: CERT Advisory CA-98.13 - TCP/IP Denial of Service</ref>
<ref source="CONFIRM" url="http://www.openbsd.org/errata23.html#tcpfix">http://www.openbsd.org/errata23.html#tcpfix</ref>
<ref source="OSVDB" url="http://www.osvdb.org/5707">5707</ref>
</refs>
<votes>
<modify count="1">Frech</modify>
<noop count="2">Northcutt, Wall</noop>
<reviewing count="1">Christey</reviewing>
</votes>
<comments>
<comment voter="Christey">A Bugtraq posting indicates that the bug has to .. .
</comment>
<comment voter="Christey">The description for BID:190, which links to CVE-1999-0052 ...
</comment>
</comments>
</item>
<item type="CVE" name="CVE-1999-0002" seq="1999-0002">
. . . .
</item>
. . . . ALTRI ITEM . . . .
</cve>
Il risultato finale è visibile nella figura scaricabile dal link seguente:
Sulla sinistra una ListBox riporta i diversi item. Selezionandone uno si provoca l’apparizione delle rispettive Descrizione, Riferimenti e Commenti nei riquadri accatastati sulla destra.
Sperimentazione del piccolo applicativo. Sorprendente! L'archivio ElementiCVE.xml è mastodontico (oltre 54 MB) e se si prova a caricarlo con un editor XML o, peggio, col Notepad, ci giriamo i pollici nel navigare in esso. Ma se usiamo il nostro VisoreXML la musica cambia radicalmente, si passa da un elemento all'altro quasi istantaneamente, con immediata apparizione dei valori correlati. Insomma si direbbe che il binding compie un'apprezzabile magia.
Il codice XAML
È interamente contenuto nel modulo MainWindow (o, nell’edizione 2008, Window1 o altro nome assegnato da noi). Lo riporto senza indugi per intero, facendo subito notare che nell’ambito delle risorse <Windows.Resources> della Window si ha il basilare nodo XmlDataProvider, che, come il nome e i suoi attributi eloquentemente indicano, funge da fornitore di una fonte dati identificata da Source secondo la chiave x:Key nella fattispecie definita secondo il nodo padre cve, mentre l’XPath, secondo una sintassi standard XML, denota il percorso /cve/item dell’elemento (item) da considerare. Circa IsAsynchronous e IsInitialLoadedEnabled rimando alla Guida.
Nota. Piuttosto si consideri, anche a futura memoria, la direttiva debug:PresentationTraceSources.TraceLevel="High", che fissa uno speciale tracciatore delle operazioni binding, segnalando anomalie che altrimenti il permissivo linguaggio XAML non rifiuta (onestamente, non l’ho sperimentato, essendo il codice adottato corretto).
<Window x:Class="MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:debug="clr-namespace:System.Diagnostics;assembly=WindowsBase"
Title="Visore XML (Common Vulnerabilities and Exposures)"
Width="600" Height="400">
<Window.Resources>
<XmlDataProvider
x:Key="cve"
Source="C:\ElementiCVE.xml"
XPath="/cve/item"
IsAsynchronous="False"
IsInitialLoadEnabled="True"
debug:PresentationTraceSources.TraceLevel="High" />
</Window.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="120" />
<ColumnDefinition Width="3" />
<ColumnDefinition Width="1*" />
</Grid.ColumnDefinitions>
<DockPanel>
<TextBox Name="filter" DockPanel.Dock="Top" />
<ListBox Name="listBox1" ItemsSource=
"{Binding Source={StaticResource cve}}" IsSynchronizedWithCurrentItem="True">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding XPath=@name}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DockPanel>
<GridSplitter Grid.Column="1"
VerticalAlignment="Stretch" HorizontalAlignment="Stretch" />
<GroupBox Grid.Column="2" Header="Dettagli CVE">
<GroupBox.DataContext>
<Binding Source="{StaticResource cve}"/>
</GroupBox.DataContext>
<StackPanel>
<WrapPanel>
<Label Height="23">Nome:</Label>
<Label FontWeight="Bold" Height="23" Content="{Binding XPath=@name}" MinWidth="100" />
<Label Height="23">Stato:</Label>
<Label FontWeight="Bold" Height="23" Content="{Binding XPath=status}" MinWidth="80" />
</WrapPanel>
<TextBlock FontSize="12" FontWeight="Bold" Background="Brown"
Foreground="White" Padding="10,2,2,2">Descrizione</TextBlock>
<TextBlock TextWrapping="Wrap" Text="{Binding XPath=desc}" Margin="10,10,10,20" />
<TextBlock FontSize="12" FontWeight="Bold" Background="Brown"
Foreground="White" Padding="10,2,2,2">Riferimenti</TextBlock>
<ListBox ItemsSource="{Binding XPath=refs/ref}" Margin="10,10,10,20" BorderThickness="0"
BorderBrush="Transparent">
<ListBox.ItemTemplate>
<DataTemplate>
<WrapPanel>
<TextBlock MinWidth="50" Text="{Binding XPath=@source}" />
</WrapPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<TextBlock FontSize="12" FontWeight="Bold" Background="Brown"
Foreground="White" Padding="10,2,2,2">Commenti</TextBlock>
<ListView ItemsSource="{Binding XPath=comments/comment}" Margin="10,10,10,20"
BorderThickness="0" Width="433" Height="100">
<ListView.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=InnerText}"/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackPanel>
</GroupBox>
</Grid>
</Window>
Limito all’essenziale la descrizione della struttura XAML, anche perché mi rivolgo a gente che se la cava con tali faccende. Comunque si ha una Grid di tre colonne. Nella prima un DockPanel contiene una TextBox seguita da una ListBox. La seconda è uno stretto divisore GridSplitter. Più articolata è la terza colonna che comprende un GroupBox a sua volta suddiviso in aree dedicate a Descrizione, Riferimenti e Commenti con uno StackPanel dotato di controlli che lascio per pigrizia all’esegesi autogestita del lettore.
Commenti essenziali ai vari Binding
Una distinzione importante è relativa al verbo XPath rispetto a Path. In buona sostanza, il secondo fissa il percorso del Binding WPF, il primo pertiene alla sintassi standard XML, ma viene, diciamo così, assimilato e supportato in WPF. Per massima comodità rivisitiamo l’incipit dell’XmlDataProvider visto in apertura:
<XmlDataProvider
x:Key="cve"
Source="C:\ElementiCVE.xml"
XPath="/cve/item"
. . . eccetera
Un altro codice tipicamente XML è @name. Vediamo come viene utilizzato dal controllo ListBox:
<ListBox Name="listBox1" ItemsSource=
"{Binding Source={StaticResource cve}}" IsSynchronizedWithCurrentItem="True">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding XPath=@name}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
A livello ListBox la proprietà ItemSource definisce il binding la cui Source a sua volta punta alla risorsa statica cve, con IsSyncronizedWithCurrentItem=”True” che garantisce appunto la sincronizzazione con tale fonte. Noi vogliamo che nell’elenco di sinistra compaiono i nomi dei vari item, che corrispondono all’attributo name. Come dovrebbe sapere chi mastica XML in tale mondo la chiocciola definisce un attributo, e nel nostro caso è la sintassi XPath=@name che provvede a estrarre il nome dall’item corrente.
Vediamo due altri esempi analoghi, reperibili all’interno di vari controlli, il secondo riguardante l’attributo source:
<TextBlockText="{Binding XPath=@name}" />
<TextBlock MinWidth="50" Text="{Binding XPath=@source}" />
In quest’altro esempio l’XPath della seconda ListBox punta invece a un nodo, quello dei singoli comment:
<ListView ItemsSource="{Binding XPath=comments/comment}" ...
Ma che fine ha fatto l’usuale Path del binding classico? Forse che il parser XmlProvider l’ha reso inutile? Non del tutto, se lo si cerca col lanternino:
<TextBlock Text="{Binding Path=InnerText}"/>
In questo caso ci vuole Path anziché XPathin quanto occorre la proprietà InnerText dell’XmlElement TextBlock.
Ci sarebbero diverse altre sottigliezze da trattare, ma mi fermo qui. Il codice fornito funziona, almeno come ricetta, e mutatis mutandis può essere adattato abbastanza agevolmente ad analoghe necessità. Per saperne di più si vada a leggere il Capitolo 11 dell’opera citata all’inizio.
Binding ADO tramite classe ad hoc
Il binding su dataset ADO, tema ovviamente centrale nel mondo WPF, può essere implementato in vari modi. Il più semplice sfrutta il controllo DataGrid (grossomodo equivalente al DataGridView dei Form). Il caso qui discusso è tratto da WPF in action, un manuale dell’Editore Manning
(www.manning.com) che chi scrive ha già raccomandato in questo blog. Il metodo adottato si basa su una classe specifica che definisce un semplice dataset formato da due tabelle, con proprietà che corrispondono ai rispettivi campi unitamente a metodi opportuni per crearle e gestirle, dopo di che istruzioni binding associate a controlli della finestra WPF, tipicamente delle TextBox, provvedono automaticamente ad attingere i valori dei campi desiderati.
Preparazione del progetto
La nostra mini applicazione ha lo scopo di gestire un insieme di indicatori (bookmars) relativi a postazioni web di vario genere. La classe in esame, denominata Biblioteca, possiede come tabella principale una Bookmarks per l’appunto, dotata di campi ovvero colonne (Column nel gergo ADO) Id, Titolo, Uri, Genere e UltimaMod (data dell’ultima modifica). Una seconda tabella reca invece i campi Nome e Quant (si noterà poi che, di fatto, non tutti i campi, pensati per rendere la classe più aperta, sono utilizzati nella mia soluzione).
Dal sito seguente si può scaricare un archivio Bibliotecario.zip comprendente l'omonimo eseguibile (fruibile solo su PC ove è installato VisualStudio 2010) più una figura .jpg che mostra il risultato da conseguire:
http://www.giannigiaccaglini.it/download/Bibliotecario.zip
Da cui si può notare che il cuore della finestra primaria sta nel riquadro in alto a sinistra. Come vedremo si tratta di una ListWiew dotata di colonne intestate (Nome e URL), ciascuna comprendente i campi ricavati tramite binding a proprietà delle classi Biblioteca fin d’ora ben intuibili (relative in particolare alla tabella Bookmars). Selezionando e cliccando su un segnalibro si ottiene la comparsa del Genere (Editore, Sito o quant’altro) in alto a destra, mentre l’indirizzo URL viene replicato più sotto. Evidente lo scopo dei pulsanti di comando in basso.
La prima operazione da compiere, fin dalla creazione del progetto, è l’assegnazione di un nome. Scegliamo, con qualche enfasi, Bibliotecario.
Allo scopo di rendere accessibile la classe Biblioteca, descritta in dettaglio fra poco unitamente al dataset Biblio che ne costituisce la base, occorrono due modifiche di tipo local al file Application.xml, che riporto per intero, per massima comodità:
</
Imports
Imports System.IO
Imports System.ComponentModel
#Region
Biblio.Tables(
I commenti che sto per esporre hanno natura descrittiva, essendo la conoscenza di ADO un presupposto (ma questa soluzione, volendo, funziona pure come ricetta). Cominciamo con i metodi Load e Save che ben si vedano sopra questo paragrafo. Come anticipato, il primo legge lo schema BiblioFileName, definito nell’archivio bookmarks.library, mentre Save accetta le modifiche apportate assegnando i dati default e, inoltre, trascrive il predetto schema. Per visualizzarlo è sufficiente aprire bookmarks.library, file automaticamente creato e collocato accanto all’eseguibile finale Bibliotecario.exe. Si può visualizzarlo con un editor di testo (Notepad incluso), comunque lo riporto qui sotto per comodità dei pigri:
<xs:element name="Bibliotecario" msdata:IsDataSet="true" msdata:UseCurrentLocale="true"><Titolo>O’ Reilly</Titolo></Bookmarks><Genere>Editore</Genere></Bookmarks><Genere>Sito</Genere></Bookmarks><Quant>0</Quant>
#Region
End
Public
Bookmarks.Rows.Add(
Biblio.AcceptChanges()
NotifyPropertyChanged(
L’istruzione conclusive, evidenziata in grassetto, secondo gli Autori del sullodato manuale Manning è quella che assicura il binding tra i campi delle tabelle di Biblio e i componenti di MainWindow.xaml. Francamente però il meccanismo di tale "magia" (proprio così la indicano i nostri autori) non mi è parso troppo chiaro. Così ho osato eliminare non solo l’istruzione NotifyPropertyChanged("Bookmarks") ma l’intero meccanismo di questa interfaccia, vale a dire l’intera Region "INotifyPropertyChanged" e l’istruzione iniziale Implements INotifyPropertyChanged. Risultato: l’applicativo continua a funzionare!
Nota
A questo punto affido il resto dei metodi e proprietà della classe Biblioteca all’esegesi autogestita di quanti possiedono perlomeno i rudimenti di ADO.
. Ovvero sembra che la soluzione se la cavi con le sole istruzioni ADO. Se qualche esperto è in grado di chiarire questo mistero, si faccia avanti.
<
<</
</
Class
DimBiblio.Save()MessageBoxEndDimEnd
L’interfaccia INotifyPropertyChanged , come dice il suo nome, interviene con un evento scatenantesi quando una certa proprietà viene aggiornata. Essendo meno nota della più popolare IComparable ne riprendo quasi pari pari l’esempio della Guida:
' Direttive Imports
... Omissis ...
Public
ValNome = valueNotifyPropertyChanged(' aggiornata la proprietà passata come argomento (propertyName) PublicLa conoscenza delle interfacce è un prerequisito, pertanto spero che bastino ii commenti incorporati, tuttavia insisto nel sottolineare l’assoluta necessità di inserire Implements INotifyPropertyChanged.PropertyChanged anche accanto al metodo NotifyPropertyChanged
<
<
</
Private
End
Private
W1.Close()
End
sono state rigettate e accettate solo dopo un primo clic su F5, inoltre nel riquadro Progettazione della Mainwindow perdurano in alto messaggi di errore che indicano l'impossibilità di trovare Biblioteca! cui si accompagna l'impossibilità di usare la Casella strumenti per apportare modifiche (cosa che respa possibile nel sottostante riquadro XAML).
Ho motivo di temere che a queste difficoltà andranno incontro molti altri soggetti di media competenza. Di qui l'appello ai massimi esperti:
per favore chiarite al popolo le possibili cause, indicando con chiari DETTAGLI i passi da compiere per evitarle!
xmlns:local="clr-namespace:Bibliotecario"
<Application.Resources>
<local:Biblioteca x:Key="Biblio" />
</Application.Resources> Sub
Nessun rilievo tranne sottolineare l’importanza, in luogo del normale Show, del metodo ShowDialog che "mette in primo piano" la Window1, restituendo alla chiusura i dati immessi dall’utente.
Problemi possibili...
Anzi probabili, che chi scrive confessa di aver risolto sì (come dimostra il mini applicativo Bibliotecario.exe) ma fortunosamente. Il punto è che le istruzioni
Exit Sub
End If
Biblio.AddBookmark(Txt1, Txt2, Txt3)
' MessageBox.Show("Tutte le caselle di testo andavano riempite!") Sub Aggiungi(ByVal sender As Object, ByVal e As RoutedEventArgs)Dim Biblio As Biblioteca = CType(FindResource("Biblio"), Biblioteca)Dim W1 As New Window1
W1.ShowDialog()
Dim Txt1 = W1.TextBox1.TextDim Txt2 = W1.TextBox2.TextDim Txt3 = W1.TextBox3.TextIf Txt1 = "" Or Txt2 = "" Or txt3 = "" Then Sub
Dopo di che la nuova Sub Aggiungi si presenta così:
MessageBox.Show("Tutte le caselle di testo vanno rienmpite!")Exit Sub
End If
Me.Close() Sub btnOK_Click(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs) Handles btnOK.ClickIf TextBox1.Text = "" Or TextBox2.Text = "" Or TextBox3.Text = "" ThenGrid>Window>
Essa fornisce tre TextBox affiancate a sinistra da Label contenenti "Nome:", "Indirizzo URL" e "Genere" indicanti a chiare lettere il significato delle caselle di testo, più un pulsante "OK", associato a questa routine cliccante, che obbliga l’utente a completare tutte e tre le caselle::
<RowDefinition />
<RowDefinition />
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Label Content="Nome:" Height="30" HorizontalAlignment="Left" Margin="16,29,0,0" Name="Label1" VerticalAlignment="Top" Width="81" />
<TextBox Height="36" HorizontalAlignment="Right" Margin="0,28,4,0" Name="TextBox1" VerticalAlignment="Top" Width="248" />
<Label Grid.Row="1" Content= "Indirizzo URL:" Height="30" HorizontalAlignment="Left" Margin="16,29,0,0" Name="Label2" VerticalAlignment="Top" Width="81" />
<TextBox Grid.Row="1" Height="36" HorizontalAlignment="Right" Margin="0,28,4,0" Name="TextBox2" VerticalAlignment="Top" Width="248" />
<Label Grid.Row="2" Content="Genere:" Height="30" HorizontalAlignment="Left" Margin="16,29,0,0" Name="Label3" VerticalAlignment="Top" Width="81" />
<TextBox Grid.Row="2" Height="36" HorizontalAlignment="Right" Margin="0,28,4,0" Name="TextBox3" VerticalAlignment="Top" Width="248" />
<Button Grid.Row="3" Name="btnOK" Content="OK" Margin="111,21,142,12" HorizontalAlignment="Center" Width="132" />
</
Grid.RowDefinitions>Window x:Class="Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Immetti dati" Height="300" Width="407">
<Grid>
Sub NotifyPropertyChanged(ByVal propertyName As String)_Implements INotifyPropertyChanged.PropertyChanged ' Da NON dimenticare!
End
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))End Sub Class
Nota.
Un perfezionamento del metodo Aggiungi
Per lo scopo didattico primario ho previsto l’aggiunta di un record fisso (relativo a Wikipedia). Ma alla fine ho escogitato una semplice miglioria, basata sull’utilizzo di una seconda Window1:
Class PersonaImplements INotifyPropertyChanged
Private ValNome As String
Public Property Nome() As String
Get
Return ValNomeEnd Get
Set(ByVal value As String)
' Quando la proprietà cambia attiva la routine NotifyPropertyChanged
"Nome")End Set
End Property
' Dichiara l’evento PropertyChanged
Public Event PropertyChanged As PropertyChangedEventHandler _Implements INotifyPropertyChanged.PropertyChanged' NotifyPropertyChanged lancia l’evento PropertyChanged allorché viene
Imports
System.ComponentModel
Imports
System.Windows.Data
' Classe che implementa l’interfaccia INotifyPropertyChanged
Sub Class
Richiamo su INotifyPropertyChanged
Biblio As Biblioteca = CType(FindResource("Biblio"), Biblioteca)"Wikipedia", "http://www.wikipedia.it", "Sito") Try
End Sub
Biblio.AddBookmark(
End
Private Sub Aggiungi(ByVal sender As Object, ByVal e As RoutedEventArgs).Show("Si deve prima selezionare un record!") Biblio As Biblioteca = CType(FindResource("Biblio"), Biblioteca)End Sub
Private Sub Cancella(ByVal sender As Object, ByVal e As RoutedEventArgs)Dim row As DataRowView = CType(bookmarks.SelectedItem, DataRowView)Try
row.Delete()
Catch ex As Exception
MainWindow
Close()
Private Sub Chiudi(ByVal sender As Object, ByVal e As RoutedEventArgs)End Sub
Private Sub Salva(ByVal sender As Object, ByVal e As RoutedEventArgs)StackPanel>
<Grid DockPanel.Dock="Bottom" DataContext="{Binding ElementName=bookmarks, Path=SelectedItem}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="1*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="1*" />
<RowDefinition Height="1*" />
</Grid.RowDefinitions>
<DockPanel Grid.Column="0" Grid.Row="0">
<Label MinWidth="50" DockPanel.Dock="Left" Content="Titolo:" />
<TextBox Text="{Binding Path=Titolo}" />
</DockPanel>
<DockPanel Grid.Column="1" Grid.Row="0">
<Label MinWidth="50" DockPanel.Dock="Left" Content="Genere:" />
<TextBox Margin="0,0,10,0" Text="{Binding Path=Genere}" />
</DockPanel>
<DockPanel Grid.Column="0" Grid.Row="1" Grid.ColumnSpan="2">
<Label MinWidth="50" Content="URL:"/>
<TextBox Margin="0,0,10,0" Text="{Binding Path=Uri}"/>
</DockPanel>
<ListView Name="bookmarks"
ItemsSource
"{
=Binding Source={StaticResource Biblio}, Path=Bookmarks}">
<ListView.View>
<GridView>
<GridViewColumn Header="Nome"
DisplayMemberBinding
="{Binding Path=Titolo}"/>
<GridViewColumn Header="URL"
DisplayMemberBinding
="{Binding Path=Uri}"/>
</GridView>
</ListView.View>
</ListView>
</Grid>
</DockPanel>Window>
I commenti? Questo intervento si rivolge a gente di medio calibro, che come minimo possiede un’infarinatura sulle tecniche binding, pertanto li affido interamente all’esegesi autogestita, favorita, spero, da un certa eloquenza del codice XAML.
Codice MainWindow.xaml.VB
È relativo agli eventi click dei vari pulsanti e a questo punto non resta che riportarlo senza ulteriori commenti, salvo osservare che per estrema brevità il metodo Aggiungi prevede l’aggiunta di un sito costante:
Biblio.AddBookmark(
"Wikipedia", "http://www.wikipedia.it", "Sito")
Lasciamo a chi fosse interessato la facile modifica ottenuta aggiungendo tre opportune caselle di testo.
Button MinWidth="60" Content="Salva" Click="Salva" />
<Button MinWidth="60" Content="Cancella" Margin="30,0,0,0"
Click
="Cancella" />
<Button MinWidth="60" Content="Aggiungi" Click="Aggiungi" />
Window x:Class="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">
<DockPanel LastChildFill="True">
<StackPanel Orientation="Horizontal" DockPanel.Dock="Bottom"
FlowDirection="RightToLeft">
<Button MinWidth="60" Content="Chiudi" Click="Chiudi" />
Codice XAML della finestra WPF
Lo riporto interamente qui di seguito.
Sub AddBookmark(ByVal Nome As String, ByVal url As String, ByVal Genere As String)New Object() {System.Math.Max(System.Threading.Interlocked.Increment(bookmarkIdent), bookmarkIdent - 1), Nome, url, Genere, DateTime.Now})"Bookmarks")
End
Sub Sub Region
Tale metodo agisce quando la proprietà-argomento propertyName subisce modifiche. Nel nostro caso ciò avviene in occasione dell’aggiunta di un bookmarck:
"INotifyPropertyChanged"
#End
Public Event PropertyChanged As PropertyChangedEventHandler _Implements INotifyPropertyChanged.PropertyChangedPrivate Sub NotifyPropertyChanged(ByVal propertyName As [String])RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
<?xml version="1.0" standalone="yes"?>
<Bibliotecario>
<xs:schema id="Bibliotecario" xmlns="" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xs:complexType>
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="Bookmarks">
<xs:complexType>
<xs:sequence>
<xs:element name="Id" type="xs:int" minOccurs="0" />
<xs:element name="Titolo" type="xs:string" minOccurs="0" />
<xs:element name="Uri" type="xs:string" minOccurs="0" />
<xs:element name="Genere" type="xs:string" minOccurs="0" />
<xs:element name="UltimaMod" type="xs:dateTime" minOccurs="0" />
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="Ident">
<xs:complexType>
<xs:sequence>
<xs:element name="Nome" type="xs:string" minOccurs="0" />
<xs:element name="Quant" type="xs:int" minOccurs="0" />
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:choice>
</xs:complexType>
</xs:element>
</xs:schema>
<Bookmarks>
<Id>1</Id>
<Uri>http://www.oreilly.com/</Uri>
<Genere>Editore</Genere>
<UltimaMod>2011-09-01T11:16:17.9401388+02:00</UltimaMod>
<Bookmarks>
<Id>2</Id>
<Titolo>Manning</Titolo>
<Uri>http://www.Manning.com/</Uri>
<UltimaMod>2011-09-01T11:16:17.9401388+02:00</UltimaMod>
<Bookmarks>
<Id>3</Id>
<Titolo>WPF Tips & tricks</Titolo>
<Uri>http://www.wpfitalia.it/</Uri>
<UltimaMod>2011-09-01T11:16:17.9401388+02:00</UltimaMod>
<Ident>
<Nome>Bookmarks</Nome>
</Ident>
</Bibliotecario>
L’eloquenza dello schema non pensa meriti altri commenti.
L’interfaccia INotifyPropertyChanged: ma serve davvero?
Proseguiamo nel nostro giro esplorativo, riportando di nuovo la regione relativa all’evento scatenato dall’interfaccia INotityPropertyChanged (non a tutti ben nota, più avanti riporto quel che ne dice la Guida):
"Operazioni sui dati"
AddBookmark(
AddBookmark(
AddBookmark(
Biblio.AcceptChanges()
Private Sub CreaBookmarksDefault()"Ident").Rows.Add(New Object() {"Bookmarks", bookmarkIdent})"O’ Reilly", "http://www.oreilly.com/", "Editore")"Manning", "http://www.Manning.com/", "Editore")"WPF Tips & tricks", "http://www.wpfitalia.it/", "Sito")End Sub
Biblio.ReadXml(BiblioFilename,
Public Sub Load()XmlReadMode.ReadSchema)End Sub
Biblio.AcceptChanges()
Biblio.WriteXml(BiblioFilename,
#End
Public Sub Save()XmlWriteMode.WriteSchema)End Sub Region
#Region
"INotifyPropertyChanged"
#End
End
Public Event PropertyChanged As PropertyChangedEventHandler _Implements INotifyPropertyChanged.PropertyChangedPrivate Sub NotifyPropertyChanged(ByVal propertyName As String)RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))End Sub Region Class System.Data
Public
Class Biblioteca
Implements INotifyPropertyChanged
Public Biblio As DataSet
Private bookmarkIdent As Integer = 0Private BiblioFilename As String = "bookmarks.library"
CreaFonteDati()
Public Sub New()If Not File.Exists(BiblioFilename) Then
CreaBookmarksDefault()
Save()
Else
Load()
End If
End Sub
Biblio =
.Columns.Add(
.Columns.Add(
.Columns.Add(
.Columns.Add(
.Columns.Add(
Private Sub CreaFonteDati()New DataSet("Bibliotecario")Dim Bookmarks As New DataTable("Bookmarks")With Bookmarks"Id", GetType(Int32))"Titolo", GetType(String))"Uri", GetType(String))"Genere", GetType(String))"UltimaMod", GetType(DateTime))End With
.Columns.Add(
.Columns.Add(
Dim Ident As New DataTable("Ident")With Ident"Nome", GetType(String))"Quant", GetType(Int32))End With
Biblio.Tables.Add(Bookmarks)
Biblio.Tables.Add(Ident)
End Sub
Public ReadOnly Property Bookmarks() As DataTable
Get
Return Biblio.Tables("Bookmarks")End Get
End Property
Bookmarks.Rows.Add(
Biblio.AcceptChanges()
NotifyPropertyChanged(
Public Sub AddBookmark(ByVal Nome As String, ByVal url As String, ByVal Genere As String)New Object() {System.Math.Max(System.Threading.Interlocked.Increment(bookmarkIdent), bookmarkIdent - 1), Nome, url, Genere, DateTime.Now})"Bookmarks")End Sub<Application x:Class="Application"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Bibliotecario"
StartupUri="MainWindow.xaml">
<Application.Resources>
<local:Biblioteca x:Key="Biblio" />
</Application.Resources>Application>
Delle due direttive locali evidenziate in grassetto, la cui sintassi do per nota, la prima aggiunge il namespace dell’intero progetto (Bibliotecario) la seconda punta alla classe Biblioteca con il dataset Biblio che funge da chiave (x:Biblio).
La classe Biblioteca e il suo dataset Bibliotecario
Senza troppo indugiare riportiamo subito l’intero codice, commentandone rapidamente le prime righe. Le direttive Imports "arruolano" le librerie per accesso a dati ADO, per l’Input/output e quella per il ComponentModel, indispensabile per gestire l’Interfaccia INotifyPropertyChanged, che viene subito fissata con la specifica istruzione Implements. Seguono le variabili Biblio (di tipo pubblico, dovendo questo dataset essere visibile all’esterno della classe), bookmarkIdent (identificatore del bookmark) e BiblioFileName, una stringa "bookmarks.library", che è il nome dell’archivio di tipo XML che descrive le caratteristiche strutturali dell’intero Bibliotecario (e non, si badi bene, della sola Biblioteca). Si osservi poi, in questa disamina iniziale, la routine d’avvio New. Essa si preoccupa anzitutto di creare il dataset Biblio, quindi controlla l’esistenza o meno dell’archivio BiblioFilename provvedendo nel primo caso a creare dei bookmark default (O’Reylly, Manning e WPF Tips, v. figura precedente) e a salvare l’archivio bookmarks.library e le modifiche a Biblio (metodo Save, v. più avanti) o altrimenti a caricare (metodo Load, v. più avanti) l’archivio bookmarks.library.
Windows Azure
Programmare per il Cloud Computing
di Fabio Cozzolino
Ediz. FAG
Il Cloud Computing targato Microsoft è dietro l’angolo? Sarà facile, oltre che conveniente, creare nuove applicazioni su tale nuvola o riciclarne di preesistenti, possibilmente ri-usando codice che abbiamo sudato a creare e collaudare? A tutte queste domande dà un significativo contributo il manuale suddetto, in termini architetturali e pratici, con abbondanza di esempi validi:
Approfittando dell’opera, in questo articolo mi propongo di focalizzare un aspetto non secondario di Azure, ovvero la prima delle seguenti due modalità che la piattaforma offre per la gestione di dati “classici”:
- Table Storage, che a sua volta fa parte di un insieme di servizi, gli Azure Storage Services
- Azure Sql
Tale scelta è giustificata in primo luogo dall’intenzione di limitarsi a fornire un’idea iniziale sul nuovo mondo, ma soprattutto per il fatto che Azure Sql è sostanzialmente conforme a Windows Sql Server 2008, mentre Table Storage ha connotati del tutto diversi e più affini all’ambiente Cloud, pertanto uno sguardo ad esso consente di farsene meglio una prima idea.
MANI AVANTI: DESUMO QUESTE NOTE DAL TESTO SUCCITATO MA NON AVENDO PROVATO AZURE NON ESCLUDO INESATTEZZE E OPINIONI DISCUTIBILI. OGNI RILIEVO O CRITICA SARA' COMUNQUE BEN ACCETTA:
Come viene ben richiamato nel primo capitolo, la Windows Azure Platform è un cloud pubblico di tipo PaaS (Platform as a Service) che cioè non si limita a offrire un’infrastruttura – su cui peraltro Microsoft ha pesantemente investito con una gran quantità di superserver dislocati nel pianeta – ma un completo sistema operativo” virtualizzato” arricchito di molte funzionalità built-in, per gestire le quali l’utente di Azure (ma sarebbe meglio chiamarlo abbonato, anzi in Inglese si parla di “tenant”, affittuario) non deve minimamente preoccuparsi. Di questi caratteri del paradigma Cloud e di Azure stesso ormai si straparla su tutte le gazzette informatiche e sul web, pertanto qui li do per noti, ricordando solo tre punti rilevanti:
- L’SDK fornito agli utenti Azure comprende un completo sistema di debug e sviluppo fruibile in locale, con un’interfaccia simile a quella cui i programmatori .NET sono avvezzi; una bella comodità anche se i risultati testati vanno poi adattati al Cloud a causa di qualche differenza fra ambiente locale, emulato, e ambiente reale su cui pubblicare le nostre applicazioni;
- Nel mondo Azure non si parla più di File System, ma di indirizzi internet, di qui una particolare sintassi (la cui trattazione sistematica non è oggetto di questo intervento, che però comunque ne fornisce spunti, spero, significativi);
- Azure è decisamente aderente alla filosofia SOA (Software As a Service), come già si comprende dagli Storage Services citati sopra.
Una tipica sessione con Azure Table Storage
I servizi Azure Storage Services comprendono a loro volta un modulo Table Storage Services più un secondo dedicato agli oggetti multimediali (blob, binary large object) e altri due per la gestione delle cosiddette queue (code) e dei drive. In questa anteprima diamo un’occhiata solo ai primi.
Una tabella gestita coi Table Storage Services è formata da colonne (campi) e righe (record) ma non aderisce alle norme SQL come magari ci si attende, tant’è vero che questo sistema database viene esplicitamente catalogata come NoSQL (più chiaro di così...). Quindi, se per un verso questo dato di fatto ci libera dai “constraints” e dalle sottili regole dei database SQL veraci, per contro ci impone una totale responsabilità sulle modalità di elaborazione. In particolare ciò vale per le operazioni di join fra tabelle correlate, com’è da attendersi, ma rende problematiche anche quelle di ordinamento, come forse non tutti si aspettavano. La definizione stessa delle tabelle Azure ne viene influenzata (per esempio, creandone con molti più campi, con ridondanze più o meno inevitabili... beninteso è un’impressione a caldo).
La situazione che sto per esaminare, sintetizzando gli esempi del bravo Cozzolino, discende in sostanza da due fattori:
-
· L’architettura SOA, service oriented che è a fondamento di Azure (in pratica è del tutto simile ai WCF Data Services, ove WCF è Windows Comunication Foudation, comunque i brani riportati si lasciano comprendere o, perlomeno, intuire);
-
· La necessità di ottenere la massima scalabilità ed efficienza in un ambiente Cloud, ove i dati possono trovarsi (si perdoni l’espressione pittoresca) “spezzettati”, “sparpagliati” su macchine virtuali (VM) dislocate negli innumerevoli server che Microsoft mette a disposizione per la piattaforma Azure; in buona sostanza inoltre ciò significa che una tabella del genere potrebbe, nel tempo, essere spostata e trovarsi suddivisa su VM diversi.
Nota. Questa situazione, di primo acchito un po’ sconcertante, accomuna gran parte se non tutte le piattaforme Cloud, allo scopo di massimizzare l’occupazione. Ma non potrebbero derivarne tempi di risposta eccessivi? Il dubbio esiste, comunque Azure li riduce con tecniche di query simultanee, inoltre permette ai propri abbonati di delimitare le aree geografiche (evitando putacaso che i dati di un italiano finiscano in India...).
Ma entriamo in medias res con un esempio tipico, relativo a un oggetto Prodotto che si definisce tramite una classe dotata di proprietà che corrispondono ai campi di una omonima tabella, più altri tre campi a sorpresa - PartitionKey, RowKey e TimeStamp - obbligatori e di nome predefinito:
[DataServiceKey ("PartitionKey", "RowKey"]
Public Class Prodotto
{
Public String PartitionKey { get; set }
Public String RowKey { get; set }
Public DateTime TimeStamp { get; set }
Public String NomeProd { get; set }
Public Double Prezzo { get; set }
Public Int Giacenza { get; set }
Public Int ScortaMin { get; set }
}
La prima riga entro parentesi quadre è una tipica direttiva rivolta ai Table Storage Services che caratterizza la tabella secondo le due chiavi dette, la prima che fissa la partizione, la seconda che serve a individuare univocamente ciascuna riga.
Nota. Per la cronaca, DataServiceKey è contenuto nel namespace System.Data.Services.Common dell’assembly System.Data.Services.Client.dll. Ma è solo una citazione volante, in queste noterelle esplorative.
Ma cosa sono questi tre nuovi campi? Liquidato subito TimeStamp che corrisponde al dato “storico” che Azure Storage fissa in ciascun elemento, diciamo che:
- RowKey dovrebbe servire a individuare univocamente il record;
- PartitionKey, sulla carta più libero, assegna a ciascun elemento una categoria che Azure Storage utilizza per ripartire nel modo più efficiente le suddivisioni nelle sue VM, secondo il suo speciale algoritmo (totalmente trasparente a noi utilizzatori).
Per capirci, nel nostro esempietto la RowKey sarebbe un codice prodotto, ovvero quello che nei casi normali della vita viene indicato con CodProd, CodArt e simili. Esempi analoghi: ISBN (per i libri), codice fiscale e via di seguito.
Quanto alla chiave di partizione, in teoria potremmo persino definirla identica in tutti i record ad esempio con “MioProdotto”, una scelta idiota non solo priva di qualsiasi utilità ma che ostacolerebbe il corretto partizionamento e la scalabilità del sistema di storage in parola. La cosa più saggia è individuare una serie articolata di categorie, prendendo così due piccioni con una fava: far contento Azure e catalogare i nostri prodotti. In parole povere, immaginando articoli di abbigliamento, converrà adottare delle PartitionKey del tipo “Copricapi”, “Camicie”, “Giacche”, “Pantaloni”, “Abiti completi” e quant'altro.
Utilizzo dello storage in perfetto stile web
Una volta definita una classe relativa a una tabella dello Azure storage come la si gestisce? La risposta è esattamente quella che i più avveduti avranno ipotizzato, ritenendo che c’entri Internet, altrimenti di che Cloud stiamo parlando? Hanno ragione e infatti viene sfruttata l’interfaccia REST (Representation State Transfer) del protocollo HTTP in particolare mediante i verbi GET, PUT, POST e DELETE (e lo stesso vale per i blob e le queue). In questo bigino do per note le specifiche nozioni, dicendo che il libro di Cozzolino ne esemplifica dapprima l’utilizzo manuale, passando poi ad esempi di codice consentiti da provvidenziali API ad hoc (un’estensione dei WCF Data Service, come già detto). Per brevità riporto solo alcuni dei brani più significativi, con scarni commenti e facendo affidamento su una pur relativa eloquenza del codice, al di là dei suoi intrichi e bizantinismi coi quali occorrerà a suo tempo ammattire.
NOTA. In quel che segue si fa riferimento alle risorse disponibili nel modulo Storage Emulator del provvidenziale SDK citato in apertura che, ripeto, permette debug e sviluppi in locale.
Accesso allo storage e definizione di un Client
Il primo passo è la definizione di credenziali di accesso alla connessione, effettuata con una nuova istanza mioAccount dello StorageCredentialsAccountAndKey, quindi di un nuovo CloudStorageAccount impostandone gli “endpoint” da utilizzare:
StorageCredentialsAccountAndKey AccountAndKey =
New StorageCredentialsAccountAndKey("devstoreaccount1", "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==")
CloudStorageAccount mioAccount = New CloudStorageAccount(AccountAndKey,
New Uri("http://127.0.0.1:10000/devstoreaccount1")
New Uri("http://127.0.0.1:10001/devstoreaccount1")
New Uri("http://127.0.0.1:10002/devstoreaccount1");
Ove devstorageaccount1 è l’account default. Quanto alla lunga stringa seguente posta a secondo argomento
“Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw=="
si tratta della chiave default che ci passa il convento Storage Emulator, unitamente alle le porte di accesso ai blob, alle queue e alle table, vale a dire 10000, 10001 e 10002, rispettivamente.
Subito dopo si provvederà alla creazione di una nuova istanza mioClient di un CloudTableClient di cui fornisco un esempio affidato totalmente al carattere quasi auto-esplicativo del codice:
CloudTableClient mioClient =
New CloudTableClient(mioAccount.TableEndPoint.ToString, AccountAndKey);
String[] Tabelle = mioClient.ListTables();
Ci si accontenti di sapere e... di credere sulla fiducia che la precedente riga di codice sfrutta la proprietà ListTables di mioClient per ottenere un elenco di tutte le tabelle in esso presenti.
Creazione di una tabella
Dando sempre per intuitive le operazioni testé descritte, con discutibile audacia salto direttamente a un esempio completo per definire e creare una tabella Prodotti formata da oggetti della classe Prodotto vista sopra. Alla definizione di questa, che va messa in testa alla procedura, seguono le operazioni di accredito e di creazione di un mioClient, stavolta utilizzandone il metodo CreateTableIfNotExist con il non inatteso argomento “Prodotti”. Tale metodo, palesemente, crea la tabella a patto che non ne esista già un’omonima e altri metodi, di chiara semantica, della stessa famiglia comprendono il (normale) CreateTable, nonché DeleteTable, DeleteTableIfExist e l’appena visto ListTables.
Non è finita: gli ultimi atti del minidramma sono la creazione di un Contesto (di servizio tabellare), di tipo TableServiceContext nel quale possiamo poi aggiungere un Prodotto.
[DataServiceKey ("PartitionKey", "RowKey"]
Public Class Prodotto
{
Public String PartitionKey { get; set }
Public String RowKey { get; set }
Public DateTime TimeStamp { get; set }
Public String NomeProd { get; set }
Public Double Prezzo { get; set }
Public Int Giacenza { get; set }
Public Int ScortaMin { get; set }
}
StorageCredentialsAccountAndKey AccountAndKey =
New StorageCredentialsAccountAndKey("devstoreaccount1", "Eby8.....")
CloudStorageAccount mioAccount = New CloudStorageAccount(AccountAndKey,
New Uri("http://127.0.0.1:10000/devstoreaccount1")
New Uri("http://127.0.0.1:10001/devstoreaccount1")
New Uri("http://127.0.0.1:10002/devstoreaccount1");
// Creazione della tabella Prodotti
CloudTableClient mioClient =
New CloudTableClient(mioAccount.TableEndPoint.ToString, AccountAndKey);
mioClient.CreateTableIfNotExist("Prodotti");
// Creazione del contesto e aggiunta di un Prodotto
TableServiceContext Contesto = mioClient.GetDataServiceContext();
Prodotto Prod = New Prodotto();
Prod.PartitionKey = "Pantaloni";
Prod.RowKey "AXY123"; //Ipotetico codice articolo
Prod.NomeProd ="Jeans vita bassa mod. 2011";
Prod.Prezzo = 250.00;
Prod.Giacenza = 450;
Prod.ScortaMin = 50
Contesto.AddObject("Prodotti", Prod);
Contesto.SaveChanges();
Commenti? Solo sulle ultime istruzioni che iniziano con la creazione del servizio Contesto di tipo TableServiceContext, ottenuto con il metodo GetDataServiceContext del mioClient. Le righe di codice che creano un nuovo Prodotto Prod e quindi ne fissano le proprietà sono parlanti, dopo di che non resta che usare i metodi del Contesto AddObject, per aggiungere Prod alla tabella Prodotti, e SaveChanges (da non dimenticare!) per consolidarne i mutamenti.
Nota. Qualcuno si sorprenderà per la mancanza di un’assegnazione a TimeStamp. Ma Il motivo è semplice, se si riflette: si tratta di un valore di data e orario che Azure automaticamente attribuisce all’atto della creazione, pertanto non possiamo modificarlo, è read-only.
Qualche semplice query, usando LINQ
Come si è visto per usare i servizi di storage di Azure c’è da pagare lo scotto delle impostazioni per il client e il contesto. Se non ho mal compreso, queste vanno ripetute tutte le volte, comunque non sarà difficile metterci una pezza con metodi ad hoc personalizzati a dovere e ri-usabili. Dopo di che il cammino è in discesa, perché le altre operazioni tipiche di query su una o più tabelle Azure si compiono normalmente – a parte le limitazioni derivanti dalla loro natura NoSQL. Gli esempi riportati qui di seguito si basano su LINQ (Language Integrated Query) linguaggio che permette di inserire interrogazioni e modifiche direttamente nel codice C# o VB.
Nota. Sarà il caso di andarselo a studiare. Consiglio in merito Programmare con Microsoft LINQ di Alessandro Del Sole – Ed. FAG.
Esempio di filtraggio (codice LINQ in neretto):
TableServiceContext Contesto = mioClient.GetDataServiceContext();
DataServiceQuery<Prodotto> query =
Contesto.CreateQuery<Prodotto>("Prodotti");
Prodotto Prod = query.Where(P => P.Giacenza <= P.ScortaMin;
Esempio di modifica (di un prezzo):
TableServiceContext Contesto = mioClient.GetDataServiceContext();
DataServiceQuery<Prodotto> query =
Contesto.CreateQuery<Prodotto>("Prodotti");
Prodotto Prod = query.Where(P => P.NomeProd == "Camicia a fiori").First;
Prod.Prezzo = 500;
Contesto.UpdateObject(Prod);
Contesto.SaveChanges;
Cancellazione:
TableServiceContext Contesto = mioClient.GetDataServiceContext();
DataServiceQuery<Prodotto> query =
Contesto.CreateQuery<Prodotto>("Prodotti");
Prodotto Prod = query.Where(P => P.NomeProd == "Camicia a fiori").First;
Contesto.DeleteObject(Prod);
Contesto.SaveChanges;
Nei vari esempi soprastanti la query viene, diciamo così, inizializzata dal metodo CreateQuery di Contesto sull’oggetto Prodotto della tabella Prodotti, dopo di che intervengono le varie istruzioni LINQ, a valle delle quali i metodi del Contesto UpdateObject, DeleteObject seguiti dal già visto SaveChanges compiono i mestieri che i rispettivi nomi a chiare lettere denitano.
E l’ordinamento? Come già anticipato con questo genere di tabelle è possibile solo sulla chiave PartitionKey (del che, forse errando, personalmente oso dubitare) e una clausola OrderBy viene sistematicamente rigettata a ostinati & illusi. Ci si può però arrangiare assegnando a una query il metodo ToList e applicando OrderBy all’elenco così ottenuto, anche se con qualche limite su cui rimando al testo di Cozzolino.
SQL or NoSQL? Questo il dilemma
Analogamente si possono re-inventare operazioni di tipo join o lookup, ma senza fruire dello standard del modello relazionale cui molti erano abituati fin dai tempi del glorioso Clipper.
Ma non va dimenticato che Cloud pubblico targato Microsoft fornisce anche Azure SQL che, ripeto, equivale a SQL Server 2008 R2 (ma al momento, temo, con qualche limite nelle feature più pregiate). E in questo ambiente tutto procede secondo il modello relazionale. Dunque una manna – fissazione di client e contesti a parte – per chi era già familiare con SQL Server. Ma allora perché ricorrere al meno “rigoroso” cugino NoSQL? La risposta dipende soprattutto dalla dimensione massima di immagazzinamento, molto maggiore con table storage (fino a 100 Terabyte) appetto agli odierni 50 Gbyte massimi che passa il convento SQL Azure. A sua volta table storage non supporta tutti i tipi di dato dell’altro. Un altro aspetto problematico si ha con le query che restituissero più di 1000 unità. Verificate, gente, verificate.
La questione è particolarmente complessa e va studiata accuratamente, caso per caso. A caldo sembrerebbe che table storage si presti poco al ri-uso, non solo per la conversione di preesistenti tabelle “on premises” ma anche per quanto riguarda le procedure, generalmente ispirate ai concetti relazionali (andranno riscritte in gran parte), mentre si presenta particolarmente idoneo per nuove applicazioni web, di tipo ASP o Silverlight.
Ma quanto mi costi?
Encomiabile è infine l’Appendice nella quale Cozzolino si è sforzato di valutare costi di una soluzione Cloud di tipo pubblico PaaS basata su Azure. Di grande interesse e particolarmente convenienti sono le soluzioni di fascia bassa, a partire da quella introduttiva, gratuita, per chi volesse sperimentare il nuovo mondo (si paga solo a consumo quel che si sfora rispetto all’offerta base).
I conti fatti dal nostro cambiano se si passa a un sistema informativo medio. Egli li valuta in un canone annuo di 6000 euro più spese ulteriori a consumo. Queste, con somma sorpresa di quanti sentono in giro parlare di costi prevalentemente “on demand” nel mondo Cloud, sono trascurabili se non irrisori. Si veda però la NOTA FINALE. Cozzolino paragona il tutto a una serie di normali server equivalenti, di prezzo pari a circa 8000 euro cui vanno aggiunti i costi delle licenze (incluso SQL Server, presumo, mentre SQL Azure è compreso nel canone Azure) e, naturalmente, quelli energetici e di manutenzione tutti a carico Microsoft. Personalmente mi permetto di obiettare che, a fronte di un canone annuo di Azure, i prezzi di acquisto dell'equivalente "on premises" andrebbero divisi per tre o per cinque, a seconda che si applichi il criterio dell’obsolescenza tecnologica o il più sparagnino ammortamento fiscale. In particolare tale discorso vale per la seconda alternativa, indicata sempre da Cozzolino, ovvero un server virtualizzato di ultima generazione che sostituisce i normali server predetti (con notevoli altri vantaggi di consolidamento ed efficienza) con ulteriori margini di espandibilità, al prezzo di 15000 euro...
NOTA FINALE - Il "canone" di cui sopra va inteso come dato indicativo MEDIO. In realtà anche Azure offre la possibilità di tarare i servizi base on demand, in particolare variando anche dinamicamente le VM necessarie, tipicamente aumentandole in periodi di punta e riducendole in quelli più tranquilli. Personalmente penso che per un'azienda normale siano difficili operazioni raffinate per cui dovrà fissare una configurazione base che assicuri la normale operatività con un certo margine. La faccenda comunque richiede una riflessione accurata.
Mi fermo qui, dichiarando doverosamente che Azure è un mondo molto più ricco di quanto questo sguardo introduttivo potrebbe indurre a pensare. Esso infatti offre diversi comodi servizi, come i blob storage (corredabili di metadati e per una più organica classificazione e ricerca, utilissima in cataloghi sul web, la gestione di code) e tool innovativi (AppFabric o Service Bus, per buttar lì due nomi a caso) che aprono prospettive interessanti. Con le quali prima o poi si dovranno fare i conti.
giannigiac@tin.it
Un tormentone che mi assilla da tempo è relativo alla gestione di Window multiple. I problemi cui si va incontro sono due:
-
1) navigare fra le diverse istanze aperte;
- 2) passare valori o modificare oggetti dall’una all’altra.
Sul secondo, in un post di questo Forum ho escogitato una very dirthy patch, sporca perché crea una nuova finestra principale (MainWindow o, comunque, quella definita con StartupURI = “. . .” in Application.xaml. In fondo a questo articolino riporto la soluzione suggerita dall’ottimo Sarati: più pulita della mia ma che comunque si direbbe che anch’essa ricorra “a suo modo” a un ‘istruzione che crea una nuova istanza di MainWindow dall’interno della finestra secondaria (a sua volta creata “come nuova” nella MainWindow d’apertura.
Sia come sia, nel frattempo ho rivisto nella parte introduttiva del libro di Alessandro del Sole- VISUAL BASIC OLTRE IL CODICE – Ed. FAG – il codice Application.Current.Windows che dà adito alle finestre attualmente aperte. Interessante! Fusse che fusse la volta buona per il mio (e altrui, credo) tormentone?
Il piccolo ma istruttivo esperimento che sto per proporre dovrebbe servire allo scopo. Si crei in aggiunta alla finestra primaria MainWindow una secondaria Window1, entrambe dotate di un semplice Button come il seguente, la prima dotata di Title=”Finestra primaria” la seconda di Title=”Finestra secondaria:
<Button Name="EsploraFinestre" Content="Esplora finestre aperte" ... />
Cliccando sul primo viene creata una nuova istanza di Window1,ma prima si hanno varie istruzioni MessageBox.Show che evidenziano Application.Current e i suoi figli Window.
Codice VB della MainWindow:
Class MainWindow
Dim MiaApplic As Application = Application.Current
Private Sub EsploraFinestre_Click(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs) Handles EsploraFinestre.Click
MessageBox.Show("Ciao", "sender: " & sender.ToString)
For Each win As Window In MiaApplic.Windows
MessageBox.Show(win.Title) ' => solo l’ISTANZA MainWindow (Window1 ancora "ignota")
Next
MessageBox.Show("window primario: " & MiaApplic.MainWindow.ToString)
Dim Win1 = New Window1
Win1.Show()
If MessageBox.Show("Chiudiamo la finestra primaria?", "Rispondi Si o no",
MessageBoxButton.YesNo) = MessageBoxResult.Yes Then
Me.Close()
End If
End Sub
End Class
Le varie MessageBox.Show si commentano da sole, insisto solo sul fatto che nel ciclo For Each win As Window in MiaApplic.Windows. . . Next è la MainWindow, sola soletta, a venir indicata. Così afferma nel suo analogo esempio Alessandro ipotizzando possibili diverse situazioni. Bene, vediamo cosa accade con la Win1 creata e mostrata nelle istruzioni successive e si tenga presente la richiesta finale di chiusura o meno di MainWindow.
Codice VB della Window1:
Public Class Window1
Dim MiaApplic As Application = Application.Current
Private Sub EsploraFinestre_Click(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs) Handles EsploraFinestre.Click
MessageBox.Show("sender: " & sender.ToString & vbLf & "Numero Windows aperte: " & MiaApplic.Windows.Count)
For Each win As Window In MiaApplic.Windows
MessageBox.Show(win.Title) ' => sia la CLASSE Window1 che MainWindow ma solo se ancora attiva)
Next
' Se MainWindow era stata chiusa, l'istruzione seguente dà errore:
' MessageBox.Show("window primario: " & MiaApplic.MainWindow.ToString)
' Si può rimediare anteponendo:
If MiaApplic.MainWindow Is Nothing Then Exit Sub
MessageBox.Show("window primario: " & MiaApplic.MainWindow.ToString)
MiaApplic.MainWindow.Activate()
End If
End Sub
End Class
I punti chiave sono il codice MiaApplic.Windows.Count e il ciclo For Each win ...Next: se MainWin non era stata chiusa il primo è pari a 2, mentre il secondo indica sia MainWindow che Window1. A questo punto ci si può divertire passando MANUALMENTE alla finestra principale e cliccarne l’unico Button, constatando che se non si accetta Me.Close il pulsante della finestra secondaria può indicare anche più di due istanze di Window1, per giunta OMONIME! Una confusione da evitare opportunamente in una soluzione reale (v. anche il suggerimento di utilizzare piuttosto Me.Hide di Serati).
Qui comunque ci stiamo divertendo. Perciò proseguiamo l’esperimento completando la parte bassa della routine dell’evento Click del Button della finestra secondaria come segue:
If MessageBox.Show("Vuoi attivare la finestra primaria?", "", MessageBoxButton.YesNo) = MessageBoxResult.Yes Then
' Purtroppo il passaggio di valori a tale istanza non sembra possibile:
' MiaApplic.MainWindow.EsploraFinestre.Content = "Evviva me!" ' Rigettata: il compilatore non vede gli oggetti di MainWindow
' MiaApplic.MainWindow.VarComune = "Buona notte!" ' E nemmeno variabili comuni, Public, Shared (o di altro tipo?)
' Comunque è possibile attivare la finestra principale!! Eureka!
' Anche se non basta per il passaggio valori, a fortiori vietata A VALLE di:
MiaApplic.MainWindow.Activate()
End If
End Sub
End Class
I commenti e la pratica mettono in luce due punti:
· MiaApplic.MainWindow.Activate() equivaleperfettamente alla manovra manuale di attivazione della finestra primaria; Eureka!
· né i controlli né i valori di variabili comuni (dimenticavo: una VarComune definita a livello Dichiarazioni nella MainWindow) si possono vedere, né tantomeno modificare.
CONCLUSIONI, su cui spero in un dibattito:
A) Siamo in presenza di un ginepraio SEMANTICO, che rende davvero sottile distinguere fra “robe” come MainWindow e Window1 intese come CLASSI e relative ISTANZE (????)
B) Alla luce di questo esperimento sembrerebbe inevitabile il “sudicio” ricorso a un’istruzione Dim MiaMainW As New MainWindow e simili se si vogliono modifiche a elementi da una Window a un’altra.
Comunque “sento” di essere a un passo dalla meta. Il giallo continua.
. . . . .
Soluzione del giallo
Pensa e ripensa alla fine mi sono reso conto che consiste nell’utilizzo dell’evento Activated di MainWindow. Per dare maggior concretezza alla trovata, inseriamo un paio di controlli nelle due finestre:
-
· nella MainWindow una Label di nome “lblMainW” e di contenuto default, putacaso, “Oggi c’è il sole”;
-
· nella Window1 una TextBox denominata “txtBoxW1”.
Fermo restando il ciclo della routine dell’evento Click del Button “EsploraFinestre”di Window1” e imponendo che esso culmini con l’istruzione fatidica MiaApplic.MainWindow.Activate() [ per semplicità, ignoriamo casistiche alternative, magari foriere di bug, che qui non interessano...] ecco infine una versione conclusiva, ridotta all’essenziale della Sub relativa all’evento Click del Button “EsploraFinestre” di MainWindow:
Class MainWindow
Dim MiaApplic As Application = Application.Current
Dim Win1 As Window1
Private Sub EsploraFinestre_Click(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs) Handles EsploraFinestre.Click
Win1 = New Window1
Win1.Show()
If MessageBox.Show("Chiudiamo la finestra primaria?", "Rispondi Si o no", MessageBoxButton.YesNo) = MessageBoxResult.Yes Then
Me.Close()
End If
End Sub
Private Sub Window_Activated(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Activated
lblMainW.Content = Win1.txtBoxW1.Text
End Sub
End Class
A buon intenditor altre parole sarebbero sprecate? Si e no, perché a ben riflettere il sudicio trucco MiaMainW As New MainWindow potrebbe servire nel caso si fosse deciso per Me.Close prima di creare ed esibire la seconda Window1 (v. comunque la soluzione di Serati qui sotto).
Insomma, a ciascuno il suo.
E NON È TUTTO: Application.Current.Mainwindow non espone soltanto il metodo Activate ma - ovviamente a condizione che MainWindow sia effettivamente APERTA – tutte le sue proprietà e metodi.
CONCLUDENDO, propongo un esempietto che sfrutta entrambi i trucchi dal sottoscritto sperimentati. Si abbia una MainWindow e una finestra secondaria Window1. Nell’una e nell’altra esiste un pulsante di richiamo mutuo. Ecco la routine di quello presente in MainWindow:
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs) Handles Button1.Click
Dim W As New Window1 ‘ Crea un’istanza di Window1
W.Activate()
W.Show()
If MessageBox.Show("Chiudo (o nascondo) la finestra principale?", "Rispondi", MessageBoxButton.YesNo) _
= MessageBoxResult.Yes Then
Me.Close()
Else
Me.Hide()
End If
End Sub
Lo scopo, didattico, è quello di proporre l’alternativa di chiusura vs. quella di occultamento di MainWindow. Nel primo caso, come si constata dopo qualche patimento, il codice Application.Current.Mainwindow.Activate dà errore a run-time. Di qui la Sub del Button1 della finestra secondaria:
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs) Handles Button1.Click
If Application.Current.MainWindow Is Nothing Then
Dim MW As New MainWindow
MW.Activate()
MW.Show()
MW.Title = "PRIMARIA"
Else
With Application.Current.MainWindow
.Activate()
.Show()
.Title = "PRINCIPALE"
End With
End If
Me.Close()
End Sub
Come si vede, essa evita la gestione dell’errore semplicemente testando Application.Current.MainWindow Is Nothing: in caso affermativo si ricorre al trucco sudicio di creazione di una nuova istanza MW di MainWindow, altrimenti si utilizza quella che era stata semplicemente nascosta ma tuttora in vita.
Notare infine che nell’uno e nell’altro caso, a dimostrazione della possibilità di passare proprietà e valori da Window1 a MainWindow, viene modificata una proprietà a caso, nella fattispecie Title di MainWindow (supposta per default “Mainwindow”) resa pari a “PRIMARIA” oppure “PRINCIPALE” al solo scopo didattico di diversificare le due situazioni.
La brillante soluzione Sarati
PROBLEMA:
C'è una finestra principale con una Textbox e un Button che deve mostrare una finestra secondaria chiudendosi.
La finestra secondaria, anche essa con una textbox e un bottone, all'inizio mostra nella textbox il contenuto della finestra principale. Al click del bottone rimostra la finestra principale.
TERMINOLOGIA:
- MainWindow = nome della CLASSE della finestra principale
- originWindow = ISTANZA di MainWindow
- ChildWindow = nome della CLASSE della finestra secondaria
- subwindow = ISTANZA di ChildWindow
ESEMPIO1:
Class MainWindow
Private subWindow As ChildWindow = Nothing
Private Sub ButtonMain_Click(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs) Handles ButtonMain.Click
subWindow = New ChildWindow(Me) 'istanza della finestra secondaria
Me.Hide() 'nasconde se stesso
subWindow.Show() 'mostra la finestra secondaria
End Sub
End Class
Public Class ChildWindow
Private originWindow As MainWindow = Nothing
Public Sub New()
InitializeComponent()
End Sub
Public Sub New(ByVal m As MainWindow) 'COSTRUTTORE USATO
InitializeComponent()
Me.originWindow = m 'Salva instanza in proprietà
Me.TextBoxChild.Text = Me.originWindow.TextBoxMain.Text
End Sub
Private Sub ButtonChild_Click(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs) Handles ButtonChild.Click
Me.Hide() ' nasconde me stesso
Me.originWindow.TextBoxMain.Text = Me.TextBoxChild.Text
Me.originWindow.Show() 'mostro finestra di origine
End Sub
End Class
PROBLEMI. L'applicazione non si chiude se si fa clic sulla x della ChildWindow. Per risolvere si deve mettere in Application.xaml la proprietà ShutdownMode="OnMainWindowClose" e poi chiudere con originwindow.Close() all'evento OnClose della childWindow (oppure mostrare la finestra principale...)
ESEMPIO2:
Per il problema "agire direttamente e senza equivoci su tutti gli oggetti del foglio o documento aperto" puoi agire così: Mettere una classe con proprietà statica a tutte le finestre che crei:
Public Class WindowReferences 'da ovunque puoi accedere a questi oggetti
Public Shared originWindow As MainWindow
Public Shared subWindow As ChildWindow
End Class
e ovunque accedi con quei puntatori:
Class MainWindow
Public Sub New()
InitializeComponent()
WindowReferences.originWindow = Me
WindowReferences.subWindow = New ChildWindow()
End Sub
Private Sub ButtonMain_Click(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs) Handles ButtonMain.Click
Me.Hide() ' nasconde se stesso
WindowReferences.subWindow.TextBoxChild.Text = Me.TextBoxMain.Text
WindowReferences.subWindow.Show() 'mostra la finestra secondaria
End Sub
End Class
Public Class ChildWindow
Public Sub New()
InitializeComponent()
End Sub
Private Sub ButtonChild_Click(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs) Handles ButtonChild.Click
Me.Hide() ' nasconde me stesso
WindowReferences.originWindow.TextBoxMain.Text = Me.TextBoxChild.Text
WindowReferences.originWindow.Show() 'mostro finestra di origine
End Sub
End Class
CONCLUSIONI: Personalmente a me non piace come approccio... Avere una finestra principale che scompare non mi pare bello. Inoltre, se memorizzi le istanze delle finestre secondarie, ricorda che tieni in memoria tutte le finestre! Inoltre devi evitare (come dicevo prima) i Close (da sostituire con Hide altrimenti l'istanza non è più utilizzabile).
A me continua a piacere una singola finestra principale sempre visibile, e se serve una secondaria, la principale è visibile e la secondaria la mostro con ShowDialog.
Detto questo, il codice sopra è un esempio... ci sono molti affinamenti da fare...
Fino a ieri chi programma applicativi invidiava in cuor suo la semplicità offerta dal “linguaggio” (dichiarativo) HTML e derivati per creare pagine Web davvero ricche. Ah poter lavorare con altrettanta facilità da codice VB, C# e compagnia cantante! WPF e Silverlight consentono di farlo in vari modi, il più diretto e comodo è il potente oggetto FlowDocument , sostanzialmente in virtù dell’adozione della sua anima, appunto, dichiarativa: l’XAML.
Piccola digressione. Ma l’XML, di cui XAML è figlio, non è nato in origine per separare dati e formati? Vero, ma presto ci si è resi conto che la sua flessibilità permette schemi di qualsiasi tipo, invadendo così anche scopi formattanti. Si pensi al formato Open XML di Office 2008 e 2010, che comprende componenti dedicati ai formati di celle Excel, paragrafi, parole di Word. (Che poi ne derivi una certa confusione è un’altra, anzi è la solita storia Informatica…).
La classe FlowDocument è relativa a un oggetto potente, un vero e proprio documento di testo che si articola in oggetti-figli genericamente catalogati come Block (v. Guida), ma che in pratica hanno nomi eloquenti. Il più importante è Paragraph – uno o generalmente più d’uno -, cui corrisponde in XAML una coppia di tag omonime entro la quale si può inserire un brano di testo. A sua volta un sub-testo può essere racchiuso fra <Bold>...</Bold> o <Italic>...</Italic> ecc., a denotare e far apparire a video grassetti, corsivi e quant’altro. Ancor più bella è la possibilità di includere altri oggetti d’ogni tipo, come forme, immagini nonché controlli.
In questo piccolo tutorial entro in medias res con un esempio semplice ma tipico. Prima è necessaria una premessa:
Un FlowDocument deve essere racchiuso in un contenitore. Il più potente e comodo dei quali è lo specifico FlowDocumentReader. In pratica in XAML si avrà una situazione come la seguente:
Figura 1
Quest’altra figura illustra una situazione dopo varie manovre tramite il lettore e allargamento della finestra fino a visualizzare l’intero documento.
Figura 2
Si osservi l’adeguamento automatico della larghezza dei paragrafi nonché la presenza di un grassetto, un corsivo, tre pulsanti di opzione, una lista con quattro elementi , due dei quali corredati di shape, e un pulsante di comando. La figura precedente mostra quel che accade cliccando su di esso.
Per non deludere Impazienti & Curiosi ecco la banale routine responsabile, associata all’evento Click di tale Button, denominato per eccesso d’immaginazione, puls1:
Sub Raddoppia()
Dim pulsH = puls1.Height
Dim pulsW = puls1.Width
puls1.Height *= 2 : puls1.Width *= 2
MessageBox.Show("Ora ripristino le dimensioni del pulsante")
puls1.Height = pulsH
puls1.Width = pulsW
End Sub
Tutto il codice XAML
Dopo quanto detto ritengo quasi del tutto sufficiente riportare la parte dichiarativa del programmino, affidandomi all’eloquenza delle varie tag. Aggiungo solo che per prendere due piccioni ho utilizzato anche un controllo Expander, a monte del FlowDocumentReader. In tal modo il nostro documento può essere collassato (e occultato) o espanso per una sua più o meno ampia visualizzazione.
<Window x:Class="MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="FlowDocumentReader" Height="450" Width="525">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="250*" />
<RowDefinition Height="61*" />
</Grid.RowDefinitions>
<Expander Header="Espandi il documento" Name="Expander1" Margin="0,0,0,39">
<FlowDocumentReader Margin="9,0,-9,0" Height="244">
<FlowDocument FontSize="12" xml:space="preserve">
<Paragraph TextAlignment="Center" FontSize="22">
<Bold>Lettore di documenti</Bold></Paragraph>
<Paragraph>
Un FlowDocumentReader permette di inserire testi qualsiasi, variamente <Italic>formattati</Italic>, dopo di che tale contenuto, <Bold>FlowDocument</Bold>, viene visualizzato nello spazio disponibile - in modo simile a quello di una pagina HTML.
</Paragraph>
<Paragraph>Inoltre si può inserire un pulsante come quello seguente:
<Button Name="puls1" Height="40" Width="100" Click="Raddoppia">Clicca e stupisci</Button> Il clic raddoppia/ripristina le dimensioni del Button. Grazioso, vero?
Scegli una di queste opzioni:
<StackPanel>
<RadioButton IsChecked="True">Si</RadioButton>
<RadioButton>No</RadioButton>
<RadioButton>Non so</RadioButton>
</StackPanel>
</Paragraph>
<Paragraph><Bold>Si possono anche avere delle liste:</Bold></Paragraph>
<List>
<ListItem><Paragraph>Primo elemento</Paragraph></ListItem>
<ListItem><Paragraph>Secondo elemento</Paragraph></ListItem>
<ListItem><Paragraph>Terzo elemento: un cerchio! <Ellipse Fill="Red"
Width="20" Height="20"></Ellipse></Paragraph></ListItem>
<ListItem><Paragraph>Quarto elemento: quadrato! <Rectangle
Fill="Blue" Width="20" Height="20"></Rectangle>
</Paragraph></ListItem>
</List>
</FlowDocument>
</FlowDocumentReader>
</Expander>
<Button Grid.Row="1" Height="20" Name="PulsPrimario" Click="ProvaBis" Content="Espandi..." Margin="0,28,0,12" />
</Grid>
</Window>
NOTA - Il precedente listato potrebbe non dare risultato del tutto fedele rispetto alle figure precedenti in quanto qui riprodotto a mano. L'opzione xml:space="preserve" serve a mantenere gli spazi rispetto allo standard XML, ma non garantisco che tutto sia OK col codice testé riportato...
Altre quisquilie a proposito di Expander
Con l’occasione fornisco due semplici routine relative ai due opposti eventi dell’Expander, cui nel predetto XAML è stato affibbiato il banal nome Expander1:
Private Sub Expander1_Expanded(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs) Handles Expander1.Expanded
Expander1.Header = "Comprimi"
End Sub
Private Sub Expander1_Collapsed(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs) Handles Expander1.Collapsed
Expander1.Header = "Espandi il documento"
End Sub
Hanno lo scopo di modificare opportunamente l’intestazione (Header).
A proposito di Expander, espongo infine una perplessità e una richiesta, temo, utopistica. La prima è la constatazione, salvo segreti che ignoro, che l’Expander non si può applicare a singoli paragrafi, a meno di non suddividere un documento e applicare un FlowDocumentReader a ciascuno spezzone (il che non mi pare bello). La richiesta sarebbe la possibilità (esposta da vari siti di quotidiani ecc.) di esibire solo la parte iniziale di un articolo espandibile. Si può fare lo stesso con WPF? E come?
La figura precedente mostra questo una fase iniziale del ben noto giochino da me realizzato in WPF sfruttandone la Grid. Le operazioni da compiere procedono per coppie di clic, il primo su un anello origine, il secondo su quello destinazione. Fino a ottenere la situazione seguente:

in cui compare un pulsante dall'etichetta eloquente. Cliccandolo, tutti gli anelli sono impilati sulla prima colonna (della Grid).
La Grid di una finestra Wpf si può prestare a operazioni dinamiche relative agli oggetti contenuti nelle sue celle. L’ho già fatto con due giochino: il Gioco del 15, descritto nel mio articolo introduttivo a WPF su WpfItalia.it e in una simulazione di Finali Campionato descritta in questo blog. L’idea è quella di surrogare in qualche modo una DataGridView se non un foglio di lavoro. Pretesa, però, alquanto impropria e fonte di equivoci, soprattutto perché la Grid che il convento Wpf passa NON si compone di celle come oggetti indirizzabili: sono gli oggetti a godere di proprietà Grid.RowProperty e Grid.ColumnProperty, per cui individuarne uno in base alle coordinate è problematico e oltretutto una cella del genere ne può contenere più d’uno. Si deve insistere nel dire che individuare o “selezionare” una cella vuota è privo di senso e nel far presente che una Grid non possiede eventi propri: gli eventi definiti nella tag <Grid ... > (es. <Grid Click=”MiaRoutine” ...> accomunano quelli dei figli della Grid.
Comunque ci si arrangia, evitando l’appesantimento di add-in più flessibili. Questo terzo esempio didattico è stato realizzato con qualche pena ma ritengo sia non solo curioso ma costituisca utili ammaestramenti e salutari riflessioni magari con possibili varianti rispetto alle soluzioni qui adottate.
Il gioco delle Torri di Hanoi
Per comodità questo modellino è zippato assieme a quello del Gioco del 15. Tale file compresso si può scaricare da
http://www.giannigiaccaglini.it/download/HanoiGioco15.zip
La sezione XAML
Tutti i controlli in gioco sono dei Button, inclusi quelli di colore verde della riga in basso, denominati Base1, Base2 e Base3. Questi non sono anelli del gioco ma si sono resi indispensabili affinché l’evento Click per quanto detto sopra non si scatena su una cella vuota. Va poi sottolineata una scelta, opportuna, per cui i controlli di base sono tutti e tre scritti nelle righe XAML terminali. In tal modo quelli degli anelli “veri e propri”, i cui nomi dall’alto verso il basso vanno da Anello1 ad Anello2 fino ad Anello5, sono a loro volta disposti in modo da avere indici da 0 a 1 fino a 4.
Ma ecco tutto il codice XAML, all’inizio del quale va evidenziata la direttiva ButtonBase.Click =”Hanoi” che associa tutti i pulsanti contenuti nella nostra Grid dal prosaico nome “Griglia” a una routine comunitaria di nome “Hanoi”.
<Window x:Class="MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Torri di Hanoi" Height="425" Width="525">
<Grid Name="Griglia" ButtonBase.Click ="Hanoi" Height="409" Width="512">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
<RowDefinition />
<RowDefinition />
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Button Height="68" HorizontalAlignment="Left" Margin="57,1,0,0" Name="Anello1" VerticalAlignment="Top" Width="51" Grid.RowSpan="2" Background="#FFE20E0E" />
<Button Grid.Row="1" Height="68" HorizontalAlignment="Left" Margin="44,0,0,0" Name="Anello2" VerticalAlignment="Top" Width="77" BorderBrush="Black" Background="#FF0C1DE8" />
<Button Grid.Row="2" Height="69" HorizontalAlignment="Left" Margin="29,0,0,0" Name="Anello3" VerticalAlignment="Top" Width="107" Background="#FFD6E517" />
<Button Grid.Row="3" Height="69" HorizontalAlignment="Left" Margin="12,0,0,0" Name="Anello4" VerticalAlignment="Top" Width="141" Grid.RowSpan="2" Background="#FF585F0A" />
<Button Grid.Row="4" Height="68" HorizontalAlignment="Left" Name="Anello5" VerticalAlignment="Top" Width="171" Background="#FF830C0C" />
<Button Grid.Column="0" Grid.Row="5" Height="54" HorizontalAlignment="Left" Name="Base1" VerticalAlignment="Top" Width="171" Background="#FF1EF509" />
<Button Grid.Column="1" Grid.Row="5" Height="54" HorizontalAlignment="Left" Name="Base2" VerticalAlignment="Top" Width="171" Background="#FF31F210" />
<Button Grid.Column="2" Grid.Row="5" Height="54" HorizontalAlignment="Left" Name="Base3" VerticalAlignment="Top" Width="171" Background="#FF31F210" />
<Button Grid.Column="2" Grid.Row="5" Name="PulsRipeti" Click="Ripeti" Visibility="Hidden" Margin="0,0,0,14.167" >
Nuovo Gioco
</Button>
</Grid>
</Window>
Si perdoni la rozzezza degli aspetti estetici (che NON mi stavano a cuore per nulla, semmai può essere valida una variante basata su stili). A quanto anticipato aggiungo solo l’invito a notare come l’ultimo Button denominato PulsRipeti si trova sopra Base3 ma essendo inizialmente occultato con Visibility=”Hidden” nel corso del gioco non è attivabile dall’utente che vede e può cliccare solo su Base3. Scatenando la routine “Hanoi”. Ma quando al termine appare il button resuscitato il clic su di esso attiva invece la “PulsRipeti” in quanto la routine definita a livello Button predomina su quella comunitaria.
Il codice Visual Basic
Prima di riportare il listato, che, come confido, la brava gente che frequenta questo sito saprà comprendere anche da sola, ritengo importanti due osservazioni:
· L’indice di un pulsante nella famiglia dei “figli” della Grid – Griglia.Children nel nostro caso – resta immutato e si può dire che tale indice costituisce l’identità stessa del controllo, di cui nel corso del programma vengono modificate riga e colonna;
· La regola del gioco di fatto non considera (come si potrebbe pensare in un primo tempo) la larghezza dei vari anelli ma proprio l’indice di ciascuno, che grazie alla (oculata) digitazione del codice XAML, procede in senso decrescente dall’alto verso il basso.
Ma ecco tutto il codice.
Class MainWindow
Dim Pila1() As Integer = {0, 1, 2, 3, 4, 9}
Dim Pila2() As Integer = {9, 9, 9, 9, 9, 9}
Dim Pila3() As Integer = {9, 9, 9, 9, 9, 9}
Dim RigheTop() As Integer = {0, 5, 5}
Dim PulsCliccato As Button
Dim IndPulsCliccato As Integer = 0
Dim ColOrigine As Integer = 0
Dim PilaOrig As Array = Nothing
Private Sub Hanoi(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs)
Dim Sorgente = e.Source
' Assumi la colonna dell’origine
Dim Col As Integer = e.Source.GetValue(Grid.ColumnProperty)
Dim Vett() = {Pila1, Pila2, Pila3}
Dim IndAnello = Vett(Col)(0)
Dim RigaTop = RigheTop(Col)
If PulsCliccato Is Nothing Then ' Il controllo funziona come Switch!
If RigaTop = 5 Then Exit Sub
PulsCliccato = Griglia.Children(IndAnello)
IndPulsCliccato = IndAnello
ColOrigine = Col
PilaOrig = Vett(Col)
Else
If Col = ColOrigine Then Exit Sub ' Esci se il clic è su colonna origine
Dim Msg = "L'anello sovrapposto dev'essere più piccolo!"
If Not IndPulsCliccato < Vett(Col)(0) Then
MessageBox.Show(Msg, "Ricorda...", MessageBoxButton.OK, MessageBoxImage.Warning)
PulsCliccato = Nothing ' Come non detto...
Exit Sub
End If
PulsCliccato.SetValue(Grid.ColumnProperty, Col)
PulsCliccato.SetValue(Grid.RowProperty, RigaTop - 1)
PulsCliccato = Nothing
' Pop sulla PilaOrig
PopPila(PilaOrig)
' ReDim Preserve PilaOrig(PilaOrig.Length - 2) ' Rigettata!
' Aggiorna RigheTop(ColOrigine)
RigheTop(ColOrigine) += 1
' Push su Vett(Col)
PushPila(Vett(Col))
' Aggiorna RigheTop(Col)
RigheTop(Col) -= 1
' Verifica risultato finale
Msg = "Gioco concluso con successo"
' Le righe seguenti, ridondanti e più chiare, sono poi state
' sostituite con la successiva If RigheTop(2) Then ecc.
' Dim strPila3 = Pila3(0).ToString & Pila3(1).ToString & Pila3(2).ToString _
' & Pila3(3).ToString & Pila3(4).ToString
'If strPila3 = "01234" Then ' (Pila3 completa)
'MessageBox.Show(Msg, "", MessageBoxButton.OK, MessageBoxImage.Exclamation)
If RigheTop(2) = 0 Then ' (Pila3 completa)
MessageBox.Show(Msg, "", MessageBoxButton.OK, MessageBoxImage.Exclamation)
PulsRipeti.Visibility = Windows.Visibility.Visible
End If
End If
End Sub
Private Sub PopPila(ByRef Pila As Array)
Dim IndMax = Pila.Length - 1
' Dim MinElem = Pila(0)
For i = 0 To IndMax - 1
Pila(i) = Pila(i + 1)
Next
Pila(IndMax) = 9
' ReDim Preserve Pila(3) 'Rigettata!
End Sub
Private Sub PushPila(ByRef Pila As Array)
Dim IndMax = Pila.Length - 1
' Dim MaxElem = Pila(IndMax)
For i = IndMax To 1 Step -1
Pila(i) = Pila(i - 1)
Next
Pila(0) = IndPulsCliccato
End Sub
Private Sub Ripeti()
For i = 0 To 4
With Griglia.Children(i)
.SetValue(Grid.ColumnProperty, 0)
.SetValue(Grid.RowProperty, i)
End With
Next
Dim P1() As Integer = {0, 1, 2, 3, 4, 9}
Pila1 = P1
Dim P2() As Integer = {9, 9, 9, 9, 9, 9}
Pila2 = P2
Dim P3() As Integer = {9, 9, 9, 9, 9, 9}
Pila3 = P3
Dim RT() As Integer = {0, 5, 5}
RigheTop = RT
PulsRipeti.Visibility = Windows.Visibility.Hidden
End Sub
End Class
Commenti essenziali. Li limito ai punti più particolari, che entrambi corrispondono a mie personali scoperte ma che forse solo ai più esperti vengono in mente.
· Le variabili “generali”, ossia definite nelle Dichiarazioni, Pila1, Pila2 e Pila3 sono vettori che “fotografano” gli indici delle tre pile di anelli, inizializzate coi valori 0, 1, 2, 3, 4, 9 la prima e con tutti 9 le altre, con 9 che funge da “tappo”. Sono soggette a operazioni di pop e push ottenute coi metodi PopPila e rispettivamente PushPila atte a farne uscire e rientrare opportunamente l’indice iniziale dell’insieme. Il bello della storia è l’utilizzo di un vettore di vettori, con le istruzioni un po’ speciali Dim Vett() = {Pila1, Pila2, Pila3} e Dim IndAnello = Vett(Col)(0), la prima che fissa in Vett le tre pile (mano a mano modificate dal programma), la seconda che assai sinteticamente pone in IndAnello il primo termine della pila individuata da Vett(Col) ove Col è la colonna (da 0 a 2) di una pila. In tal modo ho evitato di ricorrere una struttura Select Case .
· La variabile delle Dichiarazioni PulsCliccato, inizializzata a Nothing, sul primo clic registra il Button appunto cliccato dall’utente con l’istruzione cruciale
PulsCliccato = Griglia.Children(IndAnello)
Come indica il commento, PulsCliccato funge da switch con Is Nothing, per alternare i casi If ed Else e poteva essere sostituito con una variabile booleana, ma l’ho lasciato perché forse non tutti pensano che una variabile Button (o altro controllo) di fatto coincide con il pulsante via via registrato, in particolare le modifiche alle sue proprietà si applicano all’”originale” (diciamo così), ne nostro caso alle Grid.RowProperty e Grid.ColumnProperty modificando il posizionamento nella Grid. Direi che è una specie di gemello o, probabilmente, un puntatore.
Discorso sulle possibili alternative
Anziché procedere con commenti dettagliati preferisco solo aggiungere due osservazioni. La prima è la constatazione, denunciata da commenti inseriti, che istruzioni di Redim Preserve relativi ad insiemi indirizzati indirettamente sono rifiutate. Qui non mi dilungo sul motivo di questa faccenda di cui comunque va preso atto. Come riflettendo si comprende, è da questo fatto che è derivato l’utilizzo di insiemi Pila1, Pila2 e Pila3 di lunghezza fissa (con i “tappi” 9 scorrevoli). Peccato, perché il vettore di vettori suggerito consente insiemi di dimensioni diversificate, ma ahimé sempre non ridimensionabili.
Ma non si poteva ricorrere al tipo Stack? La risposta è negativa, come indicano le righe di codice seguenti:
' Variabili a liv. Dichiarazioni NON accettano valori iniziali come
' Dim Pila1 = {0} o Pila1 As Stack = {0}
' per la natura stessa degli Stack
' Uniche dichiarazioni lecite:
Dim Pila1 As Stack(Of Integer)
Dim Pila2 As Stack(Of Integer)
Dim Pila3 As Stack(Of Integer)
Private Sub Prova()
. . . .
' Istruzioni a livello routine
Sub MiaSub()
Dim Vett = {Pila1, Pila2, Pila3} 'Dà errore su Vett
' Dim Vett As Array = {Pila1, Pila2, Pila3} ' Idem c.s.,
' idem con As New ArrayList
Dim Vett() = {Pila1, Pila2, Pila3} ' E' accettato
Vett(1).Push(4) ' Accettato dal compilatore, dà errore a run-time
Dim x = Pila2.Pop ' Idem c.s.
End Sub
La seconda osservazione è relativa all’insieme RigheTop(). Inizializzato coi valori 0, 5 e 5, nel corso del programmino spazia opportunamente sulle righe “di testa” da affibbiare a ciascuna pila, una scelta che differisce da quella delle tre pile. Di qui l’altra domanda: non si poteva fare altrettanto con queste ultime?
Stavolta la risposta è affermativa, in quanto se si riflette non conta tanto l’intera situazione delle pile bensì solo l’indice di testa. Si tratterebbe di un analogo insieme diciamo IndiciTop anch’esso inizializzato coi valori 0, 5 e 5 (anziché 9...). che evita il ricorso alle Sub di pop e di push viste sopra. Ma ho preferito lasciare la cosa per esercizio a chi ne ha voglia, anzitutto per pigrizia ma anche ritenendo che la soluzione “vettoriale” meritasse di essere illustrata, perché curiosa e da tenere a mente per altre occasioni.
Per tutto il resto non voglio togliere a chi legge il piacere di esaminare in dettaglio il procedimento, che alla luce di quanto appena detto e con un po’ di pazienza e il debug passo-passo, conto sia chiaro a tutti.
Azure piattaforma ormai obbligata...
Ancora una volta, gli esami non finiscono mai... Pertanto è d'obbligo cominciare a studiare, spupazzandosi testi che da qualche tempo abbondano. Così mi permetto di citare i seguenti:Azure in action
Il Cloud computing ormai non è solo un paradigma à-la-page ma una realtà. E dopo il varo della piattaforma Azure da parte di Microsoft sta coinvolgendo con prospettive decisamente allettanti tutti gli sviluppatori professionisti dell'area .NET e, in particolare, coloro che si occupano di WPF o SilverLight per quanto riguarda le interfacce. La buona notizia è che quanto è stato implementato per usi normali ("on premises" ovvero nelle sedi, diciamo così, terrene, in contrapposizione a "on cloud") può migrare abbastanza felicemente sulla Nuvola. Quella cattiva è che il Cloud e Azure in particolare comporta regole del tutto nuove, più trasparenti rispetto agli attuali sistemi operativi o, meglio: secondo un modello "astratto", che cioò astrae dalla precisa ubicazione sulle famose virtual machines (VM) che Azure mette a disposizione su enormi risorse hardware e software dislocate in tutto il globo.
AZURE. IL SISTEMA OPERATIVO E LA PIATTAFORMA PER IL CLOUD COMPUTING
Brunetti Roberto - Mondadori Informatica
€ 29,75
Programming WCF Services: Mastering WCF and the Azure AppFabric Service Bus
O 'REILLY -
€ 40,39
Azure in Action
di Prince Brian H., Hay Chris - MANNING PUBLICATIONS - 01/04/2010
€ 36,68
I prezzi indicati sono quelli scontati del 15% offeri da gorilla.it .
Fra quelli introduttivi mi sono dedicato in particolare al terzo che mi sembra un buon viatico per chi inizia, potendo poi proseguire anche da solo o su temi Azure più avanzati come la AppFabric di Azure e il Service Bus.
Il testo di Chris Hay e Brian H. Prince esordisce mettendo subito in evidenza la duplice natura di questo complesso ambiente:
· è un autentico sistema operative per il cloud offerto da Microsoft, che metta a disposizione degli utenti una quantità impressionante di risorse hardware e software dislocate nel pianeta (due negli Usa, due in Europa, altre in Asia) in modo dal tutto trasparente;
· si compone di un insieme di tool e API per sviluppare applicazioni sul cloud in ambiente Visual Studio 2010 o 2008 principalmente coi linguaggi Visual Basic o, a scelta, C#.
Fra le API basti citare WCF (Windows Communication Foundation) per implementare servizi di comunicazione per il cloud, ASP.NET in versione anch’essa ad hoc per il cloud e la speciale piattaforma AppFabric .
Si tratta di un nuovo paradigma, avente il pregio per chi vi ricorre di non doversi più preoccupare nemmeno di dove”fisicamente” risiedono le soluzioni di nuova generazione ma, perciò stesso, richiede una particolare sintassi per definirne la collocazione in modo “astratto” ovvero che astrae dalle famose macchine virtuali gestite in sostanza a cura di Microsoft. Di conseguenza non si parla più di dischi C:, D: ecc. e nemmeno di directory come in Windows,ma si utilizza una diversa nomenclatura.
Il libro non entra subito nei dettagli della codifica dei molti snippet e modelli in C# forniti, ma dedica un ampio primo capitolo e parte del secondo a illustrare la nuova realtà, pertanto la lettura giova pure a chi non deve programmare, come analisti, edp manager e magari dirigenti aziendali.
Anche per questi motivi riport alcuni stralci che ritengo utili a comprendere la validità di Azure chiarendo i nuovi concetti.
In a large enterprise project one of us worked on, 15 percent of the work hours was spent planning the development, quality assurance, and production environments. Most of this time was used to define hardware requirements, acquire capital expenditure approval, and deal with vendor management. We could’ve shipped much sooner if we’d been able to focus on the application and not the underlying infrastructure and platform. Many organizations take three to six months just to deploy a server! You won’t require this much time to complete the entire process using Windows Azure.
As you might have already gathered, the Windows Azure platform encompasses Microsoft’s complete cloud offering. Every service that Microsoft considers to be part of the cloud will be included under this banner. If the whole cloud thing passed you by, there isn’t really anything magical about it. The cloud refers to a bunch of servers that host and run your applications, or to an offering of services that are consumed (think web service).
The main difference between a cloud offering and a non-cloud offering is that the infrastructure is abstracted away—in the cloud, you don’t care about the physical hardware that hosts your service. Another difference is that most public cloud solutions are offered as a metered service, meaning you pay for the resources that you use (compute time, disk space, bandwidth, and so on) as and when you use them.
The Windows Azure platform splits into three parts: Windows Azure, SQL Azure, and the Windows Azure platform AppFabric.
The SDK provides the following Visual Basic templates that you can use in your solutions:
· ASP.NET Web Role—This template creates an ASP.NET project, preconfigured with an accompanying Azure project.
· ASP.NET MVC2 Web Role—This template creates a project similar to the ASP.NET Web Role template, but is prewired to support the MVC2 framework.
· WCF Service Web Role—Planning on hosting a Windows Communication Foundation (WCF) service instead of a normal web application? Then this is the project for you. You set this up like a normal WCF project, using sample files for your first service.
· Worker Role—This template creates a class library project, preconfigured with a related Azure project. You should use this project if you’re building a background processing service.
· CGI Web Role—This project template creates the required files needed to host a FastCGI project, which we’ll cover in chapter 6.
· Blank Cloud Service—This isn’t really a template…
Commento fugace: dal precedente elenco si ricava o, perlomeno, si intuisce che in Azure vengono definiti dei (cosiddetti) nuovi “ruoli” (Role) e servizi.
Dopo di che l'avventura con Azure può proseguire, caricando sul Cloud anche preesistenti applicazioni e, comunque, salvaguardando i propri investimenti concettuali in .NET in genere e in WPF / Silverlight in particolare.