Questo articolo ha lo scopo di riassumere brevemente gli "intoppi" che ho dovuto superare nell'aprire un file di excel da Asp.Net; non si propone quindi come guida "assoluta", pertanto se avete osservazioni o critiche da fare.....fate pure.
Avevo la necessità di aprire un file excel (esistente) per leggerne dei contenuti, e dato che non aveva una forma coerente (per essere letto come connection source), mi serviva ricorrere alla pestifera COMInterop e scandirlo "a mano". Premetto che in questo caso stavo lavorando (ahimè) con VS2003 (ma sinceramente credo che sia identica la storia per il VS2005).
Ormai arrugginito (erano lustri che non leggevo excel da codice...pensate....da vb6!!!! ;-)), dicevo, ormai arrugginito parto dai primi passi, ed aggiungo un riferimento alla "Microsoft Excel 11.0 Object Library" (quindi in VS...Tasto Destro su References > Aggiungi Riferimento > Scelgo nella scheda COM il componente "Microsoft Excel 11.0 Object Library"), e verifico che nel folder References siano stati aggiunti i riferimenti a Microsoft.Office.Core, ed Excel.
Bene, a questo punto mi scrivo una pagina web (WebForm1.aspx) da cui simulare l'upload del file, e una classe (Utilities.cs) che mi gestisca l'interazione con Excel (
oltre a dare altri metodi utili, come il "ShowMessageBox").
Quindi nella classe definisco la mia "public string importPnlFile(string filename)", che provvederà ad aprire il file excel (partecipanti.xls) e prenderà dati a casaccio.
A questo punto iniziano a comparire anche le righe di codice (se volete posso girarvi il sorgente progetto...batsa chiedere...è gratis :-)).
Mi creo le mie variabili del caso (Excel.ApplicationClass, Excel.Workbook, Excel.Worksheet), e le istanzio nel modo seguente:
Microsoft.Office.Interop.Excel.ApplicationClass xlsApp= new Microsoft.Office.Interop.Excel.ApplicationClass();
Microsoft.Office.Interop.Excel.Workbook wkb = XlsApp.Workbooks.Open(filename,missing,false,missing,
missing,missing,missing,missing,missing,missing,missing,missing,missing,missing,missing);
Microsoft.Office.Interop.Excel.Worksheet wks = (Microsoft.Office.Interop.Excel.Worksheet)wkb.Worksheets["Info"];
Ma già qui salta fuori il primo problema.
Se infatti proviamo ad eseguire queste sole tre righe di codice otteniamo una "System.UnauthorizedAccessException" con message {"Access is denied." }, già alla prima istruzione. Perchè?
Sempre la solita menata dell'utente ASPNET e delle sue credenziali. Dobbiamo quindi assegnare diritti in più all'utente ASPNET ....già ma dove??? Proprio
nella DCOM Application Microsof.Excel...quindi Start > Administrative Tools > Component Services, nella finestra che otteniamo espandiamo il nodo Component Services sulla sinistra (...consiglio= cliccate sull'immagine, il "+" non basta), quindi espandiamo Computers, e ancora MyComputer, quindi "DCOM Config"; bene, qui dentro cerchiamo
la DCOM Application con nome Microsoft.Excel.Application (ovvio che dovete avere l'Excel installato), chiediamone le proprietà e nella scheda Security, selezionando il Customize facciamo click su Edit ed aggiungiamo nel modo consueto l'utente ASPNET all'elenco, ed assegnamogli pieni diritti (non fatemi scrivere come, ma se avete bisogno chiedete pure).
Bene!!! Fatto questo riavviamo il ns progetto (che vi giro in allegato) e vedremo che riusciamo ora a creare l'istanza dell'Excel.ApplicationClass(),
già, ma, per i più fortunati, alla richiesta di istanza dell' Excel.Workbook, ci imbatteremo in una nuova rogna di tipo "System.Runtime.InteropServices.COMException" che ci lamenta un {"Old format or invalid type library." }. Che ca**o vuole ora sto st****o???? E' un bughettino che Bill (...non proprio lui in persona, eh..) consiglia di risolvere attraverso la CultureInfo (http://support.microsoft.com/kb/320369/en-us), che dovrà essere modificata nel currentThread prima di chiedere l'apertura del file, e reimpostata alla fine dei "lavori". Quindi:
//Prima di usare le istanze Excel
System.Globalization.CultureInfo oldCI=System.Threading.Thread.CurrentThread.CurrentCulture;
System.Threading.Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo("en-US");
/*
.
.
CODE HERE
.
.*/
//Dopo aver usato le istanze Excel
System.Threading.Thread.CurrentThread.CurrentCulture = oldCI;
A questo punto non ci resta che scrivere il codice che ci serve, che in questo esempio aprirà il file excel, leggerà dei dati, ne scriverà, e chiuderà e distruggerà il tutto.
Soprattutto chiuderà e distruggerà...mi raccomando.
Stiamo entrando in Interoperabilità col codice non gestito, quindi cerchiamo di fare le cose per bene con il rilascio degli oggetti, a meno che non vogliate
ritrovarvi decine e decine di processi Excel zombie sulla macchina in questione.
Perche? Perchè gli spazi di memoria dei due mondi (.Net e COM) sono gestiti in modo assolutamente differente (risparmiatemi anche qui la teoria del perchè...cque se occorre chiedete). Quindi bisogna cercare di avere sempre una variabile esplicita di riferimento agli oggetti, e chiederne il rilascio non appena avrete finito di usarla (questo si farà attraverso il metodo System.Runtime.InteropServices.Marshal.ReleaseComObject(object)).
Mi raccomando alla precisione e alle sequenze da rispettare. Nell'esempio proposto le dipendenze seguono il flusso Excel.Range < Excel.WorkSheet < Excel.WorkBook < Excel.Application, è sbagliato quindi finalizzare gli oggetti in un ordine differente (ma tanto ve ne accorgereste ;-D).
Quindi un riassunto di come si presenterebbe ora il codice, con i dovuti commenti:
object missing = Type.Missing;
//Prima di usare le istanze Excel
System.Globalization.CultureInfo oldCI=System.Threading.Thread.CurrentThread.CurrentCulture;
System.Threading.Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo("en-US");
Microsoft.Office.Interop.Excel.ApplicationClass xlsApp=null;
Microsoft.Office.Interop.Excel.Workbook wkb = null;
Microsoft.Office.Interop.Excel.Worksheet wks =null;
Microsoft.Office.Interop.Excel.Range rng1=null;
Microsoft.Office.Interop.Excel.Range rng2=null;
Microsoft.Office.Interop.Excel.Range rng3=null;
Microsoft.Office.Interop.Excel.Range rngWrite=null;
System.Text.StringBuilder sb = new System.Text.StringBuilder();
try
{
xlsApp = new Microsoft.Office.Interop.Excel.ApplicationClass();
xlsApp.Visible=false;
//i parametri indesiderati vanno sostituiti con un object di tipo Type.Missing
wkb = xlsApp.Workbooks.Open(filename,missing,false,missing,missing,missing,missing,
missing,missing,missing,missing,missing,missing,missing,missing);
//ottenere l'istanza di un worksheet tramite l'oggetto workbook
wks = (Worksheet)wkb.Worksheets["Info"];
//sempre...prima di lavorare con uno foglio bisogna attivarlo
wks.Activate();
//partiamo dalla riga 2 (int iRow=2) per saltare l'intestazione della tabella,
//in casi normali si parte dalla 1 (e non dalla 0 come verrebbe naturale. Idem per le colonne).
for (int iRow=2;iRow<=36000;iRow++)
{
if (rng1!=null)System.Runtime.InteropServices.Marshal.ReleaseComObject(rng1);
rng1 = (Range)wks.Cells[iRow,1];
if (rng1.Value2==null)
{
break;
}
else
{
/*Perchè usare l'assegnamento ai range, invece di usare la sintassi più comoda
* "string xtx = ((Microsoft.Office.Interop.Excel.Range)wks.Cells[iRow,1]).Value2;"?
*Per evitare che vengano creati riferimenti che non possiamo controllare
* e quindi processi zombie; infatti, dovremo chiamare
* System.Runtime.InteropServices.Marshal.ReleaseComObject(object)
* per ogni oggetto della gerarchia creato
*Sembra maniacale, ma vi evita molte rotture*/
if (rng1!=null)System.Runtime.InteropServices.Marshal.ReleaseComObject(rng1);
rng1 = (Range)wks.Cells[iRow,1];
if (rng2!=null)System.Runtime.InteropServices.Marshal.ReleaseComObject(rng2);
rng2 = (Range)wks.Cells[iRow,2];
if (rng3!=null)System.Runtime.InteropServices.Marshal.ReleaseComObject(rng3);
rng3= (Range)wks.Cells[iRow,3];
if (rngWrite!=null)System.Runtime.InteropServices.Marshal.ReleaseComObject(rngWrite);
rngWrite= (Range)wks.Cells[iRow,4];
/*Il codice non farà altro che leggere l'elenco dei partecipanti dal file
* aggiornarne i dati relativi alla data di iscrizione (quindi scriviamo in una cella)
* e restituire l'elenco dei partecipanti da visualizzare con un alert
* Inutile dirvi che potete provare con qualunqe file, cambiando la logica
* ed inserendo i dovuti test di validazione dei dati (cosa che io non farò)*/
sb.Append(rng2.Value2.ToString()+" " + rng1.Value2.ToString()+
", iscritto il "+rng3.Value2.ToString()+"\\r\\n");
TimeSpan ts = DateTime.Now.Subtract(DateTime.Parse(rng3.Value2.ToString()));
rngWrite.Value2=ts.TotalDays;
}
}
if (rng1!=null)System.Runtime.InteropServices.Marshal.ReleaseComObject(rng1);
if (rng2!=null)System.Runtime.InteropServices.Marshal.ReleaseComObject(rng2);
if (rng3!=null)System.Runtime.InteropServices.Marshal.ReleaseComObject(rng3);
if (rngWrite!=null)System.Runtime.InteropServices.Marshal.ReleaseComObject(rngWrite);
if (wks!=null)System.Runtime.InteropServices.Marshal.ReleaseComObject(wks);
if (wkb!=null)
{
wkb.Close(true,missing,missing);
System.Runtime.InteropServices.Marshal.ReleaseComObject(wkb);
wkb=null;
}
if (xlsApp!=null)
{
xlsApp.Quit();
System.Runtime.InteropServices.Marshal.ReleaseComObject(xlsApp);
xlsApp=null;
}
//CODE HERE
//Dopo aver usato le istanze Excel
System.Threading.Thread.CurrentThread.CurrentCulture = oldCI;
return sb.ToString();
}
catch(Exception e)
{
if (rng1!=null)System.Runtime.InteropServices.Marshal.ReleaseComObject(rng1);
if (rng2!=null)System.Runtime.InteropServices.Marshal.ReleaseComObject(rng2);
if (rng3!=null)System.Runtime.InteropServices.Marshal.ReleaseComObject(rng3);
if (rngWrite!=null)System.Runtime.InteropServices.Marshal.ReleaseComObject(rngWrite);
if (wks!=null)System.Runtime.InteropServices.Marshal.ReleaseComObject(wks);
if (wkb!=null)
{
wkb.Close(true,missing,missing);
System.Runtime.InteropServices.Marshal.ReleaseComObject(wkb);
wkb=null;
}
if (xlsApp!=null)
{
xlsApp.Quit();
System.Runtime.InteropServices.Marshal.ReleaseComObject(xlsApp);
xlsApp=null;
}
if (oldCI != null)System.Threading.Thread.CurrentThread.CurrentCulture = oldCI;
return e.Message;
}
Siamo quindi alla fine (...finalmente).
Nella pagina web che chiama l'oggetto Utilities, ho inserito due chiamate allo spazzino, per essere davvero certo che non mi rimangano zombies nella macchina; quindi:
GC.Collect();
GC.WaitForPendingFinalizers();
Questo è quanto. Vi ricordo che avete la possibilità di chiedere un piccolo progetto di esempio, e di fare domande se ne avete.
Spero di essere d'aiuto, e che siate così gentili da lasciarmi un commento per farmi sapere se e quando vi è stato utile questo articolo.
So che non è di chissà quale complessità, ma se vedrò che sono bene accetti i miei articoli, continuerò a scriverne anche su qualcosa di più complesso (...una è già in arrivo..lavoro permettendo sul Net.Remoting).
Ma in linea generale sarà facile che scriva più sulle rotture da perdita di tempo...ma chissà.
THE END
Libero Balsamo
http://www.liberobalsamo.net