Alessandro Del Sole's Blog

{ A programming space about Microsoft® .NET® }
posts - 1909, comments - 2047, trackbacks - 352

My Links

News

Your host

This is me! Questo spazio è dedicato a Microsoft® .NET®, di cui sono molto appassionato :-)

Cookie e Privacy

Microsoft MVP

My MVP Profile

Microsoft Certified Professional

Microsoft Specialist

Xamarin Certified Mobile Developer

Il mio libro su VB 2015!

Pre-ordina il mio libro su VB 2015 Pre-ordina il mio libro "Visual Basic 2015 Unleashed". Clicca sulla copertina per informazioni!

Il mio libro su WPF 4.5.1!

Clicca sulla copertina per informazioni! E' uscito il mio libro "Programmare con WPF 4.5.1". Clicca sulla copertina per informazioni!

These postings are provided 'AS IS' for entertainment purposes only with absolutely no warranty expressed or implied and confer no rights.
If you're not an Italian user, please visit my English blog

Le vostre visite

I'm a VB!

Guarda la mia intervista a Seattle

Follow me on Twitter!

Altri spazi

GitHub
I miei progetti open-source su GitHub

Article Categories

Archives

Post Categories

Image Galleries

Privacy Policy

Creare una griglia in WPF per leggere e modificare cartelle di Excel con LINQ-to-XML (VB 2008)

In un mio articolo appena pubblicato su Visual Basic Tips & Tricks, abbiamo visto come creare, in Windows Presentation Foundation, una griglia “editabile” per la visualizzazione e la modifica di dati provenienti da interrogazioni formulate attraverso LINQ-to-Entities su un database SQL per il quale è stato creato un modello a oggetti grazie ad ADO.NET Entity Framework.

 

In questo post vediamo come una tecnica simile sia adattabile alla lettura, alla modifica e alla scrittura di cartelle di lavoro di Microsoft Excel in formato XML, sfruttando LINQ-to-XML e gli XML literals di Visual Basic 2008, nonchè l’inferenza di schemi XML. Alla fine del post troverete il link per scaricare il progetto Visual Basic 2008 completo.

 

Si parte, quindi, da una semplice cartella di lavoro in Excel salvata in formato XML, il cui contenuto può rappresentare, per esempio, un elenco di blogger della nostra community, come riportato in figura:

 

 

 

Ora creiamo un nuovo progetto WPF per Visual Basic 2008. Tramite il comando Project|Add existing item, includiamo nel progetto la cartella di lavoro di Excel al fine di lavorare su una copia. Affinchè la nostra cartella di lavoro sia raggiungibile anche a run-time, fate clic destro sul suo nome in Solution Explorer e attivate le sue proprietà. Come mostrato nella seguente figura, impostate la proprietà Build Action come Content e la Copy to ouput directory come Copy if newer:

 

 

 

Ora che abbiamo i dati su cui lavorare, dobbiamo ricordarci che lavoreremo con LINQ-to-Xml. È quindi buona norma quella di inferire uno schema XML derivato dalla nostra cartella di lavoro, affinchè l’IntelliSense sia in grado di mostrare gli elementi della cartella stessa allorquando scriveremo le query. Ne abbiamo già parlato in questo vecchio post, ad ogni buon conto questo ci servirà da ripasso. Ergo, tramite il comando Project|Add new item, aggiungete un nuovo elemento al progetto chiamato XML To Schema assegnando al nuovo schema il nome BloggersSchema.Xsd, come in figura:

 

 

Come potete vedere, in Solution Explorer compaiono ora tre schemi Xsd complementari tra loro e che definiscono lo schema cui si conforma la nostra cartella di lavoro di Excel. Passiamo ora a definire l’interfaccia che sarà piuttosto essenziale. Avremo infatti una ListView e tre pulsanti (aggiunta, rimozione e salvataggio dei dati).

Dopo aver attivato l’editor di codice XAML sul file Window1.xaml, iniziamo col suddividere la Grid in due righe:

 

    <Grid>    

        <Grid.RowDefinitions>

            <RowDefinition/>

            <RowDefinition Height="40"/>

        </Grid.RowDefinitions>

Ora implementiamo la ListView, alla quale applicheremo, tra breve, il layout di una griglia:

 

        <ListView Grid.Row="0"  Name="BloggersListView" IsSynchronizedWithCurrentItem="True"

                  ItemsSource="{Binding Path=''}" >

           

            <ListView.ItemContainerStyle>

                <Style TargetType="ListViewItem">

                    <Setter Property="HorizontalContentAlignment" Value="Stretch" />

                    <EventSetter Event="GotFocus" Handler="Item_GotFocus"/>

                </Style>

            </ListView.ItemContainerStyle>

Di questo codice sottolineiamo le seguenti particolarità: la proprietà IsSynchronizedWithCurrentItem ci permette di assicurarci che ciascun elemento della ListView sia sincronizzato con il corrispondente elemento della collection che associeremo da codice Visual Basic mentre stiamo dicendo a WPF che la proprietà ItemsSource (quindi quella che popola il controllo) è popolata tramite data-binding ma attualmente in sospeso.

 

L’ItemContainerStyle ci serve per definire il layout di ciascun elemento, in questo caso lo utilizziamo affinchè ogni elemento della ListView sia largo quanto la colonna della griglia (Stretch). La gestione dell’evento GotFocus, invece, ci tornerà più chiara durante l’esposizione del codice Visual Basic. Se avete letto il mio articolo segnalato all’inizio del post, avete imparato che, in WPF, per creare una griglia si assegna la visualizzazione di tipo GridView alla ListView. Così facendo, la ListView assume la forma di griglia in cui ci sono delle colonne e, per ciascuna delle celle riferibili alle colonne stesse, è necessario impostare il template di visualizzazione dei dati (GridViewColumn.CellTemplate). Sfruttando il DataTemplate, diremo a WPF in che modo deve visualizzare i nostri dati:

 

            <ListView.View>

                <GridView>

                    <GridViewColumn Header="BloggerID" Width="80">

                        <GridViewColumn.CellTemplate>

                            <DataTemplate>

                                <TextBox Text="{Binding Path=BloggerID}"

                                 Margin="-6,0,-6,0" />

                            </DataTemplate>

                        </GridViewColumn.CellTemplate>

                    </GridViewColumn>

 

                    <GridViewColumn Header="Blogger Name" Width="120">

                        <GridViewColumn.CellTemplate>

                            <DataTemplate>

                                <TextBox Text="{Binding Path=BloggerName}"

                                 Margin="-6,0,-6,0" />

                            </DataTemplate>

                        </GridViewColumn.CellTemplate>

                    </GridViewColumn>

 

                    <GridViewColumn Header="Blog URL" Width="220">

                        <GridViewColumn.CellTemplate>

                            <DataTemplate>

                                <TextBox Text="{Binding Path=URL}"

                                 Margin="-6,0,-6,0" />

                            </DataTemplate>

                        </GridViewColumn.CellTemplate>

                    </GridViewColumn>

                </GridView>

            </ListView.View>

        </ListView>

 

Così facendo, ciascuna cella sarà rappresentata da una TextBox modificabile, il cui valore è determinato tramite data-binding a partire dalla relativa proprietà di un oggetto Blogger che implementeremo da codice Visual Basic (assegnata tramite Path). Questo ci porta a riflettere, come ho anche spiegato nell’altro articolo, sul fatto che il DataTemplate può essere altamente personalizzabile sfruttando praticamente ogni controllo corrispondente a un particolare tipo di dato (potremmo visualizzare una foto con un Image, dei booleani con una CheckBox). Da ultimo, aggiungiamo nella seconda riga un contenitore StackPanel con tre semplici pulsanti per le operazioni che ci occorrono:

 

        <StackPanel Grid.Row="1" Orientation="Horizontal">

            <Button Width="100" Height="30" Content="Salva" Name="SaveButton" Margin="10,0,0,0"/>

            <Button Width="100" Height="30" Content="Aggiungi" Name="AddButton" Margin="10,0,0,0" />

            <Button Width="100" Height="30" Content="Elimina" Name="DeleteButton" Margin="10,0,0,0"/>

        </StackPanel>

    </Grid>

 

Passiamo ora all’editor di codice Visual Basic, dove ci divertiremo senz’altro di più J Quando abbiamo definito lo schema XML, sono stati generati degli elementi nei file Xsd corrispondenti ai namespace Xml definiti nella cartella di lavoro di Excel. Affinchè l’IntelliSense sia in grado di sfruttarli, è necessario importare detti namespace nel modo seguente:

 

Imports <xmlns="urn:schemas-microsoft-com:office:spreadsheet">

Imports <xmlns:o="urn:schemas-microsoft-com:office:office">

Imports <xmlns:x="urn:schemas-microsoft-com:office:excel">

Imports <xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet">

Imports <xmlns:html="http://www.w3.org/TR/REC-html40">

 

Si tratta di tutti i namespace definiti nella nostra cartella di lavoro. A questo punto dobbiamo definire una classe che rappresenti ciascun blogger che recupereremo dalle singole righe della cartella di lavoro. La classe, molto essenziale, può essere la seguente:

 

    Class VbBlogger

 

        Private _bloggerID As String

        Private _bloggerName As String

        Private _url As String

 

        Property BloggerID() As String

            Get

                Return _bloggerID

            End Get

            Set(ByVal value As String)

                _bloggerID = value

            End Set

        End Property

 

        Property BloggerName() As String

            Get

                Return _bloggerName

            End Get

            Set(ByVal value As String)

                _bloggerName = value

            End Set

        End Property

 

        Property URL() As String

            Get

                Return _url

            End Get

            Set(ByVal value As String)

                _url = value

            End Set

        End Property

    End Class

 

Dichiariamo, a livello della classe Window1, alcuni oggetti che ci serviranno poi:

 

    'L'oggetto da collegare alla ListView

    Private BloggerView As ListCollectionView

    'Un riferimento al documento XML caricato

    Private BloggersWorkBook As XDocument

    'Un elenco dei blogger da istanziare successivamente

    Private Elenco As List(Of VbBlogger)

 

Nell’articolo su VB T&T, abbiamo utilizzato una collezione di tipo BindingListCollectionView per collegare alla ListView una collezione di oggetti che abbiamo recuperato con LINQ-to-Entities. Tale collezione si utilizza nei confronti di oggetti che derivano da ADO.NET. Ma questo non è il nostro caso, quindi facciamo uso della ListCollectionView che si utilizza con oggetti che implementano la interfaccia IList. Infatti, noi estrarremo un elenco di blogger dal documento XML ma convertiremo il risultato della query da IEnumerable a List(Of T). Il motivo è semplice: un tipo IEnumerable si assegna alla collezione CollectionView la quale non espone metodi per l’aggiunta e la rimozione, ma solo per la navigazione. La ListCollectionView, invece, fa proprio al caso nostro: ci permette di sfruttare oggetti non ADO.NET e al tempo stesso di modificarli oltre che sfogliarli.

 

A questo punto scriviamo un metodo che legga la cartella di lavoro Xml ed estragga tutti i blogger di VB T&T che essa contiene, convertendo il risultato in List(Of VbBlogger):

 

    Private Function GetBloggers() As List(Of VbBlogger)

 

        BloggersWorkBook = XDocument.Load("Data/Bloggers.xml")

 

        'Salta la prima riga, che nel foglio di lavoro è l'intestazione.

        Dim bloggers = From blogger In BloggersWorkBook...<Row> _

        Let BloggerID = blogger.<Cell>(0) _

        Let BloggerName = blogger.<Cell>(1) _

        Let URL = blogger.<Cell>(2) _

        Where BloggerID IsNot Nothing AndAlso _

                   BloggerName IsNot Nothing AndAlso _

                   URL IsNot Nothing _

        Select New VbBlogger With { _

                    .BloggerID = BloggerID.Value, _

                    .BloggerName = BloggerName.Value, _

                    .URL = URL.Value} _

        Skip 1

 

        Return bloggers.ToList

    End Function

 

Durante la scrittura del codice, noterete come l’IntelliSense sia attivo anche in LINQ-to-XML grazie all’inferenza dello schema precedentemente applicata. Quando la finestra dell’applicazione si apre, viene finalizzato il data-binding assegnando il DataContext della finestra nonchè popolando la collezione BloggerView con lo stesso contenuto del DataContext. Questo è sufficiente a far sì che la ListView sia associata alla BloggerView:

 

    Private Sub Window1_Loaded(ByVal sender As Object, ByVal e As System.Windows.RoutedEventArgs) Handles Me.Loaded

 

        Elenco = GetBloggers()

        Me.DataContext = Elenco

        Me.BloggerView = CType(CollectionViewSource.GetDefaultView(Me.DataContext), ListCollectionView)

 

    End Sub

 

Per assicurarci che al ricevimento del focus ciascun elemento della ListView sia sincronizzato al corrispondente elemento della collezione associata, gestiamo il seguente evento:

 

    Private Sub Item_GotFocus(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs)

 

        Dim item = CType(sender, ListViewItem)

        Me.BloggersListView.SelectedItem = item.DataContext

    End Sub

 

Ora ci occupiamo di gestire l’evento Click del pulsante di aggiunta dati. Lo facciamo in questo modo:

 

    Private Sub AddButton_Click(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs) Handles AddButton.Click

 

        Dim blogger = CType(Me.BloggerView.AddNew, VbBlogger)

        blogger.BloggerID = "0"

        blogger.URL = "http://community.visual-basic.it"

        Me.BloggerView.CommitNew()

 

    End Sub

 

WPF 3.5 SP 1 semplifica molto il lavoro sulle collezioni con i metodi AddNew e CommitNew. Il primo aggiunge alla collezione l’oggetto di tipo specificato, il secondo invia il nuovo oggetto alla collezione. In questo caso definiamo delle proprietà di default che verranno visualizzate all’aggiunta di un nuovo blogger nella griglia e che poi dovremo modificare. Per rimuovere un oggetto dalla collezione, invece, possiamo fare uso del metodo RemoveAt:

 

    Private Sub DeleteButton_Click(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs) Handles DeleteButton.Click

        If Me.BloggerView.CurrentPosition > -1 Then

            Me.BloggerView.RemoveAt(Me.BloggerView.CurrentPosition)

        End If

    End Sub

 

È utile sottolineare come abbiamo utilizzato la medesima metodologia vista per la collection BindingListCollectionView. Una delle parti più interessanti, a mio avviso, riguarda il salvataggio della cartella di lavoro. Faremo infatti un utilizzo intensivo degli XML Literals.

 

Definiamo un nuovo metodo per salvare il lavoro; il primo compito del metodo è quello di eseguire una query che sfrutti le espressioni incorporate per generare tanti oggetti Row (le righe della cartella di lavoro di Excel) quanti sono i blogger contenuti nella collezione:

 

    Private Sub SaveWorkBook()

 

        Dim newBloggersList = From blogger In Elenco _

                            Select <Row>

                                       <Cell><Data ss:Type="Number"><%= blogger.BloggerID %></Data></Cell>

                                       <Cell><Data ss:Type="String"><%= blogger.BloggerName %></Data></Cell>

                                       <Cell ss:StyleID="s63" ss:HRef=<%= blogger.URL %>><Data

                                           ss:Type="String"><%= blogger.URL %></Data></Cell>

                                   </Row>

Se vi serve una mano a capire il discorso sulle espressioni incorporate, oltre al mio libro su LINQ vi segnalo questo mio articolo su VB T&T. A questo punto abbiamo una collezione di righe, che dovremo aggiungere alla cartella di lavoro dinamicamente. Come si fa? Semplice: aprite in Visual Studio 2008 la cartella di Excel con l’editor XML e copiate nella clipboard tutto il contenuto. Dopodichè, scrivete il seguente codice incollando il documento copiato dopo il simbolo “=” e sostituendo le varie Row con l’espressione incorporata che trovate nel codice:

 

        Dim WorkBook = <?xml version="1.0"?>

                       <?mso-application progid="Excel.Sheet"?>

                       <Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet"

                           xmlns:o="urn:schemas-microsoft-com:office:office"

                           xmlns:x="urn:schemas-microsoft-com:office:excel"

                           xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet"

                           xmlns:html="http://www.w3.org/TR/REC-html40">

                           <DocumentProperties xmlns="urn:schemas-microsoft-com:office:office">

                               <Author>Alessandro</Author>

                               <LastAuthor>Alessandro</LastAuthor>

                               <Created>2008-10-24T16:44:12Z</Created>

                               <Version>12.00</Version>

                           </DocumentProperties>

                           <ExcelWorkbook xmlns="urn:schemas-microsoft-com:office:excel">

                               <WindowHeight>12240</WindowHeight>

                               <WindowWidth>19095</WindowWidth>

                               <WindowTopX>120</WindowTopX>

                               <WindowTopY>135</WindowTopY>

                               <ProtectStructure>False</ProtectStructure>

                               <ProtectWindows>False</ProtectWindows>

                           </ExcelWorkbook>

                           <Styles>

                               <Style ss:ID="Default" ss:Name="Normal">

                                   <Alignment ss:Vertical="Bottom"/>

                                   <Borders/>

                                   <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="11" ss:Color="#000000"/>

                                   <Interior/>

                                   <NumberFormat/>

                                   <Protection/>

                               </Style>

                               <Style ss:ID="s62" ss:Name="Hyperlink">

                                   <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="11" ss:Color="#0000FF"

                                       ss:Underline="Single"/>

                               </Style>

                               <Style ss:ID="s63" ss:Parent="s62">

                                   <Alignment ss:Vertical="Bottom"/>

                                   <Protection/>

                               </Style>

                               <Style ss:ID="s64">

                                   <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="11" ss:Color="#000000"

                                       ss:Bold="1"/>

                               </Style>

                           </Styles>

                           <Worksheet ss:Name="Sheet1">

                               <Table ss:ExpandedColumnCount="3" ss:ExpandedRowCount="7" x:FullColumns="1"

                                   x:FullRows="1" ss:DefaultRowHeight="15">

                                   <Column ss:AutoFitWidth="0" ss:Width="67.5"/>

                                   <Column ss:AutoFitWidth="0" ss:Width="105.75"/>

                                   <Column ss:AutoFitWidth="0" ss:Width="212.25"/>

                                   <Row>

                                       <Cell ss:StyleID="s64"><Data ss:Type="String">BloggerID</Data></Cell>

                                       <Cell ss:StyleID="s64"><Data ss:Type="String">BloggerName</Data></Cell>

                                       <Cell ss:StyleID="s64"><Data ss:Type="String">URL</Data></Cell>

                                   </Row>

                                   <%= newBloggersList %>

                               </Table>

                               <WorksheetOptions xmlns="urn:schemas-microsoft-com:office:excel">

                                   <PageSetup>

                                       <Header x:Margin="0.3"/>

                                       <Footer x:Margin="0.3"/>

                                       <PageMargins x:Bottom="0.75" x:Left="0.7" x:Right="0.7" x:Top="0.75"/>

                                   </PageSetup>

                                   <Print>

                                       <ValidPrinterInfo/>

                                       <HorizontalResolution>600</HorizontalResolution>

                                       <VerticalResolution>600</VerticalResolution>

                                   </Print>

                                   <Selected/>

                                   <Panes>

                                       <Pane>

                                           <Number>3</Number>

                                           <ActiveRow>7</ActiveRow>

                                       </Pane>

                                   </Panes>

                                   <ProtectObjects>False</ProtectObjects>

                                   <ProtectScenarios>False</ProtectScenarios>

                               </WorksheetOptions>

                           </Worksheet>

                           <Worksheet ss:Name="Sheet2">

                               <Table ss:ExpandedColumnCount="1" ss:ExpandedRowCount="1" x:FullColumns="1"

                                   x:FullRows="1" ss:DefaultRowHeight="15">

                               </Table>

                               <WorksheetOptions xmlns="urn:schemas-microsoft-com:office:excel">

                                   <PageSetup>

                                       <Header x:Margin="0.3"/>

                                       <Footer x:Margin="0.3"/>

                                       <PageMargins x:Bottom="0.75" x:Left="0.7" x:Right="0.7" x:Top="0.75"/>

                                   </PageSetup>

                                   <ProtectObjects>False</ProtectObjects>

                                   <ProtectScenarios>False</ProtectScenarios>

                               </WorksheetOptions>

                           </Worksheet>

                           <Worksheet ss:Name="Sheet3">

                               <Table ss:ExpandedColumnCount="1" ss:ExpandedRowCount="1" x:FullColumns="1"

                                   x:FullRows="1" ss:DefaultRowHeight="15">

                               </Table>

                               <WorksheetOptions xmlns="urn:schemas-microsoft-com:office:excel">

                                   <PageSetup>

                                       <Header x:Margin="0.3"/>

                                       <Footer x:Margin="0.3"/>

                                       <PageMargins x:Bottom="0.75" x:Left="0.7" x:Right="0.7" x:Top="0.75"/>

                                   </PageSetup>

                                   <ProtectObjects>False</ProtectObjects>

                                   <ProtectScenarios>False</ProtectScenarios>

                               </WorksheetOptions>

                           </Worksheet>

                       </Workbook>

Dopo questo copia-incolla pauroso, spero che apprezzerete molto di più la potenza degli XML Literals di Visual Basic 2008, che ci permettono di utilizzare interi documenti XML esistenti. In realtà, non avete la necessità di conoscere lo schema della cartella di Excel, semplicemente dovete capire dove posizionare l’espressione incorporata che contiene le Row generate dinamicamente (quindi potete confrontare i due documenti). Il metodo si finalizza col salvataggio della cartella: 

 

        WorkBook.Save("Data/Bloggers.xml")

    End Sub

 

Il pulsante di salvataggio si limiterà quindi a richiamare il metodo:

 

    Private Sub SaveButton_Click(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs) Handles SaveButton.Click

        SaveWorkBook()

    End Sub

 

Avviando l’applicazione otterremo il seguente, primo risultato:

 

 

Potremmo aggiungere un nuovo blogger, salvare la cartella, riavviare l’applicazione per vedere il risultato delle modifiche:

 

 

Se poi provate ad aprire la cartella di lavoro in Excel 2007, potrete osservare anche in quella sede le modifiche apportate. In questo post abbiamo quindi imparato:

 

·         che WPF non è solo multimedialità avanzata;

·         che LINQ-to-XML può essere utilizzato in maniera proficua per gestire cartelle di Microsoft Excel;

·         che LINQ-to-XML e WPF possono essere integrati per la visualizzazione e la modifica di dati tramite interfaccia;

·         che la collection ListCollectionView ci permette di manipolare oggetti non provenienti da ADO.NET;

·         a creare una ListView “editabile”;

·         a utilizzare il DataTemplate nelle celle per stabilire le modalità di visualizzazione/modifica dei dati.

 

Spero che vi sia stato utile. Scarica il codice sorgente da VB T&T.

 

Alessandro

Print | posted on lunedì 27 ottobre 2008 02:14 | Filed Under [ .NET Framework Visual Basic Windows Presentation Foundation LINQ ]

Powered by:
Powered By Subtext Powered By ASP.NET