Binaire data in XML
XML-documenten hebben een enorm aantal toepassingen. Je kunt er gegevens op een georganiseerde manier in kwijt. De gegevens zijn als een object vanuit je .NET code te manipuleren, maar ook als tekst naar buiten te brengen. Deze tekstrepresentatie is ook door het menselijk oog te lezen en bijzonder makkelijk over het (inter-)netwerk te transporteren. Maar je kunt in een XML-document ook binaire data kwijt. In dit verhaal ga ik deze mogelijkheid uitdiepen.
We gaan werken met een webservice die XML-documenten ontvangt en levert. Elk document bevat de naam van een bestand, de naam van een map en de binaire inhoud van het bestand zelf. In deze demo wordt de webservice gebruikt door Tablet PC’s. Op het scherm van een Tablet PC kun je met een pen tekenen, en het resultaat daarvan wordt in een digitaal inkt formaat opgelagen. De webservice laat Tablet PC’s bestanden met digitale inkt uitwisselen. Een tweede webservice biedt een GIF-representatie van de inkt. Een Pocket PC applicatie gaat die gebruiken.
We gaan werken met een webservice die XML documenten ontvangt en levert
Ik ga niet bij elk onderdeel van de demo alle stappen doorlopen. Hoe je in Visual Studio een nieuwe webservice maakt of consumeert, is vast bekend en anders is daarover wel meer te vinden in het SDN-magazine, op de SDN-website en in de online documentatie.
De Tekenlokaal-webservice
Centraal in dit verhaal staat de Tekenlokaal-webservice. Deze biedt de volgende functionaliteit:
- Publiceert een XSD-schema voor de uit te wisselen data
- Heeft een Portfolio-methode die een aantal tekeningen teruggeeft
- Heeft een Inzending-methode die een nieuwe tekening toevoegt.
Base64binary
Een XML-document wordt beschreven in een XML-schema in de XML Schema Definition Language (XSD). In het schema wordt de data beschreven en gestructureerd. In de .NET wereld lopen de kreten XML-document en XML-dataset wat door elkaar. Als gegevens zich in een tabelvorm gaan herhalen, bijvoorbeeld omdat ze uit een database tabel komen, wordt er meestal gesproken over een XML-dataset, maar de structuur van een XML-document kan veel grilliger zijn. Een XML-dataset is dus een specifieke vorm van een XML-document. Alle XML-documenten worden beschreven in de XSD-schema taal en je kunt ze in Visual Studio visueel ontwerpen met de XML-schema designer.
Alle XML documenten worden beschreven in de XSD-schema taal
De webservice verstuurt tekeningen in een envelop. In één envelop passen meerdere tekeningen. In Visual Studio voeg ik de dataset Envelop toe aan de webservice.

Fig. 1: Het schema van de envelop
In het schema van de dataset zie je één tabel: Tekening. Deze tabel heeft drie elementen: Titel, Artiest en Plaatje. Deze elementen hebben een type. Titel en Artiest zijn strings maar Plaatje is van het type base64Binary. Dit type is onderdeel van de XML-schema taal en wordt dan ook door iedere lezer van het document begrepen. In je code is het Plaatje-veld een array van bytes. Bij het naar tekst serializeren van het XML-document wordt dit array van bytes gecodeerd als één lange reeks van karakters.
De PortFolio-methode laat zien hoe je met dit Plaatje-veld werkt. In zijn parameter krijgt de methode de naam van een map mee. De code maakt een Envelop XML-dataset aan, vult deze en retourneert hem.
[WebMethod()]
public Envelop PortFolio(string uitMap)
{
Envelop envelop = new Envelop();
DirectoryInfo pictDir =
new DirectoryInfo(virtDir(uitMap));
FileInfo[] pictFiles = pictDir.GetFiles("*.ink");
// Doorloop gevonden bestanden
foreach (FileInfo pictFile in pictFiles)
{
long pictSize = pictFile.Length;
byte[] buffer = new byte[pictSize];
FileStream fs = null;
try
{
// Lees inhoud bestand in buffer
fs = new FileStream(pictFile.FullName,
FileMode.Open, FileAccess.Read);
fs.Read(buffer, 0, (int) pictSize);
// Voeg nieuwe rij toe aan XML dataset
envelop.Tekening.AddTekeningRow(
pictFile.Name, uitMap, buffer);
}
finally
{
if (fs != null)
fs.Close();
}
}
return envelop;
}
De methode krijgt de naam van een map binnen. Dit is een virtuele directory. De helper virtDir zorgt ervoor dat deze webcode de juiste locatie op de schijf van de webserver weet te vinden. Als een bestand is gevonden, wordt het buffer-array van bytes gedeclareerd en vanuit een FileStream gevuld. De envelop is een getypeerde dataset. De methode AddTekeningRow voegt in één regel code een nieuwe tekening aan het document toe. De buffer van bytes, met daarin de inhoud van het bestand, is een parameter voor de methode.
Het resultaat is in je browser te zien.

Fig. 2: Het resultaat van de webservice in een browser
De XML bevat de verschillende werken van de artiest. Naam en titel zijn goed te lezen maar tussen de tags staat één enorm lange string van verder nietszeggende letters. Dat is de base64Binary gecodeerde inhoud van het bestand.
Schrijfrechten voor de webservice
De webservice leest en schrijft naar het filesysteem van de webserver. Lezen is meestal geen punt, maar wil de service ook bestanden weg kunnen schrijven, dan moet je iets aan de rechten doen. Ik maak een museum-subdirectory aan in de directory van de webservice en geef in deze subdirectory schrijfrechten aan het ASPNET-account. Je ASP.NET applicatie draait onder dat account.

Fig. 3: Geef de webservice rechten
De helper methodes verwijzen naar deze map:
protected string museumLocatie
{
get
{
return Server.MapPath("Museum");
}
}
protected string virtDir(string artiest)
{
return string.Format(@"{0}\{1}", museumLocatie, artiest);
}
De Server.MapPath-method levert de fysieke locatie van de map op. Hier heb ik de virtuele directory hard gecodeerd, maar als je hem in de web.config zou zetten, wordt de service natuurlijk een stuk flexibeler.
Een bestand ontvangen in de webservice
Nu de webservice voldoende rechten heeft, kunnen we hem ook nieuwe bestanden laten aanmaken. De Inzending method ontvangt een XML-dataset en maakt bestanden van de gevonden tekeningen.
[WebMethod()]
public void Inzending(Envelop envelop)
{
foreach (Envelop.TekeningRow tekening
in envelop.Tekening)
{
DirectoryInfo artiest =
new DirectoryInfo(virtDir(tekening.Artiest));
try
{
// Is er al een map voor deze artiest ?
if (! artiest.Exists)
artiest.Create();
FileStream fs = null;
try
{
// Open filestream
fs = new FileStream(
localFile(artiest.FullName, tekening.Titel),
FileMode.Create, FileAccess.Write);
// Schrijf inhoud veld naar de stream
fs.Write(tekening.Plaatje, 0,
tekening.Plaatje.Length);
}
finally
{
if (fs != null)
fs.Close();
}
}
catch
{
// Controleer rechten
}
}
}
De code doorloopt de gevonden rijen van de Tekening-tabel. Eerst controleert die of er een map voor de artiest bestaat en maakt die zonodig aan. Om het bestand te schrijven wordt een FileStream geopend. Het Plaatje veld is een array van bytes; dit kan rechtstreeks door de filestream naar schijf worden geschreven.
Ink-objecten kunnen veel makkelijker gemanipuleerd worden dan gewone bitmaps
Tekeningen maken met de tablet
Als consument van de webservice neem ik een kleine applicatie voor de meest kunstzinnige incarnatie van een Personal Computer: de Tablet PC. Tekeningen gemaakt met een tablet worden opgeslagen in een native Ink-formaat. Ink-objecten kunnen veel makkelijker gemanipuleerd worden dan gewone bitmaps. De Tablet-applicatie zal zijn tekeningen in dit Ink-formaat aanbieden aan de webservice en verwacht ze ook in Ink-formaat weer terug.
De Tablet PC laat zich makkelijk en prettig programmeren. Een uitvoeriger behandeling daarvan is te vinden in het SDN-magazine #83 of het Microsoft .NET magazine #10. Deze demo gebruikt een Windows UserControl als tekenblad. De usercontrol legt een inkoverlay over een panel, regelt het beheer van de inkoverlay en publiceert de belangrijkste eigenschappen.
public class TekenVel : System.Windows.Forms.UserControl
{
private System.Windows.Forms.Panel panel1;
private System.ComponentModel.Container components
= null;
private InkOverlay ic;
public TekenVel()
{
InitializeComponent();
ic = new InkOverlay(panel1);
if (! this.DesignMode)
ic.Enabled = true;
}
public Ink Inkt
{
get
{
return ic.Ink;
}
}
public bool KanTekenen
{
get
{
return ic.Enabled;
}
set
{
ic.Enabled = value;
}
}
public void LaatZien()
{
panel1.Invalidate();
}
///
/// Clean up any resources being used.
///
protected override void Dispose( bool disposing )
{
if( disposing )
{
if(components != null)
{
if (ic != null)
ic.Dispose();
components.Dispose();
}
}
base.Dispose( disposing );
}
}
Het verzamelen van de inkt wordt gedaan door een InkOverLay-object. Dit is geen managed resource; het is dus belangrijk om deze op te ruimen in de Dispose-methode. In de constructor wordt gekeken of de control in de designer wordt gebruikt alvorens de inkoverlay te enablen. Als je dat niet doet, kun je designtime al tekenen en wordt het wat lastiger om met de pen te layout-en. De inkt gaan we straks ook vanuit de code bewerken. Om het resultaat daarvan te zien moet je een redraw van het panel afdwingen door Invalidate aan te roepen.

Fig. 4: Tekenen met de tablet
De usercontrol komt op een Windows Form en de gebruiker kan tekenen. Het resultaat daarvan gaat naar de webservice.
private void buttonVerzend_Click(
object sender, System.EventArgs e)
{
// Byte buffer voor inkt
byte[]buffer = tekenVel1.Inkt.Save(
PersistenceFormat.Base64InkSerializedFormat);
// XML dataset instantieren
TekenLokaal.Envelop env =
new TekenBlok.TekenLokaal.Envelop();
// Inkt als nieuwe rij van tabel toevoegen
env.Tekening.AddTekeningRow(
textBox1.Text, textBox2.Text, buffer);
// Proxy webservice aanmaken
TekenLokaal.TekenLokaal tk =
new TekenLokaal.TekenLokaal();
// Invoke webservice
tk.Inzending(env);
}
In de parameter van de Save-methode van het inkt object geef je op in welk formaat je de inkt wil saven. Base64 is hier één van de standaardopties. Het resultaat is dat je de inkt in een base64Binary gecodeerde array van bytes terugkrijgt. Een XML-envelop wordt aangemaakt, de byte buffer met artiestnaam en titel wordt in nieuwe Tekening rij gestopt en de envelop kan verstuurd worden naar de webservice.
Tekeningen opvragen bij de webservice gaat net zo makkelijk.
private void buttonLees_Click(
object sender, System.EventArgs e)
{
// Proxy webservice aanmaken
TekenLokaal.TekenLokaal tk =
new TekenLokaal.TekenLokaal();
// Invoke webservice en ontvang XML dataset
TekenLokaal.Envelop env = tk.PortFolio(textBox2.Text);
tekenVel1.KanTekenen = false;
// Doorloop de rijen van de tabel
foreach (TekenLokaal.Envelop.TekeningRow tekening
in env.Tekening)
{
// Vul byte buffer uit tabel-kolom
byte[] buffer = tekening.Plaatje;
// Nieuw inktobject aanmaken en vullen uit
// byte buffer
Ink newInk = new Ink();
newInk.Load(buffer);
// Nieuw ingelezen inkt toevoegen aan inktpaneel
tekenVel1.Inkt.AddStrokesAtRectangle(
newInk.Strokes, newInk.GetBoundingBox());
}
tekenVel1.LaatZien();
tekenVel1.KanTekenen = true;
}
De envelop wordt door het invoken van de PortFolio-methode bij de webservice opgehaald. Op de inmiddels bekende wijze wordt het byte-array met daarin de inkt ingelezen. Een nieuw Ink-object wordt aangemaakt en leest het byte-array in. Dit nieuwe ink-object wordt toegevoegd aan de bestaande tekening. Het is van belang de onderliggende InkOverlay tijdelijk te disablen, anders krijg je vervelende excepties. Om te zien wat het geworden is moet je een repaint van de control afdwingen. Het resultaat zal zijn dat de tekening een combinatie is van de door de webservice geleverde plaatjes, waar je dan weer verder mee kan gaan werken.

Fig. 5: Gecombineerde tekening
Zonder schema
De Tekenlokaal-webservice is erg krachtig, maar heeft één groot nadeel. Voor het samenstellen en manipuleren van het XML-document gebruikt die een schema. Bij het consumeren van de webservice zal een .NET client, zoals de Tablet PC, vanuit dit schema een getypeerde dataset class genereren. Helaas kan niet elke consument van webservices met schemas werken. Daarvoor hoef je niet ver van huis te gaan. Zo kent het .NET compact framework geen getypeerde datasets en deze zal de Tekenlokaal-webservice dan ook niet willen importeren. Toch kan zo’n applicatie wel met binaire data in een XML-document werken.
Ik voeg een nieuwe webservice toe. Deze levert de plaatjes in GIF-formaat terug in een ongetypeerde dataset. Een Pocket PC applicatie consumeert de webservice en toont het plaatje. Ik voeg de nieuwe webservice toe aan het bestaande webservice-project. Voor de buitenwereld worden er twee verschillende webservices aangeboden. Beiden worden geïmplementeerd in dezelfde assembly en kunnen gebruik maken van gedeelde code.
De webservice moet een conversie van de data uitvoeren. De data is opgeslagen in inkt-formaat en wordt geleverd in GIF-formaat. De conversie wordt uitgevoerd door de Tablet PC API. De webserver hoeft geen Tablet PC te zijn, maar de Tablet API wordt wel geïnstalleerd om on the fly te converteren.
De Schilderij-methode gebruikt intern een getypeerde dataset. Vanuit de gevonden bestanden wordt dit op de bekende manier gevuld.
[WebMethod()]
public DataSet Schilderij(string artiest, string titel)
{
Envelop env = new Envelop();
DirectoryInfo pictDir =
new DirectoryInfo(virtDir(artiest));
// Bestaat de map ?
if (pictDir.Exists)
{
FileInfo[] pictFiles =
pictDir.GetFiles(string.Format(
"{0}{1}", titel, extensie));
// Zijn er files ?
if (pictFiles.Length > 0)
{
FileInfo pictFile = pictFiles[0];
long pictSize = pictFile.Length;
byte[] bufferIn = new byte[pictSize];
FileStream fs = null;
try
{
// Open file met inkt
fs = new FileStream(pictFile.FullName,
FileMode.Open, FileAccess.Read);
fs.Read(bufferIn, 0, (int) pictSize);
// Laad de inkt in een nieuw Ink object
Ink kladje = new Ink();
kladje.Load(bufferIn);
// Save de inkt in Gif formaat
byte[] bufferUit =
kladje.Save(PersistenceFormat.Gif);
env.Tekening.AddTekeningRow(
pictFile.Name, artiest, bufferUit);
}
finally
{
if (fs != null)
fs.Close();
}
}
}
return env;
}
Er wordt een nieuw Ink-object aangemaakt waar de inkt in wordt geladen. De Save-methode van dit Ink-object levert in een tweede byte-array het plaatje in GIF-formaat. (De code is niet geoptimaliseerd voor productie, de header van deze GIF bevat ook nog de oorspronkelijke strokes. Een tablet-consument van de webservice kan deze er weer uit lezen. Voor de Pocket PC client is het overhead).
De methode retourneert de envelop als ongetypeerde dataset. De typering was handig om snel en duidelijk te kunnen coderen, maar helaas kan de Pocket-consument er alleen ongetypeerd mee overweg.
Als je deze Galerie-webservice in je browser bekijkt, zie je dat het resultaat vrijwel dezelfde informatie bevat als de Tekenlokaal-webservice.

Fig. 6: De galerie webservice in de browser
Weer is het plaatje een lange rij nietszeggende karakters.
Pocket PC client
In Visual Studio voeg ik een smart device application toe. Deze consumeert de webservice. Voor wie dat in meer detail na wil lezen, verwijs ik naar het verhaal “Smart apps voor smart devices” op de SDN-website. Waar je op moet letten is, dat de Pocket PC (emulator) moeite gaat hebben met de hostnaam van de service. Als url van de webservice genereert die iets als http://LocalHost/Tablet/TekenLokaal/Galerie.asmx. Over dat localhost gaat je Pocket PC struikelen, zijn DNS zal daar geen raad mee weten. Het IP-adres van de webserver nemen werkt goed; de weservice url wordt nu http://192.168.1.102/Tablet/TekenLokaal/Galerie.asmx.
Ik werk hier quick en dirty met het resultaat van de webservice. Het schema kon niet worden ingelezen, maar ik weet wel dat het resultaat een dataset is met een tabel Tekening en dat de derde kolom het plaatje in base64Binary-codering bevat. Met deze kennis ga ik het ongetypeerde resultaat van de service te lijf. De code leest een envelop van de webservice en bewaart het gevonden plaatje in een file. Een picturebox toont het. In het Windows Forms framework kun je een picture rechtstreeks in een picturebox laden; in het compact framework kan een picturebox alleen de inhoud van een bestand tonen.
private void buttonLees_Click(
object sender, System.EventArgs e)
{
// Invoke de galerie webservice
Galerie.Galerie gl = new Galerie.Galerie();
DataSet ds =
gl.Schilderij(textBox1.Text, textBox2.Text);
DataTable dt = ds.Tables["Tekening"];
if (dt.Rows.Count > 0)
{
const string fileName = "Plaatje.gif";
// De derde kolom bevat een array van bytes
byte[] buffer = dt.Rows[0].ItemArray[2] as Byte[];
FileStream fs = null;
try
{
// Schrijf array van bytes naar file
fs = new FileStream(fileName,
FileMode.Create, FileAccess.Write);
fs.Write(buffer, 0, buffer.Length);
}
finally
{
if (fs != null)
{
fs.Close();
pictureBox1.Image = new Bitmap(fileName);
}
}
}
}
Als resultaat kan ik nu mijn tablet-scribblings op m’n PDA bekijken.

Fig. 7: Tablet-scriblings op een PDA
Andere clients
Voor de Pocket PC heb ik gebruikt gemaakt van het DataSet-type in het .NET framework. Dat maakt het uitlezen van de XML een stuk makkelijker. Maar ook zonder datasets kun je binaire data in XML versturen. De XML van de dataset is ook als string terug te geven. Vanuit de string kan de client dan een XML-document opbouwen en hier met DOM doorheen navigeren. Dat kost wat meer moeite, maar als resultaat krijg je altijd wel de binary64-string te pakken. Het mooie daarvan is dus dat het in platte tekstrepresentatie makkelijk te manipuleren is, maar de inhoud kan alle binaire data bevatten die je maar wilt.