Vítejte na blog.vyvojar.cz Přihlásit | Registrovat | Pomoc

A opět do oblak …

Minulý rok v září uspořádal Microsoft konferenci BuildWindows. Zde mimo jiné zazněla i myšlenka, že budoucí vývoj pro Windows bude probíhat v HTML a javascriptu. Následovaly úvahy o budoucnosti .NETu, C#, jak budou aplikace vypadat apod. V tomto příspěvku nehodlám tyto úvahy rozvíjet – spíše ukážu, jak napsat aplikaci v HTML. I ta bude ale závislá na C# – web stránky jsou realizováný pomocí MVC technologie a celý backend generování kartiček je napsán v C#. Klientská část naproti tomu spoléhá zcela na javascript a vzhled stránkua je řešen pomocí CSS – samotná stránka tak obsahuje téměř čisté HTML.

Před pár týdny jsem zde popsal řešení pro tisk kartiček – nejprve jsem se věnoval tomu, jak generovat vlastní PDF dokumentu, v druhém pokračování pak přesunu tohoto generování do cloudu. Oba díly naleznete zde:

V tomto příspěvku popíši, jak vytvářet vlastní kartičky. Pomocí formuláře v prohlížeči se zadají jednotlivé hodnoty a ty se posléze odešlou na server a zpět dostanete PDF dokument. Podobných stránek jsou ale na webu mraky, takže zkusím odlišit se trochu funkčností – vytváření kartiček bude fungovat tak, že jen zadáte slovíčko a vyhledají se jak jeho překlad do češtiny, tak i přepis jeho výslovnosti a příklad použití. Tyto vyhledané údaje si budete moci upravit. Navíc se kartičky budou lokálně ukládat ve vašem prohlížeči, práci tedy budete moci kdykoliv přerušit  a opět v ní začít bez výslovného ukládání.

Jak získat údaje o slovíčku

Na webu je plno on line slovníků – já si vybral dva, WordReference EnCz Slovník a Oxford Advanced Learner’s Dictionary.  Pro jejich využíti je ale potřeba vyřešit tyto problémy:

  • jak je volat a předat jím slovo, které hledáme
  • jak je volat v rámci naší stránky – tedy cross-domain problém
  • jak vyhledat potřebné údaje v tom, co nám stránky vratí

                                                                      image    image

Volání

To je ta nejjednodušší část, v případě stránek Oxford slovníku stačí volat adresu  http://oald8.oxfordlearnersdictionaries.com/dictionary/ + dané slovo, v případě WordReference pak http://www.wordreference.com/encz/  + dané slovo;

Řešení Cross-domain omezení

Z jedné stránky není možné volat a získat obsah z jiné domény. Částečně je to řešeno tím, že lze volat a použít skript z jiné domény a pokud tedy dotyčný web umí vracet JSON data, není potřeba nic vymýšlet a lze použít přímo metod knihovny jQuery a Ajax volání. Nicméně  v našem případě žádná z vybraných stránek toto nepodporuje. Naštěstí i zde existuje řešení a to služba YQL společnosti Yahoo a knihovna jquery.xdomainajax.js.

YQL je vlastně web služba, která získá obsah stránky, upraví jej tak, aby jej bylo možné zaslat jako skript (rozuměj ve formátu Json) a tak jej pošle volajícímu:

image

Knihovna xdomainajax pak jen volání YQL obaluje tak, aby s ním bylo možné pracovat tak, jako s ajax voláním, takže skript, který slovník volá, vypadá stejně, jako pro normální ajax volání:

image

Vyhledání údajů

To je opět snadná část, využívám možností jQuery selektorů a vyhledám příslušné informace – vyhledávání pro oba zvolené slovníky lze najít v souboru search.js

 

Ukládání v prohlížeči

Zde si opět usnadním práci pomocí existující knihovny jStorage. Každou vytvořenou kartičku ukládám lokálně v prohlížeči.  Pro zobrazení je pak využíván formulář, který se případně, v případě že má uživatel zájem o PDF kartičky, odesílá na server. Využívá se zde tzv. nesouvislých indexů, které jsem popsal v příspěvku  ASP.NET MVC3–Binding příklad.

 

Možnosti exportu

Uživateli jsou nabízeny dvě možnosti, jak data dostat z prohlížeče “ven”. Buď si může data vyexportovat jako csv soubor a nebo je odeslat na server a získat PDF dokument. Řešení popsané v příspěvku Vzhůru do oblak bylo ještě rozšířeno o možnost přijímat data z formuláře.

 

Postup práce s aplikací

Prosím mějte na paměti, že aplikace je jen příklad, není to žádné mistrovské dílo a má spíše jen demonstrovat, že lze vyvinout v HTML aplikaci s komfortem desktopové aplikace. Základní rozložení je velmi jednoduché, aplikace má tři hlavní části – v horní se zadává hledané slovíčko, prostřední slouží pro náhled a editaci kartičky a dole je seznam kartiček:

 

image

 

Do horního políčka napíšete slovo, pro které chcete vytvořit kartičku a stisknete Enter – ikonky slovníků se změní na dobu, kdy se slovo vyhledává a signalizují tak probíhající činnost:

image

Jakmile jsou údaje nalezeny, vyplní se kartičky nalezenými údaji:

image

Je možné na kterýkoliv údaj kliknout a změnit jej:

image

Jakmile jste s kartičkou spokojeni, klikněte na tlačítko Ulož a slovíčko se objeví v seznamu:

image

  • Kliknutím na kterékoliv slovíčko v seznamu se kartička vyplní jeho údaji a je možné kartičku aktualizovat či odstranit.
  • Práci můžete kdykoliv přerušit a prohlížeč zavřít – všechna data jsou uložena automaticky a při opětovném zavolání stránky se tato obnoví i se seznamem.
  • Výsledky své práce můžete uložit jak ve formátu CSV, tak si i nechat zaslat slovíčka v PDF dokumentu jako kartičky.

Další zajímavosti a popis řešení

Pro správu skriptů a css soubory je použita knihovna Cassette, kterou jsem ve stručnosti popsal v příspěvku ASP.NET– kazeta stále žije…...

Každá část obrazovky má svůj vlastní css styl:

  • inputBox.css – styly pro vrchní vyhledávací box
  • card.css – styly pro kartičky
  • cards.css – styly pro seznam kartiček
  • basicLayout.css – styly pro obecný vzhled stránky, tj. umístnění hlavičky, patičky apod.
  • site.css – styl dodaný MS

Všimněte si, že dva poslední styly nejsou přímo odkazovány ani ve view, ano v layoutu – je na ne odkaz v prvních třech souborech:

image

Funkčnost na straně klienta je také rozdělena do několika javascriptů:

  • buttonEvents.js – co se provede po kliku na tlačítka a také co se má provést po načtení stránky a vytvoření nové kartičky v seznamu (createEntry)
  • flashcard.js – je zde metoda pro export do csv (exportData),  testování, zda podobná kartička již neexistuje (isCardAlreadyStored) , vypsání seznamu uložených kartiček (listCard), vyplnění kartičky (fillCard) a také seznam operací, které se provedou, pokud vyhledáváte nové slovíčko (submitEdit)
  • search.js – dvě metody pro vyhledávání ve zvolených slovnících, pro každý slovník jedna

Jak pro skripty, tak i pro styly je použit bundle.txt soubor, který upřesňuje řazení skriptu a stylů.

Mimo tito skripty jsou použity tyto knihovny:

  • jQuery – netřeba představovat
  • jQuery.editable.js – umožňuje editaci na stránce po kliknutí, použití je velmi jednoduché,
  • jQuery.xdomainajax.js – umožňuje snadné použíti YQL, volá se jako bežné Ajax volání
  • jStorage.js – sjednocuje práci s lokálním uložištěm (HTML 5)

Aplikace funguje správně jen v posledních verzích prohlížečů Chrome, Opera, Firefox a asi i v IE (zde nefunguje export do csv). Cílem nebylo ani tak nabídnout naprostou funkčnost, spíše ukázat, jak udělat funkční aplikaci čistě v prohlížeči – i serverová podpora je omezena na generování PDF dokumentů, které nelze z asi pochopitelných důvodů generovat v prohlížeči (a nebo o tom zatím nevím).

Zdrojový kód

Aplikace běží na adrese http://flashcard.apphb.com/ a zdrojové kódy jsou k prohlédnutí na CodePlex. Komentáře v kódu moc nejsou, snažil jsem se vše popsat v tomto článku.

Posted by mstr | 10 Comments
Vedeno pod: , , , , ,

ASP.NET– kazeta stále žije…..

Nadpis je asi trochu matoucí  a  snad na vysvětlenou pro ty mladší  by bylo dobré zmínit, že kdysi dávno se hudba nekopírovala pomocí mp3, ale existovaly analogové kazety. Kopírování bylo zdlouhavé a s každou kopií klesala kvalita. Názvem tohoto média se inspiroval autor zajímavé knihovny(nepátral jsem proč a doufám, že to neznamená, že kvalita knihovny bude s každou verzí klesat).

Knihovnu  nalezenete na stránkách http://getcassette.net/.  Může vám pomoci při vývoji WebForms a nebo MVC webů (já ji použil pro zatím jeden malý projekt v MVC).

Řeší problém se správou skriptů a stylů –pro snazší údržbu a vývoj je vhodné mít styly uložené ve více css souborech, podobně i skripty. Jenže čím více toho musí prohlížeč pro zobrazení stránek stáhnout, tím je celková odezva stránek pomalejší a zátěž serveru a využití internetového spojení vyšší. A přesně tento problém Cassette řeší – při vývoji a ladění webu  zachovává malé soubory – můžete tedy snadno využívat klientských ladících nástrojů například ve Firefoxu, ostrá verze pak obsahuje soubory jak sloučené, tak minimalizované.

V případě MVC pak navíc nemusí stránky(views) obsahovat spousty tagů se skript soubory či linky na css. Stačí jen deklarovat, které soubory jsou pro dané View potřeba:

  1. @{
  2.     ViewBag.Title = "Index";
  3.  
  4.     Bundles.Reference("Scripts/Cards/export.js");
  5.     Bundles.Reference("Scripts/Cards/flashcard.js");
  6.     Bundles.Reference("Scripts/Cards/search.js");
  7.     Bundles.Reference("Content/inputBox.css");
  8.     Bundles.Reference("Content/card.css");
  9.     Bundles.Reference("Content/cards.css");
  10. }
  11. <div id="inputBox">

V MasterView souboru pak stačí na příslušných místech, kde mají byt tagy, zavolat:

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4.     <title>@ViewBag.Title</title>
  5.     @Bundles.RenderScripts()
  6.     @Bundles.RenderStylesheets()
  7. </head>
  8. <body>
  9.     <div class="wrapper">

Všimněte si tak=, že nemusíte přímo odkazovat na jQuery knihovnu – ve skriptech, které si vytvoříte se stačí na tuto knihovnu odkázat a bude tak zahrnuta do vašich skriptů:

image

(ten odkaz by jste tam dávali asi tak jako tak, díky tomu vám začne fungovat intelliSense nápověda pro JQuery metody).

Totéž platí i o stylech, i  v nich můžete napsat odkaz na “nadřízený” styl:

image

Nyní můžeme v případě, že svůj web ladíme, získat všechny soubory:

image

Celková velikost jen skriptů je přes 600kB:

image

Naopak jakmile vývoj dokončíme a web nasadíme, začne nám Cassette soubory slučovat a minimalizovat:

image

Například JavaScripty jsou nyní jen v jednom souboru, který má velikost téměř sedmkrát menší:


image

Na webu autora knihovny  a nebo fórech pak lze nalézt další tipy, třeba jak vypnout minimalizaci:

 

  1. bundles.Add<ScriptBundle>("Scripts", b => b.Processor = new ScriptPipeline().Remove<MinifyAssets>());

 

Další informace o této knihovně jsou k dispozici na webu knihovny, popřípadě na tomto blogu. Instalace je jednoduchá, základní použití je popsáno na webu autora.

Posted by mstr | 10 Comments
Vedeno pod: , , , ,

ASP.NET MVC3–Binding příklad

V tomto příspěvku popíši, jak namapovat data pomocí “nesouvislých indexů” – ony se ty anglické výrazy špatně překládají, respektive nejsem jazykový odborník.

Problém

Představte si, že máte k dispozici seznam telefonních čísle a jmen a chcete je zobrazit uživateli, který pomocí zaškrtávátka určí, které z nich chce vložit do svého seznamu kontaktů. Běžně se tento problém řeší tak, že se uvažuje, že každý záznam má své ID (identifkátor).

Ale zkusme vyřešit situaci, kdy prostě žádné ID není k dipozici a nebo ho použít nechceme, tedy třída vypadá takto:

  1. public class Contact
  2. {
  3.     public string Name { get; set; }
  4.     public string Phone { get; set; }
  5. }

Zdroj dat nám nahradí tento kód:

  1. public static IEnumerable<Contact> Get()
  2. {
  3.     return new Contact[] {
  4.         new Contact() {Name= "Pepa", Phone= "123456789"},
  5.         new Contact() {Name= "Petr", Phone=  "987654321"},
  6.         new Contact() {Name= "Lopuch", Phone= "456789123"}
  7.     }.AsEnumerable();
  8. }

Kontroler není složitý:

  1. public class HomeController : Controller
  2. {
  3.     [HttpGet]
  4.     public ActionResult Index()
  5.     {
  6.         return View(Contact.Get());
  7.     }
  8. }

A View Index pak vypadá takto:

  1. @model IEnumerable<Contact>
  2. @{
  3.     ViewBag.Title = "Index";
  4. }
  5. <h2>Import - contacts</h2>
  6.  
  7. @using (Html.BeginForm())
  8. {
  9.     foreach (var contact in Model)
  10.     {
  11.     <input type="checkbox" name="index" value="@contact.GetHashCode()" /> @string.Format("{0}-{1}", contact.Name, contact.Phone) <br />
  12.     <input type="hidden" name="@string.Format("[{0}].Name", contact.GetHashCode())" value="@contact.Name" />
  13.     <input type="hidden" name="@string.Format("[{0}].Phone", contact.GetHashCode())" value="@contact.Phone" />
  14.     }
  15.     <input type="submit" value="send" />
  16. }

 

Metoda kontrolleru, který bude zpracovávat přijímaná data, pak vypadá takto:

  1. [HttpPost]
  2. public ActionResult Index(Contact[] contacts)
  3. {
  4.     return View("IndexPosted", contacts);
  5. }

 

a ve View IndexPosted vypisujeme přijaté kontakty pomocí jednoduchého kódu:

  1. <h2>Posted contects</h2>
  2. @foreach (var contact in Model)
  3. {
  4.      @string.Format("{0}-{1}", contact.Name, contact.Phone) <br />
  5. }

A takto se to chová:

imagea po odeslání stlačením tlačítka Send: image

Závěr

Veškeré kouzlo spočívá v checkboxu pojmenovaném index. Hodnota checkboxu je na server odeslána pouze v případě, že je zaškrtnut. MVC3 framework na serveru se pak snaží vytvořit kolekci objektů třídy  Contact (viz cílová akce formuláře Index(Contact[]  contacts)) – a hodnutu index použivá pro nalezení hodnot jednotlivých objektů.

Samozřejmě by se dal tento jednoduchý příklad řešit i jinak, ale všimněte si některých výhod tohoto řešení:

  • uživateli neposíláme žádné ID a Hashcode, který ho do jisté míry nahrazuje, je naprosto umělá hodnota.
  • Metodu pro zpracování dat do formuláře můžeme použít i pro zpracování normálního formuláře
  • díky tomu, ze využíváme model zpracování MVC frameworku, došlo by i k oveření všech podmínek, které bychom pro model případně definovali.

Celý zdrojový kód projektu BindingExample je dostupný na CodePlex – ale prakticky vše je uvedeno v tomto článku.

Poznámka na konec – index se nemusí jmenovat jen index, ale můžeme používat i prefixů při řešení složitějších případů:

  1. @Html.CheckBox("phones.index", new { value = contact.GetHashCode() })
  2.     @Html.Hidden(string.Format("phones[{0}].Name", contact.GetHashCode()), contact.Name)
Posted by mstr | 11 Comments

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í.

Posted by mstr | 6 Comments

Attachment(s): Slovicka.txt

Víkendová rychlovka–tisk kartiček

Mé školou povinné děti se učí angličtinu. Což znamená učit se slovíčka. Mají slovníčky, jenže ty mají některé nevýhody – například se často naučíte slovíčka v nechtěném kontextu ostatních slov, tedy nakonec je umíte pěkně vyjmenovat v řadě, ale otázka na nahodilé slovíčko zůstane bez odpovědi. A nebo se špatně zvýrazňují obtížná slovíčka, aby jste si je mohli častěji opakovat.

 

Jistou naději jsem vkládal do elektronických pomůcek  - jenže chytrý mobil sloužil všemu možnému, jen ne na opakovaní slovíček a tak putoval pryč. Jedinou možností se tak jeví klasické papírové kartičky – jenže jejich příprava bere čas, obzvláště pokud by jste si je měli sami psát. Nutné je také zmínit, že na rozdíl od aplikaci jako AnyMemo jsou kartičky zcela tiché a slovíčko vám tedy nepřečtou.

 

Dal jsem tedy dohromady slovíčka na listu v sešitě Excel, stáhnul knihovnu iTextSharp a udělal aplikaci, která si údaje z Excelu načte a vytvoří PDF s kartičkami – ty stačí pak již jen vytisknout, rozstříhat a učit se, učit se, učit se (© Lenin).

 

Excel Sheet jako vstup

Formát informací je jednoduchý – slovíčko, přepis výslovnosti, příklad použití, český význam a překlad příkladu – vše na listě s názvem Sheet1 (pokud se list jmenuje jinak, musí se upravit příkaz pro načtení v programu) . Excel sheet pak vypadá  takto:

image

Poznámka – lze samozřejmě použít i jiný formát, ale musí se pak upravit aplikace – není to těžké. Použit je Excel 2010.

 

iTextSharp

Tuhle levnou, protože zadarmo, knihovnu na tvorbu PDF naleznete na této adrese. Trochu času zabralo přijít na nejlepší a nejjednodušší způsob vykreslení kartiček – nakonec každou kartičku vykresluji jako tabulku na určité pozici. Výhodou tabulek je možnost určit jak font použitý na obsah buňky, tak i pozadí buňky, její velikost a ohraničení. Obsah lze i otočit a lze tak dosáhnout možnosti přetáčet kartičky po delší straně s obráceným textem.

Vzhledem k tomu, že jsme v Evropě, tiskne se na papír A4, s okrajem 10mm (výsledné pédéefko by tedy mělo jít vytisknout na jakékoliv tiskárně):

image

Vlastní aplikace je velmi jednoduchá – otevře se Excel soubor a z něj se načítají data – to se odehrává v metodě ReadAndPrintExcelSheet. Ta načítá údaje ze sešitu a vytváří jednoduché objekty pro obě strany kartiček (objekty tříd ForeignFlashcardContent a NativeFlashcardContent). Ty pak vloží do pole na správné místo (aby při tisku na obě strany se obě strany kartiček správně “slicovaly”). Jakmile je načten dostatečný počet kartiček pro tisk stránky (v mém případě se tiskne 8 řádků po 4 sloupcích, tedy dohromady 32 kartiček), zavolá se vlastní “tisk” do PDF. Pro každý načtený údaj zhotovena kartička (metody  CreateForeignFlashcard a CreateNativeFlashcard) a ta je umístněna na stránku (metoda PrintFlashCardPage).  A to je vše.

Abych nemusel počítat body při generování PDF, je zde metoda mmToDots, která zadanou míru v milimetrech převede na počet bodů, se kterým metody iTextSharp knihovny pracují.

Abych mohl vykreslovat speciální znaky (hlavně u výslovnosti), používám jaké základ všech písem v dokumentu font Arial:

BaseFont baseFont = iTextSharp.text.pdf.BaseFont.CreateFont(@"C:/Windows/Fonts/ARIAL.TTF", BaseFont.IDENTITY_H, iTextSharp.text.pdf.BaseFont.EMBEDDED);

Celý kód je napsán prakticky jen ve statických metodách – nehledejte tam žádné architektonické kouzla a čáry, je to prostě jednoduchá a jednoúčelová aplikace, která ovšem udělá co má: kartičky.

Umístnění vstupního Excel souboru je v app.config – celý program lze samozřejmě upravit na čtení jména souboru z příkazové řádky apod. – každopádně nezapomeňte :

app.config
  1. <?xml version="1.0" encoding="utf-8" ?>
  2. <configuration>
  3.     <connectionStrings>
  4.         <add name="Excel" connectionString="Dsn=Excel Files;dbq=D:\Slovicka.xlsx;driverid=790;maxbuffersize=2048;pagetimeout=5" providerName="System.Data.Odbc" />
  5.     </connectionStrings>
  6. </configuration>

a výstupní Flashcards.pdf pak naleznete ve svých dokumentech díky tomuto příkazu:

  1. PdfWriter writer = PdfWriter.GetInstance(pdfDocument,
  2.                                          new FileStream(
  3.                                             Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments),
  4.                                                         "Flashcards.pdf"),
  5.                                             FileMode.Create));


A toto je “hardwarový” výstup celého programu:

IMG_7305

 

Zdrojový kód je  přiložen včetně použitého xlsx souboru v balíčku zip.

Posted by mstr | 6 Comments
Vedeno pod: ,

Attachment(s): Flashcards.zip

“Vyber vše” aneb kombinace ASP.NET MVC a jQuery

V tomto článku popíši možné řešení zadání, podle kterého se má uživateli zobrazit seznam možností ve formě zaškrtávacího políčka, přičemž jedno ze zaškrtávacích políček má sloužit jako “Vyber vše” – pokud jej uživatel zaškrtne, zaškrtnou se všechny možnosti, pokud zaškrtnutí zruší, zruší se i všechny možnosti:

 image     image     image

Řešení podobného zadání jsou na netu mraky – takže k tomuto mraku přispěji dalším obláčkem.  Aby řešení bylo co nejvíce univerzální, stanovil jsem si tyto podmínky:

  • Všechny ovládané zaškrtávátka musí být uzavřeny do elementu <fieldset>
  • První zaškrtávátko slouží pro výběr všeho.

Obecné Html a javacript

Html kód splňující výše uvedené podmínky  pak vypadá takto:

<fieldset>
    <input type="checkbox" name="all" /><label for="all">Vyber ve</label><br /><br/>
    <input type="checkbox" name="praha" /><label for="praha">Praha</label><br />
    <input type="checkbox" name="brno" /><label for="brno">Brno</label><br />
    <input type="checkbox" name="ostrava" /><label for="ostrava">Ostrava</label>
</fieldset>

a odpovídající jQuery javascript pak takto:

selectAllCheckbox.js
  1. jQuery(function () {
  2.     jQuery('fieldset :checkbox').click(function () {
  3.         var $checkboxes = jQuery(this).closest('fieldset').find(':checkbox');
  4.         if ($checkboxes.index(this) == 0) {
  5.             $checkboxes.not(this).attr('checked', this.checked);
  6.         }
  7.         else {
  8.             $checkboxes.setSelectAllCheckbox();
  9.         }
  10.     });
  11.  
  12.     jQuery('fieldset').each(function () {
  13.         jQuery(this).find(':checkbox').setSelectAllCheckbox();
  14.     });
  15. })
  16.  
  17. jQuery.fn.setSelectAllCheckbox = function SetSelectAll() {
  18.     var first = this.first();
  19.     jQuery(first).attr('checked', this.not(first).filter(':checked').length == (this.length - 1));
  20. }

Metoda na řádcích 17.-20. nastaví předané ovládací zaškrtávátko buď do stavu zaškrtnuto a nebo nezaškrtnuto podle stavu ostatní zaškrtávátka (ovládací zaškrtávátko bude zaškrtnuto, pokud všechna ostatní jsou zaškrtnuta.

Kód na řádcích 2-10 zajišťuje, že při každém kliknutí na zaškrtávátko umístněné v elementu <fieldset> je zjištěno, zda se jedná o první zaškrtávátko v daném elementu. Pokud ano, nastaví se všechna ostatní zaškrtávátka podle stavu právě kliknutého (řádek 5.). Pokud ne, provede se funkce na řádcích 17. – 20.

Kód na řádcích 12. 15. slouží pro správná nastavení po načtení stránky do prohlížeče.

Upozorňuji, že kód není dokonalý, například neřeší důsledně situace, kdy je do sebe zanořeno více elementů fieldset.

Html helper method

Element <fieldset> nemusím generovat ručně. Můžeme použít rozšíření HtmlHelperu a použít tuto konstrukci v našem view. Tvorbu jednoduchých rozšíření nemá asi cenu popisovat, proto rovnou popíši něco o stupeň složitější, tedy konstrukci podobnou @using(Html.BeginForm….):

@using (Html.BeginSelectAllCheckbox())
{
    <input type="checkbox" name="all" /><label for="all">Vyber ve</label><br />
    @Html.CheckBox("Praha")<label>Praha</label><br />
    @Html.CheckBox("Brno")<label>Brno</label><br />
    @Html.CheckBox("Ostrava")<label>Ostrava</label>
}

Upozornění - je mi jasné, že v tomto případě se může hloubavému čtenáři zdát, že jde o přístup “s kanónem na vrabce”, ale chci na tomto jednoduchém příkladu demonstrovat tvorbu vlastních rozšíření – v některém z dalších příspěvků chci popsat složitější řešení a není tedy na škodu  začít jednoduchým příkladem.

Metoda BeginSelectAllCheckbox je pak statické metoda statické třídy, která vypadá takto:

public static class HelperExtension
    {
        public static IDisposable BeginSelectAllCheckbox(this HtmlHelper helper )
        {
            Action begin = () => {helper.ViewContext.Writer.Write("<fieldset>");};
            Action end = () =>    {helper.ViewContext.Writer.Write("</fieldset>");};

            return new DisposableHelper(begin, end);
        }
        
        private class DisposableHelper : IDisposable
        {
            private readonly Action end;

            public DisposableHelper(Action begin, Action end)
            {
                this.end = end;
                begin();
            }

            public void Dispose()
            {
                end();
            }
        }
    }

Tvorba vlastních rozšíření HtmlHelperu objektu v MVC tedy není složitá a princip je doufám z kódu patrný – díky using dojde kzavolání Dispose metody, která provede závěrečnou akci – zavolá metodu, kterou jsme při vytváření třídy definovali pomocí delegáta Action.

Přiložen je i ukázkový MVC projekt s veškerým kódem.

Posted by mstr | 5 Comments
Vedeno pod: , , , , ,

Attachment(s): DEMO_Select_All.zip

MVC3: odkaz v textu

MVC3 nabízí tuto metodu, pokud chceme na stránce vypsat odkaz:

@Html.ActionLink("klikni zde","Index")

Co ale dělat, pokud chceme vypsat odkaz jako součást delšího text, například:

Nejste registrován v našem systému, prosím klikněte zde a zaregistrujte se.

 

Řešením může byt rozdělení textu na části:

<p>Nejste registrován v našem systému, prosím klikněte @Html.ActionLink("zde","Index") a zaregistrujte se.</p>

To je fungující řešení – problém s ním ale nastane v okamžiku, kdy začneme stránku lokalizovat a texty umístíme do resource souborů – obvykle se pak celý text rozdělí na části:

image

a z nich se pak generuje stránka:

<p>@Resources.FirstPart @Html.ActionLink("zde","Index") @Resources.SecondPart</p>

 

To ale není příliš pěkné – vznikají zbytečné klíče. Spíše by bylo lepší mít toto:

image

Pokud ale zapíšeme tento kód:

<p>@string.Format(Resources.Registrace, Html.ActionLink("zde","Index"))</p>

objeví se nám na stránce:

image

Což není co bychom asi chtěli. Správný zápis totiž je:

<p>@Html.Raw(string.Format(Html.Encode(Resources.Registrace), Html.ActionLink("zde","Index")))</p>
A dostaneme:
image

Vysvětlení:

  • Html.Encode použijeme, abychom správně zakodovali HTML značky v původním textu, tedy aby resource mohl například obsahovat text “Musí být <> od 0”.
  • Html.Raw pak brání dalšímu kódování a text je vypsán tak jak je – nedojde tak k Html kodování vloženého odkazu.


                    
				            
Posted by mstr | 5 Comments
Vedeno pod: , , ,

CSS sprity a seznamy

Ač nerad, přeci jen jsem občas dotlačen k práci na uživatelském rozhraní :-). V rámci jednoho webového projektu vznikl požadavek použít CSS sprity  místo odrážek.

Pokud někdo neví, co jsou to CSS sprity, tak snad pomůže tento odkaz.

Přímé použití v seznamech je ale, jak jsem posléze zjistil, problematické. Prvek seznamu je obvykle větší než obrázek ve spritu a tak je často zobrazeno ze spritu více, než by bylo vhodné. Ani můj oblíbený stackoverflow mi nenabídl řešení – většina příspěvků, co jsem našel, radila buď předělat sprite tak, aby obsahoval obrázky jen vertikálně  a nebo  navrhovala použít element div jako kontejneru.

Pouze vertikální uspořádání spritu nebylo možné – sprite byl dodán a nebylo možné jej měnit. Použítí divu se mi moc nelíbílo, kód pak vypadal tak nějak divně:

<ul>
<li class="menu">
<div class="saveIcon"></div>Save
</li>
</ul>

Mým cílem je ale mnohem obyčejnější zápis:

<ul class="menu">
<li class="save">save</li>
<li class="edit">edit</li>
</ul>
a při použití jednoduchého spritu:
sprite_example

 

dosáhnout tohoto vzhledu:

image

o vše se pak postará tento css kód:

ul.menu
{
list-style: none outside none;
padding: 0;
margin: 0;
font-weight:bold;
}

ul.menu li
{
display: inline-block;
margin-left: 20px;
}

ul.menu li:before
{
content:url(content/empty_space.gif);
background-image: url(Content/sprite_example.jpg);
background-repeat: no-repeat;
margin-right:5px;
display: inline-block;
}

li.save:before
{
background-position: 0px -53px;
}

li.edit:before
{
background-position: -45px -53px;
}

Finta spočívá v použítí průhledného obrázku s rozměry odpovídajícími ikoně ze spritu. Ostatní již odpovídá běžnému postupu při používání Css spritu.

Následně je i velmi jednoduché změnit obrázek při najetí myší, stačí doplnit tento styl:

li.save:hover:before
{
background-position: 0px 0px;
}
a výsledkem je:
image Přiložena je i jednoduchá stránka s obrázky a css stylem.
Posted by mstr | 0 Comments
Vedeno pod:

Attachment(s): CssSpriteList.zip

Visual Studio – seskupení souborů

Jeden kamarád si prohlížel jeden z mých projektů a zaujalo ho, že v Solution Explorer vidí některé soubory pěkně sloučené do skupinky. Asi takhle:

image

A chtěl vědět, jak jsem toho docilil. Je to pomocí zajímavé knihovničky VSCommands, kterou naleznete na adrese http://vscommands.com/.

Je k dispozici ve dvou verzích, placené a neplacené. Neplacené verze má některé omezení, ale výše uvedené zvládne.

Posted by mstr | 9 Comments

DRY – databáze podruhé

V první části jsem ukázal, jak zjednodušit a centralizovat práci s databázi. V tomto díle zajdu ještě dál a kód se stane ještě více obecnějším.

Connection string

Informaci o tom, jak se připojit k databázi máme nyní přímo v kódu:

using (SqlConnection connection = new SqlConnection(@"Data Source=.\SQLEXPRESS;Initial Catalog=Names;Integrated Security=True;Pooling=False"))

 To není úplně ideální, neboť případná změna znamená zásah do kódu. Lepším řešením je přesunout tuto informaci do configuračního souboru (App.config). Změna tak znamená jen přepsání textové informace.

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<connectionStrings>
<add name="NamesDatabase" connectionString="Data Source=.\SQLEXPRESS;Initial Catalog=Names;Integrated Security=True;Pooling=False"/>
</connectionStrings>
</configuration>

 a kód upravíme takto:

using (SqlConnection connection = new SqlConnection(ConfigurationManager.ConnectionStrings["NamesDatabase"].ConnectionString))

 Změna databáze

Kód je teď více univerzální – můžeme změnit databázi, se kterou chceme pracovat. Ale co když nechceme použít MS SQL databázi, ale třeba SqLite?
Na první pohled se zdá, že budeme muset změnit kód a místo vytváření SqlConnection vytvořit objekt třídy SQLiteConnection:
 
using (SQLiteConnection connection = new SQLiteConnection(ConfigurationManager.ConnectionStrings["NamesDatabase"].ConnectionString))

a  podobné změny provést i pro command atd.

A pravděpodobně skončíme s několika metodami, jednou pro každý typ databáze:

static void WorkWithSQLDatabase(Action<SqlCommand> processCommand)
{
using (SqlConnection connection = new SqlConnection(ConfigurationManager.ConnectionStrings["NamesDatabase"].ConnectionString))
{
connection.Open();

using (SqlCommand command = connection.CreateCommand())
{
processCommand(command);
}
}
}
static void WorkWithSQLITEDatabase(Action<SQLiteCommand> processCommand)
{

using (SQLiteConnection connection = new SQLiteConnection(ConfigurationManager.ConnectionStrings["NamesDatabase"].ConnectionString))
{
connection.Open();

using (SQLiteCommand command = connection.CreateCommand())
{
processCommand(command);
}
}
}

A jak je asi jasné, zase opakujeme kód, jen s mírnými změnami. Jak to napravit?
 
Obě třídy, SqlConnection a SQLiteConnection, jsou potomky abstraktní třídy DbConnection. Stačí tedy celý kód upravit takto:
 
static void WorkWithDatabase(Action<DbCommand> processCommand)
{
using (DbConnection connection = new SqlConnection(ConfigurationManager.ConnectionStrings["NamesDatabase"].ConnectionString))
{
connection.Open();

using (DbCommand command = connection.CreateCommand())
{
processCommand(command);
}
}
}

a jediným problémem, který nyní máme je jak vytvořit konkrétní objekt třídy DbConnection – tedy musíme napsat kód, který rozliší s jakou databázi chceme pracovat a následně buď zavolá: new SqlConnection a nebo new SQLiteConnection.

Tento úkol často vede k řešení, kdy předáváme nějaký textový parameter a na základě jeho hodnoty rozhodujeme, který objekt vytvoříme.  Kód pak vypadá nějak takto:

if (databasename = "SQLite")
connection = new SQLiteConnection();

if (databasename = "MSSQL")
connection = new SqlConnection();

Vítejte v továrně

Předchozí kód není pěkný. Naštěstí MS na toto myslel a používá návrhový vzor Factory přesně pro tuto příležitost – v ADO.NET máme k dispozici DbProviderFactory. Tato továrna umí vyrobit objekt třídy, kterou určíme – zbývá tady jen vyřešit, kam informaci o tom, který objekt chceme vytvořit, uložíme. A pokud jsme dokázali uložit connectionString do konfiguračního souboru, bylo by  nejlepší tuto informaci uložit tamtéž.

V uzlu  connectionStringu v App.config máme možnost definovat i další atributy. Jedním z nich je i providerName. Tento atribut můžeme použít i v našem kódu a ve spolupráci s DbProviderFactory ziskat objekt té správné třídy:

public static void WorkWithDatabase(string connectionName, Action<DbCommand> processCommand)
{
string providerName = ConfigurationManager.ConnectionStrings[connectionName].ProviderName;
string connectionString = ConfigurationManager.ConnectionStrings[connectionName].ConnectionString;

DbProviderFactory factory = DbProviderFactories.GetFactory(providerName);

using (DbConnection connection = factory.CreateConnection())
{
connection.ConnectionString = connectionString;
connection.Open();

using (DbCommand command = connection.CreateCommand())
{
processCommand(command);
}
}
}

Konfigurační soubor App.Config musí pak obsahovat:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<connectionStrings>
<add name="NamesDatabase" connectionString="Data Source=.\SQLEXPRESS;Initial Catalog=Names;Integrated Security=True;Pooling=False" providerName="System.Data.SqlClient"/>
</connectionStrings>
</configuration>
 
A naše hlavní metoda se pak změní takto:
static void Main(string[] args)
{
int id = default(int);
string name = string.Empty;
string commandText = string.Empty;

Action<DbCommand> find = (DbCommand command) =>
{
command.CommandText = commandText;
DbParameter dbParameter;
dbParameter = command.CreateParameter();
dbParameter.ParameterName = "@Name";
dbParameter.Value = name;
command.Parameters.Add(dbParameter);

using (DbDataReader reader = command.ExecuteReader())
{
if (!reader.HasRows)
Console.WriteLine("Sorry, nothing found");

while (reader.Read())
Console.WriteLine(reader.GetValue(0));
}
};

Console.WriteLine("Enter Id to search for:"); id = int.Parse(Console.ReadLine());
name = id.ToString();
commandText = "SELECT Name FROM Names WHERE Id=@Name";
WorkWithDatabase("NamesDatabase", find);

Console.WriteLine("Enter name to search for:"); name = Console.ReadLine();
commandText = "SELECT Id FROM Names WHERE Name=@Name";
WorkWithDatabase("NamesDatabase", find);

Console.WriteLine("Thanks for using."); Console.ReadLine();
}

 Závěr

Ukázali jsme si, jak využít konfigurační soubor k uložení informací, které můžeme změnit bez zásahu do kódu a jak napsat obecný kód, který tyto informace bude umět zpracovat. Další vylepšení budou ukázány v pokračování.

Posted by mstr | 1 Comments

Jak na DRY – databáze poprvé

Pokud někoho zkratka DRY mate, tak je to zkrácenina anglického  Don’t Repeat Yourself – neboli “neopakujte se”.  Samozřejmě je to ideál a nejde ho vždy a všude dosáhnout, v následujícím článku chci jen ukázat, jak toho docílit v případě práce s databázemi. Dále je ukázáno použít anonymní metody a využití delegátů, konkrétně generického Action<T>.

Velmi jednoduchý úkol

Máme jednoduchou databázi s jednou tabulkou, která má všehovšudy jen dva sloupce – Id a Name . A potřebujeme napsat aplikaci, která buď na základě zadaného identifikátoru (tj. hodnoty sloupce id) vyhledá jméno  a nebo naopak, na základě zadaného jména vyhledá identifikátor. Nic složitého.

Naše aplikace bude vypadat takto:

static void Main(string[] args)

{

Console.WriteLine("Enter Id to search for:"); int id = int.Parse(Console.ReadLine());

using (SqlConnection connection = new SqlConnection(@"Data Source=.\SQLEXPRESS;Initial Catalog=Names;Integrated Security=True;Pooling=False"))

{

connection.Open();

using (SqlCommand command = connection.CreateCommand())

{

command.CommandText = "SELECT Name FROM Names WHERE Id=" + id.ToString();

using (SqlDataReader reader = command.ExecuteReader())

{

if (!reader.HasRows)

Console.WriteLine("Sorry, no name found");


while (reader.Read())

Console.WriteLine(reader.GetString(0));

}

}

}

Console.WriteLine("Enter name to search for:"); string name = Console.ReadLine();

using (SqlConnection connection = new SqlConnection(@"Data Source=.\SQLEXPRESS;Initial Catalog=Names;Integrated Security=True;Pooling=False"))

{

connection.Open();

using (SqlCommand command = connection.CreateCommand())

{

command.CommandText = "SELECT Id FROM Names WHERE Name='" + name + "'";


using (SqlDataReader reader = command.ExecuteReader())

{

if (!reader.HasRows)

Console.WriteLine("Sorry, no id found");

while (reader.Read())

Console.WriteLine(reader.GetInt32(0));

}

}

}

Console.WriteLine("Thanks for using."); Console.ReadLine();

}


varování: skládání SQL dotazu, tj. použití vstupu od uživatele, nikdy nepoužívejte!

v kódu také  není nijak řešeno zachycení chyb, ošetření vstupu od uživatele a podobně.

Zjednodušujeme

Pokud se na kód pořádně podíváme, zjistíme, že obsahuje na pohled dvakrát stejný kód – vytvoření SqlConnection, vytvoření  SqlCommand a následně zpracování vrácených záznamů. Přitom kód je praktický stejný až do místa, kde definujeme CommandText.

Zkusme tedy společný kód vyčlenit do samostatné metody a využít delegáty – delegát je odkaz na metodu. Protože již víme, že se náš kód liší až od místa, kde začínáme pracovat s objektem třídy SqlCommand, potřebujeme odkaz na metodu, která bude  jako parametr akceptovat objekt třídy SqlCommand.
 
Nejprve tedy vytvoříme metodu pro základní práci s databází:
 
static void WorkWithDatabase(Action<SqlCommand> processCommand) 
{
using (SqlConnection connection = new SqlConnection(@"Data Source=.\SQLEXPRESS;Initial Catalog=Names;Integrated Security=True;Pooling=False"))
{
connection.Open();

using (SqlCommand command = connection.CreateCommand())
{
processCommand(command);
}
}
}

V hlavní metodě nyní použijeme anonymní metodu pro práci s objektem třídy SqlCommand (metodu jsem zjednodušil a upravil tak, aby byla použitelná pro oba typy vyhledávání. Samozřejmě je možné vytvořit více anonymních metod apod):
 
 

static void Main(string[] args)
{
int id = default(int);
string name = string.Empty;
string commandText = string.Empty;



Action<SqlCommand> find = (SqlCommand command) =>
{
command.CommandText = commandText;

using (SqlDataReader reader = command.ExecuteReader())
{
if (!reader.HasRows)
Console.WriteLine("Sorry, nothing found");

while (reader.Read())
Console.WriteLine(reader.GetValue(0));
}

};



Console.WriteLine("Enter Id to search for:"); id = int.Parse(Console.ReadLine());
commandText = "SELECT Name FROM Names WHERE Id=" + id.ToString();
WorkWithDatabase(find);



Console.WriteLine("Enter name to search for:"); name = Console.ReadLine();
commandText = "SELECT Id FROM Names WHERE Name='" + name + "'";
WorkWithDatabase(find);

Console.WriteLine("Thanks for using."); Console.ReadLine();
}


Výhody – základní kód pro práci s databázi teď existuje jen jednou. Při programování se můžeme soustředit jen na to co opravdu chceme dělat (sestavení SQL příkazu) a nemusíme znovu psát (vkládat) opětovně stejný kód.
 

Závěr

Někoho možná napadne, že výše uvedené, za předpokladu, že si vyhledávání a výpis výsledků zjednodušíme, může vypadat i takto:

static void Main(string[] args)
{



Console.WriteLine("Enter an Id to search for:");int id = int.Parse(Console.ReadLine());
WorkWithDatabase("SELECT Name FROM Names WHERE Id=" + id.ToString());

Console.WriteLine("Enter a name to search for:"); string name = Console.ReadLine();

WorkWithDatabase("SELECT Id FROM Names WHERE Name='" + name + "'");

Console.WriteLine("Thanks for using."); Console.ReadLine();
}

static void WorkWithDatabase(string commandText)
{
using (SqlConnection connection = new SqlConnection(@"Data Source=.\SQLEXPRESS;Initial Catalog=Names;Integrated Security=True;Pooling=False"))
{
connection.Open();

using (SqlCommand command = connection.CreateCommand())
{
command.CommandText = commandText;

using (SqlDataReader reader = command.ExecuteReader())
{
if (!reader.HasRows)
Console.WriteLine("Sorry, no name found");

while (reader.Read())
Console.WriteLine(reader.GetValue(0));
}
}
}
}


I když by se mohlo zdát, že je to ještě elegantnější a kratší řešení, není tomu tak. Problémy nastanou v okamžiku, kdy budeme chtít s daty z databáze pracovat jinak, budeme muset psát další a další metody podobné metodě WorkWithDatabase a tak opět spadneme do původního kódu a porušíme princip DRY.
 
V některém příštím pokračování ukážu, jak udělat řešení ještě univerzálnější.
Posted by mstr | 3 Comments
Vedeno pod: , ,

Budíček dle Apple – aneb pracujeme s datem a časem

Asi jste zaznamenali, že nové telefony od Apple měli tento rok problém probudit své šťastné majitele po změně letního času. Přesnou příčinu nevím, ale protože práce s datem a časem není úplně triviální pokusím se zde shrnout některé zásady a ukázat, kam jejich porušení může vést.

Časová pásma

Na různých místech jsou různé časy – to je asi známé. Zatímco v Brně obědváme, v New Yorku se lidé teprve probouzejí a naopak v  australském  Perthu jsou lidé už pomalu po večeři. Ostatně si můžete i na svém desktopu umístit několik hodin ukazujících časy na různých místech. A nebo si můžete čas přepočítat na těchto stránkách.

Aby v tom byl pořádek, rozdělil se svět na časové zóny. Každá zóna má svůj název a definovanou odchylku  od UTC (Universal Time Coordinate) – což je zjednodušeně řečeno čas na nultém poledníku, který prochází Greenwich  (část Londýna). Česká republika tak leží v zóně s názvem Central Europe Standard Time s ochylkou  +1:00 od UTC – což znamená, že pokud je UTC = 9 hodin, je u  nás 10, tedy o hodinu více (proto to znaménko plus +). Naopak v Rio De  Janeiro, které se nachází v pásmu –2:00  je 7 hodin, tedy o dvě hodiny méně (což je symbolizováno znamením mínus -).

Co je ještě důležité vědět: UTC bývá také nazýváno jako Zulu či GMT a  odchylka nemusí být nutně v celých hodinách, ale je běžná i odchylka například o 5 hodin a 30 minut.

DST – ďáblův dar

DST je zkratka Daylight saving time, u nás známá jako letní čas. Prakticky se projevuje tak, že se na jaře vyspíte o hodinu méně a poté obíháte hodiny v bytě a posunujete ručičku dopředu, aby jste si totéž zopakovali na podzim s tím, že sílu na posunutí ručičky dozadu můžete načerpat během hodiny, o kterou se váš spánek prodlouží. Praktický smysl to nemá a programátorům to jen komplikuje život – protože ve skutečnosti jsme předefinovali chování časové zóny – pokud jsem v předchozí stati uvedl, že je u nás 10 hodin, byl správný zápis tohoto údaje 10:00 UTC+1:00. Ale jakmile přejdeme na DST, letní čas, máme sice i nadále 10 hodin, ale posuv vůči UTC jsou dvě hodiny a správný zápis je 10:00 UTC+2:00. A název zóny zní najednou ….

To ještě není to nejhorší – protože DST je výmysl politiků, není datum zavedení DST  jednotné. Spojené státy tak přechází na DST o dva týdny dříve a časový rozdíl mezi NewYorkem a Prahou není tak šest hodin, ale jen pět.

Základní .NET třídy-struktury pro práci s časem

V prostředí .NET jsou k dispozici třídy DateTime a TimeZone. DateTime slouží k vyjádření času, který může být jednoho z těchto typů":

  • local
  • universal
  • unspecified

Local znamená, že hodnota vyjadřuje čas v zóně, ve které se nachází počítač. Universal je pak čas UTC. Unspecified označuje jakýkoliv čas mimo dva předešlé. Pro práci platí tyto hlavní zásady:

  • pracujeme (=provádíme operace) vždy s  univerzálním časem
  • lokální čas používáme jen při zpracování vstupu od uživatele a při prezentaci výsledku uživateli

Porušení těchto zásad vede v zásadě k chybnému programu.  Pokusím se to ukázat na následujícím příkladu, kdy program počítá vždy čas za hodinu:

DateTime currentTime = new DateTime(2011, 3, 27, 1, 30, 0, 0, DateTimeKind.Local);
DateTime futureTime = currentTime.AddHours(1);

Console.WriteLine("{0:o} - {1:o}", currentTime, futureTime);
 
A co dostaneme:
image
Tento výsledek je špatně – 27.3.2011 přecházíme na letní čas a 2:30 tedy vůbec nebude. Správně by se všechny výpočty měli provádět v UTC:
DateTime currentTime = new DateTime(2011, 3, 27, 1, 30, 0, 0, DateTimeKind.Local);
DateTime futureTime = currentTime.ToUniversalTime().AddHours(1).ToLocalTime();

Console.WriteLine("{0:o} - {1:o}", currentTime, futureTime);

a poté již dostaneme správný výsledek:
image 
Třída TimaZone nám dokáže zejména vracet údaje pro časovou zónu počítače, na kterém běží naše aplikace.  Její praktické použití je tak omezené.
 

Ukládáme čas

Ukládání časových údajů se řídí zásadami, které vyplývají z hlavních zásad:

  • ukládáme vždy čas v UTC
  • ukládáme údaj identifikující časovou zónu uživatele

Představme si, že jsme si uložili čas jako řetězec a to pro účely buzení uživatele. Čas jsme ukládali v létě a uložili jsme si tuto hodnotu a uložili jsme si údaj v lokálním čase uživatele a uložili jsme si ho včetně posunu – prostě tak, jako kdybychom plně nepochopili význam výše uvedených pravidel. Poté se pokusíme každý den určit, kdy uživatele probudit.

DateTime wakeUpTime = DateTime.Parse("6:00+2:00");

Console.WriteLine("{0:o}", wakeUpTime);
 
a program funguje, pěkne nás vzbudí:
image
Jenže pak se změní čas a doposud bezchybný program nás začne budit o hodiny dříve:
image
Takže takto bychom čas ukládat určitě neměli – vždy ho ukládáme nejlépe v UTC a s informací o zóně, kde má původ. Časový posun vůči UTC tímto údajem rozhodně není, neboť  se může díky DST změnit.
 

Globalizujeme

s výše uvedenými věcmi nešlo vystačit. Programátoři  se mohli setkat například s požadavkem, aby na počítači v Praze zobrazili mimo místního pražského času i čas v Los Angeles. Tento úkol není tak triviální jak se může zdát – někteří jej řešili tak, že od místního času odečetli devět hodin (Praha je v zoně UTC+1:00, Los Angeles v zoně UTC-8:00, rozdíl devět hodin a stačí tedy napsat:

DateTime currentTime = DateTime.Now;
DateTime losAngelesTime = currentTime.AddHours(-9);

Console.WriteLine("{0:o} - {1:o}", currentTime, losAngelesTime);
 
Program nám vypíše:
image
Na první pohled vypadá vše v pořádku. Můžeme si ale všimnout, že čas Los Angeles má chybně vyjádřený posuv. A celý příklad obsahuje ještě jednu chybu -  nebere v úvahu DST. Tři týdny v roce bude tedy přepočet chybný.
Uvádění časového posunu se můžeme samozřejmě vyhnout, ale DST chybu odstraníme stěží. Naštěstí máme v .NET 3.5 více možností – je zde nová struktura DateTimeOffset a nová třída pro práci s časovými zónami TimeZoneInfo.
 

DateTimeOffset

je vlastně rozšířením DateTime, ale mim datumu a  času drží i informaci o posunu vůči UTC. Jinak se ničím neliší a při práci s ní je nutno dodržovat veškeré zásady práce s DateTime.
 
Následující příklad sice vyřeší správné zobrazení, ale pořád je tam skrytá chyba při přechodu na letní čas a naopak:
 
DateTime currentTime = DateTime.Now;
DateTime losAngelesTime = DateTime.SpecifyKind(currentTime.AddHours(-9), DateTimeKind.Unspecified);
DateTimeOffset formattedLosAngelesTime = new DateTimeOffset(losAngelesTime, new TimeSpan( -8, 0, 0));

Console.WriteLine("fff{0:o} - {1:o}", currentTime, formattedLosAngelesTime);

 
Výpis bude obsahovat správné časové posuny vůči UTC:
 
 image
Ale pozor, jak se můžeme přesvědčit, budou tyto údaje zobrazovány i v okamžiku, kdy je časová zóna v Los Angeles stále v zóně -7:00
image
Proto existuje třída TimeZoneInfo.
 

TimeZoneInfo

Pomocí této třídy si můžeme vytvořit objekt pro danou časovou zónu a s jeho pomocí vytvářet časy, které jsou platné v této časové zóně. Navíc nám statická metoda této třídy zpřístupňuje seznam všech časových zón tak, jak jsou definovány v OS a udržovány pomocí aktualizací Microsoftem. Měli bychom tak mít zaručenou aktualizaci  pokud dojde ke změně.
 
TimeZoneInfo pacificTimeZone = TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time");
DateTime currentUtcTime = DateTime.UtcNow;
DateTime currentLosAngelesTime = TimeZoneInfo.ConvertTimeFromUtc(currentUtcTime, pacificTimeZone);


Console.WriteLine("{0:o}", new DateTimeOffset(currentLosAngelesTime, pacificTimeZone.GetUtcOffset(currentLosAngelesTime)));
 
Jak během normálního, tak i zimního času dostaneme správný tvar, během letního (DST):
image
tak i  v zimním období:
image

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Objekty třídy TimeZoneInfo nám zpřístupňují mnoho informací o dané časové zóně. řeknou nám, jestli daný čas v dané zoně vůbec existuje (v případě, že se přechází ze zimního na letní čas, chybí nám celá hodina od 2 do 3 ráno) a nebo naopak jestli daný čas neexistuje dvakrát (pokud přecházíme z letního na zimní čas opakuje se v noci celá hodina od 2 do 3 ráno. Objekt nám pro dané datum a čas je schopen vrátit správnou odchylku od UTC. Některé možnosti  jsou zachyceny v přechozí ukázce i následující ukázce:

 

TimeZoneInfo centralEuropeTimeZone = TimeZoneInfo.FindSystemTimeZoneById("Central Europe Standard Time");

//31.10. 2010 - 2:30 AM
DateTime shiftSummerWinterTime = new DateTime(2010, 10, 31, 2, 30, 0, DateTimeKind.Unspecified);
//27.3. 2011 - 2:30 AM
DateTime shiftWinterSummerTime = new DateTime(2011, 3, 27, 2, 30, 0, DateTimeKind.Unspecified);



 

 


Console.WriteLine("Summer to winter - two possibilities? {0}", centralEuropeTimeZone.IsAmbiguousTime(shiftSummerWinterTime)); // true
Console.WriteLine("Summer to winter - exists? {0}", !centralEuropeTimeZone.IsInvalidTime(shiftSummerWinterTime)); //true
Console.WriteLine("Winter to summer - two possibilities? {0}", centralEuropeTimeZone.IsAmbiguousTime(shiftWinterSummerTime)); //false
Console.WriteLine("Winter to summer - exists?{0}", !centralEuropeTimeZone.IsInvalidTime(shiftWinterSummerTime)); //false




foreach (var zone in TimeZoneInfo.GetSystemTimeZones())
{
Console.WriteLine("{0} {1} {2} {3} {4} {5}",
zone.Id, //Central Europe Standard Time
zone.StandardName, //Central Europe Standard Time
zone.DisplayName, //(UTC+01:00) Belgrade, Bratislava, Budapest, Ljubljana, Prague
zone.DaylightName, //Central Europe Daylight Time
zone.BaseUtcOffset, //01:00:00
zone.SupportsDaylightSavingTime //true
);
}


 

A nakonec ještě stručné schéma jak správně pracovat s časy a datumy:

image Poznámka: ukázky v tomto příspěvku byly spouštěny na PC ve středoevropském časovém pásmu, kde 31.10.2010 došlo k přechodu na zimní čas a 27.3. 2011 dojde k přechodu na letní čas.

Posted by mstr | 7 Comments
Vedeno pod: ,

Ukázky kódu z knížek je třeba brát s rezervou

V knížce Pro ASP.NET MVC 2 Framework  (Apress, S. Sanderson) se autor věnuje i novým vlastnostem C# 3.0 a líčí jak s pomocí Linqu  realizovat doteď pracné operace (kapitola C# 3 Language Features, stránka 78) . Pro demonstraci si vybral kod pro vyběr  tří největších hodnot z pole čísel a uvádí tento kód jako příklad toho, jak se tento problém dosud řešil:

static int[] FindMaxValuesNet2(int number, int[] unsortedValues)
{
int[] localCopy = new int[unsortedValues.Length];

unsortedValues.CopyTo(localCopy, 0);

Array.Sort(localCopy);

int[] returnValues = new int[number];

for (int index = 0; index < number; index++)
{
returnValues[index] = localCopy[localCopy.Length - 1 - index];
}

return returnValues;
}
 
aby hned ukázal, jak to lze řešit v .NET 3 za použití Linqu:
 
static IEnumerable<int> FindMaxValuesNet3(int number, int[] unsortedValues)
{
return (from value in unsortedValues orderby value descending select value).Take(number);
}

(poznámka: původní ukázky jsem upravil – doplnil jsem obálku metod, počet vyhledaných čísel je předáván parametrem a kopii pole používám aby nedošlo k narušení  pořadí původního pole).

Autor vyzdvihuje jednoduchost a přehlednost kódu – a čtenář může nabýt dojmu, že oba přístupy jsou shodné i co se týče rychlosti a jediné správné.  Zkusil jsem si proto  změřit jak dlouho to trvá, tedy porovnat Linq s autorovým klasickým přístupem:

 

static void Main(string[] args)
{
Random random = new Random();

int[] unsortedRandomValues = new int[100000];

for (int index = 0; index < unsortedRandomValues.Length; index++)
{
unsortedRandomValues[index] = random.Next();
}

MeasureTakenTime(3, unsortedRandomValues, FindMaxValuesNet2);
MeasureTakenTime(3, unsortedRandomValues, FindMaxValuesNet3);

Console.ReadLine();

}


static void MeasureTakenTime(int number, int[] unsortedValues, Func<int, int[], IEnumerable<int>> find)
{
Stopwatch stopwatch = new Stopwatch();

stopwatch.Start();
string numbers = string.Join(",", find(number, unsortedValues));
stopwatch.Stop();

Console.WriteLine("{0}, {1} ms: {2}", find.Method.Name, stopwatch.ElapsedMilliseconds, numbers);
}

a výsledky?
 image

 

 

 

Klasický přístup  (klasický dle autora) je čtyřikrát rychlejší než nový cool způsob pomocí Linqu.

Jenže ani to, co autor vydává za klasický přístup není zrovna optimální. Kopie pole, přetřídění……Přitom stačí něco jednoduchého:

static int[] FindMaxValues(int number, int[] unsortedValues)
{
int[] foundValues = new int[number];
int lowestValueIndex = number - 1;

for (int index = 0; index < foundValues.Length; index++)
foundValues[index] = int.MinValue;

for (int unsortedIndex = 0; unsortedIndex < unsortedValues.Length; unsortedIndex++)
{
if (unsortedValues[unsortedIndex] < foundValues[lowestValueIndex])
continue;

for (int index = 0; index < foundValues.Length; index++)
{
if (unsortedValues[unsortedIndex] > foundValues[index])
{
Array.Copy(foundValues, index, foundValues, index + 1, foundValues.Length -1- index);
foundValues[index] = unsortedValues[unsortedIndex];
break;
}
}
}

return foundValues;
}

a výsledky vypadají  hned jinak:
image
Výše uvedené platí pro  sto tisíc položek, ale pokud bychom chtěli prohledat deset milionů:
 
static void Main(string[] args)
{
Random random = new Random();

int[] unsortedRandomValues = new int[10000000];

for (int index = 0; index < unsortedRandomValues.Length; index++)
{
unsortedRandomValues[index] = random.Next();
}

MeasureTakenTime(3, unsortedRandomValues, FindMaxValuesNet2);
MeasureTakenTime(3, unsortedRandomValues, FindMaxValuesNet3);
MeasureTakenTime(3, unsortedRandomValues, FindMaxValues);
Console.ReadLine();
}
je rozdil ještě výraznější (uznávám, že je to extrémní příklad, ale pěkně na něm vyniknou odlišnosti):

image
Zkrátka implementace algoritmu dvacetkrát rychlejší než autorem uváděný “klasický postup” a i ten je téměř sedmkrát rychlejší než Linq.
 
Z výše uvedeného je vidět, že studium “klasických” algoritmů a postupů má pořád smysl a že příklady v knížkách je potřeba brát s rezervou.  Samozřejmě že někdy (často) je lepší rychleji naprogramovat, než ladit  složitější kód implementující algoritmus – ale občas mám pocit, že se na původní postupy úplně zapomíná – viz knížka, ze které jsem uváděl příklady.

 

Posted by mstr | 9 Comments
Vedeno pod: , ,

LINQ - jak na dynamickou podmínku

LINQ asi není potřeba nijak detailně představovat. Asi každý .NET vývojář se již setkal s LINQem, nejčastěji ve formě dotazu, kde byla jeho podoba již programově dána a měnit se mohl snad jen nějaký parameter dotazu:

public void Linq1() 
{
    int[] numbers = { 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 };
 
    var lowNums =  from n in numbers
                   where n < 5
                   select n;
 
    foreach (var x in lowNums) 
    {
        Console.WriteLine(x);
    }
}

 

Poznámka: tento příklad byl převzat ze stránky 101  LINQ Samples - výborný zdroj pro studium.

Jak to ale řešit, pokud chceme podmínku sestavit dynamicky, například provozujeme realitní server a chceme jej rozšířit o službu, která na základě zaslaných podmínek v textové podobě vrátí seznam vyhovujících nemovitosti?

 

Databáze nemovitostí

Řešení se pokusím vysvětli v tomto článku. Příklad bude maximálně zjednodušen, proto nebudeme dělat žádné rozhraní webové služby, ale spokojíme se s výstupem na konzolu. Též náše realitní databáze bude maximálně zjednodušena a u každého objektu si budeme držet jen tři údaje:

  • název místa, kde nemovitost leží
  • typ nemovitosti (výběr z číselníku)
  • počet ložnic

Class diagram pak vypádá takto:

ClassDiagram1

 

Naším vstupem pak bude řetězec ve tvaru: Název vlastnosti:přípustné hodnoty|Název vlastnosti: přípustné hodnoty,

Při předání řetězce "Location:Humpolec, Polna|PropertyType:Castle,Flat,Chateu,TerraceHouse|Bedrooms:1-20" budeme očekávat seznam všech bytů, zámků a hradů s dvěmi až dvaceti ložnicemi nacházejícími se v lokalitě Polná a Humpolec. Při předání řetězce "Bedrooms:5-10|PropertyType:Castle" pak zase seznam všech hradů s pěti až deseti ložnicemi.

Potřebovali bychom tedy nějak dynamicky sestavit omezující podmínku výběru, neboli filtr, neboli predikát - tedy pokud bych použil příklad ze začátku článku, potřebujeme dynamicky sestavit část "where n < 5".

To vyřešíme tak, že si definujeme metody pro sestavení každého filtru (predikátu), který bychom mohli potřebovat. Náš případ je zjednodušený - máme tři vlastnosti podle kterých chceme vyhledávat, budeme tedy definovat tři metody pro:

  • získání predikátu omezujícího na základě lokality
  • získání predikátu omezujícího na základě typu nemovitosti
  • získání predikátu omezujícího na základě rozsahu počtu ložnic

 

Příprava - převedení řetězce na hodnotu z číselníku

Požadovaný  typ nemovitosti je předáván ve formě řetězce, kde jsou jednotlivé typy oddělené čárkou. Tento řetězec musíme převést na kolekci hodnot našeho enumerátoru. K tomu nám poslouží statická funkce:

public static IEnumerable<PropertyType> GetPropertyTypesFromString(string types)
{
   List<PropertyType> propertyTypes = new List<PropertyType>();
   PropertyType propertyType;
 
   var rawPropertyTypes = types.Split(",".ToCharArray());
    
   foreach (string rawPropertyType in rawPropertyTypes)
   {
      propertyType = (PropertyType) Enum.Parse(typeof(PropertyType), rawPropertyType);
      propertyTypes.Add(propertyType);
   }
 
   return propertyTypes;
}

 

 

Metody pro získání predikátů

Bude se jednat o statické metody. Pro získání predikátu omezujího na základě lokality nemovitosti použijeme tuto statickou metodu:

public static Expression<Func<IRealProperty, bool>> IsInLocation(string locations)
{
     var predicate = PredicateBuilder.False<IRealProperty>();
     var rawLocations = locations.Split(",".ToCharArray(), StringSplitOptions.RemoveEmptyEntries);
    
     foreach (string location in rawLocations)
     {
        string tempLocation = location.Trim();
        predicate = predicate.Or(p => p.Location.Contains(tempLocation));
     }
    
     return predicate;
}

Pokud by snad někoho zarážel příkaz string tempLocation = location.Trim(); - je tam jednoduše proto, že LINQ věci se vykonávají v okamžiku, kdy jsou použity, ne kdy jsou definovány. Pokud bychom tento řádek vynechaly a další řádek použili ve tvaru predicate = predicate.Or(p => p.Location.Contains(location)); byla by použita hodnota proměnné location v okamžiku volání, tedy v tomto případě poslední název lokace. Díky příkazu string tempLocation = location.Trim();  je tedy pro každou podmínku použita vlastní proměnná držící jedinečnou hodnotu lokace v okamžiku použití.

V příkladu jsou použiti metody třídy PredicateBuilder - to je pomocná třída pro sestavování predikátů. Pokud bych to popsal polopaticky, tak předchozí funkce sestaví podmínku (v případě, že vyhledávácí řetězec obsahoval "Location:Humpolec, Polna|PropertyType:Castle,Flat,Chateu,TerraceHouse|Bedrooms:1-20" :

podmínka = False Or Location='Humpolec' Or Location='Polna'

 

Třídu PredicateBuilder jsem si vypůjčil ze stránky Dynamically Composing Expression Predicates - lze ji v různých obdobách nalézt i jinde, ale myslím, že tohle byl původní zdroj-

 

Podobně by vypadaly i metody pro omezení dle typu nemovitosti:
 
public static Expression<Func<IRealProperty, bool>> IsOfType(string types)                                  
{
   var predicate = PredicateBuilder.False<IRealProperty>();
   var propertyTypes = GetPropertyTypesFromString(types);
 
   foreach (PropertyType propertyType in propertyTypes)
   {
      PropertyType tempType = propertyType;
      predicate = predicate.Or(p => p.Type ==tempType);
   }
 
   return predicate;
}

 

a i metoda omezující dle poču ložnic:

public static Expression<Func<IRealProperty, bool>> HasBedroomCount(int min, int max)
{
    var predicate = PredicateBuilder.False<IRealProperty>();
    
    predicate = predicate.Or(p => p.BedroomCount >=min && p.BedroomCount<=max);
    
    return predicate;
}

 

Zpracování vyhledávací podmínky

Po definici jednotlivých metod již stačí jednoduše zpracovat předávaný řetězec a jednotlivé metody zkombinovat. Pro jednoduchost nejsou přidány žádné kontroly formální správnosti předávaného řetězce - spoléháme se, že tvar bude dodržen:

string searchConditions = "Location:Humpolec, Polna|PropertyType:Castle,Flat,Chateu,TerraceHouse|Bedrooms:1-20";
var conditions = searchConditions.Split("|".ToCharArray());
 
var predicate = PredicateBuilder.True<IRealProperty>();
 
foreach (string condition in conditions)
{
   var keyValuePair = condition.Split(":".ToCharArray());
 
   switch (keyValuePair[0])
   {
      case "PropertyType":
         predicate = predicate.And(RealProperty.IsOfType(keyValuePair[1]));
         break;
 
      case "Bedrooms":
         var bedroomRange = keyValuePair[1].Split("-".ToCharArray());
         int min= int.Parse(bedroomRange[0]);
         int max = int.Parse(bedroomRange[1]);
         predicate = predicate.And(RealProperty.HasBedroomCount(min, max));
         break;
 
      case "Location":
         predicate = predicate.And(RealProperty.IsInLocation(keyValuePair[1]));
         break;
   }
}

 

Provedení dotazu

Nyní stačí získaný předpis jen zkompilovat, použít v LINQ dotazu a výsledek vypsat:

Func<IRealProperty, bool> SatisfiesSearchConditions = predicate.Compile();
 
var query = from property in propertis
            where SatisfiesSearchConditions(property)
            select property;
 
foreach(RealProperty property in query)
{
   Console.WriteLine(property.ToString());
}

 

A je to.

 

Závěr

Snažil jsem ukázat, jak lze dynamicky konstruovat omezovací podmínky LINQ dotazu. V některém volném pokračování ukáži jak lze podobně provést i seřazení výsledků a i reálné použítí. Ačkoliv byl tento článek založen na zjednodušeném příkladě, myslím že není problém přenést principy zde ukázané jinam.

Zdrojový kód příkladu je jako obvykle přiložen v zip balíčku.

Na závěr tedy ještě přehledně dva odkazy zmíněné v článku, kde lze nalézt další informace a studijní materiál:

Posted by mstr | 2 Comments
Vedeno pod: , ,

Attachment(s): LinqSearch.zip

Rozšíření možností Visual Studia - Add-in (2)

V předchozím díle jsem ukázal, jak si vytvořit základní obálku pro add-in. Nyní ukáži, jak napsat metodu, která něco provede - to něco je přesun using direktiv do těla namespace. Není to až tak těžké.

Jistě si vzpomenete, že v části 1. jsme získali referenci na objekt DTE2, což je vlastně IDE Visual Studia. Díky tomu získáme přístup k našemu kódu. Zjednodušeně lze strukturu, se kterou budeme pracovat, nakreslit takto:

DTE

Je třeba si uvědomit, že pod DTE objektem je zabalene COM rozhraní, navíc zatížené různými nelogičnostmi (například Document má i vlastnost Language, která vrací název programovacího jazyka, napříkla CSharp či vb. Stejně nazvanou property nalezneme i v CodeElement či FileCodeModel, ale tentokrat vraci už string s GUID kodem jazyka z kolekce CodeModelLanguageConstants).

Vlastní kod bude velmi jednoduchý:

  1. získáme seznam usings
  2. získáme seznam namespaců
  3. odstraním usings
  4. vložíme usings do namespaců
  5. zarovnáme kód.

 

Získání seznamu usings a namespaců

Získáme aktivní dokument (z něj došlo k zavolání add-inu). Přes ProjectItem se dostaneme k vlastnímu modelu kódu. Staří nám procházet první úroveň a vyhledat všechny using a namespace prvky. V případě using rovnou získáme i kód daného prvku:

 

Document document = applicationObject.ActiveDocument;
ProjectItem projectItem = document.ProjectItem;
FileCodeModel2 codeModel = projectItem.FileCodeModel as FileCodeModel2;
 
List<CodeElement> usings = new List<CodeElement>();
List<CodeNamespace> namespaces = new List<CodeNamespace>();
 
StringBuilder sb = new StringBuilder();
sb.AppendLine();
 
foreach (CodeElement element in codeModel.CodeElements)
{
    switch (element.Kind)
    {
        case vsCMElement.vsCMElementImportStmt:
 
            usings.Add(element);
 
            int startPoint = element.GetStartPoint(vsCMPart.vsCMPartWholeWithAttributes).Line;
            int endPoint = element.GetEndPoint(vsCMPart.vsCMPartWholeWithAttributes).Line;
            sb.AppendLine(element.StartPoint.CreateEditPoint().GetLines(startPoint, endPoint + 1).Trim());
            break;
 
        case vsCMElement.vsCMElementNamespace:
            namespaces.Add((CodeNamespace)element);
            break;
    }
}

 

Ostranění usings

Zde využijeme metodu Remove:

foreach (CodeElement element in usings)
{  
    codeModel.Remove(element);
}
 

Vložení usings do namespace prkvu

Zde využijeme String Builder objekt naplěný při získávání seznamů prvků a vložíme jej do každého prvku namespace, který jsme našli:
foreach (CodeNamespace namespaceElement in namespaces)
{
    EditPoint ep = namespaceElement.GetStartPoint(vsCMPart.vsCMPartNavigate).CreateEditPoint();
    ep.LineDown(1);
    ep.Insert(sb.ToString());
    ep.SmartFormat(namespaceElement.GetEndPoint(vsCMPart.vsCMPartNavigate));
}

Jistě jste si už všimli, že ze zdrojovým kódem pracujeme pomocí EditPointů. Získáme začátek daného kodového elementu, posuneme ukazatal o řádek níže (ep.LineDown), vložíme prvek a provedeme zarovnání celého elementu.

 

Úprava kódu

Po odstraněných using z rootu kodu tam zůstaly prázdné řádky. Odstraníme tedy i je - krokujeme od prvního řádku a odstraňujeme všechny nalezene prázdné řádky z rootu kódu dokud nedojdeme k začátku definice prvního namespace. Používá se zde další objekt TextSelection:

CodeNamespace firstNamespace = namespaces[0];
TextSelection selection = document.Selection as TextSelection;
 
int lineNumber = 1;
int stopLine = firstNamespace.GetStartPoint(vsCMPart.vsCMPartWholeWithAttributes).Line;
 
while (lineNumber < stopLine)
{
    selection.GotoLine(lineNumber, true);
    selection.DeleteWhitespace(vsWhitespaceOptions.vsWhitespaceOptionsVertical);
    lineNumber++;
    stopLine = firstNamespace.GetStartPoint(vsCMPart.vsCMPartWholeWithAttributes).Line;
}
 
selection.Cancel();

 

Drobné úpravy

V kíod se provádí několik změn. Pokud by uživatel použil po skončení běhu našeho kódu Undo, vracely by se provedene změny postupně. Pokud chceme, aby se jednim stiskem Undo vrátili změny v kodu do stavu před spuštěním našeho add-inu, použijeme před změnamy příkaz:

applicationObject.UndoContext.Open("SortCode Macro", false);

a po úspěšném provedení změn pak zavoláme:

applicationObject.UndoContext.Close();

Celý náš kód uzavřeme do bloku try...catch....finally:

applicationObject.UndoContext.Open("SortCode Macro", false);
 
try
{
    // the main routine
}
catch
{
    applicationObject.UndoContext.SetAborted();
}
finally
{
    applicationObject.UndoContext.Close();
}

 

Spuštění

Pro testování mám jednoduchý zdrojový kód, po vyvolání kontextové nápovědy se objeví známá nabídka Reorder usings :

 

image

Po spuštění pak dostaneme:

image

A jediné stisknutí Undo nás vrátí do výchozího stavu.

 

Závěr

Ukázal jsem jednoduchou práci s kódem. Add-in lze i mírně vylepšovat, například k stavovému řádku a výpisu hlášení o úspěšním provedení akce se dostaneme jednoduše pomocí:

applicationObject.StatusBar.Text = "Usings successfuly moved.";

Přiložen je aktualizovaný soubor Connect.cs. Pokud vás to zaujalo, rád se k tématu tvorby add-inlů vrátím - napište do komentářů, co by se vám líbilo.

Posted by mstr | 5 Comments
Vedeno pod: ,

Attachment(s): Connect.zip
Více článků Další stránka »