Adatkötés .NET módra – nem eléggé rugalmas

Azt nem kell senkinek ecsetelnem, hogy a .NET-et adatkötés, a DataBinding mennyire nagy szolgálatot tesz, mennyire megkönnyíti az objektumok és felhasználói felületek összekötését, a kapcsolat tipusos kezelését. De mégis belefutottunk tegnap egy hiányosságba és 4 munkaórányi problémamegoldás után el kellett vetnünk tervünket, más megoldást kellett keresni.

Az adatkötésben van egy kis automatizmus, mégpedig az, hogy a rendszer egy vezérlőhöz (UI elem) történő adatkötés során megpróbálja kitalálni, hogy melyik adatkötési mechanizmust akarjuk használni. Egészen pontosan, ha egy TextEdit-hez szeretnénk egy property-t kötni, akkor két dolog történhet.

  1. Amennyiben a forrás objektum egy “sima” objektum, úgy az adatkötés a megnevezett (stringként átadott) property-hez megtörténik. Ha jól paramétereztük fel az adatkötést, akkor a beviteli mező változása a property-ben rögtön megjelenik és vica-versa.
  2. Ha a forrás objektum valamilyen gyűjtemény (IList, IBindingList, Array), akkor a megnevezett property, amihez kötni szeretnénk a gyűjtemény elemeiben kell, hogy létezzen. És az adatkötés is ide történik. List<Customer>-hez való adatkötésnél például a “Name” propertynek a Customer osztályban kell léteznie. Ilyenkor (gyűjtemény esetén) a gyűjtemény elemét vizsgálja a databinding és ott keresi a property-t.

Ez utóbbi teljesen kézenfekvő, hiszen ha egy listát akarunk valahova kötni, akkor a lista egy oszlopát akarjuk valahova kötni. Első esetben egy PropertyManager jön létre, utóbbi esetben pedig egy CurrencyManager. Mindkettőn értelmezett a lépkedés, de az elsőn nincs nagyon értelme.

binding

A mi problémánk viszont a következő. Nem tudjuk befolyásolni, nem tudjuk elérni, hogy egy gyűjtemény típusú forrás objektum adott property-jéhez kössünk vezérlőt. Egy listának is van Count-ja. Ehhez nem tudtunk hozzákötni mondjuk egy Label-t, mert az adatkötés problémaként adta vissza nekünk, hogy a Customer objektumnak nincs Count-ja. De nem is oda akartunk kötni!!!

A probléma megoldása abban rejlik, hogy a property nevét nem szabad kitölteni. Ez már amolyan vicces dolog, hogy egy “változó” megadok a rá való hivatkozással, majd/és opcionálisan még stringként is hozzáírhatom a metódushoz a “változó” nevét.

Jó megoldás: label.DataBindigs.Add("Text", list.Count, "")
Nem jó megoldás: label.DataBindigs.Add("Text", list, "Count")

Hmm, állítólag WPF-ben az adatkötés jobb. Nemsokára úgyis indul egy WPF-es koncepció-projektünk, majd kiderül…

RTF normalizálás – vissza-visszatérő fájlformátumok

Régen elfeledett formátumok térnek vissza néha-néha. Mekkora találmány volt régen (állítólag 1987-ben) az RTF formátum. Elfeledtük azóta, mindenhol a HTML, XHTML, XML a divat. Most újra elővettük egy projektünk erejéig és bele is futottunk egy feladatba. De pont ezért dolgozunk itt, kihívásokra várunk.

Ügyfelünk dokumentumokat szeretne készíteni úgy, hogy azokat dolgozóadatokkal tölti fel. Ne kelljen az újonnan belépő dolgozónak word-ben megszerkeszteni a belépőkártya igénylő lapját, a munkaidőnyilvántartó lapját. És még sorolhatnánk…

Office dokumentumok kicsit nehézkesen kezelhetőek, célunk volt, hogy az adatok behelyettesítése után a lapok elküdhetőek legyenek a nyomtatón kívül PDF-be, vágólapra, stb. Ezért jött az ötlet, legyen RTF. Van jól használható RTF editorunk, abban a felhasználó a dokumentumait elkészíti, adatbázisban tárolhatja. Persze elhelyez benne helyettesítő maszkokat: #NEV#, #SZULDATUM#. (Ezek formátuma a régi rendszerről való átállás miatt kötött volt).

document

A projekt ezen része elkészült, az adatok a megfelelő algoritmus szerint cserélődtek, a #NEV# helyére bekerült a dolgozó neve és így tovább. A tesztek során kiderült, hogy átlagos felhasználás (nem programozói, azaz laboratóriumi környezetben) a maszkok kicserélése nem is megy olyan szépen, mint azt hittük. Kiderült a turpisság: Ahogy a felhasználó szerkeszti a szöveget és néha-néha visszatöröl, újra beleír, az RTF szerkesztő új szekciót nyit neki az RTF forrásban. A megjelenítést ez nem befolyásolja, de a cserénél előfordul, hogy a szöveget nem tudjuk cserélni, mert a #NEV# maszk 3 részre bomlott és #N, EV és # lett belőle. String.Replace() pedig csak nevetett rajtunk.

Normalizálnunk kellett az RTF-et. Egy teljes RTF értelmezőt (szétbontó, építő) írni nem lett vona kivitelezhető, tekintve a projekt ezen moduljának költségvetését. Azonban egy minimális korrekciós algoritmus belefért, amely az egymás után következő, egyformán formázott elemeket összevonja eggyé:

{f1cf0 Ar}{f1cf0 ra szeretnék}{f1cf0 rámuta}{f1cf0 tni}

Tervezésünket siker koronázta, végre megmozgattuk SQL parancsokban ellaposodott, algoritmikus “agyféltekénket” is.

Office 2010 – felkészülés

Cégünk megkezdte az Office 2010 rendszerrel való együttműködésre való felkészülést.

Sok energiát fektetünk abba, hogy minél jobban kiismerjük a rendszer újdonságait és ezeket a jövőben ki is tudjuk használni. Rendszereink kompatibilisek maradnak a korábbi Office verziókkal is, de terveink szerint számos új lehetőséget fogunk felhasználni következő programverzióinkban.

office2010

Egy nagyon jó marketing film (úgynevezett trailer) érhető el itt.

Felhasználói szintű információkat tett közzé a Microsoft az alábbi oldalon.

Sharepoint 2010 sokkal szakmaibb szemmel (úgynevezett sneak videón) az alábbi linken.

Igazán nagy kihívás lesz számunkra, hogy ezeket az eszközöket a hazai környezetbe illesszük, tekintettel az itthoni cégek finanszírozási kérdéseire és nem utolsó sorban az újdonságtól való “félelem” leküzdésére.

Administrator – a kifejezés eredete

A számítástechnikában mindenki ismeri az administrator szót, sőt a sima felhasználó is biztosan találkozott már vele, mert pár kivételtől eltekintve minden számítógépes program, amely felhasználókat kezel alapértelmezetten az ADMIN felhasználót létrehozza. Kivételként említhetjük az Oracle-nél alkalmazott Scott, James, Adams, stb. felhasználókat, illetve az MsSql SA-ját, ami kifejtve szintén tartalmazza az Admin szót (SysAdmin). De honnan maga a kifejezés?

Annyira régi, annyira elterjedt, hogy bele sem gondolunk, azt valószínűsíthetnénk, hogy Kolombusz ültette el az első Admin fát az újvilágban vagy az amerikai kontinens Kazinczy-ja lehetett az első, aki ezt a kifejezést alkalmazta.

Csapatunkat mégsem hagyta nyugodni a dolog. Szóösszetételekben gondolkodtunk és beugrott egy kifejezés összetétel: AdMinistrator. A ministrator a miniszter szóra hasonlít, a latin minister-ből eredhet, ami szolgát jelent. Ennek politikai, közigazgatási jelentősségét most ne firtassuk! Az Ad pedig nyilvánvalóan hirdetést jelent, újkori rövidítése a szintén újkori advertisement szónak. (Ennek latin forrása nincs, régi korokban nem volt hirdetés).

symboltechcard800

Talán ráhibáztunk a kialakulás módjára is. A web-es, elektronikus hirdetések szolgáját, aki kezelte, adminisztrálta (önmagával magyarázzuk a dolgot?) a hirdetéseket hívhatták AdMinistratornak. Az, hogy a számítógéphez jól értő emberek a hirdetések mellett más dolgokat is kezeltek, nyilvánvaló következménye az eseményeknek.

A nálunk dolgozó Ad Miniszterek (adatbázis, IIS, Small Business) kérjük ne érezzék rosszul magukat a szolga szó olvasása közben, csapatunk nagyrabecsült tagjai ők, a szó régi jelentését már elfelejtettük.

HashList – Dictionary többszörös kulccsal

A pascal-os időszámításhoz képest a modern framework-ökben számos segédeszköz rendelkezésre áll, sok adattároló szerkezetet használhatunk, sőt a nagy ugrás, a generikus adattípusokra az ember úgy gondol, mint a netovábbra, a végtelenre. De volt, amit nem találtunk meg…

KeyValue párokat szerettünk volna tárolni, Key szerint keresni, Value-kat kinyerni. Jött az ötlet Dictionary<Key, Value>. Specifikáció szerint viszont ennek egy Key-je egyszer szerepelhet a gyűjteményben, minden további értékeadás felülírja az előzőt. Azaz a Key-eknek egyedinek kell lenniük. Pár éve már sejtettük ezt.

Nekünk viszont irányítószámokat kell tárolnunk és hála a magyar közigazgatásnak nem minden település kap saját számot, előfordul, hogy egynél több település fut ugyanolyan “kód” (irányítószám) alatt. Az ötlet, mely szerint Dictionary<int, List<string>> adatszerkezetbe rakjuk az információkat, jónak tűnt, de hátha ezt már valaki kidolgozta.

És ekkor találtuk meg többedszerre az NGenerics családot, amely számos, C#-ból kimaradt, bonyolultságuk és sokrétűségük miatt speciális tudást igénylő generikus típussal engedi bővíteni a készletet. Ennek HashList osztályát használtuk, amely az alábbi nyelvi konstrukcióban engedte feltölteni ZIP (IRSZ) törzsünket.

 

citylist.Add(2066, "Szár");
citylist.Add(2066, "Újbarok");

 

Az osztálygyűjtemény nagyon nagy. Szerettük volna kerülni az irányítószám kezelés miatt akár 30-40 másodperccel is megnövekedett fordítási időt. Ezért csak bizonyos részeket használtunk fel belőle. Matematikusok tervezték, tele van interfésszel, látogató termintával és minden szépséggel, emiatt az általunk használt osztályhoz a következő fájlok mindegyikére szükség volt:

 

HashList.cs
IVisitable.cs
IVisitableCollection.cs
IVisitableDictionary.cs
IVisitor.cs
VisitableHashtable.cs

 

Projektünk sikeresen kiadásra került, a projektben résztvevő személyek mindegyike sikeresnek értékelte a megoldást. Köszönjük NGenerics!

Ha várni kell a Windowsban – A jó öreg homokóra

Gyakran fordul elő, hogy homokórát kell megjelenítenünk, mert valamilyen művelet hosszabb időt vehet igénybe. A felhasználó pedig várakozzon, ahelyett, hogy a beviteli mezőket próbálja szerkeszteni vagy a gombokat működésre bírni.

Régebbi programozási nyelvekben (Delphi, FoxPro) a hosszú műveletek automatikusan beállították a homokóra üzemmódot. Viszont nem kezelték jól a beágyazott műveleteket, azaz 10 darab SELECT végrehajtása egymás után 10 homokórás villogást eredményezett. Ez a mai világban inkább szégyenletes, mint felhasználóbarát.

 

Ezt a problémát oldottuk meg az alábbi kódrészlettel:

using System;
using System.Windows.Forms;
namespace SymbolTech.Common
{
    public class WaitCursor : IDisposable
    {
        private static int waitcursorlevel = 0;

        public WaitCursor()
        {
            waitcursorlevel++;
            if (waitcursorlevel > 0)
                Cursor.Current = Cursors.WaitCursor;
        }

        public void Dispose()
        {
            waitcursorlevel--;
            if (waitcursorlevel == 0)
                Cursor.Current = Cursors.Default;
        }
    }
}

Minden (hosszabb) műveletet beágyazunk egy using(new WaitCursor()) blokkba. Ezzel a következő előnyöket érjük el:

  1. A műveletek minden esetben “homokórázni” fognak.
  2. A beágyazott műveletek (külső blokk már homokórában dolgozik, a belső, mondjuk 100 iteráció is bekapcsolja a homkórát) nem fogják villogtatni a kurzort.
  3. Véletlenül sem felejtjük el visszakapcsolni az alapértelmezett kurzort, amennyiben a műveletek végrehajtásra kerültek.

 

Lássunk egy példát, a használatára:

        private void DummyMethod()
        {
            using (new WaitCursor())
            {
                Thread.Sleep(100);
            }
        }
        private void button1_Click(object sender, EventArgs e)
        {
            using (new WaitCursor())
            {
                Thread.Sleep(500);
                for (int i = 0; i < 10; i++)
                    DummyMethod();
            }
        }

A példában egy beágyazott, többször (10x) végrehajtott műveletet látunk, amelyek összességében 1.5mp-re átkapcsolnak homokórára, de közben nem villog a kurzor.

TabCsapda – hol a kurzor?

Tesztelő csapatunk fura hibával állt elő, nemsokára megjelenő program release-ünk áttekintésekor. A TAB-bal való navigáció – amely a Win95 környékén jelenhetett meg és minden felhasználó hozzászokott az elmúlt 15 évben – valahol elveszti működését és megáll.

A GridControl volt a hibás, ugyanis furcsa működéséből adódóan egy táblázatban is lehet tabbal lépkedni, vagy a sorok vagy a cellák között és amikor a végére érünk, nem tudunk merre menni. Nem is tudom, hogy miért ez az alapértelmezett működés, de kikapcsoltuk. Sok dolgunk nem volt, 2 (!!!) sort kellett a megfelelő helyen adaptálni és az összes termékünk következő kiadásában megjelenik a TabCsapda elleni megoldás.

Kivételek típusai – melyiket dobjam?

A kivételkezelés alatt sok fejlesztő a catch ág megvalósítását gondolja, de ugyanolyan fontos a kivételek eldobása is. Nem szabad azzal megelégedni, hogy dobunk egy ApplicationException-t, sokkal precízebb, ha a típusos esetekben (rossz paraméter, nem jó képformátum) a beépített kivételosztályokat használjuk.

Lássuk, mik ezek:

Kivétel osztály Kiváltás oka
SystemException Futásidejű hiba, a kivételes ősosztálya
AccessException Egy típus elemeléréseinek hibája (metódus, mező, property)
ArgumentException Metódushívás esetén hibás paraméter
ArgumentNullException Metódushívás esetén null paraméter, ha azt a metódus nem tudja kezelni
ArgumentOutOfRangeException Paraméter értéke adott határokon kívül esik
ArithmeticException “Matematikai” hiba
ArrayTypeMismatchException Típusos tömbön végzett művelet egy idegen típussal
BadImageFormatException Rossz képformátum
CoreException Futásidejű kivételes ősosztálya
DivideByZeroException Nullával való osztás
FormatException Argumentum formátuma nem helyes (pl: String.Format)
IndexOutOfRangeException Tömb indexelése túlmutat a határokon
InvalidCastExpression Futásidejű Cast művelet nem hajtható végre
InvalidOperationException Nem megfelelő (idejű?) művelet hívása
MissingMemberException DLL verziószám ütközés, eltérés metódushívás közben
NotFiniteNumberException Nem valós szám (decimal, float; NaN, Infinity)
NotSupportedException Nem létező metódus hívása (reflection?)
NullReferenceException NULL értékű változó által hivatkozott objektum elérése
OutOfMemoryException Memória elfogyás
StackOverflowException Verem műveletek memória elfogyása (rekurízió)

A fenti lista számos lehetőséget kínál a fejlesztőknek a megfelelő kivétel eldobásában. Ezek használata nagyban megkönnyíti a hibakezelést és hibakeresést, a metódus írója pedig publikálhatja, hogy mit várt és mit kapott.

Egyedi sorosítás – IXMLSerializable megvalósítás

C#, remek nyelv. Szinte mindent lehet sorosítani, XML-be menteni. Nem is kell kézzel ezeket megírni, összerakni. De egy pár dolog kimaradt. Talán oka is van, hogy miért, de ez nem lényeg. A lényeg, hogy meg lehet valósítani.

A generic List<>-et lehet sorosítani, de előtte kell egy plusz származtatási szinten készíteni. Dictionary<T,U>-t nem lehet. De van megoldás. Csináljunk egy sorosítható Dictionary-t.

    [XmlRoot("Dictionary")]
    public class SerializableDictionary<TKey, TValue> : IXmlSerializable
    {
        public XmlSchema GetSchema()
        {
            return null;
        }
        public void ReadXml(XmlReader reader)
        {
            XmlSerializer keySerializer = new XmlSerializer(typeof(TKey));
            XmlSerializer valueSerializer = new XmlSerializer(typeof(TValue));
            bool wasEmpty = reader.IsEmptyElement;
            reader.Read();
            if (wasEmpty)
                return;
            while (reader.NodeType != XmlNodeType.EndElement)
                try
                {
                    reader.ReadStartElement("Item");
                    reader.ReadStartElement("Key");
                    TKey key = (TKey)keySerializer.Deserialize(reader);
                    reader.ReadEndElement();
                    reader.ReadStartElement("Value");
                    TValue value = (TValue)valueSerializer.Deserialize(reader);
                    reader.ReadEndElement();
                    reader.ReadEndElement();
                    reader.MoveToContent();
                    this.Add(key, value);
                }
                catch { }
            reader.ReadEndElement();
        }
        public void WriteXml(XmlWriter writer)
        {
            XmlSerializer keySerializer = new XmlSerializer(typeof(TKey));
            XmlSerializer valueSerializer = new XmlSerializer(typeof(TValue));
            foreach (TKey key in this.Keys)
            {
                writer.WriteStartElement("Item");
                writer.WriteStartElement("Key");
                keySerializer.Serialize(writer, key);
                writer.WriteEndElement();
                writer.WriteStartElement("Value");
                TValue value = this[key];
                valueSerializer.Serialize(writer, value);
                writer.WriteEndElement();
                writer.WriteEndElement();
            }
        }
    }

Ahogy látható, csak a saját szintünket kell megvalósítani, a TKey és TValue elemek sorosításával már a saját osztályuk foglalkozik. Extrém eset ha a TValue is egy sorosítható Dictionary (valós példát nem tudunk jelenleg mondani), ilyenkor ennek az objektumnak a sorosításakor szintén a fenti algoritmus fog lefutni.

Diagram engine – miért C#

Ügyviteli rendszereknél ritka, hogy diagramot jelenítünk meg valamit, hiszen minden adat táblákban van, azok megjelenítéséhez pedig ideális valamiféle grid. Néha szoktak beépített grafikon segítségével felhasználókat elképráztatni, de az egyedi rajzolásos digram nem mindennapos.

Az üzletkötőink, ügyviteli szakembereink sem értették, hogy mit akarunk ezzel, miért nem jó szerintünk a sima kis lista. Csak a végén értették meg, hogy mit is akartunk.

Ezt:

voucherdiagram

Hogyan is jött létre ez a – joggal innovációnak nevezhető – modul, amely minden ügyviteli termékünkben megjelenik és sok pozitív felhasználói visszajelzést indukált? A bizonylatok és egyéb adatelemek közti reláció, adatkapcsolat adott, erre épül maga az alkalmazás. No de ezt hogy jelenítsük meg egy gráfban, hogy  felhasználó is kedvet kapjon és használja?

Csapatunk 3 napon keresztül tervezett, brainstormingolt. Számos ötlet jelent meg a fejekben és a nagy prezentációs falon, ezen ötletek kb. 50% bele is került a megvalósult rendszerbe. Az elvárások tisztázása után kezdődött a fejlesztés. Programnyelv C#, ennek GDI és GDI+ lehetőségei kiváló kiaknázásra való területet jelentettek számunkra.

Lépések:

  • Építsünk egy saját controlt.
  • Bármilyen megjelenni kívánó objektum feleljen megy egy IDiagramDrawItem interfésznek, amely egy metódust definiál void Draw() és információt szolgáltat arról, hogy melyik rétegben jelenik meg a kirajzolandó adatelem int ZOrder { get; }.
  • Rajzolás megvalósítása
    • override Paint()
    • void ClearAndDrawBackground()
    • void SortByZOrder()
    • foreach(... item.Draw()...)

Már a tervezési fázisban is láthatóvá vált, hogy lesznek optimalizálási kérdések, problémák, amelyek a témakör izgalmasabb részét jelentik.

Repository, hogy ne szaggasson a kép. Egyedileg rajzoló eljárások rákfenéje a sok GDI objektum, amely memóriában és időben is költséges. A memóriabeli költségeket, mivel IDisposasble, meg lehet oldani, de sokáig tart minden OnPaintben Font-os, Pen-t és Brush-t létrehozni. Erre született megoldásként, hogy a diagram ezeket publikálja, tárolja, elérhetővé teszi eg példányban, egy alkalommal való létrehozással a rajzolni képes objektumok felé. Performancia mérést végeztünk, egy átlagos diagram 740 alkalommal tud kirajzolódni egy másodperc alatt. Nem rossz, megfelel.

GraphicsPath, hogy lekerekített sarkú legyen a doboz. A C# GDI+ lehetőséget kínál arra, hogy vonalak és görbék segítségével egy “utat” hozzunk létre, amely felhasználható rajzolásra és kitöltésre egyaránt. Megvalósítottuk. Mivel ez is költséges, minden objektum a mérete alapján (amely nem állítható megjelenítés közben) első felhasználáskor (late-init) létrehozza a GraphicsPath objektumot.

LinearGradientBrush, hogy átmenetes legyen a dobozok háttere. A legnagyobb kihívás a színátmenetes doboz volt, amely egy pontoktól függő GradientBrush lett. Ennek szintén elég egy példányban léteznie, de a doboz pozíciójának függvényében kell felparaméterezni. A (rendszer által) mozgatható dobozok pedig ezen tulajdonságukat gyakran változtatják, de erre is született megoldás: late-init és re-init-by-move.

És ekkor még nem ért véget a gondolkodás, a dobozok esztétikus elhelyezésének algoritmusa felér egy komolyabb diplomamunka témakörével, erről egy következő cikkben.