neděle 18. prosince 2011

Vzhůru do oblak!…..a stejně skončíme v přístavu

Minulý příspěvek popisoval jednoduchou konzolovou aplikaci, která z údajů ze sešitu programu Excel udělá PDF dokument s kartičkami. Takový dokument pak stačilo jen vytisknout, rozstříhat či rozřezat a papírové kartičky byly hotové.
Tento příspěvek popíše, jak tuto aplikaci upravit tak, že:
  • umožní načítat z libovolného zdroje dat (například místo Excel z CSV souboru)
  • zdrojový kód bude přístupný v aktuální podobě každému a nemusí se přidávat jako příloha ke stažení
  • aplikace bude dostupná přes webové stránky – tedy pošleme soubor a obdržíme zpět PDF dokument
A proč nadpis “Vzhůru do oblak!”? – výsledný web bude v cloudu. To je dnes takové moderní slovo – v prostředí .NET nejčastěji spojované s Azure. Ale Azure nepoužijeme – Microsoft sice nabízí trial verze, jenže ani u nich není zaručeno, že na konci za to nebude platit (zde si neodpustím poznámku, že MS na Azure a WP7 předvádí jak velká firma není schopna správných rozhodnutí a nechápe, co je pro masivnější používání daných produktů klíčové – ale to je jen můj povzdech a soukromý názor). Takže místo Azure použijeme služeb AppHarbor.
Aktualizace 18.12 17:00 – jak jsem se dočetl zde, MS konečně umožnil bezplatné zkoušení. I tak je krok za AppHarbor.

Úprava stávající aplikace

Nejprve změníme typ z konzolové aplikace na knihovnu. To je jednoduchá změna, klikneme pravým tlačítkem na název projektu a ve vlastnostech změním typ:
image
Nyní se musí upravit samotná aplikace. Nejprve upravíme načítání dat – nyní je v kódu napevno zakodóváno načítání přes ADO.NET z Excel souboru:
Stávající kód
  1. void ReadAndPrintExcelSheet(...)
  2.         {
  3.             ...
  4.             Action<DbCommand> read = (command) =>
  5.             {
  6.                 command.CommandText = "SELECT * FROM [Sheet1$]";
  7.  
  8.                 using (DbDataReader reader = command.ExecuteReader())
  9.                 {
  10.                     ...
  11.                     while (reader.Read())
  12.                     {
  13.                         ....
  14.                     }
  15.                 }
  16.                 ...
  17.             };
  18.  
  19.             Db.Work("Excel", read);
  20.         }
Tohoto provázání se zbavíme poměrně snadno – zavedeme tento interface ICardData:
public interface ICardData
{
    string Word { get; }
    string Pronunciation { get; }
    string Example { get;  }
    string Meaning { get;  }
    string ExampleTranslation { get; }
}
Do definice třídy pro generování PDF pak přidáme delegáta  Func<IEnumerable<ICardData>>, který bude ukazovat na metodu, která bude schopna poskytnout údaje o kartičkách – přičemž nyní naší třídy pro generování PDF už nebude zajímat, jak to tato metoda dělá (tedy jestli čte databázi, CSV soubor, google dokument atd.):
Upravený kód
  1. Func<IEnumerable<ICardData>> getCardData;
  2. ...
  3. void ReadAndPrintExcelSheet(...)
  4. {
  5.     ...
  6.  
  7.     foreach (var cardData in getCardData())
  8.     {
  9.         ....
  10.     }
  11.  
  12.     ...
  13. }
Pokud porovnáte nový a předešlý kód navzájem, došlo ke zjednodušení. Dle mého názoru je kód i přehlednější. Zbývá ještě udělat metodu pro delegáta getCardData :-).

Poznámka pro ICardData"

Interface ICardData je implementován abstraktní třídou CardData – tato abstraktní třída pak slouží jako předek pro třídy navázané na konkrétní zdroje dat (bude ukázáno dále):
  1. public abstract class CardData: ICardData
  2. {
  3.     public string Word { get; protected set; }
  4.     public string Pronunciation { get; protected set; }
  5.     public string Example { get; protected  set; }
  6.     public string Meaning { get; protected set; }
  7.     public string ExampleTranslation { get; protected set; }
  8. }
V zdrojovém kódu je implementována i původní  podpora pro čtení Excel souborů – ale tento kód není využit (budou se zpracovávat jen CSV soubory). Slouží tedy spíše jako ukázka, jak implementovat podporu dalších zdrojů údajů o kartičkách. Jedná se o třídy DbCardDataProvider.cs, DbDataReaderCardData.cs a Database.cs.

Čteme CSV soubor

Pro čtení CSV souboru jsem si “vypůjčil” kód od Janathana Wooda.  Provedl jsem jen pár úprav – zajímá mne jen čtení ze streamu, místo čárky jsem jako oddělovač definoval středník. Výsledek je v souboru CsvStreamReader.cs.
Kód této třídy je jednoduchý – umí vzít vstupní stream a postupně jej číst “po řádcích”, přičemž každý řádek načte do objektu třídy CsvRow. To dělá metoda ReadRow(CsvRow), která vrací true, dokud nedosáhneme konce dat a aktualizuje vlastnosti objektu CsvRow tak, aby obsahoval data právě načteného řádku.
Nyní ještě zbývá udělat pomocnou třídu CsvStreamReaderCardDataProvider, která nám poskytne metodu pro delegáta Func<IEnumerable<ICardData>> pro třídu generující PDF dokument. 






  1. public class CsvStreamReaderCardDataProvider : IDisposable

  2. {

  3.     CsvStreamReader reader = null;

  4.  

  5.     public CsvStreamReaderCardDataProvider(MemoryStream stream)

  6.     {

  7.         this.reader = new CsvStreamReader(stream);

  8.     }

  9.  

  10.     public IEnumerable<ICardData> GetCardData()

  11.     {

  12.  

  13.         CsvRow row = new CsvRow();

  14.  

  15.         while (reader.ReadRow(row))

  16.         {

  17.             if (row.Count < 4)

  18.                 continue;

  19.  

  20.             CsvStreamReaderCardData card = new CsvStreamReaderCardData(row);

  21.  

  22.             yield return card;

  23.         }

  24.     }

  25. }





Funkčnost třídy je jednoduchá, vytvoří si instanci CsvStreamReaderu a poté v metodě GetCarData čte řádky, převede CsvRow na objekt třídy implementující ICardData – v tomto případě na objekt třídy CsvStreamReaderCardData, která je potomkem abstraktní třídy CardData:






  1. public class CsvStreamReaderCardData : CardData

  2. {

  3.     public CsvStreamReaderCardData(CsvRow row)

  4.     {

  5.         this.Word = row[0];

  6.         this.Pronunciation = row[1];

  7.         this.Example = row[2];

  8.         this.Meaning = row[3];

  9.         if (row.Count > 4)

  10.             this.ExampleTranslation = row[4];

  11.         else

  12.             this.ExampleTranslation = string.Empty;

  13.     }

  14. }





 



Proč vlastně stream a ne soubor (aneb iTextSharp v paměti)?



Jak jsem nastínil již v úvodu, má celá aplikace bežet v cloudu. Tam ukládání a čtení souborů může představovat problém – a protože nepředpokládám velké soubory, tak se veškeré zpracování (upload od uživatele a čtení) odehraje jen v paměti.


Výše uvedené má ještě jeden dopad – při generování PDF souboru se tento vytvářel na disku. Nyní i tento dokument musíme vytvořit v paměti a poté poslat uživateli zpět, tedy místo tohoto kódu:






  1. PdfWriter writer = PdfWriter.GetInstance(pdfDocument,

  2.                         new FileStream(

  3.                         Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments),

  4.                         "Flashcards.pdf"),

  5.                         FileMode.Create));





použijeme tento kód:






  1. var stream = new MemoryStream();

  2. PdfWriter writer = iTextSharp.text.pdf.PdfWriter.GetInstance(pdfDocument, stream);

  3. writer.CloseStream = false;








Souhrn změn původní aplikace



Mimo změny v typu aplikace (z konzolové na knihovnu) a způsobu načítání dat došlo i k dalším změnám:



  • Hlavní kód se přesunul ze souboru Program.cs do souboru Creator.cs, stejně se změnili i názvy tříd (program na Creator).


  • Třída Creator má jednu hlavní metodu CreatePdf, která vyžaduje dva vstupní paramatry – metodu pro získání údajů kartiček a třídu fontů, které se mají použít


  • správné umístnění kartičky na A4 je řešeno pomocí dvou statických funkcí BackNextPositionProvider a FrontNExtPositionProvider – zpřehlednil se tak tisk stránky, který se odehrává v metodě PrintFlashCardPage


  • kartičky se ze zdoje načítají do jediného pole, to se navíc při zpracování stránky neinicilizuje znovu, ale pouze se vyprázdní příkazem Array.Clear(pageCardData, 0, pageCardData.Length);



Zpracování je nyní toto:



  1. zavolá se metoda CreatePdf – inicilizuje se pdf dokument


  2. Načtou se kartičky pro stránku pomocí odkazu  na metodu getCardData


  3. Zavolá se metoda pro vložení stránky, které se mimo pole kartiček  předají  i odkazy na metodu vyrábějící kartičky pro rubovou stránku a odpovídají metoda pro výpočet polohy kartiček na stránce (tedy odkazy na metody CreateForeignFlashcard a FrontNextPositionProvider)


  4. Totéž se provede i pro lícovou stránku, jen se předávají metody CreateNativeFlashcard a BackNextPositionProvider


  5. Metoda CreatePdf vrátí vytvořený Pfd dokument jako stream


Mimo výše uvedené jsou v projektu třídy pro získání dat z CSV souborů a ze sešitů Excel.





Třída FlashcardFonts



Tato třída obsahuje fonty používané pro vypsání textů na kartičky. Protože má aplikace fungovat na webu, je nutné šetřit systémové prostředky a tedy stačí vytvořit tyto fonty jen jednou a poté je sdílet pro všechna volání objektů třídy Creator a jejich metody CreatePdf – pro všechna tato volání se použije stejná instance.


Protože jsou kartičky vykreslovány pomocí fontu Arial, je tento font přidán do AppData webovské aplikace a ve tříde FlashcardFonts pak slouží pro inicializaci základního fontu, ze kterého pak výchází ostatní.








Web aplikace



Web aplikace využívá knihovnu pro tvorbu Pdf dokumentů. Je to MVC3 aplikace vytvořená ze šablony visual studia, obsahuje jen jeden controller nazvaný Home, který zobrazí jednoduchý formulář umožňující upload souboru:









  1. @using (Html.BeginForm("UploadCsv", "Home", FormMethod.Post, new { enctype = "multipart/form-data" }))

  2. {

  3.     <input type="file" name="file" id="file" />

  4.     <input type="submit" value="submit" />

  5. }





Data odeslaná tímto formulářem na server jsou zpracována v metodě UploadCsv:






  1. MemoryStream downloadStream;

  2. string fileName;

  3.  

  4. using (MemoryStream uploadedStream = new MemoryStream())

  5. {

  6.     Request.Files[0].InputStream.CopyTo(uploadedStream);

  7.     fileName = Path.GetFileNameWithoutExtension(Request.Files[0].FileName);

  8.     uploadedStream.Position = 0;

  9.  

  10.     using (CsvStreamReaderCardDataProvider provider = new CsvStreamReaderCardDataProvider(uploadedStream))

  11.     {

  12.         

  13.         Creator creator = new Creator();

  14.         downloadStream = creator.CreatePdf(provider.GetCardData, fonts);

  15.     }

  16. }

  17.  

  18. downloadStream.Position = 0;

  19.  

  20. return new FileStreamResult(downloadStream, "application/pdf")

  21. {

  22.     FileDownloadName = fileName + "_flashcard.pdf"

  23. };





Obsah odeslaného souboru je vložen do streamu (Request.Files[0].InputStream.CopyTo(uploadedStream);) , následně je tento stream čten pomocí CsvReaderu (ten je skryt v objektu provider)  pomocí objektu provider . Následně je zavolán Creator, jako zdroj údajů o kartičkách mu slouží metoda GetCardData objektu provider – objekt creator pak  vytvoří stream obsahující Pdf dokument. Ten je následně odeslán zpět uživateli jako application/pdf s názvem odpovídajíc šabloně <jméno souboru>_flashcard.pdf.





Sdílíme zdrojový kód –CodePlex



V příloze k tomuto článku už nenaleznete zdrojový kód popisovaného řešení – ten je nyní umístněn na CodePlex serveru a to na adrese http://flashcardcreator.codeplex.com. Zdrojový kód si můžete volně procházet přímo v prohlížeči, popřípadě stáhnout a upravit jak chcete:


image


Poznámka – co je to CodePlex?



Dle wikipedie: CodePlex je internetový projekt společnosti Microsoft určený k hostování otevřeného softwaru. CodePlex byl založen v květnu 2009. K repozitářům lze přistupovat pomocí verzovacích systémů Team Foundation Server nebo Subversion. Uživatelé mají dále k dispozici nástroje pro sledování požadavků, chyb, podporu RSS, statistiky, diskuzní fóra,, vlastní Wiki atd. Ač se většina zdejších projektů týká .NET Frameworku, včetně ASP.NET a Microsoft SharePointu, jsou zde projekty zabývající se SQL, WPF a Windows Forms a další.





Jdeme do oblak



Náš projekt z CodePlex můžeme vystavit přímo na web díky službě AppHarbor. Základní verze služby je zdarma a zprovoznění je za normálních podmínek otázkou několika minut. V nápovědě AppHarbor naleznete i postup, jak spojit CodePlex s vaší aplikací – a při každém commitu/check-inu zdrojového kódu dojde i k aktualizace aplikace na AppHarbor.


Zatím jsem se ale setkal s těmito problémy:



  • ne každý commit zdrojového kódu na CodePlex se promítne do AppHarborAppHarbor dělá build jednou za cca 14-15 minut – i tak je mezi aktualizací zdrojových kódů na CodePlex a aktualizací AppHarbor občas několikaminutová prodleva:





    CodePlex:


    image


    AppHarbor:


    image


    image


  • Vložil jsem souboru arial.ttf do složky App_Data, ale zapomněl jsem nastavit (přenastavit) ve vlastnostech Build Action na Content.  Což vedlo k tomu, že při lokálním spuštění vše fungovalo, ale na AppHarbor soubor fyzicky nebyl – trvalo mi dost dlouho, než jsem přišel na to, kde je chyba – uznávám, je to školácká chyba. Kdybych použil svůj osvědčený postup spočívající v samostatném webu na lokálním IIS a deploymentu z VS, tak bych na to přišel dříve.






























Nepopisuji podrobně, jak s oběma bezplatnými službami pracovat – obě požadují vytvoření účtu, což by nikomu nemělo činit problém a založení projektu na obou z nich je velmi jednoduché a přímočaré – v případě problémů existují diskuzní fóra i nápovědy u obou služeb.


Služba běží na adrese http://flashcard.apphb.com a základní rozhraní je velmi jednoduché:


image


Stačí vybrat soubor (ukázka souboru ve formátu UTF-8 CSV je přiložena jako slovicka.txt) a odeslat na server. Zpět vám přijde PDF dokument.


Závěr



Prosím mějte na paměti, že kód je pouze demonstrační.  Řetez událostí vypadá při použití CodePlex a AppHarbor takto: vývojář udělá změny a provede check-in změn do CodePlex. CodePlex upozorní AppHarbor na změnu, AppHarbor získá z CodePlex zdrojové kódy, zbuilduje je a nasadí.

Žádné komentáře:

Okomentovat