C# - Real-Time Data Server (RTD), avagy automatikusan frissülő Excel cellák

forráskód letöltése
Képzeljünk el egy olyan Excel táblázatot, melynek celláiba az adatokat saját alkalmazásunk szolgáltatja, ráadásul valós időben frissítve akár. Mivel az adatok saját alkalmazásunkból kerülnek át az Excel celláiba, így az adatforrás és annak előállítási módja csak rajtunk múlik. Hogy milyen időközönként frissülnek a cellák az szintén csak az alkalmazásunkon múlik, vagyis attól függ, hogy az adataink milyen gyakorisággal változnak. Az Excel celláiba csupán egy egyszerű függvényhívást kell írni, pl.: =RTD("alkalmazás név";paraméterek) formájában és máris megkapjuk a programunk által visszaadott értéket. Ha az adatokhoz mondjuk egy grafikon is tartozik, akkor ennek tartalma is dinamikus lesz: ahogy változnak az adatok, úgy követi ezt a grafikon is.
Ha elképzeltük mindezt, akkor most valósítsuk is meg...
A megoldáshoz egy speciális DLL-t kell készítenünk, melyet COM szerverként regisztrálva használhatunk. Amikor az Excel egy-egy cellájába egy „=RTD(...” függvényhívás kerül, akkor az ott hivatkozott program indul el és ad értéket a cellának. Ezt az értéket ettől kezdve épp úgy felhasználhatjuk az Excel-ben mint bármely más függvény eredményét. Különbség csak annyi lesz, hogy e cellának értéke automatikusan változni fog, ahogy azt a programunkban előírjuk.
Nézzük most miként is történik a megvalósítás: hozzunk létre egy új projektet, mely Class Library típusú legyen, vagyis DLL állományba kerüljön a programunk. Ezt COM szerverként szeretnénk üzemeltetni, így a megfelelő regisztrációhoz válasszuk ki az RTDLibrary nevű projektünket, majd jobb gomb és Properties menüpont. A megjelenő ablakban válasszuk a Configuration Properties - Build elemet. Ekkor elérhetővé válik a Register for COM interop tulajdonság, melyet állítsunk igazra.
További előkészületként szükséges még az Excel névtér használatához annak importálása is. Ehhez válasszuk a Project - Add reference menüpontot is, majd a COM lapon keressük elő a Microsoft Excel 10.0 Object Library elemet. A verziószám lehet különböző, de a Real-Time Data Server szolgáltatás használatához minimálisan az Excel 10-es verzióját, vagyis az Excel XP-t kell használnunk. Ezek után már hivatkozhatunk az Excel névtérre:
using Excel;
Alkalmazásunk regisztrációjához, illetve későbbi azonosításához szükségünk lesz egy névre. Ezt az egyedi sztringet a ProgId attribútumba kell megadnunk alkalmazásunk részére. A létrehozott osztályban az IRtdServer interfész implementálása válik szükségessé, hogy megvalósíthassuk a kívánt funkciót.
  [ProgId("Animare.RTDTest"), ComVisible(true)]
	public class RTDClass: IRtdServer
	{
     ...
Jelen példánkban a következő feladatot valósítjuk meg: olyan számokat szolgáltatunk az Excel cellák számára, melyeknek értéke véletlenszerűen nő, vagy csökken. A számok 0 és 1000 közé kell hogy essenek minden esetben. A feladat egyszerűsége miatt sok szolgáltatását nem használjuk ki a RTD-nek, jelen cikknek nem feladata egy valós helyzetben használt alkalmazás készítése, hanem csak annak bemutatása, hogy milyen alapvető lépések szükségesek egy ilyen lehetőség megvalósításához.
A további lépések előtt tegyünk egy kis kitérőt és nézzük meg, hogy miként is alkalmazhatjuk majd az Excel cellában az alkalmazásunkat. Ez az alábbi képlet beszúrásával oldható meg:
=RTD("Animare.RTDTest";;1)
Az RTD függvény első paraméterében azonosítjuk az alkalmazásunkat. Ide kerül az a sztring, melyet a ProgId attribútumban megadtunk. A függvény második paraméterében megadhatnánk egy gépnevet. Erre akkor van szükségünk, ha alkalmazásunk nem azon a számítógépen fut, ahol az Excel tábla van, hanem a hálózat egy másik gépén. A harmadik és a további paraméterek tetszőleges értékeket tartalmazhatnak. Ezeket az értékeket alkalmazásunkból ki tudjuk majd olvasni és tetszés szerint felhasználni. Ezt a lehetőséget tudjuk kihasználni annak érdekében, hogy az Excel tábla különböző celláiba alkalmazásunk által különböző értékeket adjunk vissza, különféle feltételeknek megfelelően.
Térjünk most vissza az alkalmazásunk fejlesztéséhez. Itt nincs más teendőnk, mint az IRtdServer interfész függvényeihez a szükséges kódot megírnunk.
A ServerStart függvény akkor kerül meghívásra, amikor az Excel táblában egy RTD hívás található és az Excel aktivizálja programunkat. Itt paraméterként kapunk egy IRTDUpdateEvent interfészt, melynek segítségével elvégezhetjük majd az automatikus adatfrissítést. Erre később visszatérünk még, most csak annyi a teendőnk, hogy tároljuk ennek a paraméternek az értékét egy saját változónkba.
    public int ServerStart(Excel.IRTDUpdateEvent CallbackObject)
    {
      updateEvent = CallbackObject;
Az automatikus adatfrissítést most úgy valósítjuk meg, hogy létrehozunk egy Timer komponenst, melyet megkérünk, hogy másodpercenként hozza létre az Elapsed nevű eseményét. Itt fogjuk majd az Excel tábla celláit frissíteni.
      timer = new Timer(1000);      
      timer.Elapsed+=new ElapsedEventHandler(TimerElapsed);
      timer.Enabled=true;
      return 1;
    }
A ServerTerminate függvény akkor kerül meghívásra, ha alkalmazásunkra a továbbiakban már nincs szükség. Ekkor nincs más teendőnk mint leállítjuk az időzítőt, hogy a továbbiakban ne frissüljenek a cellák.
    public void ServerTerminate()
    {
      timer.Enabled=false;
    }
A következő függvény a ConnectData nevű lesz. Ez akkor kerül meghívásra mikor az Excel értelmezte az RTD hívást, már létrehozta a kapcsolatot a programunkkal (ServerStart) és a cellába beírná a kezdő értéket. A ConnectData által visszaadott értéke lesz a cella kezdőértéke. Mivel az Excel tábla több RTD hívást is tartalmazhat, melyet azonos alkalmazás szolgál ki, így szükség van arra, hogy az egyes cellákat meg tudjuk különböztetni egymástól. Ezért minden ilyen cella kap egy egyedi azonosítót. A ConnectData annyiszor kerül meghívásra, ahány ilyen cella van. A függvény első, TopicID paraméterében kapjuk meg a cella azonosító számát, melyet célszerű eltárolnunk, hogy később hivatkozhassunk erre a cellára. A tároláshoz most egy NameValueCollection-t használunk. Ebbe a kollekcióba olyan elemeket tudunk tárolni, melyeknél két sztring kap helyet elemenként. Az első sztringben fogjuk tárolni a TopicID cella azonosító számot, a másikban pedig a cella aktuális értékét, melyet most 500-ra állítunk, mint kezdő értéket.
Kapunk egy Strings nevű paramétert is, mely egy olyan tömb, amelybe az RTD hívásánál megadott tetszőleges számú paraméterünk kaphat helyet. Jelen példánkban erre nincs szükségünk, így ezt nem használjuk.
    public object ConnectData(int TopicID, ref Array Strings, ref bool GetNewValues)
    {       
      data.Add(TopicID.ToString(), "500");
      return 500;
    }
Amikor egy-egy RTD cellára már nincs szükség, mert például a felhasználó törölte ezt a cellát az Excel táblából, akkor kerül meghívásra a DisconnectData. Itt a paraméterként kapott TopicID tartalmazza a törlendő érték azonosítószámát. Ekkor csak annyi a teendőnk, hogy a NameValueCollection kollekciónkból töröljük az ott tárolt értéket.
    public void DisconnectData(int TopicID)
    {
      data.Remove(TopicID.ToString());  
    }
A RefreshData függvény akkor kerül meghívásra, ha az Excel tábla RTD celláinak tartalmát frissíteni kell. Ez a függvény egy tömböt vár visszatérési értékként. Ebbe a tömbbe írhatjuk le, hogy aktuálisan milyen értékeket szeretnénk a cellákhoz rendelni. A tömbnek kétdimenziósnak kell lennie: az egyik dimenzió a cella azonosítókat tartalmazza (TopicID), a másik az adott cellához tartozó aktuális értéket. A tömb elemei object típusúak. A paraméterként kapott TopicCount-ba kell megadnunk az Excel számára, hogy a visszaadandó tömb hány elemet tartalmaz, vagyis hány cellának az adatait adjuk vissza, mivel nem kötelező állandóan az összes cellát frissíteni.
Nincs tehát más teendőnk, mint létrehozni egy kétdimenziós tömböt, melybe elfér az összes adatunk és ezt egy for ciklussal feltölteni a tárolt NameValueCollection kollekciónkból, majd az így létrehozott tömböt visszaadni az Excel számára.
    public Array RefreshData(ref int TopicCount)
    {
      object[,] a = new object[2, data.Count];

      TopicCount=data.Count;
      
      for (int i=0; i<data.Count; i++)
      {
        a[0, i] = Convert.ToInt32(data.GetKey(i));
        a[1, i] = Convert.ToInt32(data.GetValues(i)[0]);
      }
      return a;
    }
Végül már csak egy dolgunk maradt: meg kell határoznunk, hogy a frissítés mikor jöjjön létre, vagyis az Excel mikor hívja meg a RefreshData függvényünket. Ehhez, mint már említettük a Timer-t használjuk fel. Amikor lejár a beállított egy másodperc, akkor meghívjuk az UpdateData függvényünket, mely elvégzi a tárolt adatok frissítését. Ezt követően már csak az Excel-t kell értesítenünk, hogy az adatok frissültek, ideje kiolvasni azokat. Ehhez a ServerStart függvény hívásakor kapott IRTDUpdateEvent interfész UpdateNotify függvényét kell meghívnunk. Ennek hatására az Excel meghívja a RefreshData függvényt és frissíti a cellákat.
    public void TimerElapsed(object source, ElapsedEventArgs e)
    {
      UpdateData();
      updateEvent.UpdateNotify();
    }
Az UpdateData függvény végigmegy az összes tárolt adaton és véletlenszerűen növeli/csökkenti az aktuális értéket 0 és 1000 között.
Ezzel alkalmazásunk el is készült. Fordítsuk most le. A DLL létrejöttével a regisztráció is megtörténik, hiszen ezt beállítottuk az elején. Nézzük most hogyan is tudjuk felhasználni a programunkat. Létrehozunk egy Excel táblát és cellába beírjuk az alábbi hivatkozást:
=RTD("Animare.RTDTest";;1)
Ennek eredménye egy hibaüzenet lesz! Ez annak köszönhető, hogy az Excel-ben alapértelmezetten a legmagasabb védelmi szint van beállítva, mely megakadályozza, hogy kártékony programkódok lefussanak, ami helyes is, csak épp lehetetlené teszi saját alkalmazásunk futását is. Ennek megoldásaképp csökkentsük a védelmi szintet közepesre. Így minden program futtatása előtt egy kérdést kapunk az Excel-től, hogy engedélyezzük-e vagy sem. Ezzel a kártékony kódok futását a továbbiakban is megakadályozhatjuk, de saját alkalmazásunk futtatására most már mód nyílik.
Ehhez válasszuk az Excel-ben az Eszközök - Makrók - Biztonság menüpontot. A megjelenő ablakban a biztonsági szintet állítsuk közepesre.

Ha most megnyitjuk a RTD cellákat tartalmazó Excel táblát, akkor kapunk egy figyelmeztető üzenetet, melyben engedélyezhetjük, vagy tilthatjuk a program futását. Az itt megjelenő ablakban egy érdekes dologra figyelhetünk fel: az üzenet szerint a futtatandó állomány a C:\WINDOWS\System32\mscoree.dll és nem is a saját alkalmazásunk lenne. Ennek oka a következő: a .NET-ben készült DLL COM szerverként való működéséhez elengedhetetlen némi külső segítség, mely a mscoree.dll lesz. Ez fogja majd aktivizálni a mi DLL-ünket, de ennek részletei már egy külön téma.
Ha megnézzük a Windows regisztrációs adatbázisban az Animare.RTDTest bejegyzést, akkor ott látható, hogy C:\WINDOWS\System32\mscoree.dll van megadva futtatandó alkalmazásként, de található egy másik bejegyzés is, mely már ténylegesen a mi programunkra hivatkozik:
CodeBase   file:///F:/So/Cso/0214/RTDLibrary/bin/Debug/RTDLibrary.DLL