Search  
Friday, November 24, 2017 ..:: Articole » Proiectaţi propriul dvs. generator de rapoarte ::.. Register  Login
 Articole Minimize

Proiectaţi propriul dvs. generator de rapoarte

Autor: Mark Wilson
Notă: ...Şi toate popoarele se rugară cu jale: "O, mare zeu Microsoft, când ai creat puternicul dar imperfectul Visual FoxPro, de ce ai creat şi instrumente de proiectare a rapoartelor despre care toţi sunt de acord că sunt atât de proaste încât put până la ceruri?"... Ei bine, înainte de a începe să rupeţi hainele de pe voi de deznădejde, manipulând instrumentele învechite incluse în Visual FoxPro, sau de a introduce produse ale altor firme de software, luaţi în calcul varianta creării propriului dvs. generator de rapoarte, folosind componentele ActiveX pe care le aveţi deja.

Rareori puteţi găsi o aplicaţie care să nu aibă nici un fel de date de ieşire. Nu contează cât vorbim despre o societate complet lipsită de hârtii, clienţii şi utilizatorii obişnuiţi nu cred nimic decât dacă pot tipări o hârtie şi s-o bage într-un dosar. În caz contrar, totul este magie şi miracol - o iluzie optică ce poate dispărea la următoarea apăsare de tastă.

Dar o aplicaţie bine făcută trebuie să fie capabilă să facă mult mai mult decât atât. Utilizatorii vor să poată lua aceste date de ieşire - gata formatate - şi să le afişeze, să le tipărească, să le salveze, să le încarce, să le trimită prin e-mail, să le combine în alte rapoarte, să le detalieze, ş.a.m.d.

Într-un târziu, am renunţat să folosesc instrumentele de raportare din Visual FoxPro, dar am rămas cu o întrebare în minte: CUM NAIBA FAC UN RAPORT DECENT? La început, generam textul RTF într-un fişier, apoi cu o aplicaţie ordinară verificam directorul respectiv şi tipăream toate fişierele pe care le găseam acolo, folosind o instanţă a lui Microsoft Word. Dar în Visual FoxPro 6.0 mi s-a ivit o nouă posibilitate: pot să creez o clasă bazată pe un formular, care conţine două controale ActiveX: WebBrowser şi RichText. Astfel, pot să folosesc această clasă pentru a formata, afişa şi tipări rapoartele fără să părăsesc aplicaţia şi folosind flexibilitatea a două limbaje de formatare a documentelor: RTF şi HTML.

Detalierea strategiei

Pentru mine, există doi factori determinanţi în proiectarea clasei: ea trebuie să fie simplă şi robustă, şi trebuie să fie utilizabilă şi de utilizatori şi de dezvoltatori în aceeaşi măsură.

Utilizatorii trebuie să aibă posibilitatea să vizualizeze raportul pe ecran şi să facă orice vor cu un singur click. Acest lucru implică adăugarea unor butoane sau a unor opţiuni în meniu care să permită salvarea, tipărirea, ataşarea la un e-mail, ş.a.m.d. Trebuie să includem şi condiţii suplimentare dacă dorim ca utilizatorul să poată să detalieze - exact, să "click-ăie" pe un articol din raport şi să obţină un subraport care conţine date privitoare la acel articol.

Pe de altă parte, dezvoltatorii trebuie să poată genera un raport creând o instanţă a acelei clase, apoi să o populeze cu date folosind numai câteva linii de cod, fără să fie nevoiţi să cunoască detalii despre limbajul de formatare a documentului folosit. Acest lucru poate fi îndeplinit printr-o serie de metode ale clasei, invocate de către programul care instanţiază clasa. Trucul constă în a face aceste metode cât mai intuitive, astfel încât programatorul să se poată concentra asupra datelor, nu asupra formatării textului.

În mod ideal, această clasă trebuie să fie complet portabilă, să poată fi folosită fără modificări în orice aplicaţie care necesită date de ieşire, şi adaptabilă la aplicaţia părinte după dorinţă. De asemenea, trebuie să suporte instanţe multiple, deoarece utilizatorii vor de multe ori să revizuiască şi să compare mai multe rapoarte simultan.

Folosirea controlului WebBrowser

Cu toate că RTF este în unele privinţe cel mai versatil limbaj de formatare a documentelor, am hotărât să folosesc WebBrowser (şi implicit - HTML), deoarece controlul RichText este capabil de foarte puţine lucruri, în implementarea sa curentă.

Controlul WebBrowser are şi el propriile sale părţi proaste. În principiu, este o versiune redusă a lui Microsoft Internet Explorer, încapsulată sub forma unui control ActiveX. În procesul de reducere a sa, Microsoft a eliminat o mare parte din funcţionalitate; ca atare, el nu funcţionează exact în acelaşi mod ca şi Internet Explorer. Acesta este motivul pentru care controlul ignoră o parte din proprietăţi - ele sunt moştenite din Internet Explorer. Cea mai mare parte din aceste proprietăţi se referă la componentele lui Internet Explorer care nu pot fi instanţiate de controlul WebBrowser, şi anume tot ce este în afara ferestrei browser-ului: bara de stare, caseta de adrese, butoanele Forward şi Back, ş.a.m.d. Vestea bună este că există evenimente care permit simularea acestor componente, dacă le doriţi; vestea proastă este că va trebui să faceţi dvs. totul pentru aceasta.

Programatorii care au încapsulat controlul WebBrowser nu l-au prevăzut cu multe metode. În locul lor, au implementat o metodă intermediar pentru toate celelalte, numită ExecWB, prevăzută cu parametri care indică acţiunea de executat şi dacă utilizatorul trebuie consultat în vreo privinţă. De exemplu, ExecWB(6,1) determină controlul WebBrowser să tipărească documentul curent ("6" înseamnă tipărire), afişând utilizatorului ("1") caseta de tipărire standard. Astfel, metoda ExecWB extinde posibilităţile limitate ale controlului WebBrowser.

Pentru că, după cum bine ştiţi, un plus într-ul loc înseamnă un minus în alt loc, această metodă necesită o gestionare serioasă a erorilor. Anumite acţiuni asupra controlului WebBrowser pot duce la afişarea unui mesaj înspăimântător şi de neînţeles care vă acuză că "anulaţi un document neînregistrat". În particular, veţi obţine acest mesaj dacă daţi "Cancel" în caseta de dialog Print. Situaţia este des întâlnită, şi merită să depuneţi un efort pentru a trata problema. De asemenea, dacă un document nu reuşeşte să se încarce, lăsând controlul WebBrowser vid, toate comenzile care conţin metoda ExecWB vor afişa acelaşi mesaj.

Gestionarea specială a erorilor este mai dificilă în Visual FoxPro (în special pentru clasele reutilizabile). În mod ideal, aplicaţia ar trebui să activeze modul special de gestionare a erorilor atât timp cât obiectul WebBrowser este activ şi să revină la modul original de gestionare a erorilor (oricare ar fi acela) când obiectul este eliminat din memorie. Pentru a fi refolosibilă, clasa ar trebui să revină la gestionarul activ înainte de a se activa clasa. Din păcate, nu există o metodă evidentă de a citi codul dat de ON ERROR, aşa că revenirea la vechiul gestionar de erori este dificilă. Soluţia pe care am adoptat-o a fost de a include o metodă, numită StoreErrorCode, care execută comanda LIST STATUS TO FILE şi apoi parcurge acest fişier, căutând o linie cu ON ERROR. Dacă găseşte una, metoda încarcă numele gestionarului de erori şi îl depozitează într-o proprietate a clasei. Metoda StoreErrorCode este apelată din metoda Init a clasei. Metoda Activate a controlului WebBrowser activează gestionarul propriu (cu ajutorul comenzii ON ERROR), iar metoda Deactivate readuce gestionarul de erori la valoarea iniţială. În acest fel, gestionarul de erori propriu controlului este activ numai pe durata existenţei controlului însuşi.

O chestiune în legătură cu HTML: Nu voi încerca să explic cum se scrie cod HTML. Pe net există o multitudine de documentaţii excelente. Scopul meu este să demonstrez că folosirea eficientă a limbajului poate determina ca clasa despre care este vorba în această expunere să nu necesite deloc folosirea HTML în programul care o apelează.

Interfaţa

Necesităţile esenţiale variază de la programator la programator şi de la aplicaţie la aplicaţie. Lucrul important este că interfaţa (metoda prin care raportul este creat în interiorul aplicaţiei) este simplă şi poate manipula toate elementele de bază ale unui text HTML: body text, text styles, nivele multiple ale titlurilor, liste şi tabele.

Clasa descrisă în această expunere, numită WebReporter, crează un fişier text cu extensia .HTM, scrie raportul în acest fişier, apoi afişează fişierul într-un control WebBrowser. Mai precis: clasa va crea fişierul de-abia la sfârşit. Tot raportul este depozitat într-o proprietate a clasei, care este citită şi scrisă în fişier. Sunt două motive pentru care procedez aşa: mai întâi, fişierul text este creat şi deschis cu funcţii low-level. Este indicat ca un astfel de fişier să stea deschis cât mai puţin timp. Al doilea este că oferă mai multă flexibilitate programatorului: toate variabilele determinate de conţinutul raportului - cum ar fi titlul raportului, lăţimea, marginile sau stilurile - pot fi transmise clasei WebReporter în orice moment, chiar dacă regulile HTML specifică faptul că acestea trebuie scrise la începutul codului HTML.

Această clasă are o multitudine de metode publice pe care programatorul le poate folosi pentru a crea, manipula şi afişa raportul. Cele mai importante sunt BeginReport, Write şi EndReport. BeginReport este folosită pentru a determina clasa WebReporter să înceapă construirea unui raport. metoda Write scrie paragrafele în el iar EndReport crează fişierul şi scrie raportul în acel fişier, închide fişierul şi (opţional) îl afişează utilizatorului.

Un raport obişnuit poate fi creat folosind numai aceste trei metode, dar pe lângă ele mai sunt câteva, cu rol ajutător, cum ar fi: SetBold, SetItalic, SetUnderline, SetFontFace, SetFontSize, SetFontColor, ş.a.m.d.

De asemenea, este simplu să adaugi liste numerotate sau "bulleted lists" (eu le numesc liste cu "guguloaie" :-)). Metodele folosite sunt BeginList (care primeşte o valoare booleană pentru a determina dacă lista este numerotată) şi EndList. BeginList, în afara faptului că scrie codul HTML necesar pentru a începe o listă, dă valori numerice unei proprietăţi a clasei (nReportInList), care specifică faptul că următoarele linii transmise metodei Write sunt poziţii din listă, nu paragrafe. La următoarea apelare a metodei Write, ea va verifica valoarea proprietăţii nReportInList, şi dacă valoarea este diferită de zero, va folosi tagul HTML corespunzător listelor, nu paragrafelor. Mai mult, fiind numerică, proprietatea nReportInList permite crearea de liste multi-nivel şi combinarea listelor numerotate cu listele cu marcaje. Mai jos aveţi un exemplu de cod care permite crearea unui raport sumar cu o listă simplă:

* Crează un raport, şi transmite titlul raportului
oRep.BeginReport("TestReport")

* Paragraful de introducere, aldin
oRep.SetBold(.T.)
oRep.Write("rmează o listă de articole:")
oRep.SetBold

* Folosim o variabilă booleană pentru a indica o listă numerotată
LOCAL lNumerotat
lNumerotat=.T.
* Începe o listă numerotată
* Linia următoare incrementează proprietatea nReportInList de la 0 la 1
oRep.BeginList(lNumerotat)
oRep.Write("Articolul 1")
oRep.Write("Articolul 2")
oRep.Write("Articolul 3 - cu o listă subordonată")
* Începe cea de-a doua listă, cu marcaje
* Notă: acest tip de listă este implicit - fără parametri
* Linia următoare incremenează proprietatea nReportInList de la 1 la 2
oRep.BeginList
oRep.Write("Subarticol 1")
oRep.Write(Subarticol 2")
* Închide cea de-a doua listă, decrementând nReportInList
oRep.EndList
oRep.Write("Articol 4")
* Închide şi prima listă
oRep.EndList
oRep.EndReport

Raportul este ilustrat în figura următoare:

repview1.gif


Procesul de generare a unui tabel este similar. Aveţi două metode: BeginTable şi EndTable. BeginTable primeşte parametri care specifică numărul de coloane, lăţimea tabelului, lăţimea marginii celulelor, şi (ca şi BeginList) stabileşte un parametru numeric astfel încât metoda Write să ştie că trebuie să creeze o celulă în loc de listă. Metoda Write ţine evidenţa coloanelor pe care le scrie, adăugând automat tagurile start-of-row şi end-of-row.

Aici aveţi codul care crează un tabel simplu:

* Crează un raport, şi transmite titlul raportului
oRep.BeginReport("Test Report")
* Paragraful de introducere, aldin
oRep.SetBold(.T.)
oRep.Write("Acesta este un tabel:")
oRep.SetBold
* Începe un tabel cu 3 coloane
LOCAL lnColumns, lnTableWidth, lnBorderWidth
lnColumns = 3
lnTableWidth = 0 && Zero = dimensionare automată a tabelului
lnBorderWidth = 1
oRep.BeginTable(lnColumns, lnTableWidth, lnBorderWidth)
* Scrie capetele de coloană, aldin
oRep.SetBold(.T.)
oRep.Write("Descriere")
oRep.Write("Cantitate")
oRep.Write("Procent")
oRep.SetBold(.F.)
* Scrie primul rând
oRep.Write("Sunt în coloana 1 acum")
oRep.Write(43230)
oRep.Write(TRANSFORM(134.5, "999.9%"))
* Scrie al doilea rând
oRep.Write("Sunt din nou în coloana 1")
oRep.SetUnderline(.T.)
oRep.Write(229000)
oRep.Write(TRANSFORM(124.5, "999.9%"))
oRep.SetUnderline(.F.)
* Scrie rândul de total
oRep.Write("Total:")
oRep.Write(272230)
oRep.Write(TRANSFORM(259, "999.9%"))
* Finalizarea tabelului
oRep.EndTable
oRep.EndReport

Raportul este ilustrat în figura următoare:

repview2.gif


Este de remarcat faptul că metoda Write este configurată să accepte şi valori numerice (nu numai şiruri de caractere). Acest lucru permite transmiterea unui şir întreg de numere fără o convertire prealabilă. Metoda Write gestionează aceste numere prin intermediul funcţiei TRANSFORM; masca pe care o foloseşte este stabilită în proprietatea cNumericMask, care este modificată cu ajutorul metodei SetNumericMask.

Este posibil (şi chiar foarte probabil) să stabiliţi măşti diferite pentru coloane diferite. Pentru a putea face asta, aveţi la dispoziţie metoda SetColumnMask, care primeşte ca parametri numărul coloanei şi masca ei. Transmiterea parametrului zero determină folosirea aceleiaşi măşti pentru toate coloanele, permiţând stabilirea unei măşti implicite pentru coloane. Aceste măşti sunt folosite doar dacă metoda Write primeşte valori numerice.

În multe cazuri, conţinutul tabelului din raport este prezent deja într-o tabelă pe disc. Ca atare, apare ca evidentă necesitatea de a transforma o tabelă de pe disc într-un tabel HTML, fără să fim nevoiţi să scriem fiecare celulă în parte. Programatorul ar putea doar să creeze tabela necesară (procedând în mod obişnuit, ca pentru un raport nativ Visual FoxPro), apoi să apeleze metodele care transformă tabela de pe disc în tabel de raport, ca în exemplul următor:

SELECT nume, SUM(vanzari) AS vanzari, ;
	SUM(incasari) AS incasari, ;
	FROM vanzari ORDER BY 1 GROUP BY 1 ;
	INTO CURSOR crVanzari
* Crează raportul şi transmite titlul raportului
oRep.BeginReport("Test Report")
* Paragraful de introducere, aldin
oRep.SetBold(.T.)
oRep.Write("Acesta este un tabel generat dintr-un DBF:")
oRep.SetBold
* BeginTableFromDBF preia structura unui DBF
oRep.BeginTableFromDBF("crVanzari")
* Se pot formata si numerele
* col. 0 = se aplică tuturor coloanelor numerice
oRep.SetColumnMask(0,"@Z( 999,999.99")
* Se descriu si capetele de coloană
oRep.SetColumnHeads("Agent","Vânzări","Încasări")
* Se indică şi coloanele care trebuie însumate în rândul
* de totaluri de la finalul tabelului
* Se transmite un şir CSV (şir delimitat prin virgule)
oRep.SetColumnSum(".F.,.T.,.T.")
* Metoda EndTableFromDBF scrie tabelul.
oRep.EndTableFromDBF
oRep.EndReport

Până la apelarea metodei EndTableFromDBF, în fişier nu se scrie nimic. În acest fel, puteţi mai întâi să aranjaţi tabelul, să stabiliţi coloanele şi capul de tabel, etc. Metoda SetColumnMask stochează măştile folosite pentru afişarea valorilor numerice în tabel; dacă nu este folosită, atunci sunt folosite valorile implicite definite în structura tabelei. Tabelul are şi un cap de tabel, şi aici pot fi descrise titlurile coloanelor folosind metoda SetColumnHeads. Dacă nu folosiţi metoda, vor fi folosite numele câmpurilor din tabela de pe disc. Metoda SetColumnSum stabileşte dacă la sfârşitul tabelului apare şi un rând de total şi ce coloane se însumează acolo. Rezultatul codului precedent este afişat în figura următoare:

repview3.gif


Detalierea rapoartelor

Un alt avantaj dat de folosirea unui instrument Web pentru raportare este acela că oferă posibilitatea de a detalia înregistrările. În acest fel se respectă tendinţa utilizatorilor de a vedea mai des rapoartele pe ecran decât pe hârtie. Utilizatorii se aşteaptă ca instrumentele de raportare să fie interactive (ca şi cele de introducere de date). Interactivitatea în raportare este oferirea posibilităţii de a face click pe numele agentului şi astfel să se genereze un raport suplimentar, referitor la agentul respectiv.

Clasa WebReporter gestionează această interactivitate în două moduri: Dacă raportul dvs. conţine relativ puţine înregistrări, procedura care crează raportul poate crea şi rapoartele detaliate simultan cu acesta. Etichetele categoriilor (cum ar fi numele agentului, în exemplul anterior), pot fi legate cu un link HTML către rapoartele pregătite separat. Clasa WebReporter are o metodă specială, numită WriteWithLink, care gestionează această situaţie fără ca programatorul să îşi facă griji pentru introducerea codului HTML adecvat legăturii.

În situaţia în care raportul dvs. conţine multe înregistrări sau procesul de generare a detaliilor este complicat sau îndelungat în timp, detalierea trebuie făcută atunci când este nevoie de ea. Clasa WebReporter are o facilitate care-i permite să reacţioneze la click-ul pe un link şi să execute o procedură specificată. În loc de a trimite numele unui fişier HTML către procedure WriteWithLink, puteţi să-i trimiteţi numele procedurii, care în prealabil trebuie construită pentru a crea un raport pentru acea categorie şi de a instanţia din nou WebReporter-ul.

Această facilitate permite şi alte moduri de interactivitate. De exemplu, un raport detaliat poate fi determinat să se restrângă într-un raport general, la click-ul pe titlul raportului. Pot fi incluse şi alte comenzi care să ruleze rapoarte relatate la cel curent sau care să ofere alte informaţii colaterale.

Interfaţa utilizator

Utilizatorii se aşteaptă să poată face câteva acţiuni după deschiderea unui raport. Unele dintre ele pot fi executate direct de controlul WebBrowser, prin intermediul metodei ExecWB. Utilizatorii vor să tipărească raportul, evident, dar poate vor să îl salveze pe disc. Metoda ExecWB poate gestiona cu uşurinţă aceste lucruri (respectând modul special de gestionare a erorilor descris mai devreme). Toate aceste comezi pot fi implementate într-o bară de instrumente sau într-un meniu al formularului. De asemenea, utilizatorii vor să poată fi capabili să copieze o parte din raport (sau chiar tot) pentru a-l folosi în altă aplicaţie. În consecinţă, clasa WebReporter trebuie să fie capabilă să gestioneze comenzile "Copy to Clipboard" şi "Select All".

O altă cerinţă majoră a utilizatorilor este aceea de a putea să trimită mesaje e-mail cu rapoartele generate. Acest lucru este mult mai dificil, deoarece programul client e-mail al utilizatorului nu este predictibil. Este posibil să faceţi acest lucru folosind Active Messaging prin crearea unei instanţe MAPI.Session. Clasa WebReporter trebuie să colecteze adresele e-mail şi subiectul mesajului folosind un formular, apoi să stabilească toate condiţiile necesare pentru trimiterea mesajului. Raportul afişat poate fi ataşat la mesaj prin referirea fişierului HTML creat de procedura iniţială:

PROCEDURE SendMail
LPARAMETERS tcRecipient, tcMessageSubj, ;
	tcMessageText, tcFile
oMapiSession = CREATEOBJECT("MAPI.SESSION")
oMapiSession.Logon
oMapiMsg = oMapiSession.OutBox.Messages.Add
WITH oMapiMsg
	.Subject = tcMessageSubj
	.Text = tcMessageText
	* Adaug şi attachment-ul
	.oMapiAttach = .Attachments.Add
	WITH oMapiAttach
		.Type = 1
		.Position = 0
		.Name = tcFile
		.ReadFromFile(tcFile)
	ENDWITH
	oMapiAttach.Name = tcFile
	* Adaug şi destinatarul
	oMapiSend = .Recipients.Add
	WITH oMapiSend
		.Name = tcRecipient
		.Type = 1 && 1="To:", 2="Cc:", 3="Bcc:"
		.Resolve
	ENDWITH
ENDWITH
oMapiMsg.Update
oMapiMsg.Send(1,0,0)
oMapiSession.Logoff

Mai există o facilitate care necesită o programare deosebită: funcţiile Back şi Forward ale lui Internet Explorer. Implementarea acestor funcţii este simplă: adăugaţi două butoane la formular. Aceste butoane trebuie să apeleze metodele GoBack şi GoForward ale controlului WebBrowser. Dar pentru a simula în totalitate funcţionalitatea browserului, aceste butoane trebuie să fie active doar dacă există pagini anterioare şi/sau următoare în istoricul browser-ului. Pentru a face asta, trebuie să adăugaţi nişte cod în evenimentul CommandStateChange al controlului WebBrowser. Vă mai aduceţi aminte că de fapt toate facilităţile oferite de Internet Explorer sunt acolo, dar sunt invizibile, şi ca atare, ignorate? Evenimentul CommandStateChange monitorizează activarea şi dezactivarea butoanelor reale (dar inaccesibile) ale lui Internet Explorer. Aceste evenimente trebuie să fie reflectate şi de cele două butoane ale formularului nostru:

LPARAMETERS tnCommand, tlEnable
DO CASE
	CASE tnCommand = 1
		* S-a schimbat starea butonului Forward
		ThisForm.cmdForward.Enabled = tlEnable
	CASE tnCommand = 2
		* S-a schimbat starea butonului Back
		ThisForm.cmdBack.Enabled = tlEnable
	ENDCASE

Cel mai des veţi folosi aceste butoane în detalierea rapoartelor, unde utilizatorul va face click pe o categorie şi raportul asociat va apare în aceeaşi fereastră, sau dacă veţi opta pentru varianta de a crea o singură clasă WebReporter per aplicaţie, şi toate rapoartele dvs. vor fi direcţionate către ea. În aceste situaţii, butoanele Back şi Forward vor fi necesare pentru a putea compara rapoartele şi a permite revenirea la primul raport. Alternativa o reprezintă instanţierea lui WebReporter pentru fiecare raport în parte şi afişarea simultană pe ecran a lor.

În loc de încheiere

Utilizarea controlului WebBrowser permite programatorului Visual FoxPro să acceseze toate facilităţile lui Internet Explorer (şi oricâte ar avea de zis partizanii altor browser-e, totuşi Internet Explorer este un browser bun - în primul rând datorită integrării native cu sistemul de operare). Partea frumoasă este că puteţi adăuga oricât de multă sau oricât de puţin HTML doriţi. Puteţi chiar să faceţi lucruri de fineţe, cum ar fi DHTML, VBScript sau JavaScript. Aplicaţiile Visual FoxPro pot include chiar proceduri specifice pentru a crea rapoarte ca nişte pagini Web. Clasa WebReporter poate fi modificată pentru a importa "style sheets" sau pentru a executa alte funcţii (cum ar fi introducerea automată a logo-ului firmei) pentru a standardiza rapoartele...

Alte particularizări ale clasei pot include o automatizare a trimiterii rapoartelor prin e-mail (mailto:), permiterea modificării rapoartelor de către utilizator, ş.a.m.d.

Ce părere credeţi că vor avea utilizatorii dvs. când rapoartele pe care le generaţi din aplicaţia dvs. cântă, clipesc, le afişează liste combo cu opţiuni posibile, îşi schimbă culoarea când duc mouse-ul pe text, etc, etc, etc?

Clasa WebReporter nu este soluţia perfectă, dar este un instrument de raportare mult mai bun şi mai flexibil decât cel pe care Microsoft s-a "deranjat" să îl pună la dispoziţia programatorilor Visual FoxPro. Şi credeţi-mă, comunitatea aceasta merită ceva mai bun...

Descărcaţi WebReporter.


    

 Google Ads Minimize

    

Copyright 2002-2013 Profox   Terms Of Use  Privacy Statement