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

Pragmatik

Vezmu to zkratkou. Je to sice dál, ale zato horší cesta.

  • OT: Prázdninové počasí

    • Polovina prázdnin proprší.
    • Předchozí příznivé portugalské přímořské proudění přeruší přes Polsko postupující příliv parami přesyceného pobaltského povětří přinášejícího početné přeháňky.
    • Pivovary pocítí prudký pokles poptávky.
    • Prodejci plaveckých pomůcek poznají potíže.
    • Pláže plováren přestanou poskytovat příjemné poležení.
    • Průtok potoků povážlivě poroste.
    • Počítače předpovídají povodně.
    • Převozníci preventivně přeruší plavbu.
    • Půjčovny plavidel připravují pramice.
    • Policie plánuje povolat posily.
    • Penzisté podléhají podvodným praktikám podomních prodavačů protipovodňových postelí.
    • Politici protestují proti plánovanému přerušení poslaneckých prázdnin.
    • Počet potopených pozemků překvapí pojišťovny.
    • Postižené průmyslové podniky propustí polovinu pracovníků.
    • Pražané pytlují písek.
    • Potopené Podolí potřebuje pomoc.
    • Pražský primátor pláče.
    • Pesimisté prchají.
    • Pomoz pánbůh.
  • VS2010 a ladění externě spouštěných dll knihoven

    Potřeboval jsem opravit jeden bug ve starším projektu, dělaném ješte v .NET 2.0. Jedná se o dll knihovnu s COM rozhraním, která je volána z Excelu. V projektu mám už od dob VS2005 nastavenu záložku Debug takto:

    • Start external program: C:\Program Files\Microsoft Office\Office12\EXCEL.EXE
    • Command line arguments: C:\Projekty\Pragmatik\Pragmatik.xla

    Pragmatik.xla je soubor typu “Doplněk aplikace MS Excel” a obsahuje v podstatě jen makro, které zavolá Pragmatik.DLL, vytvoří z ní instanci hlavního objektu aplikace Pragmatik a předá jí odkaz na Excel, aby tato aplikace mohla Excel ovládat. Veškerý další kód Pragmatiku je už v té DLL knihovně a s výše uvedeným nastavením se dal vždy bez problémů debugovat.

    Po přechodu na VS2010 jsem však zjistil, že aplikace se v debug modu sice spustí a funguje, ale nezastavuje se na Breakpointech. Nebudu zbytečně popisovat, co vše jsem marně zkusil, nabídnu rovnou vysvětlení a řešení, které jsem našel na Visual Studio Debugger Team Blog

    Ve stručnosti jde o to, že pokud DLL knihovnu psanou v .NET 2.0 nebo 3.5 spouštíme pomocí externího EXE, nepozná VS2010 spávně cílový framework a použije debug engine z .NET 4.0. A ten pak nedebuguje. Řešením je udělat pro spouštěcí exe program extra konfigurační soubor a v něm explicitně určit, jaká verze frameworku se má pro debugování použít.

    V mém případě stačilo udělat soubor Excel.exe.config a do něj napsat:

    <?xml version="1.0" encoding="utf-8" ?>
    <configuration>
      <startup>
        <supportedRuntime version="v2.0.50727" />
      </startup>
    </configuration>

    Netýká se to zdaleka jen Excelu, tak se to třeba bude někomu hodit…

  • Jde to i bez generik

    K jednodušší řešením občas vede cesta přes ty složitější, takže problém popsaný v minulém příspěvku lze udělat i jednodušeji a bez generik:

    Class OsobaService

        Function GetByID(ByVal ID As Integer) As Osoba

            Dim O As New Osoba

            Me.Open(O, ID)

            Return O

        End Function

        Friend Sub Open(ByVal O As Osoba, ByVal ID As Integer)

            O.Jmeno = "Jan"

            O.Prijmeni = "Novák"

        End Sub

    End Class

    Class ZamestnanecService

        Inherits OsobaService

     

        Overloads Function GetByID(ByVal ID As Integer) As Zamestnanec

            Dim Z As New Zamestnanec

            Me.Open(Z, ID)

            Return Z

        End Function

     

        Friend Overloads Sub Open(ByVal Z As Zamestnanec, ByVal id As Integer)

            MyBase.Open(Z, id)

            Z.Funkce = "prodavač"

        End Sub

    End Class

    Nu což, alespoň jsem si vyzkoušel, jak generika fungují …

  • Využití vlastních generických tříd

    Při uvažování nad architekturou business vrstvy jsem narazil na zajímavý problém v implementaci dědičnosti, který mě nakonec přivedl až k použití vlastních generických tříd. Protože si myslím, že to není masově známá problematika, rozhodl jsem se o tom napsat tento článek.

    Výchozí situace

    • mám třídu Osoba s vlastnostmi Jmeno a Prijmeni
    • jejím dědicem je třída Zamestnanec, která navíc přidává vlastnost Funkce
    • pro obě třídy potřebuji mít někde metody Open a Save, tak aby i v těchto metodách bylo využito dědičnosti.
    • pro účely tohoto příkladu postačí, když metoda Open naplní třídu konkrétní hodnotou a metoda Save zobrazí data v MsgBoxu

    Nejjednodušší řešení, které ale nechci

    Nejjednodušší učebnicový postup přikládám jen pro úplnost a pro to, abych řekl, proč se mi nehodí. Pro potřeby mé zamýšlené architektury bych totiž metody Open a Save raději neměl uvnitř objektů Osoba a Zaměstnanec, ale někde venku. Takže takto to nechci:

    Public Class Osoba

        Private _jmeno As String

        Private _prijmeni As String

        Public Property Jmeno() As String

            Get

                Return _jmeno

            End Get

            Set(ByVal value As String)

                _jmeno = value

            End Set

        End Property

        Public Property Prijmeni() As String

            Get

                Return _prijmeni

            End Get

            Set(ByVal value As String)

                _prijmeni = value

            End Set

        End Property

        Overridable Sub Open(ByVal ID As Integer)

            Me.Jmeno = "Jan"

            Me.Prijmeni = "Novák"

        End Sub

        Overridable Sub Save()

            MsgBox(Me.Jmeno)

            MsgBox(Me.Prijmeni)

        End Sub

    End Class

    Public Class Zamestnanec

        Inherits Osoba

        Private _funkce As String

        Property Funkce() As String

            Get

                Return _funkce

            End Get

            Set(ByVal value As String)

                _funkce = value

            End Set

        End Property

     

        Public Overrides Sub Open(ByVal ID As Integer)

            MyBase.Open(ID)

            Me.Funkce = "prodavač"

        End Sub

     

        Public Overrides Sub Save()

            MyBase.Save()

            MsgBox(Me.Funkce)

        End Sub

    End Class

    Řešení, které chci, ale které nefunguje

    Ve třídách Osoba a Zamestnanec nechám jen vlastnosti, metody vyvedu do zvláštních tříd OsobaService a ZamestnanecService, nějak takto:

    Class OsobaService

        Overridable Sub Save(ByVal O As Osoba)

            MsgBox(O.Jmeno)

            MsgBox(O.Prijmeni)

        End Sub

        Overridable Function GetByID(ByVal Id As String) As Osoba

            Dim O As New Osoba

            O.Jmeno = "Jan"

            O.Prijmeni = "Novák"

            Return O

        End Function

    End Class

    Až dosud žádný problém. Ten nastane až při pokusu implementovat ZamestnanecService:

    Class ZamestnanecService

        Inherits OsobaService

     

        Overloads Sub Save(ByVal Z As Zamestnanec)

            MyBase.Save(Z)

            MsgBox(Z.Funkce)

        End Sub

        Overloads Function GetByID(ByVal ID As String) As Zamestnanec

            Dim Z As Zamestnanec = MyBase.GetByID(ID)

            Z.Funkce = "prodavač"

            Return Z

        End Function

    End Class

    Možná že rovnou vidíte, proč metoda GetByID nemůže fungovat, já se přiznám, že mě to nejprve nedošlo. Až když jsem při prvním spuštění dostal InvalidCastException “Objekt Osoba nelze přetypovat na Objekt Zamestnanec”, bylo mi to jasné - pokud vytvoření instance nechám na bázové třídě, vytváří se vždy instance třídy Osoba a tu už na zaměstnance nepředělám.

    Mohu se tomu vyhnout třeba takto:

    Class OsobaService

        Sub Open(ByVal O As Osoba)

            O.Jmeno = "Jan"

            O.Prijmeni = "Novák"

        End Sub

    End Class

    Class ZamestnanecService

        Inherits OsobaService

        Overloads Sub Open(ByVal Z As Zamestnanec)

            MyBase.Open(Z)

            Z.Funkce = "prodavač"

        End Sub

    End Class

    To bych se ale v podstatě vrátil k prvnímu řešení, jen rozdělenému do 2 tříd. Musím mít nejprve vytvořenou instanci objektu, abych mohl objekt naplnit daty. Původně zamýšlené rozhraní s děditelnými metodami GetByID se mi líbí mnohem víc. Lze ho ale vůbec nějak realizovat?

    Finální řešení

    S použitím generických tříd se mi podařilo překonat problém s vytvářením instancí tímto způsobem: 

    Public Class OsobaService(Of Type As {New, Osoba})

        Overridable Sub Save(ByVal Value As Type)

            MsgBox(Value.Jmeno)

            MsgBox(Value.Prijmeni)

        End Sub

        Overridable Function GetByID(ByVal ID As Integer) As Type

            Dim Value = New Type

            With Value

                .Jmeno = "Jan"

                .Prijmeni = "Novák"

            End With

            Return Value

        End Function

    End Class

    Public Class ZamestnanecService(Of Type As {New, Zamestnanec})

        Inherits OsobaService(Of Type)

        Public Overrides Sub Save(ByVal Value As Type)

            MyBase.Save(Value)

            MsgBox(Value.Funkce)

        End Sub

        Public Overrides Function GetByID(ByVal ID As Integer) As Type

            Dim Value = MyBase.GetByID(ID)

            Value.Funkce = "řidič"

            Return Value

        End Function

    End Class

    Třída OsobaService je takto schopna pracovat s jakýmkoli typem přetypovatelným na Osoba, která má  bezparametrický konstruktor. Její metoda GetByID tak nevytváří natvrdo instance typu Osoba, ale typu dle parametru Type.

    Pro potřeby klientské aplikace je ještě vhodné udělat společné rozhraní servisní vrstvy:

    Class Service

        Private _osoba As New OsobaService(Of Osoba)

        Private _zamestnanec As New ZamestnanecService(Of Zamestnanec)

        ReadOnly Property Osoba() As OsobaService(Of Osoba)

            Get

                Return _osoba

            End Get

        End Property

     

        ReadOnly Property Zamestnanec() As ZamestnanecService(Of Zamestnanec)

            Get

                Return _zamestnanec

            End Get

        End Property

    End Class

    Klientská aplikace pak pro získání objektu Zametnanec s ID=1 použije toto jednoduché volání:

    Z = Service.Zamestnanec.GetByID(1)

    Závěr

    Kompletní zdrojový kód je k dispozici zde. Uvítám i všechny alespoň trochu konstruktivní názory a připomínky a pokud jsem snad přehlédl nějaké snadnější řešení, tak sem s ním :)

  • Vyhledávání v DataTable z hlediska výkonu

    V předchozích 2 postech (1 a 2) jsem se věnoval  procházení záznamů z DataTable a ukazoval jsem výsledky testů, které dokazují, jak drastický vliv na výkon může mít nesprávná deklarace typů a následné zbytečné přetypovávání DataRow na Object. Na základě připomínek jsem toto těchto testů přidal ještě vyhledávání záznamů metodou SELECT a zde je výsledek:

    Speed2   Speed3

    • Find0 .. Find3 jsou stejné jako v minulém testu
    • Find4 je Find3 vylepšená o použití ANDALSO místo AND
    • Find 6 je původní Find4 používající PrimaryKey a metodu Find

    Novinkou je Find5, která vypadá takto:

        Function Find5(ByVal idr As String, ByVal okres As String) As String
            Dim R = DT.Select(String.Format("idr='{0}' and okres='{1}'", idr, okres))
            If R.Length = 1 Then
                Return R(0).Item("nazev")
            Else
                Return "-1"
            End If
        End Function

    Z 1. obrázku je evidentní, že metoda SELECT je velmi efektivní, plně srovnatelná s metodou FIND. Ale co když je to způsobeno jen tím, že “náhodou” vyhledávám podle stejných sloupců, ke kterým jsem ji kvůli možnosti použít metodu FIND udělal primární klíč?

    Vynechal jsem tedy vytvoření primárního klíče (a samozřejmě i spouštění testu Find6) a výsledek je na 2. obrázku. První použití metody SELECT trvalo cca 0,7 s, pak už byla rychlost neměřitelná stejně jako při použití metody FIND. Všimněte si ještě 3. řádku v 1. obrázku – vytvoření primárního indexu trvá 0,7 s, čili stejnou dobu, jako první spuštění metody SELECT ! Aniž bych někde pátral, jak je metoda SELECT implementovaná, dovoluji si odhadnout, že si pro každý dotaz, pro který nemůže použít primární klíč, vytváří nějakou indexovou strukturu. První vyhledání je pak docela pomalé, další už fungují výborně.

    Pokud tedy potřebuji nad DataTable opakovaně vykonávat stejný dotaz, je SELECT výborná volba, která je výkonnostně rovnocenná  FIND a navíc nemá její omezení použití na vyhledávání výhradně dle primárního klíče.

    Pokud bych pouštěl nad DataTable řadu různých dotazů a jejich struktura se neopakovala, mohlo by být vytváření indexů zbytečnou zátěží a jako rychlejší by v tom případě bylo napsat si vlastní cyklus. S tímto závěrem jsem ale opatrný, bylo by nutno vyzkoušet, zda SELECT opravdu při každém novém typu dotazu vytváří nové indexy nebo zda například od nějaké úrovně zaindexování již nepostupuje podle nějakého jiného algoritmu.

    PS: Za zmínku ještě stojí, že oproti očekáváni DirectCast není vždy rychlejší než ToString, někdy je to i obráceně. Zde bych se přikláněl k závěru, že z hlediska výkonu jsou DirectCast a ToString prakticky totožné.

  • Nepodceňujte režii přetypování II

    V komentáři pod posledním postem mě S(a)tano upozornil, že jsem ve svém příkladu FindName3 přehlédl ještě jedno (respektive 2) přetypování. Místo:

    If R.Item("idr") = idr And R.Item("okres") = okres Then

    doporučuje S(a)tano použít:

    If DirectCast(R.Item("idr"), String) = idr And DirectCast(R.Item("okres"), String) = okres Then
    Když už jsem měl hotový testovací prográmek, rozhodl jsem se že to vyzkouším, navíc jsem zkusil i variantu: 
    If R.Item("idr").ToString = idr And R.Item("okres").ToString = okres Then

    Výsledek je tento:

    Speed

    • Find0
      • vyhledávání pomocí cyklu
      • R as Object
      • porovnávání pomocí R.Item(“idr”)
    • Find1
      • vyhledávání pomocí cyklu
      • R as DataRow
      • porovnávání pomocí R.Item(“idr”)
    • Find2
      • vyhledávání pomocí cyklu
      • R as DataRow
      • porovnávání pomocí R.Item(“idr”).ToString
    • Find3
      • vyhledávání pomocí cyklu
      • R as DataRow
      • porovnávání pomocí DirectCast(R.Item(“idr”),String)
    • Find4
      • vyhledávání metodou Find s využitím PrimaryKey

    A závěr? Vyhledávání pomocí procházení DataTable se není třeba obávat až tak moc, jak jsem si původně myslel. Samozřejmě že pokud vyhledáváme vždy podle stejné kombinace sloupců, je nejlepší použít PrimaryKey a metodu Find, nicméně často nezbývá jiná možnost než použít cyklus. Pokud se nedopustíme zásadní chyby s přetypováním DataRow na Object jako se to povedlo mě, bude to pro řadu případů výkonnostně plně vyhovovat.

  • Nepodceňujte režii přetypování

    Dostal jsem po kolegovi  k “ladění” zdrojový kód, který fungoval sice dobře, ale příliš pomalu. Pes nebyl zakopaný příliš hluboko, velmi rychle jsem objevil hrůzu typu:

        Function FindName1(ByVal ID As String, ByVal Okres As String) As String
            Dim s = ""
            For Each R In Partners.Rows
                If R.Item("id") = ID And R.Item("okres") = Okres Then
                    s = R.Item("nazev")
                    Exit For
                End If
            Next
            Return s
        End Function
    Podobných “vyhledávacích” funkcí nad objekty typu DataTable tam bylo nespočet, pracovaly nad tabulkami o 100 000 řádků a co bylo nejhorší, ty funkce se volaly uvnitř cyklů, které měly 10 000 opakování! Náprava je v principu snadná:
        Function FindName2(ByVal ID As String, ByVal Okres As String) As String
            Dim R = Partners.Rows.Find(New Object() {ID, Okres})
            If R IsNot Nothing Then
                Return R.Item("Nazev")
            Else
                Return ""
            End If
        End Function
    Přičemž samozřejmě do procedury, která plní DataTable Partners je třeba doplnit toto: 
    Partners.PrimaryKey = New DataColumn() {Partners.Columns("id"), Partners.Columns("okres")}

    Protože tuto opravu bylo třeba provést na mnoha místech, chtěl jsem nejprve otestovat, jak podstatný vliv to bude mít na výslednou rychlost. Udělal jsem si proto jednoduchý testovací prográmek a v něm měřil rychlost funkcí FindName1 a FindName2 na DataTable o 100 000 řádků. Rozdíl byl drastický a samozřejmě úměrně rostl s pozicí hledaného řádku. Zatímco funkce FindName2 trvala při prvním spuštění cca 0,010 s a při každém dalším už méně než 0,001ms (že by se PrimaryKey indexoval až při prvním použití?), FindName1 pro první řádky DataTable běžela 0,02 s a pro řádky z konce tabulky trvala nekonečných 1,8 s.

    I zaradoval jsem se převelice a těšil se, jak pro přepsání všech funkcí typu FindName1 vše 1000x urychlím a pochvala před nastoupenou jednotkou mě nemine. Pak mě ale napadlo spočítat, kolik času celkem ušetřím, když 20 000x zavolám FindName2 místo FindName1. Zhruba 20 000 s, což je 5,5 hodiny.  Jenže tak dlouho původní “pomalý” program neběží, ten na daném vzorku dat pracuje 4 hodiny a to samozřejmě vykonává mnohem více funkcí, včetně ukládání zpracovaných dat do databáze. Jak je to možné?

    Udělal jsem i měření přímo v původním programu a s úžasem jsem zjistil, že v něm probíhá procházení řádky DataTable 100x rychleji, než v mém testovacím příkladu. Nad těmi samými daty, na tom samém stroji. Mohl jsem to nechat být, protože tak jako tak je princip použitý ve FindName2 výrazně lepší, ale asi mi dáte za pravdu, že přijít tomu na kloub bylo otázkou cti. Nebudu Vás déle napínat, řešení je zde:

        Function FindName3(ByVal ID As String, ByVal Okres As String) As String
            Dim s = ""
            For Each R As DataRow In Partners.Rows
                If R.Item("id") = ID And R.Item("okres") = Okres Then
                    s = R.Item("nazev")
                    Exit For
                End If
            Next
            Return s
        End Function

    FindName 3 je v mém případě 100x rychlejší, než FindName1. Na vině je přetypovávání objektu DataRow na Object v každém cyklu při jeho přiřazování do R. Opravdu bych nečekal, že to bude mít tak velký vliv.

    Poučení z toho plynoucí je nasnadě: Vyhýbejte se zbytečnému přetypovávání, režie s tím spojená nemusí být zanedbatelná!

  • Komponenta pro zvýrazňování syntaxe

    Pro aktuální projekt potřebuji textový editor se zvýrazňováním syntaxe. Našel jsem sice pár příkladů, jak o tuto funkci rozšířit RitchTextBox, ale vždy to bylo jen jako ukázka postupu, nikoliv jako hned použitelné řešení. Protože nemám čas se tím hlouběji zabývat, sáhl jsem po první cenově přijatelné komponentě, kterou jsem našel : EO Syntax Editor Control for Windows Forms

    Umí to parsovat ASM, Batch, CPP, CSharp, CSS, HTML, Java, JavaScript, Pascal, Perl, SQL, VB a XML, ale jdou tam dopsat i vlastní parsery. Je tam podpora pro bookmarky, číslování řádků a zvýrazňování a párování závorek. Zatím používám 30 denní trial a jsem s tím 100% spokojen. Chtějí za to jen $79, takže to nejspíš koupím. Leda že by mi někdo poradil něco jiného ...

  • Globální ErrorHandling podruhé

    V komentářích se objevil názor, že pod pojmem Globální Error Handling očekával čtenář něco jiného. Protože má asi pravdu a protože to co jsem popsal byla jen část mého řešení, přidávám toto pokračování.

    Zaprvé je třeba si jasně uvědomit, že i když uvedená technika Application.ThreadException umožňuje udělat jeden globální Try-Catch (ať už pro 1 formulář nebo pro celou aplikaci), rozhodně si tímto jediným Try-Catch nevystačíme. Samozřejmě, že je potřeba dát Try-Catch  na každé místo kódu, kde je potřeba a každou možnou výjimku tam hezky lokálně ošetřit. Řekněme, že naše busines  vrstva nabízí metodu ImportObjednavky(path as string). V jejím průběhu může dojít k řadě malérů. Soubor Path nemusí vůbec existovat, může být uzamčený, může obsahovat jiná data než očekáváme, identická objednávka  už může v systému existovat, … Prostě cokoli vás napadne. Na všechno toto je třeba v kódu té metody myslet a nějak rozumně to ošetřit. Ale má se zde zobrazovat nějaká zpráva uživateli? Rozhodně ne! Tato metoda by poté, co selže, měla vyhodit vlastní exception s popisem, proč selhala. A až tuto exception zachytí náš globální Try-Catch.

    Od tohoto postupného vnořování výjimek se mohu odchýlit v jediném případě. Pokud někde v kódu nižší vrstvě očekávám výjimku, po které není třeba uklízet, mohu se na její zpracování vykašlat a nechat ji rovnou skočit do globálního try-catch. V našem případě by se dala takto odbýt FileNotFound exception. Samozřejmě pouze pokud je někde zkraje metody a nedělal jsem před tím nějaké operace, které by bylo potřeba vzít zpět (např. založení nové objednávky v DB). Jediná nevýhoda takto odbyté výjimky je v tom, že v UI vyskočí pouze univerzální .NETí zpráva.

    Jako ukázku uvedu 2 hlášky vyvolané stejnou chybou – pokusem získat z Dictionary položku s neexistujícím klíčem.

    Pokud s chybou počítám a zpracuji ji, dostane uživatel moji vlastní výstižnou zprávu:

            If Not pItems.ContainsKey(Key) Then
                Throw New Handlers.Err.Exception("Hlavní formulář neobsahuje prvek s klíčem {0}.", Key)
            End If

    Uvedený příklad nemá přesně tu strukturu jak jsem popisoval výše, místo použití lokálního try-catch jsem té potenciální chybě předešel, což je myslím lepší. Ale ne vždy to jde, tak ještě přidávám klasičtější variantu:

     

            Try
                i = pItems(Key)
            Catch ex As KeyNotFoundException
                Throw New Handlers.Err.Exception("Hlavní formulář neobsahuje prvek s klíčem {0}.", Key)
            Catch ex As Exception
                Throw New Handlers.Err.Exception("Nepodařilo se získat prvek s klíčem {0}.", Key)
            End Try

    Každopádně výsledek bude v obou případech stejný:

     image

    Pokud na ošetření potenciální chyby zapomenu (a nebo se na ni úmyslně vykašlu, protože zde to nevadí) dostanu se nakonec také do hlavního Try-Catch, jen zpráva vypadá trochu jinak.

    image

    Tolik tedy k vnořování výjimek. Nemyslím, že to je něco objevného, asi to většina zná, jde jen o to rozlišovat, kdy jsem na úrovni UI a mohu rovnou komunikovat s uživatelem a kdy bych měl raději výjimku poslat dál. Musím ale přiznat, že když jsem před lety přecházel na VB.NET z VB6, dal mi přechod od OnError-GOTO k Try-Catch docela zabrat, zkraje jsem se neuměl obejít bez Resume Next :)

    Ale zpět k pojmu Globální ErrorHandling. Já ho chápu tak, že bez ohledu na to, zda  mám na UI vrstvě jeden Try-Catch pro každý formulář nebo zda píšu Try-Catch pod každý UI event, globální je to tehdy, pokud všechny výjimky zpracovávám jednou společnou třídou. Například takto:

            Try
     
            Catch ex As Exception
                Me.Kernel.Err.Handle(ex,HandleTypes.Abort)
            End Try

    Pak je zajištěno, že ať je to v kódu kde chce a jak chce, každá výjimka projde jednou jedinou globální instancí. Ta pak může zajistit nejen komunikaci s uživatelem, ale i logování chyb, odesílání záznamu o chybě, kontaktování HelpDesku případně cokoli dalšího vás napadne.

    Pokud dám odkaz na tu globální třídu i Bussines vrstvě, mohu i odtud komunikovat s uživatelem. Například pokud uvnitř výše zmiňované metody ImportObjednavky nějaká privátní funkce hodí výjimku, že už tato objednávka jednou v systému je, může Err.Handler umístěný uvnitř metody ImportObjednavky rovnou zjistit od uživatele, zda to vadí a ukončit operaci nebo nevadí a pokračovat dále.

            Try
                CheckOrder(ID)
            Catch ex As Exception
                If Me.Kernel.Err.Handle(ex, HandleTypes.Abort Or HandleTypes.Ignore) = HandleTypes.Abort Then Exit Sub
            End Try

    A pokud máme tu třídu Err implementovánu opravdu šikovně, pak bude někde uloženo, že uživatel Pepa v 8:30 zcela vědomě a přes varování importoval duplicitní objednávku.

  • Globální ErrorHandling

    Chtěl jsem si nedávno trochu ušetřit práci s ošetřením chyb. Mám formulář a v něm řadu výkonných metod, které mohou vyvolat výjimku. Např. takto:

        Sub aaa()
            Throw New Exception("pokusná chyba")
        End Sub
     
        Sub bbb()
            Dim i = 0
            i = "ahoj"
        End Sub
    Tyto metody jsou spouštěny jednotlivými ovládacími prvky formuláře:

     

        Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
            aaa()
        End Sub
     
        Private Sub Button2_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button2.Click
            bbb()
        End Sub

    Samozřejmě by bylo záhodno volání těchto metod nějak ošetřit, nejsnadněji takto:

        Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
            Try
                aaa()
            Catch ex As Exception
                MsgBox(ex.Message)
            End Try
        End Sub

    Vím, že by mi ruce neupadly, kdybych poctivě napsal Try..Catch blok do každé událostní procedury, ale chtěl jsem zkusit, zda to jde nějak sofistikovaněji. Matně jsem tušil, že existují nějaké události spouštěné při neošetřené výjimce, tak jsem trochu zapátral v dokumentaci a zdálo se to být snadné:

        Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
            AddHandler Application.ThreadException, AddressOf OnThreadException
        End Sub
     
        Private Sub Form1_FormClosed(ByVal sender As Object, ByVal e As System.Windows.Forms.FormClosedEventArgs) Handles Me.FormClosed
            RemoveHandler Application.ThreadException, AddressOf OnThreadException
        End Sub
     
        Private Sub OnThreadException(ByVal sender As Object, ByVal e As System.Threading.ThreadExceptionEventArgs)
            MsgBox(e.Exception.Message, MsgBoxStyle.Critical, "OnThreadException")
        End Sub
    Jenomže to nefungovalo. Docela dlouho jsem s tím laboroval, zkoušel jsem použít i AppDomain.CurrentDomain.UnhandledException, ale vše marné. Až jsem objevil toto:
    Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException)
    Teprve tím se vynutí spouštění události OnThreadException při neošetřené výjimce, zatím co jinak to závisí na konfiguraci. Přidal jsem to do kódu před spuštění formuláře a bylo to. Tedy skoro, protože stále mě otravovala jedna “maličkost”: V debug módu mi neošetřenou výjimku nejprve zobrazil debugger, až po odklepnutí Continue došlo na událost OnThreadException a moje programové ošetření  výjimky. Hledal jsem tedy, zda lze toto chování debuggeru nějak vypnout. Zjistil jsem, že je třeba v Tools/Options/Debuging disablovat volbu “Enable Just My Code (Managed only)”. Pak už vše funguje k naprosté spokojenosti.

    Mé nadšení z fungujícího testovacího příkládku poněkud opadlo po nasazení do reálné aplikace. Reálný ErrorHandler, použitý místo Msgbox(ex.message), totiž mimo jiné i zobrazuje a loguje CallStack. A ten je v tomto případě mnohem nepřehlednější, protože je v něm zachycena i dost komplikovaná cesta výjimky od původní procedury až do globálního handleru.

    Takto vypadá chybové hlášení reálné aplikace, pokud je výjimka vyvolaná metodou OpenItem ošetřena rovnou ve volající proceduře TreeView_NodeMouseDoubleClick:  

    GEH1

    A takto to vypadá, pokud je použito globální ošetření výjimek:

    GEH2

     

    Nakonec jsem ErrorHandler upravil tak, aby z CallStacku tyto “nadbytečné” řádky vynechal a globální zachytávání výjimek jsem v aplikaci ponechal. Nakolik to je moudré, posuďte sami, já jsem zatím na žádný další problém nenarazil.

    Posted 8. ledna 2009 7:45 by tomas.berny | 7 Comments
    Vedeno pod:
  • Optimální konfigurace pro VisualStudio 2008 ?

    Mé Visty 64 po roce spokojeného provozu na notebooku DELL (C2Duo T7500 @ 2,2GHz + 4 GB + 60 GB) pomalu ale jistě nazrávají k přeinstalaci. Jsou čím dál línější a nenažranější (22 GB Windows, 9 GB Program Files). Přeinstalace je pro mě vždy příležitost k zamyšlení, co za SW opravdu potřebuji a co ne, a také jaké udělat HW změny - většinou dělám reinstalaci na nový (a větší) disk. Tentokrát jsem v pokušení udělat hned 2 radikální změny:

    1. pořídit SSD Disk
    2. opustit Visty

    Obojím má své pro a proti. SSD diskům zajisté patří budoucnost, ale není na ně ještě brzy? Cca 5000 Kč za 60GB bych i dal, ale vejdu se do 60 GB? Navíc jsem v některých recenzích zaznamenal zmínky o výrazném snížení výkonu při rostoucím zaplnění disku. A dát 9000 Kč za 120 GB? To už se mi moc nechce, zvláště když si představím, kolik to bude stát za rok.

    A Visty? Už jsem si na ně zvykl, ale diskové operace jsou dle mého názoru o dost pomalejší, než byly v XP. A výhoda 64b? Většina aplikací je stejně 32b a ovladač pro můj scanner stále ještě neexistuje. Takže možnosti jsou:

    • zachovat Visty
    • vrátit se k XP (a počkat si na Windows 7)
    • zaexperimentovat s Windows Server 2008
    • a to vše 32b nebo 64b

    Co tam budu instalovat?

    • VisualStudio 2008
    • SQL 2005 + SQL 2008
    • Office 2007

    Co na tom budu dělat?

    • programovat WinForm databázové aplikace a možná i WPF XAML aplikace
    • honit SQL s databázemi < 1GB
    • spouštět Virtual PC (z extermích USB disků)

     

    Zajímalo by mě, jaký máte názor na volbu OS pro VisualStudio, případně zda už máte někdo zkušenosti s přechodem na SSD. Za všechny rady a názory předem děkuji a pokud se nakonec pro SSD rozhodnu, určitě dám vědět, jak jsem pochodil.

  • R.I.P Pragmatik

    Před časem jsem nahrál videoprezentaci předvádějící můj framework Pragmatik na příkladu vývoj jednoduchého interaktivního formuláře napojeného na databázi NorthWind. Babral jsem se s tím do rána, výsledkem je 25 jakžtak souvislých, ale dosti rozvleklých minut. Tehdy jsem s tím příliš spokojen nebyl a rozhodl jsem se to zatím nezveřejňovat a ještě to předělat. Samozřejmě, že pak jsem se k tomu už nevrátil a dnes mi je jasné, že lépe už to nenatočím :)

    Protože momentálně ani nevím, zda budu na tomto projektu pokračovat, zveřejňuji to nyní jako svůj videopomník. Je tam naživo k vidění většina z věcí, o kterých jsem psal v dřívějších postech i něco navíc, takže to snad bude zajímavé. Jen to mělo být tak 2x rychlejší - kdo najde trpělivost to shlédnout celé, má můj dík.

    Aktualizováno 8.1.2009:
    Pro Pragmatik jsem žádnou zakázku neschrastil, takže zřejmě opravdu ve stávající podobě končí. Stejně již stagnoval, takže jsem nakonec rád, že jsem se dostal k novému projektu, kde mohu na některé dobré věci z Pragmatiku navázat a naopak některých méně dobrých se vyvarovat. Takže z profesního hlediska je to nová výzva, jen ten Excel už v mé práci nebude hrát tak velkou roli. 

     

  • Prasácký kód

    Kvízová otázka - prasácký kód je:

    1. Veškerý kód aplikace Pigmatic - systému pro řízení chovu prasat.
    2. Tajné kódy, kterými se po internetu domlouvají pedofilové.
    3. Zdrojový kód psaný vývojářen A o kterém si vývojář B myslí, že by ho napsal úplně jinak.

    Odpovězde si sami a berte tento kvíz jako odlehčený úvod příspěvku na téma hodnocení kvality zdrojového kódu. Pod mým minulým příspěvkem se rozeběhla debata, zda příklad, který jsem nabídl ke stažení, je či není prasárna. Respektive, zda z faktu, že dekompilací mého exe souboru získaný C# kód je prý až "neuvěřtelná prasárna", lze či nelze vyvozovat, že i originál zdrojový kód psaný ve VB.NET je nutně také prasárna. Jelikož jsem se problematikou kompilace a dekompilace nikdy nezabýval a jazyk C# znám jen velmi zběžně, neumím posoudit, kdo má pravdu. Proto jsem tam vystavil i originální VB.NET zdrojáky a kdo chce, ať si to porovná.

    Když ale pominu problematiku dekompilace a také tradiční "multikulturní" třenice mezi céčkaři a basicáři, dostanu se k zajímavé otázce: Jak hodnotit kód? A má to vůbec smysl? Vzpoměl jsem si na příspěvek na téma Code-Review, který jsem tu před časem četl a když jsem si ho znovu dohledal, potvrdil jsem si, že s ním naprosto souhlasím. A mohu to potvrdit i vlastní zkušenosti, a to dokonce z obou stran barikády.

    V posledních 2 letech jsem si z kapacitních důvodů nechával některé části svých projektů dělat na zakázku od externích spolupracovníků a při přebírání jejich práce jsem tak byl v pozici hodnotitele. A musím přiznat, že mě často při zkoumání dodaného kódu napadaly jen samé neslušné výrazy. Někdy to byly zcela zjevné blbosti, například jeden mladičký začínající programátor nevěděl, jak pomocí HasRow a Read zpracovat sqlDataReader. Vyřešil to tedy oklikou: Nejprve si proti DB poslal příkaz "SELECT Count(*) FROM TableName WHERE ..." a DataReaderem načetl vrácenou hodnotu do N. Pak teprve zavolal požadovaný příkaz "SELECT C1,C2 FROM TableName WHERE ..." a následně cyklem FOR i=1 to N zpracoval všechny vrácené řádky. No nezabili by jste ho ?

    Jindy jsem se ale rozčiloval jen proto, že to bylo napsáno jinak než jsem očekával, nebo jiným stylem, než jsem zvyklý. Je jasné, že ve fungujícím vývojovém týmu by měly být definovány pravidla, jak má kód vypadat. Ale pokud taková definice chybí, nelze jednoznačně odsoudit kód jen proto, že autor má jiný styl než já.

    Protože pracuji na volné noze a u všech projektů jsem jediný ProjectManager a ProjectArchitect a ProjectNevímCoVšechno, v pozici hodnoceného se ocitám mnohem méně často a musím přiznat, že to je škoda. Když se po letech dívám do svých starších zdrojáků, také mnohdy lomím rukama a říkám si, proboha, proč mi tehdy nikdo neřekl, jaká to je blbost... Neřekl, protože neměl kdo. Jakkoli si své nezávislosti cením, tato absence možnosti porovnání mého názoru s názory jiných kolegů je asi její největší nevýhodou. A možná i příčinou, proč možná až příliš často ve svých projektech volím sice originálni a neobvyklá, ale zároveň i nestandardní a pro leckoho "divná" řešení.

    Absenci vnitropodnikové diskuze si částečně kompenzuji tímto blogem, kde se snažim právě takovéto "originality" prezentovat a vždy se velmi těším na komentáře, ať už jsou kladné nebo záporné. Jen ta minulá diskuse mě trochu zaskočila, protože jsem zde nikdy neměl v úmyslu řešit problematiku kódu, šlo mi spíš o věcnou stánku. A najenou jsem se ocitl v pozici veřejného Code-Review, aniž by mi bylo jasné, co je mi vlastně vytýkáno.

    Když už ale bylo toto téma nastoleno, využiji toho a zeptám se na Váš názor na něco, s čím si nejsem jistý. Často pracuji s SQL a dlouho jsem hledal optimální způsob, jak to zakódovat. Nakonec jsem se dobral k níže uvedené variantě, která mi vyhovuje, protože je úsporná a přehledná. Jen se to blbě debuguje, protože výrazy uvnitř With - End With nejdou za chodu vyčíslovat a ani nemám k dispozici proměnnou odkazující na SqlCommand. 

     

        Function GetSpCmd() As SqlCommand

            Dim C As New SqlCommand()

            C.Connection = New SqlConnection(STRCON)

            C.CommandType = CommandType.StoredProcedure

            Return C

        End Function

     

     

        Sub Test()

            With GetSpCmd()

                Try

                    .CommandText = "spFIFO_recalc"

                    .Parameters.Add("@SklKod", SqlDbType.VarChar, 15).Value = SklKod

                    .Parameters.Add("@PolKod", SqlDbType.VarChar, 15).Value = PolKod

                    .Connection.Open()

                    .ExecuteNonQuery()

                Catch ex As Exception

                    MsgBox(ex.Message, MsgBoxStyle.Critical, "Error")

                Finally

                    .Connection.Close()

                End Try

            End With

         End Sub

    Jak to vidíte? Je to dobrý nápad, špatný nápad nebo dokonce prasárna ?

  • Objektový MsgBox

    Snad v každé aplikaci mám situaci, kdy potřebuji komunikovat s uživatelem - něco mu oznámit nebo se ho na něco zeptat. Někdy si vystačím s funkcí MsgBox, ale pokud je potřeba zobrazit strukturovanější informaci či získat složitější odpověď, než jen Ano-NE, je třeba udělat speciální formulář. Abych se tomu vyhnul, rozhodl jsem se už před řadou let udělat vlastní třídu Msg, která by měla metodu Box a která by uměla mnohem více možností než funkce MsgBox.

    Tou dobou jsem také začal dělat prní vážněji míněné věci v Excelu a postupně jsem se dopracovával k první funkčí verzi svého frameworku Pragmatik. Psal se rok 1998, Excel byl ve verzi 97 a objevil se v něm pan Sponka se žlutými bublinami. Ještě než se stačil všem zprotivit, zaujal mě svým objektovým modelem umožňujícím měnit barvy, vkládat obrázky, zobrazovat checkboxy a labely a tak jsem se rozhodl (dnes vidím že asi jako jediný) využít ho ve svých programech. Napsal jsem si wraper, který usnadňoval její použití v kódu a  tak začalo mých šťastných 10 let s panem Sponkou, Mickou a hlavně Alíkem.

    18

    Trochu jsem sice znejistěl, když se Pomocník vytratil z výchozí instalace Office 2003, ale protože šel snadno doinstalovat, neměl jsem důvod nic měnit. Až s verzí 2007 přišel šok: Objektový model pomocníka sice zůstal zachován, ale nefungoval! Po chvilce pátrání v dokumentaci se mé neblahé tušení změnilo ve strašlivou jistotu - pomocník byl odstraněn a není dále podporován. Zpětná kompatibilita mých aplikací je ohrožena.

    Naštěstí jsem ale pomocníka nikdy nevolal přímo, ale vždy přes výše zmíněný wraper. To byla má spása. Přepsal jsem wrapper tak, aby místo pomocníka používal můj vlastní zobrazovací formulář. Nejprve jsem ho spíchnul jen tak hala bala, ale když začal narůstat počet zákazníků používajících Office 2007, dal jsem si tu práci a vypiplal ho k dokonalosti. Dělal jsem to spíše sobě pro radost, než pro užitek, protože uživatelé takovouto "skrytou krásu" těžko ocení. Proto se s tím chci alespoň touto cestou trochu pochlubit :)

    Takže co to umí:

    • Texty s barevně zvýrazněnými parametry
    • Značky <b> <u> <br> <tab> <colorname>
    • Labely a checkboxy
    • Obrázky v záhlaví a pod bublinou

    Ukázky kódu a dialogů:

    19

    Ukázkový příklad nabízím ke stažení zde, zveřejnění zdrojových kódů ještě zvažuji, případným zájemcům je zatím pošlu na vyžádání.

    PS pro estéty:

    Mírně infantilní ladění grafiky je záměr, navíc dělám programy pro zemědělství, proto to prasátko nebo kachnička ...

  • Informační systémy v Excelu - Validace dat a TabOrder

    Namalovat nějakou statickou šablonu typu SmartSheet je v Pragmatiku velmi jednoduché. Stačí pár popisků, pár obrysů a za chvíli můžeme mít formulář jako je třeba tento:

    12

     

    Zavoláme NamesEditor a jednotlivá editační políčka pojmenujeme, například takto:

    13

    Nyní by stačilo doplnit do SheetMenu tlačítko Save a na něj pověsit volání nějaké procedury typu spNewEmployee a primitnivní pořizovací formulář by byl na světě. Nicméně uživatelům (tedy alespoň těm z nich, co nejsou zvyklí řešit vše jen za pomocí myši) by nejspíše vadilo, že po vyplnění políčka titul jim po klávese Enter kurzor odskakuje na pole adresa1, z něj pak na pole jméno atd. Také by nebylo od věci provádět nějakou validaci datových typů a zobrazovat k jednotlivým polím nějakou nápovědu. To vše lze velmi rychle a snadno zařídit pomocí dalšího z nástrojů Pragmatiku - EventsEditoru.

    14

    V EventsEditoru se automaticky ukáží všechny pojmenované buňky a k nim atributy jako DisplayName, StatusBar, TabOrder, Required, DataType apod. Po vyplnění a uložení těchto atributů začne náš formulář fungovat mnohem sofistikovaněji:

    • Na stavovém řádku se při výběru daného políčka objevuje text ve formátu DisplayName: StatusBar
    • Klávesy Enter, Tab a ShistTab fungují dle TabOrder, přesně jak jsme zvyklí z WinFormAplikací
    • Políčka označená jako Required nejdou opustit, aniž by do nich bylo něco zadáno, u všech políček je kontrolován datový typ, chyby jsou ve srozumitelné podobě zobrazovány uživateli

    16

     

    Třešnička nakonec - EventsEditor je primárně určen k definování Eventů jednotlivých buněk, takže tam můžeme velmi snadno dopsat třeba toto:

    15

    Tím bude automaticky zajištěno, že se po zadání PSČ automaticky doplní město, respektive se po zadání neplatného PSČ zobrazí odpovídající hláška.

     

    Pro úplnost ještě přikláádm pohled na XML, do kterého EventsEditor vše ukládá a podle kterého to pak při běhu programu vše funguje:

    17

     

    Samozřejmě, že tento formulář není nic mimořádného a zrovna snadno a rychle by se dal udělat ve WinForm, nicméně tam už se neobejdeme bez psaní kódu. Můj přístup, odhlédneme-li od použití excelu, je odlišný tím, že jsem se řadu let snažil eliminovat rutinní psaní stále se opakujícího se kódu a naopak co nejvíce věcí popsat datově. Bylo to sice dál a težší cesta, ale dnes už díky tomu mohu chodit opravdovými zkratkami.

Více článků Další stránka »
Powered by Community Server (Personal Edition), by Telligent Systems
Vyvojar.cz na prodej!