IE IT ED
10 0 IE IT ED NE
MA
GA
ZI SDN NE
ZI GA MA N SD
IN DIT NUMMER O.A.:
Snel Websites Ontwikkelen met ASP.NET Dynamic Data < Metadata in JPG bestanden < Betrouwbare Services Bouwen met SQL Server < Tips voor Mobile Webdesign < Getting Started with Delphi DataSnap 2009 <
Nummer 100 februari 2009 SDN Magazine verschijnt elk kwartaal en is een uitgave van Software Development Network
100
www.sdn.nl
Advertentie Macaw
Colofon Uitgave: Software Development Network Postbus 506 7100 AM Winterswijk
[email protected] www.sdn.nl tel. 0543 518 058 fax 0543 515 399 Achtiende jaargang No. 100 • februari 2009
Bestuur van SDN: Remi Caron, voorzitter Rob Suurland, penningmeester Joop Pecht, secretaris Mark Vroom, vice-voorzitter
Redactie:
Nummer 100... Voor u ligt het 100e nummer van ons magazine, een bijzondere mijlpaal in de historie van de SDN-community. Toen ik in 1995 toetrad tot het SDGN, zoals het toen nog heette, als redactielid heb ik niet kunnen vermoeden dat ik nu als voorzitter een column zou schrijven voor nummer 100 voor ons blad.
Rob Willemsen (
[email protected])
Aan dit magazine werd meegewerkt door: Maurice de Beijer, Mark Blomsma, Rolf Craenen, Marcel van Kalken, Stefan Kamphuis, Marcel Meijer, Johan Parent, Joop Pecht, Maarten van Stam, Bob Swart, Louis vd Tol, Sandra de Ridder, Marianne van Wanrooij, Rob Willemsen en natuurlijk alle auteurs!
Listings: Zie de website www.sdn.nl voor eventuele source files uit deze uitgave.
Vormgeving en opmaak: Reclamebureau Bij Dageraad, Winterswijk www.bijdageraad.nl ©2009 Alle rechten voorbehouden. Niets uit deze uitgave mag worden overgenomen op welke wijze dan ook zonder voorafgaande schriftelijke toestemming van SDN. Tenzij anders vermeld zijn artikelen op persoonlijke titel geschreven en verwoorden zij dus niet noodzakelijkerwijs de mening van het bestuur en/of de redactie. Alle in dit magazine genoemde handelsmerken zijn het eigendom van hun respectievelijke eigenaren.
Adverteerders
Macaw 2 iAnywhere Solutions/a Sybase Company 8 Microsoft 13 Avanade 18 Sogeti 22 Barnsten/Embarcadero 26 Bergler Nederland b.v. 35 Aladdin 47 4DotNet b.v. 57 MI Consultancy BV 57 SIRA Holding BV 61 Twice-IT 66 Sybase iAnywhere / Elmo ICT Systems 71 Bridge Incubation Group b.v. 72 Adverteren? Informatie over adverteren en de advertentietarieven kunt u vinden op www.sdn.nl onder de rubriek Magazine.
Zodra een nieuw nummer op de mat valt, trek ik het cellofaan eraf, blader het initieel snel door en selecteer de artikelen die ik op een ander, rustiger tijdstip voornemens ben te lezen. Dit scenario zal voor velen van jullie waarschijnlijk herkenbaar zijn. Soms is het rustiger tijdstip niet eens in dezelfde week (of maand), en toch lees ik de geselecteerde artikelen op enig moment. De SDN Events en SDN Conference zoals we ze tegenwoordig noemen zijn net als het magazine door de jaren heen veranderd qua aanbod (networks/secties/tracks/…). De kwaliteit is echter door de jaren heen hoog gebleven en die garantie geven wij u ook voor de toekomst. Wat ooit begon als CDGN (Clipper Developer Group Netherlands) is via SDGN (Software Developer Group Netherlands) geëvolueerd naar SDN (Software Development Network). Deze diverse namen symboliseren ieder ook een periode in de afgelopen 19 jaar. CDGN speelde in de tijd waarin met name de xBase-talen als Clipper, dBase en FoxPro populair waren. Met de intrede van Delphi, Office Development en Visual Basic is CDGN omgedoopt tot SDGN. Onder die vlag hebben we diverse secties opgetuigd, waaronder een Internet-sectie. Ja, je leest het goed … een Internet-sectie. Sommige van deze secties zijn inmiddels weer afgevoerd – de Internet-sectie b.v. - of opgegaan in andere, maar Delphi b.v. is nog steeds een op zichzelf staande track. De huidige naam SDN is gebaseerd op de gedachte dat de talenoorlog (‘mijn taal is lekker beter dan de jouwe’) geluwd is en strominglobbyisten (Java vs .Net) een staakt-het-vuren hebben afgesproken. Het landschap verandert door de steeds verder oprukkende interop tussen systemen, talen en platformen. En er zijn producten ontstaan die als basis dienen voor oplossingen, zoals SharePoint en CRM maar ook nieuwe initiatieven als Cloud Computing & Storage en DSL (Domain Specific Languages). Dit alles bij elkaar heeft in onze beleving de focus verlegd van puur taalgeoriënteerd naar development in bredere zin. Al deze ontwikkelingen hebben ook hun effect op het dagelijks werk van de moderne ontwikkelaar, en daarom ook op de wijze waarop wij als SDN ons aanbod afstemmen op de community. In 2009 en uiteraard ook de daarop volgende jaren zullen wij ons aanbod blijven actualiseren aan de werkelijkheid van het moment. De kans dat ik voor nummer 200 weer het toetsenbord zal hanteren om een column te schrijven lijkt me niet heel groot. Tegen die tijd wordt er wellicht zelfs gelachen om het fenomeen toetsenbord, wie zal het zeggen… Ik wens je veel leesplezier, nu en de komende jaren, en hoop je te mogen verwelkomen op een van de evenementen in 2009, 2010, ... Remi Caron, voorzitter SDN •
magazine voor software development 3
Inhoud 03
Voorwoord: nummer 100
Remy Caron
04 05
Inhoudsopgave Snel Websites Ontwikkelen met ASP.NET Dynamic Data
Klaas Depenbrock
09
Metadata in JPG bestanden
Peter van der Sman
14
Rapporteren in de 21e Eeuw
Dennis Vroegop
19
Train of Thought: Confessions of a Conference Speaker
Cary Jensen
23
ASP.NET onder de Motorkap: Heeft ASP.NET nog Toekomst?
Michiel van Otegem
24
Tips voor Mobile Webdesign
Sandra de Ridder
27
Betrouwbare Services Bouwen met SQL Server
Frans van der Geer en Andre van Leeuwen
36
SDN: nummer 100
Ernst Peter Tamminga, Ton Hofstede, Ad van de Lisdonk, Mark Blomsma en Remi Caron
38
Fun met WebParts in ASP.Net
Bert Dingemans
SDN EVENT
43
Interesting Things: Why Newton was Agile and the Titanic was Not
Sander Hoogendoorn
44
inclusief ALV 30 maart 2009 De Bergse Bossen, Driebergen
Keystroke Combinations in Delphi's code Editor
Cary Jensen
46
Smartmem, Zo Smart Als de Naam Zegt?
Maurice de Beijer
47 48
Agenda ASP.NET Webapplicaties Eindelijk Unit-Testbaar
Henry Cordes
52
IBM Rational Business Developer with Enterprise Generation Language Java zonder Java
Ulf Büchner
58
LINQ to SQL and DotNetNuke: A Perfect Fit for Rapid Development
Brandon Haynes
62
How Aspects Make Multithreaded WPF Easier
Gael Fraiteur
67
Getting Started with Delphi DataSnap 2009
Pawel Glowacki
.NET ASP
Klaas Depenbrock
Snel Websites Ontwikkelen met
ASP.NET Dynamic Data Met de oplevering van ASP.NET 3.5 SP1 is het Dynamic Data framework beschikbaar gekomen voor de webontwikkelaar. Dynamic Data is een framework voor het snel ontwikkelen van data-driven websites. Op basis van een datamodel wordt runtime een website gegenereerd. Het opmerkelijke hierbij is, dat het framework gebruik maakt van zogenaamde templates voor het tonen en muteren van webpagina’s. Deze templates kunnen aangepast worden aan de eigen eisen en wensen. Het Dynamic Data framework is nog volop in ontwikkeling; regelmatig wordt op Codeplex een nieuwe update vrijgegeven met daarin nieuwe of verbeterde functionaliteit. In dit artikel ga ik in op hoe het Dynamic Data framework is opgebouwd, zodat we een beter inzicht krijgen in de werking van dit nieuwe framework.
Het Dynamic Data framework is nog volop in ontwikkeling Infrastructuur Als je een Dynamic Data website wilt gaan ontwikkelen, dan start je door een specifieke template te kiezen, zie figuur 1.
Als je de Dynamic Data Website Wizard template hebt gekozen, dan creëert Visual Studio een project met daarin een folder ”Dynamic Data” en een aantal subfolders. Figuur 2 toont de initiële solution structuur van een Dynamic Data website.
Fig. 2: Dynamic Data project structuur Een aantal van deze folders bevatten de templates en controls die gebruikt worden bij het genereren van de website. De volgende tabel beschrijft de structuur van de folder “DynamicData”. Map
Omschrijving
Content
Deze folder bevat de controls en images die gebruikt worden voor het genereren van de webpagina’s .
CustomPages
Dit is een lege folder, in deze folder worden de templates geplaatst die de pageTemplates vervangen voor bepaalde tabellen.
FieldTemplates Deze folder bevat de templates voor het tonen en muteren van data types. PageTemplates Deze folder bevat de templates voor het tonen en muteren van pagina’s.
Tabel 1: Folders in een Dynamic Data project
Fig. 1: Visual Studio project templates
Naast de folder “DynamicData” creëert Visual Studio ook nog enkele andere bestanden voor een Dynamic Data Website; dit zijn: • Web.Config; het configuratiebestand met o.a verwijzingen naar DynamicData assemblies • Default.aspx; in dit bestand wordt het menu opgebouwd voor het initieel tonen van de lijst met tabellen uit het datamodel • Global.asax; in dit bestand worden bij het Application_Start event de routes gedefinieerd • Site.css; de stylesheet voor de gegenereerde website • Site.Master; de masterpage voor de gegenereerde website met daarin een contentplaceholder waarin de gegenereerde pagina’s worden getoond.
magazine voor software development 5
.NET ASP Naast deze bestanden bevat de folder “DynamicData” ook een aantal subfolders. Deze subfolders bevatten de templates voor het genereren van pagina’s en controls. Page templates Dynamic Data kent twee verschillende type templates; page templates en field templates. Page templates zijn gewone ASPX pagina’s met daarin voorzieningen om Dynamic Data te ondersteunen. Er worden vijf page templates gedefinieerd: • List.aspx • Details.aspx • ListDetails.aspx • Insert.aspx • Edit.aspx In iedere template is een DynamicDataManager klasse gedefinieerd; deze is verplicht om Dynamic Data ondersteuning mogelijk te maken in het .NET framework. Deze klasse wordt in het Page_Init event van de template geinitialiseerd. <%@ Page Language="C#" MasterPageFile="~/Site.master" CodeFile="List.aspx.cs" Inherits="List" %>
<%@ Register src="~/DynamicData/Content/GridViewPager.ascx" tagname="GridViewPager" tagprefix="asp" %>
<%@ Register src="~/DynamicData/Content/FilterUserControl.ascx" tagname="DynamicFilter" tagprefix="asp" %>
ContentPlaceHolderID="ContentPlaceHolder1" Runat="Server">
……
runat="server" AutoLoadForeignKeys="true" />
Listing 1: Fragmenten uit een page template met DynamicDataControls In listing 1 is een fragment uit de template “List.aspx” te zien. Er worden onder andere twee controls geregistreerd uit de “DynamicData/Content” folder. Vervolgens wordt ook de DynamicDataManager geïnitialiseerd. Daarnaast zijn er enkele specifieke controls die gebruikt kunnen worden bij het ontwerpen van ASP pagina’s; dit zijn onder andere een validator control (DynamicValidator) en een filter control (DynamicFilter). Voor de rest kan een template ontworpen worden met de bestaande ASP.NET controls.
Templates aanpassen om je website te customizen Door het aanpassen van de templates aan je eigen wensen is het mogelijk om de website volledig te customizen. Er zijn twee manieren om een Dynamic Data Website te customizen. Ten eerste door de page templates zelf aan te passen. Je kunt dit doen door in de folder “DynamicData\PageTemplates” één van de vijf templates te wijzigen. Alle wijzigingen die in de page templates gedaan worden gelden voor de gehele website. Daarnaast kunnen de templates aangepast worden voor een specifieke tabel. Dit doe je door in de folder “DynamicDate\CustomPages” een subfolder te maken met de tabelnaam van de klasse waarvoor deze specifieke template gebruikt moet worden. Figuur 3 verduidelijkt dit:
6
MAGAZINE
Fig. 3: Custom pages In bovenstaande situatie geldt dat voor de customers tabel een lokale template gebruikt wordt om een klant (customer) te muteren, namelijk de Edit.aspx template in de folder “CustomPages\Customers”. Voor alle andere tabellen wordt de Edit.aspx template uit de folder “PageTemplates” gebruikt. Field templates Naast page templates kent Dynamic Data ook field templates; een field template verzorgt de mapping tussen de data op het scherm en diezelfde data in het datamodel. Een field template overerft zijn gedrag van de klasse “System.Web.DynamicData.FieldTemplateUserControl”. Deze klasse zorgt voor de toegang tot de informatie in het datamodel. De bestaande field templates kunnen aangepast worden, maar je kunt ook zelf field templates ontwikkelen. Dynamic Data kent een aantal standaard field templates. Dit zijn templates voor het tonen en/of muteren van booleans, één op veel relaties, foreign key relaties, datums en numerieke waarden. Als je een bestaande field template wijzigt, dan geldt deze wijziging voor de gehele website. Wil je dit niet, dan is het alternatief om een nieuwe field template te creëren. Een nieuwe field template kan op verschillende manieren gebruikt worden binnen een Dynamic Data website. Allereerst kan de field template gebruikt worden om b.v. in een page template in een gridview een kolom te tonen. De “DynamicData” control heeft een property “UIHint” waarin de naam van de field template ingevuld wordt. Listing 2 toont dit.
Listing 2: UIHint property van DynamicControl Daarnaast is het ook mogelijk om met de attribuutklasse UIHint in het datamodel voor bepaalde properties aan te geven hoe deze getoond moeten worden. Dit wordt in listing 3 verder uitgewerkt.
Een field template laat je een dataveld customizen
.NET ASP …
// that you want all tables in the data model to support
using System.ComponentModel.DataAnnotations;
// To control scaffolding for individual tables,
using System.Web.DynamicData;
// a scaffold (i.e. templates) view. // create a partial class for
/// <summary>
// the table and apply the [Scaffold(true)] attribute
/// Summary description for Customer
// to the partial class.
///
// Note: Make sure that you change "YourDataContextType" // to the name of the data context
[MetadataType(typeof(CustomerMetadata))]
// class in your application.
public partial class Customer
model.RegisterContext(typeof(NorthWindDataContext),
{}
public class CustomerMetadata {
[UIHint("ShowPhone")] public object Phone;
}
Listing 3: UIHintAttribuut in datamodel Het voordeel van listing 3 is dat de field template “ShowPhone” gebruikt wordt in alle schermen waar “Phone” getoond moet worden, dit in tegenstelling tot het voorbeeld in listing 2. Hier zal de betreffende template het attribuut “Phone” met deze UIHint tonen. Data sources We hebben nu een idee hoe Dynamic Data een website genereert, maar om echt een website te genereren hebben we een datasource nodig. In ons voorbeeld doen we dit met een Linq to SQL datasource, maar Linq to Entities werkt ook. Om een Linq to SQL datasource te koppelen aan Dynamic Data, voeren we de volgende stappen uit: 1. Creëer een folder App_Code 2. Voeg een nieuw “Linq to SQL classes”-item toe aan deze folder 3. Noem dit item NorthWind.dbml 4. Dubbelklik op NorthWind.dbml 5. Dubbelklik op Server Explorer 6. Selecteer een database en sleep de tabellen naar NorthWind.dbml
new ContextConfiguration()
…
{ ScaffoldAllTables = true });
…
Listing 4: Registreren NorthWindDataContext in DynamicData.cs De laatste regel in listing 4 geeft aan dat we de “NorthWindData Context” registreren bij DynamicData. Dit is alles we moeten doen; we kunnen nu het project compileren en runnen. Routing Het allereerste wat opvalt als je de website opstart die met Dynamic Data gegenereerd is, is dat het lijkt alsof het framework gebruik maakt van ASP.NET pagina’s bij het tonen van deze website. Als je naar figuur 5 kijkt, zie je de volgende URL: http://localhost:52620/WebSite3/Orders/ListDetails.aspx.
Als we dit gedaan hebben, dan ziet de folder “App_Code” er uit als in figuur 4:
Fig. 5: Dynamic Data URL
Fig. 4: NorthWind Datasource Het enige wat we nu moeten doen is deze datasource koppelen aan Dynamic Data. Dit doen we op de door het bestand “DynamicData.cs” voor een aantal regels de commentaartekens te verwijderen. public static void RegisterRoutes(RouteCollection routes) { MetaModel model = new MetaModel();
// IMPORTANT: DATA MODEL REGISTRATION
// Uncomment this line to register LINQ to SQL classes
// or an ADO.NET Entity Data model for ASP.NET Dynamic Data. // Set ScaffoldAllTables = true only if you are sure
Hierboven heb ik aangegeven, dat het Dynamic Data framework een website genereert op basis van templates. De navigatie tussen de verschillende pagina’s in deze website gebeurt op basis van zogenaamde routes. Deze routes worden in het project gedefinieerd. Zo’n route stelt ons in staat om url’s te definiëren die niet direct aan een specifiek bestand op het file systeem gerelateerd zijn. Als de url aan een bepaalde routedefinitie voldoet, dan wordt hier een actie op ondernomen. Voldoet de url niet aan een routedefinitie, dan wordt aangenomen dat de url naar een bepaald bestand refereert, bijvoorbeeld een .aspx pagina. Hieruit volgt dat Dynamic Data ook te integreren is met een ‘gewone’ ASP.Net website. Een routedefinitie bestaat uit paramaters die gescheiden worden door een “/”. Zo’n routedefinitie kan er als volgt uitzien: {parameter1}/{parameter2}/{parameter3}
Routering is ook dynamisch magazine voor software development 7
.NET ASP Hierbij zijn parameter1, parameter2 en parameter3 zogenaamde url-parameters; deze url-parameters moeten altijd tussen “{“ en “}” staan om ze herkenbaar te maken voor het framework. Als de url geparsed wordt, dan worden de parameters geëvalueerd. Alle karakters in de url-string die ongelijk zijn aan het “/” -teken en niet tussen “{“ en “}” staan, worden als constanten gezien en als zodanig geëvalueerd. Het is mogelijk om tussen twee scheidingstekens meerdere urlparameters te plaatsen. Deze moeten dan wel gescheiden zijn door een constante. Onderstaande tabel geeft enkele voorbeelden van route definities met bijbehorende voorbeelden. Route Definitie
Voorbeeld
{table}/list.aspx
/customer/list.aspx
{controller}/{action}/{id}
/customer/Show/10
Tabel 2: Route definities In het eerste voorbeeld wordt waarschijnlijk een lijst met customers getoond. In het tweede voorbeeld wordt waarschijnlijk customer met id=10 getoond. Deze routes worden vooraf gedefinieerd. Bij Dynamic Data wordt in het event Application_Start in de global.asax een functie aangeroepen die de routes definieert. In de templates is de functionaliteit ingevuld hoe er genavigeerd wordt, Dynamic Data valideert deze navigatie met de gedefinieerde routes en zorgt ervoor dat de betreffende pagina gegenereerd wordt. Routing is niet specifiek voor Dynamic Data. Ook het MVC framework maakt gebruik van routing. Uit de namespace van routing (“System.Web.Routing”) volgt dat routing ook in een ‘normale’ ASP.NET website gebruikt kan worden.
tenslotte de manier van navigeren tussen de gegenereerde webpagina’s. Niet alleen templates kunnen we aanpassen aan onze eisen en wensen,we kunnen de gehele website customizen naar onze eigen inzichten. Dit maakt Dynamic Data een krachtig framework voor het genereren van data-driven websites. Zoals we gezien hebben is het mogelijk om zonder zelf een regel code schrijven een website te genereren. Het enige wat we gedaan hebben is een datamodel definiëren. Toepassingen waar Dynamic Data geschikt voor is, zijn bijvoorbeeld prototyping en het onderhouden van stamgegevens. Of Dynamic Data een concurrent voor bestaande generator tools wordt, is nog niet helemaal duidelijk. Wat natuurlijk in het voordeel van Microsoft spreekt is, dat alle nieuwe functionaliteit direct met elkaar integreert. Zo kan Dynamic Data gecombineerd worden met het door Microsoft ontwikkelde MVC framework. Daarnaast wordt er nog volop aan Dynamic Data gesleuteld en worden er regelmatig nieuwe updates gereleased. •
Conclusie In dit artikel heb ik meer inzicht gegeven in het Dynamic Data framework; ik heb de structuur besproken van een Dynamic Data project. Daarnaast heb ik de page en de field templates beschreven en
Advertentie Sybase iAnywhere
Klaas Depenbrock Klaas Depenbrock werkt bij Capgemini als software architect en houdt zich bezig met custom software development. Zijn interesse gaat uit naar nieuwe Microsoft technologieën en de integratie van deze technologieën met de verschillende Microsoft producten.
DELPHI
Peter van der Sman
Metadata in JPG bestanden Om mijn foto’s, en vooral dia’s, te archiveren maakte ik tot nu toe allerlei lijstjes waarin ik beschreef waar en wanneer de foto/dia was gemaakt en - vooral ;-) - wat het voorstelde. Niet altijd het meest leuke werk maar wel nuttig. Eerst gewoon op papier en sinds 1986 in bestandjes op de computer. Maar ja, hoe gaat dat? Zoek dat bestandje uit 1986 maar weer eens op. En als je het dan gevonden hebt - dat lukt mij meestal nog wel -, kan je het dan nog wel lezen? Goede kans van niet! En als het wel lukt, dan zit je nog met het probleem hoe de juiste dia aan de info uit het bestand te koppelen. Of andersom: waar zijn toch mijn oude dia’s van Delphi (die plaats in Griekenland) gebleven? In de huidige tijd van digitale fotografie kunnen we het probleem van die koppeling een stuk vereenvoudigen. We kunnen de informatie direct aan de foto koppelen, en veel van de informatie krijgen we al gratis mee. Bij de opname wordt al veel informatie aan het bestand gekoppeld en die kunnen we uitlezen en zelfs wijzigen. Daarbij gaat het dan met name om opname’s die als JPEG (=“Joint Photographic Experts Group”) worden opgeslagen. In de meeste gevallen is dat voor ons, amateurs, goed genoeg.
We kunnen de informatie direct aan de foto koppelen, en veel van de informatie krijgen we al gratis mee En natuurlijk willen we de applicatie in Delphi 2009 kunnen laten draaien en willen we in staat zijn om echte Unicode teksten toe te voegen. De opbouw van JPEG bestanden Een JPEG bestand is opgebouwd uit een aantal segmenten. Ieder segment bevat een blok data. Schematisch ziet het er als volgt uit: $FF$D8
//beginmarker (SOI: Start of Image)
$FF$xx$yyyy
//specificatie segment 1 met ID $xx
$FF$xx$yyyy
//specificatie segment 2 ID $xx
. . . . . . . . . .
$FF$xx$yyyy . . . . .
$FF$D9
//data segment 1 ($yyyy -2 bytes) //data segment 2 ($yyyy -2 bytes) //specificatie segment n ID $xx
//data segment n ($yyyy -2 bytes) //endmarker (EOI: End of Image)
Het lezen van een bestand komt er dus op neer de segmenten stuk voor stuk te lezen en de voor ons interessante gegevens eruit te halen. In het kader van dit artikel zijn we vooral geïnteresseerd in de metadata en die is over het algemeen ergens vooraan in het bestand opgeslagen, dat wil zeggen in de eerste segmenten. Dat betekent dat we in de praktijk maar een paar segmenten hoeven te lezen en de informatie over het plaatje zelf volledig kunnen negeren. Overigens volgt uit de specificatie dat er heel goed geldige JPEG bestanden kunnen bestaan zonder een grafische inhoud!
Het inlezen van de metadata komt er dus eigenlijk op neer de datasegmenten in een loop te doorlopen en de bijbehorende data te decoderen Bij het lezen van de segmenten zijn er een paar details die in de gaten gehouden moeten worden: • Het eerste byte van het segment moet $FF (karakter 255) zijn. Als we iets anders tegenkomen dan zitten we in de problemen. Vanaf dat punt moeten we de data ongemoeid laten (feitelijk is het bestand dan ongeldig). • Het derde en vierde byte van het segment bevatten de omvang van het segment. En die omvang is inclusief deze twee bytes, maar exclusief de specificatie. Daarnaast is de byteorder anders dan we gewend zijn. Als we deze twee bytes in een “Word” inlezen, moet de bytevolgorde even omgekeerd worden, bijvoorbeeld door een “swap” aan te roepen. Uit het feit dat de omvang in twee bytes wordt opgeslagen volgt overigens direct dat een segment nooit meer dan 65536 bytes kan bevatten. Het inlezen van de metadata komt er dus eigenlijk op neer de datasegmenten in een loop te doorlopen en de bijbehorende data te decoderen. Rest de vraag welke data dat is en hoe die te decoderen. Er zijn verschillende kandidaten. In dit artikel komen er twee aan bod: EXIF en IPTC. Een basisimplementatie Uit de bovenstaande tekst zou wellicht de suggestie kunnen zijn ontstaan dat het lezen van JPEG bestanden tamelijk triviaal is, maar dat is het toch niet. Zoals dat wel vaker gaat, kom je er meestal later achter dat de zaken die makkelijk lijken lastig blijken (andersom komt ook wel voor maar toch niet zo vaak). Daarom is het meestal wel verstandig om als je met dit soort zaken begint eens rond te struinen op internet om te kijken wat anderen al voor je bedacht hebben. Daar is wel het een en ander te vinden, het ene stukje info beter dan het andere. Maar ik heb geen info gevonden die én in Delphi werkt én om kan gaan met Unicode, en dus ook niet met Delphi 2009. En ik zal het maar toegeven: ik had eerst een implementatie die zeer veelbelovend leek. Pas toen ik op een gegeven moment helemaal vastliep, en dus wist waar een specifiek probleem zat en waarop ik verder moest zoeken, kwam ik terecht op
magazine voor software development 9
DELPHI
de website van Gerry McGuire (http://mcguirez.homestead.com). Die heeft, al weer lang geleden, een implementatie gemaakt die we kunnen gebruiken als uitgangspunt. Die implementatie heeft echter één groot probleem als we hem willen gebruiken in Delphi 2009: als basis voor de dataopslag is gebruik gemaakt van strings en chars. En daar zit nu net het probleem: karakters zijn in Delphi 2009 geherdefinieerd van 1 byte naar 2 bytes. Om de boel in eerste instantie aan de praat te krijgen moest ik van alle strings een AnsiString maken en van alle Chars een AnsiChar. Na nog wat kleinere aanpassingen draait het dan gewoon onder Delphi 2009. Dat is een truc die wel vaker werkt als je snel een oude applicatie wilt compileren onder Delphi 2009, maar het is natuurlijk niet wat je eigenlijk wilt. Het levert in ieder geval een spectaculair aantal compiler warnings op. Je kunt het allemaal wel kloppend krijgen, inclusief het lezen en schrijven van Unicode teksten, maar fraai is het allemaal niet. Met dit als uitgangspunt heb ik dit artikel geschreven. Een deel van de code heb ik inmiddels al herschreven en tegen de tijd dat je dit leest, zal de download waarschijnlijk (bijna) geheel geconverteerd zijn naar echte Delphi 2009 code. EXIF tags EXIF staat voor “EXchangeble Imagefile Format for digital still cameras”. De oudste versie die ik heb kunnen achterhalen is versie 2.1 uit 1998, en de nieuwste is versie 2.2 uit 2002. De informatie is allemaal te vinden in PDF bestanden op www.Exif.org. Het komt allemaal uit Japan en dat kunnen we merken aan de PDF’s. Die zijn weliswaar in het Engels, maar kennelijk is er af en toe toch nog een Japans karakter blijven hangen. En Acrobat Reader geeft de betreffende pagina’s, in ieder geval op mijn computer, niet weer. Het zal wel zijn omdat ik toevallig de Japanse karaktersets niet heb geïnstalleerd, maar dat gebeurt dan natuurlijk net op de pagina die je nodig hebt. Gelukkig is er dan nog internet. Google levert zo’n 70 miljoen hits op de zoekterm “exif”, dus nadere specificatie van wat je zoekt is wel zinvol.
Google levert zo’n 70 miljoen hits op de zoekterm “exif”, dus nadere specificatie van wat je zoekt is wel zinvol Het segmentID voor de EXIF tag is $D8 (225). De tag heeft een heuse header die er als volgt uit ziet: type
TEXIFheader= packed record Marker
: word;
Size
: word;
NULL
: AnsiChar;
ID
//$FF$D8
//Total segment size -2
: array[0..3] of AnsiChar; //'Exif'
Padding : byte
end;
//#0 //#0
Dit is dus inclusief de algemene segment header zoals hiervoor beschreven, en met een size waarvan we de bytes moeten omdraaien en die begint te tellen bij zichzelf. Ik ben overigens geen implementaties tegengekomen die bij het lezen daadwerkelijk controleren of het ID ook echt op “Exif” gesteld is, maar bij het schrijven moet je de header natuurlijk wel netjes in orde maken. Als we de header hebben gelezen kunnen we beginnen met het decoderen van het segment. De EXIF tag decoderen De inhoud van een EXIF tag ziet er een beetje uit als een eenvoudige harde schijf. Het komt er op neer dat er een “data directory” is, een tabel met data-ingangen, en een blok met “echte” data.
10
MAGAZINE
Het is allemaal opgebouwd volgens de TIFF specificatie. En dus beginnen we met het lezen van een 8-byte TIFF-header: type
TTIFFHeader = packed record ByteOrder : Word; i42
Offset
end;
: Word;
: Cardinal;
De “ByteOrder” geeft aan hoe de gegevens zijn geschreven. Er zijn twee opties: • $4949 (‘II’) -little endian • $4D4D (‘MM’)-big endian De veld i42 heeft geen andere betekenis dan dat het de waarde van 42 moet bevatten. De offset geeft aan waar de data begint. Meestal zal dit veld de waarde “8” bevatten: de data begint dan direct na de header. En vervolgens wordt de data gelezen. In eerste instantie ziet dat er eenvoudig uit. De data bestaat uit een aantal 10-byte records met de volgende structuur: type
TIfdTag = packed record ID
: Word;
//Tag number
count
: Cardinal;
//Aantal elementen
Typ
Offset
end;
: Word;
: Cardinal;
//Type tag
//Offset
Het ID geeft aan over welke informatie we het hebben. Zo geeft, als voorbeeld, de waarde 256 aan dat we het over de “ImageWidth” hebben. Het “Typ” geeft aan over wat voor type data we het hebben: is het een tekst, een byte, een word, een integer of wat dan ook. Er is een aantal typen mogelijk (zie hieronder). De “count” geeft aan hoeveel gegevens er zijn opgenomen. Dat kan bijvoorbeeld het aantal karakters van de tekst zijn, maar er zijn ook gegevens die uit een reeks van getallen bestaan. En count bevat het aantal elementen, en niet het aantal bytes. Een count van 10 betekent voor een tekstveld dus 10 bytes (de basis is een 1 byte char), voor een integerveld zijn het er 40. De “Offset” kan twee dingen betekenen: als de gegevens bij elkaar niet groter dan 4 bytes lang zijn, dan worden deze gegevens direct in het veld “Offset” geschreven, en anders is het een verwijzing naar waar het dan wel staat. Wat het ingewikkeld maakt is dat sommige ID’s weer verwijzen naar een “nieuwe directory”. En dat betekent dat op de locatie van “offset” moet worden begonnen deze directory uit te lezen. Welke ID’s dat zijn volgt uit de specificatie van EXIF2.2. En dan is er nog informatie die wijst naar data met een specifieke opmaak. Het is een kwestie van de documentatie goed bestuderen en de juiste codes gebruiken. Het is allemaal geregeld in de download. En dan weet je ook waar je makkelijk vast kan lopen. Unicode teksten Om te laten zien dat we een Unicode tekst kunnen schrijven heb ik als voorbeeld een foto van Delphi genomen en daar de tekst Delphi aan toegevoegd. En om er een echt Unicode voorbeeld van te maken doe ik dat op zijn Grieks, dus “Δελφοἰ”. Moeten we alleen nog een plekje vinden om die tekst kwijt te raken. Het standaard veld “UserComment” is een geschikte kandidaat. Dit is een karakter veld. De eerste acht karakters geven de codering aan. Voor ons zijn de volgende twee van belang:
DELPHI ASCII#0#0#0 + ASCII-tekst + #0 UNICODE#0
+ Unicode-tekst + #0#0
Gewoon uitschrijven wat je wilt dus! Verder bestaan nog de Japanse karakterset en “ongedefinieerd”, gewoon 8 maal #0. Een potentieel probleem is dat bij ASCII niet is gespecificeerd met welke codepage is geschreven. Natuurlijk word je wel geacht met een standaard codepage te schrijven maar in de praktijk gebeurt dat dus niet (altijd). Bij het schrijven van de tekst is het een kwestie van kijken of we Unicode nodig hebben. De juiste bytestring kunnen we met de betreffende TEncoding genereren. In de implementatie gebruik ik voor schrijven de standaardcodering. Bij het lezen kan gekozen worden met welke TEncoding de data gelezen moet worden. Voor meer details wordt verwezen naar de download. De EXIF-tag wijzigen Behalve de “UserComment” kunnen we natuurlijk ook alle andere metadata uit de EXIF tag wijzigen. Ik kan je echter aanraden dat alleen te doen als je ook daadwerkelijk weet wat en waarom. Als voorbeeld: ik was eens in een programma tegengekomen dat je JPG foto’s “lossless” kon roteren. Een handige optie! Dus toen ik in de EXIF documentatie een optie “Orientation” zag dacht ik “HA!”. Mis dus. Je kunt aan dit veld wijzigen wat je wilt, je foto zal er niet van gaan draaien. En toen ik het maar eens met dat programma deed om te controleren wat er gebeurde, bleek er nog veel meer te wijzingen. Zo worden de velden “hoogte” en “breedte” omgewisseld, om maar iets voor de hand liggends te noemen. Wat verder ook opvalt in de documentatie is dat veel tekstvelden expliciet in ASCII worden geschreven. Het veld “UserComment” is het enige waarbij over Unicode wordt gesproken. Ik ben in ieder geval geen andere tegengekomen.
Het veld “UserComment” is het enige waarbij over Unicode wordt gesproken Nu kunnen we natuurlijk op eigen gezag velden toevoegen en deze behandelen alsof het een “UserComment” is. Als we maar niet gebruikte codes gebruiken zal het voor onze eigen bestanden nog wel goed gaan. Alleen weet je natuurlijk nooit zeker of een code echt ongebruikt is, en blijft. Dus misschien is het toch niet zo’n heel goede oplossing. Standaardvelden met niet ASCII volschrijven kan natuurlijk ook, maar is niet handig als de informatie ook nog in andere programmatuur leesbaar moet zijn. De IPTC-tag In Photoshop krijg je, als je de metagegevens opvraagt, een aantal velden te zien die meer aansluiten bij de wens om gegevens over de locatie waar de foto is genomen te bewaren. Zo zien we velden als “Omschrijving”, “Plaats” en “Land”. Het blijkt allemaal in het bestand terecht te komen als “IPTC”. IPTC staat voor “International Press Telecommunications Council”. We kunnen ze terugvinden op www.iptc.org. Daar blijkt dat ze veel meer doen dan alleen maar metadata voor foto’s specificeren. De data van het IPTC segment is relatief simpel. Het is een aantal records met de volgende opbouw: Marker Groep ID
Size Data
1 byte
1 byte
- $1c
- de “groep” van het ID, meestal 2
1 byte 2 byte x
- een geswapt word
- een aantal karakters (size-4)
Om het segment te lezen moeten we zoeken naar een marker ($1c,
ASCII 28), en vervolgens lezen we de bijbehorende data uit. Dan gaan we naar de volgende, enz. De gedachte achter Groep is verschillende groepen van gegevens te kunnen specificeren. De combinatie van Groep en ID bepaalt dan over welke informatie we het hebben. Helaas is er geen handig overzicht beschikbaar van alle groepen en bijbehorende ID’s. Als je wat verder gaat zoeken kom je al snel tegenstrijdige informatie tegen. Ik ga dus maar uit van een stukje “reversed engineering”: ik kijk gewoon hoe de gegevens die ik in Photoshop invoer in het bestand terecht zijn gekomen en dan weet ik wat het betreffende ID en groep zijn. Meestal, altijd eigenlijk, hebben we te maken met informatie in groep 2. In de download zijn de mij bekende velden opgenomen. Desgewenst kun je daar eenvoudig nieuwe informatie aan toevoegen. Officieel is de inhoud van de velden dan ook nog weer gebonden aan allerlei afspraken wat betreft lengte en opmaak (denk maar eens aan velden met “datum” en “tijd”), maar het lijkt erop dat we in de praktijk redelijk vrij zijn om te schrijven wat we willen. In de praktijk is het segment bijna altijd geschreven door Photoshop, en die voegt een header en footer toe aan het segment. Het ziet er als volgt uit: Header 'Photoshop 3.0'#0'8BIM'#4#4#0#0#0#0 Data ……
Footer '8BIM'#$04#$0B#0#0#0#0#0#0
Dat zijn nog eens headers! Bij het schrijven van het segment is het natuurlijk wel handig die header en footer mee te schrijven, dan kan Photoshop onze gegevens tenminste ook lezen! Naast deze header en footer verwacht Photoshop ook een tag met een versiespecificatie. Het wordt allemaal voor je geregeld in de download. IPTC en karaktersets De definitie van de IPTC voorziet in het specificeren van een karakterset. Meestal wordt dat echter achterwege gelaten. En dan komt het neer op raden hoe het is geschreven. Het kan fout gaan doordat een Rus of een Griek informatie heeft geschreven in zijn codepage, maar het blijkt in de praktijk ook voor te komen dat Unicode of UTF-8 is geschreven zonder dat te vermelden. Om het netjes te doen moet, bij afwijkend karaktergebruik, een record worden opgenomen waarin dat wordt gespecificeerd. Meestal wordt dan in UTF-8 geschreven en dan ziet dat record er als volgt uit: #$1c#1’Z’#0#3#$1b’%G’
Dat is dus een record met “groep=1” en “ID=90”. Als we dit record tegenkomen dan weten we dat de data gelezen moet worden als UTF-8. Het converteren van een string naar een reeks UTF-8 karakters en weer terug kan eenvoudig met de betreffende TEncoding. Als voorbeeld volgt hieronder de code om een record “stad=Δελφοί” toe te voegen aan het segment: type THead=record Marker,Groep,ID,Size1,Size2:byte end; var
lHead:THead;
begin
lBytes:TBytes;
lHead.Marker:=$1c;
//beginmarker
lHead.Marker:=90;
//ID=”city”
lHead.Marker:=2;
//groep=2
lBytes:=TEncoding.UTF8.GetBytes('Δελφοί'); //genereer bytes
lHead.Size1:=length(lBytes) div 256; //schrijf lengte
lHead.Size2:=length(lBytes) mod 256; //van de databytes
aStream.write(lHead,5);
magazine voor software development 11
DELPHI //schrijf de header
aStream.write(lBytes[0],length(lBytes)); //schrijf de databytes
end;
Als we in een andere encoding zouden willen schrijven is het enige wat we moeten doen op dit punt de encoding wijzigen en alles blijft verder werken. Het is dan natuurlijk handig deze encoding ergens als parameter of property op te geven zodat je die kunt wijzigen zonder direct het hele programma te hoeven compileren. Dat is dan ook de aanpak zoals die in download terecht is gekomen. IPTC implementeren Het goede nieuws is dat onze basisimplementatie (zie hiervoor) ook voorziet in het lezen en schrijven van de IPTC tag, maar helaas
ontbreekt wel het stuk waarin je de tekstcodering kunt specificeren. Wat nog vervelender was, was dat er een bug bleek te zitten in het schrijven van het segment. Het kwam erop neer dat ik ineens twee IPTC segmenten in mijn bestand had zitten. En Photoshop maar steeds de oude inlezen! Ik heb de procedure om te schrijven dus moeten aanpassen. Het komt er op neer dat ik het bestand eerst inlees in een (invoer)stream, en daar lees ik vervolgens de segmenten uit. Als het een segment is dat ik zelf heb gewijzigd, dan schrijf ik mijn eigen versie van het segment, en anders dan schrijf ik (een kopie van) het segment uit het invoerbestand. Dat blijkt goed te werken. En nog veel meer tags EXIF en IPTC zijn de twee segmenten die het meest gebruikt worden om (gebruikers)gegevens aan JPEG bestanden toe te voegen. En ik kan er de informatie in kwijt die ik er in kwijt wil. Ik heb dus niet verder gezocht naar de specificaties van de vele andere segmenten die in een JPEG bestand voorkomen. In de basisimplementatie wordt een aantal gelezen, en dat is prima. Ik doe er echter niets mee. Ik zal er pas naar gaan zoeken als ik ergens iets tegenkom wat ik graag wil weten maar niet in deze twee segmenten kwijt kan.
Er zijn vele programma’s in omloop die allemaal iets kunnen met de tags in JPEG, maar ze hebben de neiging het allemaal net iets anders te doen Wat me brengt naar het volgende: er zijn vele programma’s in omloop die allemaal iets kunnen met de tags in JPEG. Maar ze hebben de neiging het allemaal net iets anders te doen, en het is altijd maar de vraag of wat in jouw programma werkt ook functioneert in een ander programma. Ik zelf ga ervan uit dat het goed is als Photoshop het kan lezen, maar dat is geen garantie voor andere programma’s. In het algemeen blijken er maar teleurstellend weinig programma’s te zijn die
12
MAGAZINE
goed om kunnen gaan met Unicode. Als er al Unicode gebruikt kan worden, dan toch vaak met horten en stoten. Dan komt “Δελφοί” toch nog weer ergens als “?e?f??” tevoorschijn. Als dit gebeurt, dan wordt er ergens toch nog een “Ansistring” gebruikt. Daarbij verbaast het me altijd dat sommige karakters wel en andere niet omgezet worden naar een standaardkarakter. Waarom de “ε” wel en de “ο” niet, is voor mij een volslagen raadsel. En dit geldt dus niet alleen voor programma’s die iets doen met JPEG tags… Een voorbeeldprogramma Voor de download heb ik een klein voorbeeldprogramma gemaakt. De meest algemene invoervelden staan als TEdit op het scherm en deze gegevens kunnen direct gewijzigd worden. De overige informatie uit EXIT en IPTC worden in een Memo gedumpt. Verder is het een kwestie van een bestand lezen en schrijven.
Het is maar een voorbeeld. Desgewenst kunt je het programma met wat eenvoudige aanpassingen volledig naar je eigen inzichten wijzigen. Het programma werkt als volgt: je opent een bestand met de daarvoor bedoelde knop. Het plaatje wordt getoond en de voorgeprogrammeerde velden worden gevuld met informatie. Rechts in het scherm wordt een dump van de EXIF en de IPTC segmenten gegeven. Om gedonder met bestanden die in andere coderingen zijn geschreven te voorkomen heb ik hier twee comboboxen opgenomen waarmee de encoding voor het lezen gewijzigd kan worden. Gewoon proberen en kijken of de informatie er beter uit komt te zien… Om de (gewijzigde) gegevens weer naar het bestand te schijven wordt de knop “Opslaan” gebruikt. Daarbij wordt expliciet om een bestandsnaam gevraagd. Dat heeft het voordeel dat het zo mogelijk is dezelfde informatie in meerdere bestanden te schrijven. •
Peter van der Sman Peter van der Sman begon op de middelbare school met programmeren, in een tijd dat dit nog met ponskaarten gebeurde. De introductie van de PC maakte hij mee als chemisch analist en later als statisticus. Twee werkvelden waarin de computer een nuttig hulpmiddel is om de grote hoeveelheden (meet)data te verwerken. Midden jaren ’90 besloot hij, na zijn studie informatica, zich geheel te gaan wijden aan de automatisering. Via Prisman Consultancy houdt hij zich bezig met het maken van software en consultancy.
Advertentie Microsoft
INFORMATION
WORKER
Dennis Vroegop
Rapporteren in de 21e Eeuw Eigenlijk is het grappig. Er is bijna geen vakgebied dat zo snel verandert als het onze; de ICT sector gaat met enorme stappen vooruit. Toch is er één gebied dat achterblijft: de rapportage. Als je kijkt naar hoe we vandaag de dag een Information Worker van gegevens voorzien, zie je dat er in de afgelopen decennia niet veel is veranderd. Vreemd, aangezien informatie in onze samenleving steeds belangrijker wordt. Je kunt je afvragen hoeveel kansen er blijven liggen doordat de verantwoordelijke mensen binnen een organisatie niet beschikken over de informatie die ze nodig hebben. In de Computable stond laatst een artikel over het enorme aantal managers dat niet de juiste beslissingen neemt doordat ze niet tijdig over de juiste gegevens beschikt. Ik hoef je niet te vertellen hoeveel geld dit een organisatie kan kosten. Gelukkig is er een lichtpuntje aan de horizon: de techniek staat niet stil. Ook de rapportagetechnieken niet. De gereedschappen zijn er, we moeten nu alleen even weten wat we gaan rapporteren en vooral, hoe we dat gaan doen. In dit artikel laat ik je een mogelijkheid zien. Doe er je voordeel mee!
juiste tijd op de juiste plek waarde toevoegt, mits deze gegevens correct en volledig zijn.
Het werk van een Information Worker De term Information Worker, kortweg IW’er, bestaat al een hele tijd. Microsoft heeft deze term bedacht om een groep mensen aan te duiden die met informatie werkt. Nou ja, de officiële definitie gaat wat verder dan dat, maar in het kort komt het daar wel op neer. Voordat we gaan kijken wat een IW’er nu precies doet, is het wellicht handig om te definiëren wat informatie nu precies is. Immers: we zijn allemaal werkzaam in de ICT-sector, waarbij de C en T (communicatie en technologie) termen zijn waar we wel een voorstelling bij kunnen maken, maar de I van informatie … die blijkt voor veel mensen toch wat lastiger. Ik heb in een grijs verleden bedrijfskundige informatica gestudeerd. In die tijd heb ik veel geleerd (voornamelijk klaverjassen en 7 dagen in de week in de kroeg overleven) en ik ben het meeste daarvan al weer vergeten. Toch is er één ding dat is blijven hangen: de definitie van informatie. Om je een 4-jarige studie te besparen zal ik die hier even herhalen.
Het doel van een IW’er Laten we eens teruggaan naar de IW’er. Een IW’er is iemand die met de juiste tools van gegevens informatie kan maken. Nu we het verschil weten tussen gegevens en informatie, moet het iets duidelijker zijn wat een IW’er doet. Een IW’er neemt een hoeveelheid gegevens en transformeert dat in informatie. De IW’er verzamelt, controleert, valideert en rapporteert deze gegevens aan iemand voor wie die gegevens waarde toevoegen zodat we over informatie kunnen spreken. Uiteraard kan die 'iemand' voor wie de informatie gegenereerd wordt ook de IW’er zelf zijn.
Informatie Informatie is een set met gegevens die voor de juiste persoon op de
14
MAGAZINE
Lees de zin nog eens. Als je deze definitie in je achterhoofd houdt, zul je zien dat heel veel gegevens ten onrechte informatie genoemd worden. Hoe vaak hoor je niet iemand roepen dat de informatie onjuist was? Dat kan dus niet: gegevens moeten correct zijn voordat je van informatie kan spreken. “Ik heb de informatie te laat gekregen” is ook zo’n veelgehoorde klacht. Klopt niet. Gegevens die te laat aangeleverd worden zijn gegevens, geen informatie. Nog een voorbeeld: de aandelenmarkt stort in het derde kwartaal van 2008 in elkaar. Tja. Dit klopt, alleen hebben wij er als ICT’ers niet zo veel aan. We zijn immers geen beurshandelaren. We zijn dus niet de juiste persoon. En zelfs als we dat wel waren, dan nog is de tijd verkeerd. Ook is het niet volledig: er zijn fondsen overeind gebleven, iets wat voor een handelaar ook belangrijk is om te weten.
Informatie is een set met gegevens die voor de juiste persoon op de juiste tijd op de juiste plek waarde toevoegt, mits deze gegevens correct en volledig zijn Een andere leuke term is ‘information overload’. Daarmee wordt gesuggereerd dat er zoveel informatie beschikbaar is dat we er geen chocola meer van kunnen maken. Ook in dat geval is er geen sprake van informatie maar eerder van een (enorme) brij aan gegevens. Vooral BI-toepassingen zijn goed in het genereren hiervan. Je ziet dat veel gegevens die we nu informatie noemen geen informatie zijn. Het is erg belangrijk om het onderscheid tussen gegevens en informatie te begrijpen.
Een IW’er is iemand die met de juiste tools van gegevens informatie kan maken Is een ontwikkelaar een IW’er? Nee, de ontwikkelaar maakt tools en implementeert systemen. De ontwikkelaar genereert dus geen informatie.
INFORMATION
WORKER Is een secretaresse een IW’er? Volgens bovenstaande definitie dus wel. Zij (meestal zij, soms hij) neemt de enorme hoeveelheid gegevens die anders op het bureau van de leidinggevende terecht zou komen, filtert dit, sorteert en groepeert dit, valideert dit en geeft het in hapklare brokken door. Op die manier ontstaat informatie. Een secretaresse is dus een IW’er. Zo is er nog wel een aantal beroepsgroepen dat aan de definitie van IW’er voldoet, maar die kun je zelf wel bedenken. De gereedschappen De tijd dat een IW’er een krant kon pakken, deze naast het jaarverslag van de organisatie kon leggen en daarmee genoeg gegevens had om informatie te genereren ligt al jaren achter ons. Er zijn zoveel gegevens beschikbaar dat een IW’er de goede gereedschappen nodig heeft om in die brij van data informatie te vinden. Het is de taak van de ICT’er om de IW’er van die gereedschappen te voorzien. Sterker nog, ik denk dat dit de hoofdtaak van een ICT’er is. In vrijwel alle gevallen maken of implementeren wij systemen die de IW’er helpen zijn of haar werk goed te kunnen doen. De tools die de IW’er gebruikt moeten dus de enorme beschikbare hoeveelheid gegevens op een dusdanige manier presenteren dat er informatie van gemaakt kan worden. Natuurlijk is niet alleen de data in de systemen een bron; ook de kennis van de IW’er is een niet te verwaarlozen factor in dit proces. Een goede IW’er combineert zijn of haar kennis met de gegevens die beschikbaar zijn. De juiste gereedschappen maken dit mogelijk. Nu kennen we al een grote verscheidenheid aan gereedschappen. Er zijn verschillende rapportagetools beschikbaar (Crystal Reports, SQL Server Reporting Services, enz.). De BI-tools vallen ook onder de gereedschapsset van de IW’er: data wordt in een andere vorm gegoten en dusdanig gepresenteerd dat er iets zinnigs over te zeggen valt. En dit is nu net waar het fout gaat. Bovenstaande rapportagemiddelen zijn in wezen niet anders dan de tools die we al jaren gebruiken. Oké, het werkt allemaal wat makkelijker dan ongeveer 10 jaar geleden, maar het eindresultaat is hetzelfde. Weergave van gegevens In veel organisaties wordt data nog steeds in tabelvorm gepresenteerd. Dat lijkt een efficiënte manier van presenteren, maar in de meeste gevallen werkt het niet. Onderzoek wijst uit dat 95% van de mensen geen getallen kan lezen. Kijk eens naar de volgende tabel. Niet lezen, maar kijk er gewoon even naar:
Maand Januari Februari Maart April Mei Juni Juli Augustus Oktober November December Totaal
Omzet 2006 3254578 4254785 3214578 3214455 4512354 4521545 5451236 4511253 1556654 9233215 2878945 46603598
Omzet 2007 4215478 4531254 4577654 5412131 9474165 3211457 3221557 4233364 2457884 1222547 2311547 44869038
Omzet 2008 556544 454784 454478 544745 424544 544458 322548 321547 245447 123154 322152 4314401
Deze tabel heb ik in het echt gezien in een rapport van ongeveer 50 pagina’s dik. Dit rapport wordt iedere twee weken gemaakt, met daarin tientallen van dit soort tabellen. Op het eerste gezicht is dit een enorm efficiënte manier van presenteren: een hele hoop gegevens is beschikbaar op relatief weinig ruimte. Zonder de tabel echt te lezen kun je er al veel uit afleiden: het gaat om omzetten in de jaren 2006 tot en met 2008, ze zijn gegroepeerd per maand.
Als je echter de tabel omzet in een standaard Excel-grafiek, dan ziet het er opeens anders uit:
Fig. 1 In figuur 1 zie je dezelfde data weergegeven, maar nu in grafiekvorm. Opeens zie je meer: september ontbreekt, de omzet van mei 2007 en november 2006 zijn vele malen hoger dan je zou verwachten en de omzetten van 2008 zijn wel heel erg laag (dat heeft niets met de kredietcrisis te maken: die is pas echt begonnen in oktober en we zien dat de omzet in januari al erg laag is). Kloppen deze gegevens? Misschien is 2008 over het hele jaar erg slecht geweest en zijn mei 2007 en november 2006 wel uitschieters. Misschien is dit bedrijf altijd in september gesloten waardoor de omzet op nul uitkomt. Bij de organisatie waar deze data vandaan komt was dit niet zo: de reden van deze uitschieters was domweg een foutieve invoer van data. Je ziet dat je in een eenvoudige grafiek al meer informatie staat dan in een tabel. Of liever gezegd: een IW’er is sneller in staat om informatie te maken van de gegevens als deze als grafiek worden gepresenteerd, in plaats van als een brij met gegevens zoals in de tabel.
Een IW’er is sneller in staat om informatie te maken van de gegevens als deze als grafiek worden gepresenteerd De derde dimensie De grafiek in figuur 1 is al een enorme verbetering, maar het kan nog veel beter. Het is niet eenvoudig om de gegevens te vergelijken. Excel biedt ons de mogelijkheid om een grafiek in 3D weer te geven. Deze ziet er dan als volgt uit:
Fig. 2 Het ziet er absoluut mooi uit. Maar handig? Nee: we kunnen de gegevens van 2008 niet zien. Deze zijn verborgen achter de andere rijen. Nu kunnen we rijen en kolommen omdraaien zodat het beter leesbaar wordt, maar dat levert weer andere problemen op. Je loopt
magazine voor software development 15
INFORMATION
WORKER immers nu het risico dat andere rijen verborgen worden en dan ben je net zo ver van huis als in het begin. Even een leuk weetje: vroeger, toen Excel nog veel verkocht werd als los pakket, stond op de doos van Excel een screenshot van de geweldige grafische mogelijkheden van het pakket. Dat plaatje was een grafiek die erg leek op het plaatje in figuur 2. Echter, op dat plaatje steeg de omzet per jaar gestaag. Dat moest ook wel omdat anders de data in de latere jaren niet leesbaar was. Anders gezegd: de 3D grafiek is een leuk plaatje, maar, zoals figuur 2 duidelijk maakt, is deze weergave niet echt praktisch. En wat nu? In de jaren negentig ontstond een nieuw fenomeen: de KPI’s, wat staat voor Key Performance Indicators. KPI’s zijn een soort verzamelgrootheid van een aantal sleutelwaardes waaruit je kunt afleiden hoe je organisatie het doet. Het idee, dat niet nieuw was, kreeg veel aandacht door het toepassen ervan in balanced scorecards, zoals verwoord in het artikel “The Balanced Scorecard” van Kaplan en Norton in 1992 (wat leidde tot hun bestseller “The Balanced Scorecard: Translating Strategy into Action” uit 1996). Een balanced scorecard (BSC) is niets anders dan een zeer vereenvoudigde weergave van een aantal meetpunten in de organisatie. We kunnen de tabel uit figuur 1 dan ook als volgt gaan weergeven:
We hebben nu wel informatie over de omzet, maar we missen toch nog iets. Zou het niet mooi zijn als we meer gegevens kwijt zouden kunnen in een manier van weergeven, maar toch in staat zouden zijn om het directe inzicht te krijgen? We willen toe naar een situatie waarin we meer details zien maar toch meteen een idee krijgen van de stand van zaken. Als jij deze wens ook koestert, dan heb ik goed nieuws: die manier is er! 3D Business Visualization Zoals we vastgesteld hebben zijn mensen zoals jij en ik veel beter in staat om plaatjes om te zetten in informatie dan rijen met getallen. Het is een cliché, maar daarom niet minder waar: een plaatje zegt meer dan duizend woorden. Even een test: kun je mij vertellen hoe laat het is? Eigenlijk wil ik niet weten hoe laat het is (je leest dit artikel in het magazine dus je kunt het me ook niet vertellen), maar ik wil graag weten wat voor horloge je om hebt. In de jaren tachtig van de vorige eeuw hadden we allemaal digitale horloges. Vandaag de dag zie je die eigenlijk niet meer: we zijn allemaal weer terug naar analoge horloges. Waarom is dat? De reden daarvoor is dat de tijd 10:49 een getal is. We voelen er niets bij. Onze hersenen moeten dat vertalen in een tijd. Twee wijzers waarvan de kleine bijna op de 11 staat geeft ons echter het gevoel dat het bijna 11 uur is, en dat is meestal voldoende informatie. We hoeven dat gevoel van tijd niet meer om te zetten in onze hersenen, we weten meteen waar we aan toe zijn. Maar met plaatjes alleen zijn we er nog niet. Onze wereld is niet plat, deze bestaat uit drie dimensies. Mensen zijn erg goed in staat om in drie dimensies te zien, we kunnen veel meer informatie uit drie dimensies halen dan uit een plaatje met slechts twee dimensies. Toch hebben we gezien dat de 3D grafieken uit Excel niet echt helpen. Gelukkig kunnen we nu met de nieuwe tools, omgevingen, hardware en software die ons ter beschikking staan daar iets anders op verzinnen. We kunnen gegevens vanuit onze databronnen dusdanig weergeven dat een IW’er er snel informatie uit kan halen. Zeker met WPF en Silverlight is het maken van deze applicaties relatief eenvoudig.
We zijn allemaal weer terug naar analoge horloges
Fig. 3 We zien geen cijfers meer, maar symbolen. Een rondje betekent dat de omzet voor deze maand boven de €450.000 ligt, een driehoekje betekent dat de omzet tussen €200.000 en €450.000 ligt en onder de €200.000 zien we een ruitje. We hebben nu geen inzage meer in de exacte cijfers. Maar is dat een probleem? Dat kan een probleem zijn als we een publicatiebalans voor de Kamer van Koophandel moeten maken, maar voor de dagelijkse gang van zaken is dit niet erg. Een manager wil dagelijks kunnen zien hoe de zaken er voor staan. De rode ruiten vallen direct op en de manager weet nu meteen dat er gebieden zijn waar hij of zij aandacht aan moet besteden. Als het gaat om het krijgen van een gevoel over de stand van zaken, zijn BSC’s een geweldig gereedschap. Excel levert de mogelijkheden om BSC’s te maken, maar als je dit serieus wilt aanpakken, dan moet je uitwijken naar meer gespecialiseerde tools, zoals Microsofts Performance Point Server. Samen met SQL Server Analysis Server heb je dan een zeer krachtig BSC tool in handen.
16
MAGAZINE
Nu is het weergeven van complexe data in een 3D omgeving niets nieuws. In de 18e eeuw gebruikte Napoleon al maquettes van de slagvelden. Hij plaatste poppetjes die zijn legers voorstelden en die van de tegenstander in een model waar de veldslag plaats zou vinden. Op die manier kon hij goed bedenken hoe hij zijn soldaten in moest zetten. De Engelsen werkten nog met (twee dimensionele) kaarten en daardoor ontbrak het hen aan een stuk inzicht. Dat is de Engelsen duur komen te staan (dat hebben ze later goed gemaakt, maar dat is weer een heel ander verhaal).
Het grappige is dat uit onderzoek is gebleken dat mensen dan in hun hoofd die grafieken gaan samenvoegen tot een 3D-omgeving Stel je nu eens voor dat de data uit de eerste tabel een verzameling is van een aantal databronnen. We hebben immers alleen de totalen per maand weergegeven, en dit zal waarschijnlijk een optelsom zijn van de omzetten per filiaal (als we er even vanuit gaan dat dit de omzet van een winkelketen is). We hebben door er een tabel van te maken dus eigenlijk al gegevens weggelaten. Gegevens die wel degelijk informatie kunnen bieden: hoe verhouden de verschillende regio’s in Nederland zich tot elkaar?
INFORMATION
WORKER We kunnen dat weergeven door verschillende tabellen te maken: één voor iedere regio. Helpen doet dat niet echt: op die manier creëren we alleen maar meerdere, niet eenvoudig te interpreteren tabellen of grafieken. Het grappige is dat uit onderzoek is gebleken dat mensen dan in hun hoofd die grafieken gaan samenvoegen tot een 3D-omgeving. Bij Business Visualization doen we dat alvast voor de gebruiker. We maken nu een andere grafiek, eentje die er als volgt uitziet:
Fig. 4 Nu is dit magazine helaas niet de beste manier om deze gegevens weer te geven. In de echte applicatie kun je inzoomen en roteren, zodat je de grafiek van verschillende kanten kunt bekijken. Wellicht is dit in magazine 200 opgelost: zouden we dan eindelijk digitale magazines krijgen? ;-) Door de mogelijkheid om te roteren en te zoomen te geven voorkomen we dat gegevens niet zichtbaar zijn doordat er andere elementen voor staan. Hoewel dit een 3-dimensioneel plaatje is, spreken we liever van een multi-dimensioneel plaatje. Ten eerste klinkt dat beter, maar het dekt de lading ook beter. We hebben in dit plaatje een aantal verschillende dimensies weergegeven: 1. De omzet wordt weergegeven door de hoogte van de kolommen. 2. De regio waar de omzet is gerealiseerd zie je door de plaatsing van de kolommen. 3. De bijdrage aan de winst van de organisatie wordt weergegeven door de kleur van de kolommen (hoe groener, hoe meer winst er gemaakt wordt in die regio). 4. De omloopsnelheid van het assortiment wordt duidelijk gemaakt door de breedte van de kolommen: hoe breder, hoe korter de producten in het magazijn blijven liggen. Deze vier dimensies zijn op een dusdanige manier weergegeven dat we conclusies kunnen trekken uit het plaatje. De oostelijke provincies doen het goed: er wordt vooral in het noorden veel verkocht en we verdienen er ook veel. In het westen echter zijn de winsten aanmerkelijk lager en ook de totale omzet blijft achter bij de rest. Zeeland en Limburg hebben een erg lage omloopsnelheid, de kolommen daar zijn vrijwel niet zichtbaar. Je ziet dat je erg veel informatie kunt halen uit een 3D-grafiek, iets wat in de traditionele manier van rapporteren veel minder snel kan. En dat zonder daadwerkelijke cijfers te tonen. In één oogopslag weet je hoe het met je organisatie gaat, zonder dagelijks dikke rapporten door te moeten lezen.
De applicatie moet uiteraard wel voorzien in een drill-down mogelijkheid
De applicatie moet uiteraard wel voorzien in een drill-down mogelijkheid: door te klikken op één van de kolommen moet je details kunnen zien, zoals de gegevens per winkel, per maand of wat dan ook. Door een interactieve applicatie als deze te bouwen geef je de IW’er de mogelijkheid om door de data heen te vliegen als ware het een virtual reality-achtig spel. Dat nodigt uit tot kijken en spelen. De gegevens die daarmee inzichtelijk gemaakt worden, leveren veel sneller informatie op dan wanneer gebruik gemaakt wordt van de traditionele manieren. De technieken Het plaatje dat je ziet in figuur 4 is een screenshot van een applicatie die ik voor dit artikel gebouwd heb. Het komt dus niet uit Photoshop! Deze applicatie is geheel gebouwd in .Net 3.5 en draait op het WPFframework. Het is ook mogelijk om dit in Silverlight te bouwen, maar als je dat van plan bent wil ik je wel even waarschuwen. Silverlight ondersteunt geen 3D-grafieken. Je zult alles dus zelf moeten tekenen. In WPF is het maken van een dergelijk systeem een fluitje van een cent. Goed, je moet wat basiskennis hebben over het programmeren van 3D-omgevingen maar er zijn genoeg goede boeken over dat onderwerp te verkrijgen. Ook op mijn blog doe ik helemaal uit de doeken hoe ik dergelijke applicaties bouw. De broncode is vrij beschikbaar. De truc zit hem ook niet in het daadwerkelijk bouwen van de 3D applicatie, maar wel in het onderzoeken van welke data je weer wilt geven. Dit is echter het werk van een analist en niet van de ontwikkelaar. Naast een analist wil je er trouwens ook een graficus bij hebben; iemand die verstand van kleuren en vormen heeft. Mijn ervaring is dat game-designers erg goed zijn in het opzetten van een Business Visualization omgeving! En ja, ook met XNA kun je dergelijke rapportagetools maken (als proef heb ik dergelijke applicaties al op een Xbox 360 laten draaien…)
Mijn ervaring is dat game-designers erg goed zijn in het opzetten van een Business Visualization omgeving Conclusie Door gebruik te maken van slimme rapportages kan een Information Worker veel sneller grote hoeveelheden gegevens omzetten in informatie. De tools die vandaag de dag beschikbaar zijn worden helaas nog niet genoeg ingezet in de meeste organisaties. Door slim om te gaan met de mogelijkheden van de tools en de mogelijkheden van de menselijke geest kunnen we echter de gegevens wel zodanig presenteren, dat de IW’er zijn of haar werk veel efficiënter kan doen. Probeer eens te denken buiten de gebaande paden. Realiseer je dat een Information Worker een heel andere manier van werken en denken heeft dan een ontwikkelaar. Mensen willen graag ‘gevoel’ hebben bij data zodat ze informatie krijgen. Als je dat in je applicaties voor elkaar kunt krijgen, zul je zien dat de IW’er waar jij je applicaties voor bouwt je enorm dankbaar zal zijn. En daar gaat het ons toch om? •
Dennis Vroegop Dennis Vroegop is software architect bij Detrio, waar hij onder andere verantwoordelijk is voor het begeleiden van .NET projecten. Hij is een programmeur in hart en nieren en is een echt community mens. Momenteel is Dennis naast zijn normale werkzaamheden ook voorzitter van de Nederlandse .NET gebruikersgroep.
magazine voor software development 17
Advertentie Avanade
ED IT IE
10 0
ZI SDN NE
ED IT IE MA GA ZI NE SD N
Cary Jensen
Train of Thought:
MA
GA
Confessions of a Conference Speaker As you read this magazine, it should be impossible to ignore the fact that this is the 100th issue. This is, on so many levels, a remarkable achievement, and one that is worthy of celebration. I have written columns for a number of different magazines, and few of them have survived long enough to reach this impressive milestone (though I admit no hand in their demise). So let me begin by expressing my heartfelt congratulations to everyone involved in this magazine, past, present, and future.
I will celebrate this 100th issue by keeping this secret no longer As a regular contributor to the SDN Magazine, I have been asked to contribute something more on the personal side. And although I have always enjoyed sharing technical material, I am delighted to oblige this request. I will, to put it simply, tell on myself. Specifically, I will share with you, dear reader, an embarrassing episode that I was at one time committed to keeping secret forever. But secrets kept are lonely, at best. And therefore, I will celebrate this 100th issue by keeping this secret no longer. To begin with, I have a confession. Actually, I have two. The first is that, as a conference speaker, the conferences offered by the Software Development Network, an organization that has undergone a variety of name changes over the years, are among my favorite. The reason for this affection is simple. It’s the people involved: organizers, attendees, and fellow speakers. I know of no reoccurring conferences that so consistently bring together such a good group of people, and I appreciate it very much. Thank you for making me a part of this.
I was first introduced to this organization, which at the time was called the Software Developer Group Netherlands, in 1995. Following a talk I gave at the annual Borland International Conference in Anaheim, California, Joop Pecht, SDGN Secretary, and Ad van der Lisdonk, then President of SDGN, introduced themselves, during which they gave me a call for papers for the upcoming SDGN event. The SDGN Conference to the Max 1996 was my first conference for this organization, and I have since had the honor and pleasure to speak at 12 of this organization’s major conferences, as well as a number of the smaller events. And this leads me to my second confession. In December of 2003, I nearly missed my own SDN Event presentation. To be more precise, I missed part of it, the first part, to be exact (which must be obvious if you give it any thought). And here’s the weird part. It was a successful presentation. But you’ll have to read on if you want to know why. As I begin this confession, I imagine that your reaction might range from “Oh, who cares? It’s not the end of the world.” to “How dare you! We, who committed our time and energy to travel to these events, deserve better.” Regardless of whether you are among the “yawn” group or the “hang him up” contingent, I must admit that this mix-up was nothing short of a worse nightmare come true. I’ve been delivering corporate training, training tours, user group meetings, workshops, and the like, for more than two decades. With as much travel as I have done, and as many courses and seminars I’ve presented across North America and Europe, it’s a miracle that I haven’t encountered some situation where, due to events beyond my control, I have failed to arrive on time for the event. In most cases, I would fly into a city where I was presenting one or two days in advance. And though the odds are against it, I was never late due to weather or equipment malfunction. Although I lived with the constant dread that I would miss an event, and anticipated what actions I would take if the circumstances arose, it just didn’t happen. (I should really end that last sentence with the word “yet.” It’s like the old adage about motorcycle riding. If you ride a motorcycle, you’ve either fallen, or you will fall. As long as you keep riding a motorcycle, a fall is certain.)
If you ride a motorcycle, you’ve either fallen, or you will fall; as long as you keep riding a motorcycle, a fall is certain So, with this in mind, you might imagine that I put a fair amount of planning into my travel in order to minimize the possibility that something may happen to keep me from making my presentation. And the days leading up to December 10th, 2003, were no different.
magazine voor software development 19
0 10
ED IT IE
IE ED IT MA GA ZI NE SD N
ZI SDN NE GA
MA
This particular SDN Event was held at de Reehorst Hotel and Conference Center in Ede. This, it turns out, was a contributing factor. A second factor was that I was scheduled to give my first presentation during the day’s first time slot, immediately following registration. For those of you who’ve been to de Reehorst, you can understand why my wife, Loy Anderson (whom many of you are familiar with, and who always attends SDN events with me), and I decided to stay in Amsterdam the night prior to the conference. To put it bluntly, de Reehorst is in the middle of nowhere. Actually, it’s in the middle of a charming neighborhood, but compared to Amsterdam’s nightlife, that constitutes the middle of nowhere.
To put it bluntly, de Reehorst is in the middle of nowhere. Actually, it’s in the middle of a charming neighborhood, but compared to Amsterdam’s nightlife, that constitutes the middle of nowhere Mind you, Loy and I are not big nightlife people. We do, however, like to shop and stroll around the walking streets of central Amsterdam, and Ede simply does not offer the same opportunities for this. No problem. We’ll stay in Amsterdam the night before and take the train in the morning. As I mentioned earlier, when travel is involved, I tend to plan extensively, and this time was no different. I choose to take the 7:46 train, which would get me to the Ede-Wageningen train station at 8:45. The Hotel is a short three blocks from there, so I would be at De Reehorst at least 35 minutes or so before my talk, which was schedules to start at 9:30. In addition, and here’s the clever part, if for some reason I missed that train, there was another scheduled to depart at 8:16, which would still get me to the conference center with a few minutes to spare. With this bit of insurance, what could possibly go wrong? We left our hotel, which was very close to Amsterdam’s Central Station, at 7:20 the morning of the conference. It was a bitterly cold morning, and the sidewalks were icy. No problem, however, as we had given ourselves plenty of extra time. And we arrived at the train platform 10 minutes early. This story is a little better if you understand that Loy and I are seasoned travelers, even in Europe. For us, taking a train is no problem (which is why the SDN organizers trusted us to make the journey alone, rather than arranging for someone to drive us in the morning). But the Dutch language, of which we can understand only a little, aggravated our downfall. At 7:45, a mere minute before our train was expected, there was an announcement. My limited Dutch allowed me to comprehend that it had something to do with the arriving train. But that was all. My first clue that something was wrong was that the train arrived, right on time, from the wrong direction. The train arrived from the east, the direction of Utrecht, and I knew that we had to travel through Utrecht on our way to Ede. But what do I know about this train. Maybe once it picks up its passengers it will head back towards the east. (Please note that the monitors on the platform still showed a train to Utrecht at 7:46, and their was no indication of the train’s destination on the train itself. Believe me, I checked.) So, here we were, on the correct platform (5b) at the correct time (7:46) with a train arriving. It must be our train, right? But then there was that announcement. What was actually said? There was little time to think, and certainly not enough time to ask someone whether this train was heading towards Utrecht or not. We had to act. The train doors were
20
MAGAZINE
about to close. Loy gave me one of those urgent “what do we do?” looks, and I said “Let’s go!”. We boarded the train just as the doors closed. As soon as the train began moving, I knew I had made a mistake. The train continued on its course to the west, and I had no idea where it was going. I panicked. Obviously, this was the wrong train, and we would miss the train that was scheduled for 7:46 (and apparently, in hindsight, delayed). My best hope was that this train was a stoptrein, one that made frequent stops. We could then get off, and take the very next train back to Amsterdam Central, in time for the 8:16, which would get us to the de Reehorst in time for my talk. As most of you know, there are several different kinds of trains. As mentioned, there are the “stoptreins”, which stop often, and then there are the “sneltreins”, the fast trains, which stop infrequently. It turns out that we were on an InterCity sneltrein. Next stop, Haarlem. The trip to Haarlem took almost 15 minutes. This alone was cause for concern. If we could not get back to Amsterdam Central for the 8:16, there was no way I would arrive in time for my talk. We got off the train upon arrival in Haarlem. Loy tried to find out which platform we needed to go to in order to catch the train back to Amsterdam Central Station while I used a pay phone to call the cell phone of our conference contact, Johan Parent, with whom we had spent a lovely afternoon the day before in central Amsterdam.
In fact, this train was the next train Johan didn’t answer his cell phone, and to this day I am not even sure that I dialed the correct phone number, but I left a message anyway. As I hung up the phone, Loy approached me without having discovered where we needed to be. At this moment, I noticed that the train we had just disembarked was preparing to leave the station, and that the monitors indicated that this train was bound for Amsterdam. With almost no time to spare, we once again boarded this train as the door closed. Fortunately, this time the train left the station in the opposite direction in which it had entered (why didn’t this happen in Amsterdam?). As the train departed the station, we did a little math in our heads, and realized that, not only was this train returning to Amsterdam, but it was scheduled to arrive at 8:16. In fact, this train was the next train, our 8:16 backup train. What a relief. I should make my talk. Shortly after the train departed the Amsterdam Central station for Utrecht, where we needed to change trains, I realized that I should contact Johan in order to let him know that I would arrive only minutes before my presentation. I noticed another passenger, a young man, talking on a cell phone (I didn’t have a cell phone that worked in Europe at the time). After he hung up his call, I explained my situation to him, and asked if he would be so kind as to let me pay him to make a short call. This time I got through to Johan, and explained that I was not going to arrive until just before my talk, just as long as we made our connecting train in Utrecht. Johan thanked me for warning him of my late arrival, and said he looked forward to seeing us again. At this point, we felt like we could relax and look out the train windows at the cold, frosty fields as the train made its way. It was still quite early, and the pale blue of morning was only beginning to spread across the sky. We arrived in Utrecht with 5 minutes to spare before our next train was scheduled to depart for, among other stations, Ede-Wageningen. But while we waited on the platform, there was another announcement. Again, my Dutch language skills failed me, but we both knew that something was up. Everyone else waiting on that platform gathered their belongings and left. Having learned a hard lesson from the morning’s earlier failure,
10 0
ED IT IE
IT IE ED MA GA ZI NE SD N
ZI SDN NE GA
MA
I immediately sought out a conductor. The gentleman I found explained to me that the tracks were icy, and that our scheduled train had been canceled. The next train that could take us to EdeWageningen was leaving from another platform in twenty minutes. That did it. It was now impossible for me to make my talk. Before we caught the next train I had to contact Johan with the bad news. Maybe, I rationalized, another speaker could give a talk scheduled for later in the day in my time slot. On our way to the new platform we found another pay phone, and called Johan again. I explained to him that, at best, I would be 20 minutes or so late for my talk. I suggested a couple of possible actions to minimize the damage of my absence. Now, you are going to need a little more information before you will understand the difficult position we were in. To begin with, my scheduled talk was unusual for an SDN conference. Heck, if it had been on a Delphi, Visual Objects, VB, or other similar topic, there would probably have been a dozen or so people who could have stepped in to provide at least an introduction prior to my arrival. No, this was a talk on the Advantage Database Server, a product about which Loy and I had just recently published a book. And I could think of only one person who would likely be able to help. His name was Simon Groothuizen. Simon was the BeNeLux representative for Extended Systems, the company that owned the Advantage Database Server at the time (it is now owned by Sybase). So, I suggested two options to Johan. First, swap my first talk with that of another speaker (and this was the option that I hoped would work, but you never know. Scheduling talks can be difficult, and it simply may not be possible to swap two talks so close to the beginning of the event). Second, ask Simon to take the opportunity to give the audience an introduction to Advantage. I would then come in and cover the technical side of things. As I got off the phone with Johan, I promised I’d get to the hotel as soon as possible. In the back of my mind, however, I had doubts. Things had not gone well so far, why should they get any better now? It was still very cold, and the tracks as well as roadways were slippery. Nothing was certain at this point. Fortunately, things did start to go right. The next train arrived on time,
Nothing was certain at this point and we soon found ourselves at the Ede-Wageningen station. Before arrival, however, I had started up my laptop, loaded my PowerPoint presentation, Delphi, and the Advantage Data Architect (a graphical application for configuring Advantage objects and data dictionaries). I then placed my laptop in sleep mode, so I could quickly resume if necessary. Foot travel was difficult as Loy and I made our way to de Reehorst. The sidewalks were icy, and running was not an option, that is unless we wanted to practice falling down. Therefore, we choose the slow and steady method. I don’t recall either of us falling, not even once. We arrived at the conference center, and walked directly to the room
… that is unless we wanted to practice falling down where my talk was scheduled. On the way there we saw nobody who could confirm, one way or the other, if my talk was actually in progress. I opened the door to the conference room and walked toward the stage. On stage were two people I knew well. Speaking was Jeroen Pluimers, a long-time friend and fellow conference speaker. Next to him, apparently for moral support, was another friend and SDN member, John Blaauw. I knew that both Jeroen and John where
Advantage users. Simon, on the other hand, was nowhere to be found. (It turns out that he suffered from stage fright, and was unable to give an introduction to an audience the size of which turned out for this talk.) Fortunately, Jeroen and John were available, and pitched in at the last moment. For this, I am forever grateful. As it turns out, just as I entered the room, Jeroen had covered just about all of Advantage that he was ready for given the spur of the moment. But what he had shared provided the perfect introduction to the material that I had prepared. Upon seeing me, Jeroen smiled widely, and announced my presence to the audience. This resulted in a rather embarrassing and certainly undeserved round of applause for me. Several people mentioned to me later that it was the perfect rock star entrance. As I walked down the aisle towards the stage, I removed my coat and took my laptop out of sleep mode. It no time my slideshow appeared on the overhead projection system. And in the transition between Jeroen and I, we scarcely skipped a beat. And, as I mentioned earlier, the overall talk was a success on nearly every level. You sometimes hear the phrase “Someday we’ll be able to laugh about this.” At the time, I was sure that this would not be the case. However, I hope that you have had a laugh or two, even if it was at my expense. Indeed, it was fun sharing with you this little secret. I ask only one thing. Please don’t tell anybody. •
Cary Jensen Cary Jensen is President of Jensen Data Systems, Inc., a training and consulting company that won the 2002 and 2003 Delphi Informant Readers Choice Awards for Best Training. He is an award-winning author of 20 books, including Advantage Database Server: A Developers Guide (2007, Sybase), Building Kylix Applications (2001, Osborne/McGraw-Hill), JBuilder Essentials (1998, Osborne/ McGraw-Hill), and Delphi In Depth (1996, Osborne/McGraw-Hill). For information about onsite training or consulting services, you can contact Cary at [email protected] or visit his web site at www.JensenDataSystems.com.
SDN
TIP:
RSS Op de nieuwe SDN-site is de RSS-feed verplaatst; je zult dus je RSS-reader opnieuw moeten instellen. De nieuwe feed voor verenigingsnieuws is te vinden via: http://tinyurl.com/bca42t en de feed voor nieuwe artikelen is te vinden op http://tinyurl.com/ bmpn6d.
magazine voor software development 21
Advertentie Sogeti
.NET ASP
ASP.NET onder de Motorkap:
Heeft ASP.NET nog Toekomst? ASP.NET bestaat inmiddels al weer 7 jaar en dat is een mooi moment om eens terug te kijken en vooruit te blikken. Waren het 7 vette jaren of waren het juist 7 magere jaren en gaan we nu pas echt genieten van ASP.NET? Zijn we echt verder dan ASP “Classic”? Voor de meeste mensen die zoals ik nog ASP “Classic” en Windows DNA geprogrammeerd hebben, was ASP.NET fantastisch. Eindelijk af van dat lelijke gescript tussen de HTML, echt object georiënteerd programmeren en veel robuuster door onder andere garbage collection. Helaas zijn er ook mensen die het model van ASP.NET vervloeken, want het vereist een hele andere denkwijze dan ASP “Classic” en vooral als je hele specifieke zaken wil met de weergave, kan het objectmodel van ASP.NET nog wel eens in de weg zitten. Dat de ASP “Classic” manier goed werkt en blijft werken is te zien aan het succes van PHP, dat qua methodiek veel gemeen heeft met ASP “Classic”. Als Microsoft meer moeite had genomen om ASP te verbeteren en uit te breiden met allerlei modules die het leven van de ontwikkelaar vereenvoudigen (zoals met PHP gedaan is), waren we dan niet beter af geweest? Als je kijkt naar ASP.NET MVC (Model View Controller), is het antwoord daarop eigenlijk een schoorvoetend “ja”. ASP.NET MVC laat het model met pagina’s en controls varen voor een model waarin we (weer) een HTML sjabloon hebben waar we waardes “in prikken”. Het is veel geavanceerder dan ASP 7 jaar geleden was, maar het is niet ondenkbaar dat we nu veel verder waren geweest als Microsoft het model hetzelfde had gelaten en ons alleen de geneugten van de CLR had gegeven.
ASP.NET MVC laat het model met pagina’s en controls varen voor een model waarin we (weer) een HTML sjabloon hebben waar we waardes “in prikken” Is ASP.NET MVC de toekomst? Er zijn nu al mensen die me vragen of ASP.NET MVC het huidige model zal vervangen. Mijn antwoord hierop is dat ik dit niet op korte termijn zie gebeuren, aangezien ASP.NET MVC splinternieuw is en de meeste mensen het nog niet eens gezien hebben. Verder zit er een enorme investering in applicaties die volgens het bestaande model gebouwd zijn. Er zijn ongelooflijk veel controls op de markt die alleen geschikt zijn voor het huidige model en voordat al die functionaliteit ook in ASP.NET MVC gerealiseerd is, zijn we wel weer even verder. Het is echter ontegenzeggelijk een interessante technologie en het lijkt erop dat Microsoft meer heil ziet in ASP.NET MVC dan het steeds maar complexer worden van allerhande controls. Ook gaat Microsoft met ASP.NET MVC de concurrentie aan met technologieën om snel applicaties te bouwen, zoals Ruby on Rails en geeft het veel meer vrijheid om AJAX (Asynchronous JavaScript And XML) te gebruiken in een applicatie. Want hoe handig het UpdatePanel voor ontwikkelaars ook is, het is in feite niet meer dan een verkapte postback, en dat is nou net wat AJAX eigenlijk helemaal moet voorkomen. Moeten we allemaal AJA(X)cied worden? Nu we het toch over AJAX hebben, is dat eigenlijk wel de weg die we op moeten? AJAX leunt zwaar op de HTML DOM en de mogelijkheden
van JavaScript. Dat er al jaren door Adobe, Google en Mozilla enerzijds en Microsoft en Yahoo anderzijds onenigheid is over hoe JavaScript zich verder moet ontwikkelen laat in zekere zin het failliet zien van de huidige standaarden voor het web, zeker als je daar ook nog eens de strijd tussen Adobe AIR, Google Gears en Microsoft’s Silverlight bij betrekt. Vooral AIR en Silverlight laten pijnlijk duidelijk zien dat AJAX niet meer is dan een lapmiddel op de tekortkomingen van HTML. Ironisch genoeg had HTML eigenlijk al helemaal niet meer moeten bestaan in z’n huidige vorm. Een van de bestaansredenen van XML is het vervangen van HTML, om op die manier tot een web te komen dat bestaat uit data met betekenis: het semantische web. Hoe je de XML wilt weergeven hangt af van wat je ermee wilt doen. Is het slechts content? Dan is CSS als opmaak sausje eroverheen genoeg (eventueel met wat HTML eromheen). Is het voor een applicatie? Dan weet die applicatie aan de hand van onder andere het XML Schema wel wat ermee gedaan moet worden. Helaas is die realiteit nog steeds heel ver weg, maar er gloort hoop. Met SOAP en de WS-* specificaties zijn er voldoende standaarden om een serieuze applicatie te maken op basis van web services. Het maken van een RSS-feed is kinderlijk eenvoudig en ook de ondersteuning voor custom protocollen op basis van POX (Plain Old XML) en REST (Representation State Transfer) wordt met de dag beter. Nu moeten we alleen nog ophouden alle websites aan te bieden als HTML … maar dat is nu net waarom XML nog steeds HTML niet vervangen heeft: het is wel erg makkelijk zo. Join the (r)evolution! Het web is toe aan een radicale vernieuwing, een die net zo groot is als de ontwikkeling van het web zelf begin jaren negentig. Dit ontwikkeling is echter maar voor een heel klein deel een technische ontwikkeling. Het moet vooral van ons komen. Wij moeten stoppen met kijken naar het web als een verzameling HTML-pagina’s en zien dat het een verzameling van data, informatie en operaties moet worden. Als we dat kunnen, is de technologie die de weergave verzorgt van ondergeschikt belang. Het is immers slechts een “view” op de data. •
Michiel van Otegem Michiel van Otegem is een van de pioniers op het gebied van ASP.NET. Hij schrijft, spreekt en geeft hierover training. Hij is medeeigenaar van de community website ASPNL.com en schrijver van “ASP.NET 3.5 - de basis”, “XML - de basis” en “Sams Teach Yourself XSLT in 21 Days”. Voor zijn continue inzet voor de .NET community heeft Michiel al 6 jaar op rij de MVP Award van Microsoft ontvangen. Michiel is werkzaam bij Batavia Labs als Chief Software Architect.
magazine voor software development 23
UX INFORMATION WORKER
Sandra de Ridder
Tips voor Mobile Webdesign Waar bedrijven veel tijd en geld investeren in een reguliere website, wordt er over het algemeen nauwelijks aandacht besteed aan een mobiele variant hierop. In de afgelopen jaren is het aantal gebruikers dat een PDA of mobiele telefoon gebruikt voor internet explosief gestegen. Het kleine scherm en gebrek aan ondersteuning van JavaScript zorgen ervoor dat de meeste websites niet goed werken op een mobiele telefoon of PDA. Men doet er goed aan een aparte website te ontwikkelen voor mobiel internet om de user experience ook voor deze doelgroep te optimaliseren. In dit artikel wordt een aantal tips behandeld die je kunnen helpen bij het ontwerpen van een mobiele website. Soorten gebruikers Mobiel internet biedt toegang tot informatie op ‘vreemde’ plaatsen en in nieuwe situaties. Dit zorgt voor een hele nieuwe manier van internetgebruik en een nieuw soort gebruiker. Grote rol bij het maken van keuzes voor mobile webdesign speelt de status van de gemiddelde gebruiker: • Wat zijn ze aan het doen? • Waarom bezoeken ze de website? • Waarin zijn ze geïnteresseerd? • Waarnaar zijn ze op zoek? • Waarom bezoeken ze de website juist nu? Houd er rekening mee dat de gebruikers van het mobiele internet vaak met een ander doel naar de website komen dan gebruikers die achter hun bureau zitten.
een speciale URL gegenereerd die alle voorkeuren van de gebruiker bevat. Als de gebruiker op zijn PDA of mobiele telefoon via deze URL de site bezoekt, krijgt hij de door hem zelf samengestelde content te zien. De bezoeker die dringend informatie nodig heeft Afhankelijk van het soort website kan de betekenis van ‘dringend’ nogal variëren. Voor een klant van een webwinkel kan dringend zijn: “Mijn bestelling is nog niet binnen, waar is die nu?” Een dringendere vraag zou kunnen zijn: “Mijn trein heeft 10 minuten vertraging, haal ik mijn aansluiting nog?” Voor sommige bezoekers is alles dringend. Als je de belangrijkste informatiebehoefte van je doelgroep kent en je ervoor zorgt dat deze informatie binnen 1 of 2 klikken beschikbaar is, verhoog je de usability van je website enorm (dit geldt uiteraard ook voor reguliere websites).
Google heeft veel tijd en energie gestoken in het achterhalen van de verschillende gebruikersgroepen van mobiel internet. De web developers van Google hebben zich uiteindelijk gericht op drie soorten gebruikers en hun webapplicaties afgestemd op de wensen en eisen van deze drie groepen. Laten we deze groepen eens beter bekijken … De “gewone” gebruiker Deze gebruikers verschillen niet veel van de gemiddelde bezoeker van een reguliere website. Ze zijn niet speciaal op zoek naar specifieke informatie, maar hebben even tijd over om op het internet te surfen. Om ervoor te zorgen dat deze gebruikers vaker terugkomen op je site moeten ze geprikkeld worden door de informatie die ze tegenkomen. Laat met het oog op de beperkte schermruimte en beperkte tijd die deze gebruiker heeft, geen grote stukken informatie zien. Deel het op in hapklare brokken die bij de bezoeker genoeg interesse opwekken om nog eens terug te komen naar de site. De terugkerende gebruiker Deze gebruikers komen regelmatig terug naar je website voor specifieke informatie of data. Als je een site hebt met bijvoorbeeld file-informatie, weerberichten of sportuitslagen, dan heb je waarschijnlijk veel van dit soort gebruikers. Analyseer welke informatie veel opgevraagd wordt en maak deze makkelijk bereikbaar: toon het als eerste op de pagina. Ook voor het mobiele web geldt, dat informatie verstopt achter drie of vier klikken niet wordt bekeken. Aanbieden van gepersonaliseerde content via een mobiele website is niet eenvoudig, maar ook niet onmogelijk. Op een reguliere website kun je via een inlog bij voor jou relevante informatie komen. Op een mobiele website is inloggen niet echt eenvoudig. Een alternatief hiervoor kan zijn om terugkerende gebruikers via het reguliere web hun startpagina te laten personaliseren. De gebruiker kan via een desktop pc een persoonlijke versie van de mobiele site samenstellen. Er wordt
24
MAGAZINE
Fig. 1:Routeplannen via mobile.9292ov.nl Minder opties = effectievere interface Gebrek aan schermruimte en internetverbindingen die vaak langzamer zijn dan die op een pc dwingen je tot het maken van keuzes in de aangeboden opties en informatie. Minder opties zorgen voor een simpelere en effectievere interface, zolang de aangeboden functionaliteit de gebruiker maar laat doen wat hij wil. Aan de webdesigner de schone taak vast te stellen wat essentieel is en wat weggelaten kan worden. Dit lijkt misschien simpel, maar een website die vol staat met
UX INFORMATION WORKER
afbeeldingen, video’s en inhoud terugbrengen tot het hoogstnoodzakelijke kan een uitdaging zijn. Dat het moeilijk is om afbeeldingen helemaal weg te laten blijkt wel uit verschillende nieuwssites. De banners worden wel weggelaten, maar afbeeldingen mogen kennelijk niet ontbreken in de mobiele variant. Wikipedia heeft een goede mobiele website. Het invoervak centraal, geen toeters en bellen. De resultaten worden ook heel basic weergegeven. Jammer is alleen dat je er niet automatisch op terecht komt als je via je mobiel de url van de reguliere site invoert.
Strenge selectie van content Vanwege de eenvoud van de pagina’s en het terugbrengen van het aantal opties is de content die weergegeven wordt zorgvuldig geselecteerd. Wat hierbij opvalt is dat de inhoud zorgvuldig geselecteerd is voor de gebruiker, de optimale user experience! In de ideale webwereld zouden alle websites op die manier ontworpen en ingericht zijn, maar omdat de meeste websites commercieel zijn, ontkom je vaak niet aan elementen die niet belangrijk zijn voor de gebruiker, zoals banners. Terwijl adverteren op reguliere websites algemeen geaccepteerd is, zijn de meeste mobiele websites vrij van advertenties. Witruimte Als je websites gaat bekijken die voor het mobiele web ontworpen zijn, zul je zien dat deze websites veel witruimte bevatten. Zeker degene die makkelijk zijn in gebruik. Witruimte is belangrijk in ieder ontwerp en is meestal een uitdaging in webdesign omdat er veel informatie in beperkte ruimte aangeboden moet worden. Witruimte is nog belangrijker in mobile webdesign omdat het scherm nog kleiner is. Een volgepropte website zou erg gebruiksonvriendelijk en moeilijk te gebruiken zijn via het mobiele web.
Fig. 2 :Wikipedia back to the basic
Fig. 3 : De reguliere website van de NOS …
Gebruik van afbeeldingen Doordat de afgelopen jaren internetverbindingen steeds sneller zijn geworden, hebben de designers meer vrijheid gekregen in het gebruik van afbeeldingen en video’s. De gemiddelde bezoeker van een website via een desktop of laptop wil graag een rijk geïllustreerde, visueel aantrekkelijke website zien. Voor ontwerp van een mobiele website geldt dat afbeeldingen vaak meer fout dan goed doen. Er is een grote variatie in verbindingssnelheid en kosten van mobiele internet. Bezoekers maken zich drukker om hun verbindingssnelheid bij internet via hun mobiel. Daarnaast maakt de schermgrootte het moeilijk om afbeeldingen goed te zien en tekst moeilijker te lezen. Met toename van het aantal internetters met smartphones, die grotere schermen en hogere verbindingssnelheid hebben, is er meer mogelijk voor gebruik van afbeeldingen. Er is echter een grote groep gebruikers die nog geen smartphone heeft. Voor mobiele versies van websites is het daarom gebruikelijk weinig afbeeldingen te gebruiken. Verschillende schermformaten Waar je bij het ontwerpen van een website al rekening moet houden met de verschillende resoluties van beeldschermen, is het bij mobiele telefoons en PDA’s een nog grotere uitdaging. Doordat de techniek zich blijft ontwikkelen, veranderen de schermafmetingen ook. Moderne smartphones en PDA’s hebben grotere schermen en hogere resoluties dan die van een paar jaar geleden, maar de oudere zijn ook nog steeds in gebruik. Niet alleen moet je rekening houden met verschillende resoluties, maar ook met verschillende vormen. In figuur 5 is een aantal van deze formaten te zien. Subdomein versus .mobi of ander apart domein Terwijl sommige websites .mobi voor hun mobiele versie gebruiken, is het gebruikelijker om
Fig. 4: … en de mobiele variant hierop
Fig. 5 : Veelvoorkomende schermformaten
magazine voor software development 25
UX INFORMATION WORKER
een subdomein te gebruiken of een aparte folder op het primaire domein. Een van de grootste voordelen hiervan is dat alles in één domein blijft staan.
user experience van mobiel internet moeten verbeteren. Zij ontvangen graag feedback op deze best practices (zie http://www.w3.org/ 2005/MWI/ BPWG/).
Testen Voor het reguliere web zijn genoeg betrouwbare manieren om je website te testen. Voor het mobiele web ligt dit iets anders. De kans is aanzienlijk dat de bezoekers van je website in een situatie zijn die je niet hebt kunnen testen. Een andere uitdaging in webdesign voor het mobiele web zijn de vele verschillende typen telefoons en PDA’s die gebruikt worden. Met een goed ontwerp en goed geplande tests is het mogelijk om er vrij zeker van te zijn dat de website goed weergegeven wordt en, nog belangrijker, bruikbaar is op de meeste PDA’s en mobiele telefoons. De eenvoud van je website maakt het testen al een stuk eenvoudiger, simpelweg omdat er minder is dat mis kan gaan. Er is een aantal online en offline emulators beschikbaar waarmee je de meeste problemen al uit je website zult kunnen halen. De Google Wireless transcoder -zie http://www.google.com/gwt/n- geeft een goed beeld van hoe je website eruit zal komen te zien, al is deze niet zo streng en geeft-ie je website al snel redelijk weer. Ook Microsoft zie http://www.microsoft.com/downloads/details.aspx?FamilyId= 1A7A6B52-F89E-4354-84CE-5D19C204498A&displaylang=en heeft een emulator die als Visual Studio plugin of stand alone te gebruiken is.De beste optie is waarschijnlijk een groep mensen met verschillende soorten mobile telefoons bij elkaar te zoeken voor een goede usability test.
Resources en artikelen Als je na deze het lezen van dit artikel nieuwsgierig bent geworden naar de mogelijkheden voor webdesign voor het mobiele internet kun je op onderstaande URL’s meer informatie over mobile webdesign en voorbeelden van mobiele websites vinden. • Make your site mobile friendly
http://thinkvitamin.com/features/css/makeyour-site-mobile-friendly/
Mobile web trends for 2009
http://www.smashingmagazine.com/2009/01/ 13/mobile-web-design-trends-2009/
Designing and Developing mobile websites in the real world
http://dev.opera.com/articles/view/designingand-developing-mobile-web-site/
7 Usability Guidelines for Websites on Mobile Devices
http://www.webcredible.co.uk/user-friendlyresources/web-usability/mobile-guidelines. shtml
Google Wireless Transcoder
http://www.google.com/gwt/n
W3C Mobile Web Initiative
http://www.w3.org/Mobile/About
Mobile Awesomeness
http://www.mobileawesomeness.com/
Standaarden Er is een aantal organisaties dat zich bezig houdt met het ontwikkelen van standaarden om websites geschikt te maken voor het mobiel internet. Het W3C heeft sinds 2005 een “Mobile Section Workgroup”. Deze werkgroep heeft in december 2008 een concept versie van best
Advertentie Barnsten/ Embarcadero
Sandra de Ridder Sandra de Ridder is UX en SharePoint consultant bij Sparked (www.Sparked.nl). Voor vragen en opmerkingen is Sandra te bereiken via [email protected]. Sandra is sinds kort ook actief als medewerker van de UX track van het SDN.
DATABASES .NET
Frans van der Geer en Andre van Leeuwen
Betrouwbare Services Bouwen met
SQL Server Dit artikel beschrijft hoe een service gemaakt kan worden met SQLServer Service Broker. WCF is tegenwoordig de standaard keuze op het Microsoft platform voor het maken van services, maar dit artikel toont aan dat Service Broker een serieus alternatief kan zijn voor niet publieke services. Dit artikel beschrijft de gebruikte techniek: SQL Service Broker, XML data manipulatie in T-SQL en C# stored procedures in combinatie met LINQ ter implementatie van de service. Het artikel is van toepassing op SQLServer 2005 en 2008. Het artikel is gebaseerd op de praktijksituatie van de salarisverwerker SDB Groep in de zorgsector. Voor deze dienstverlener is een rekenservice gerealiseerd waarmee alle salariscomponenten opnieuw worden uitgerekend zodra het werkrooster van een zorgverlener wordt gewijzigd. De overheid streeft naar zoveel mogelijk handen aan het bed tegen zo laag mogelijke kosten, dus inzicht in de loonkosten helpt een zorginstelling om de zorgverlening optimaal te plannen. Aan het eind van elke salarisperiode worden de rekenresultaten als invoer gebruikt voor de salarisverwerking. Figuur 1 geeft de oplossing schematisch weer. Een gebruiker bewerkt een werkrooster via de webapplicatie. Dat resulteert in een database transactie om het gewijzigde rooster weg te schrijven. Als gevolg daarvan moeten er salariscomponenten opnieuw berekend worden. Dat wordt gedaan door asynchroon een rekenservice aan te roepen die is geïmplementeerd als een C# stored procedure in een andere database op een andere server om de on-line database zo min mogelijk te belasten. In de tweede helft van dit artikel zal de oplossing stap voor stap worden toegelicht, maar nu volgt eerst enige uitleg van Service Broker.
Fig. 1: Schematische weergave van de oplossing Wat is Service Broker? Service Broker is een wat minder bekend onderdeel van SQLServer dat sinds versie 2005 aan het product is toegevoegd. Menig lezer zal ooit een tabel met een statusveld hebben geïntroduceerd om dit via een SQLServer Agent job te laten pollen om daarmee een stored procedure te starten zodra gedetecteerd wordt dat er records zijn die aan bepaalde condities voldoen. Dat is een scenario dat nu beter met Service Broker kan worden opgelost.
Service Broker biedt de mogelijkheid om asynchroon services te laten uitvoeren via message queueing. Een service wordt geïmplementeerd met een stored procedure die gekoppeld is aan een queue. Een queue is in feite een speciaal soort tabel met een vooraf gedefinieerd schema, waarbij de belangrijkste velden het berichttype en het bericht zelf zijn. Een service kan geïnitieerd worden door een bericht naar een service (lees: queue) te sturen vanuit een database transactie. Het verwerken van het bericht uit de queue wordt vervolgens in een nieuwe transactie gedaan door een via Service Broker geïnitieerde stored procedure. Met andere woorden: Service Broker biedt de mogelijkheid tot het asynchroon laten uitvoeren van database transacties.
Service Broker biedt de mogelijkheid om asynchroon services te laten uitvoeren via message queueing Het SELECT statement kan worden gebruikt om in een queue te kijken, maar T-SQL heeft ook uitbreidingen ondergaan. Je hebt b.v. de statements CREATE QUEUE, SEND en RECEIVE om respectievelijk een queue aan te maken en berichten op een queue te plaatsen dan wel er vanaf te halen. Enkele voorbeelden volgen in de tweede helft van dit artikel. Er is locking van toepassing op berichten in de queue, zodat een bericht pas zichtbaar wordt nadat de versturende transactie gecommit is. Service Broker kan ervoor zorgen dat een stored procedure automatisch gestart wordt zodra een bericht op de queue wordt vrijgegeven. Dat doet Service Broker heel efficiënt. Er wordt niet voor elk nieuw bericht een nieuwe stored procedure instantie gestart. Er wordt pas een extra instantie gestart als blijkt dat de reeds actieve instanties het tempo waarmee berichten arriveren niet kunnen bijhouden. De database administrator kan het maximum aantal instanties per queue configureren, zodat er niet ongewenst veel resources gebruikt worden. De verzendende en ontvangende service kunnen in dezelfde database geïmplementeerd zijn, maar ook in verschillende databases en zelfs op een andere SQLServer, zoals in figuur 1 te zien is. Een service kan bovendien worden geïmplementeerd op verschillende SQL Servers tegelijk. Een bericht zal dan round-robin worden verzonden naar een van de servers om daar afgehandeld te worden. Een service kan hoog
magazine voor software development 27
DATABASES .NET
beschikbaar worden gemaakt door simpelweg de database hoog beschikbaar te maken met behulp van server clustering of database mirroring. Waarom Service Broker? De business casus vereist dat de rekenservice asynchroon maar gegarandeerd wordt uitgevoerd. Asynchroon omdat het werk van de planner niet gehinderd mag worden door het feit dat gekozen is om bij het opslaan van een rooster alles opnieuw te laten berekenen, en gegarandeerde verwerking omdat het om echt geld en om echte verlofdagen gaat. Eén van de eigenschappen van Service Broker is dat het lezen van de queue of het schrijven naar de queue in dezelfde database transactie kan plaatsvinden als het manipuleren van de data in de tabellen van de database. Vergelijk dat eens met een WCF service die via het MSMQ transport ook gegarandeerde message verwerking kan bieden, maar alleen in combinatie met de Distributed Transaction Coordinator. Dat geeft een veel minder goede performance dan Service Broker. Vanwege de extra benodigde componenten buiten SQLServer is deze oplossing ook moeilijker te configureren en te beheren. Het doorslaggevende argument om voor Service Broker te kiezen boven WCF met MSMQ is dat de berekeningen gestuurd worden door verschillende database tabellen. Als er iets wijzigt in een van die tabellen moeten berekeningen opnieuw worden gedaan. Met Service Broker is dat eenvoudig te realiseren door een bericht naar een service te sturen vanuit een database trigger.
een Conversation Group lock, die een Service Broker service automatisch vraagt zodra ze het eerste nieuwe bericht van een Conversation Group leest. Er is maar één service instantie die deze exclusieve lock kan krijgen. Andere berichten binnen dezelfde Conversation Group blijven verborgen voor andere service instanties totdat de ene service instantie geen berichten voor die Conversation Group meer in de queue vindt en de lock loslaat. Er kunnen daarna nog meer berichten arriveren binnen de dezelfde conversation Group. De eerste de beste service instantie die daarvan het eerste nieuwe bericht te pakken krijgt, plaatst opnieuw een lock en gaat er mee aan de slag. Een Conversation Group bestaat uit niets anders dan een unique identifier. De oplossing voor de salarisverwerker is dus om een Conversation Group Id per combinatie van medewerker en salarisperiode in de database te administreren en te zorgen dat elke dialoog met de juiste Conversation Group Id wordt geïnitieerd. Is SQL Server wel geschikt om complexe berekeningen te doen? SQLServer is erg goed in setsgewijze datamanipulaties met behulp van T-SQL, maar berekeningen die meer vergen dan een paar aggregatie functies kunnen beter in een procedurele taal worden geschreven. De SQLCLR komt daarbij goed van pas. Stored procedures kunnen in C# worden geschreven en de .Net assembly kan in de database worden ondergebracht. De SQLCLR kan de code uitvoeren alsof het een T-SQL procedure is. Procedurele code is dus krachtig. Maar aan de andere kant: berekeningen op basis van een verzameling ingeplande diensten zijn toch ook weer setsgewijs. Het zou mooi zijn als LINQ in de database gebruikt zou kunnen worden zodat setsgewijze expressies gecombineerd kunnen worden met ingewikkelde rekenkundige expressies.
Service Broker heeft extra het concept van een dialoog Door CLR is SQL Server geschikt voor Maar Service Broker heeft nog meer voordelen ten opzichte van WCF berekeningen met MSMQ. Wat Service Broker extra heeft ten opzichte van andere messaging systemen is het concept van een dialoog. Als twee services met elkaar moeten gaan communiceren moet vooraf eerst een contract worden gedefinieerd. Dat beschrijft welke berichttypen er tussen twee services uitgewisseld kunnen worden en met welke berichttypen een dialoog kan beginnen. Service Broker dwingt af dat een dialoog tussen twee services altijd voldoet aan het contract. De ontvangende service kan er dus op rekenen dat ze altijd berichttypen ontvangt die ze kan verwerken. Service Broker garandeert bovendien dat er maar één service instantie tegelijk actief kan zijn met verwerking van berichten van dezelfde dialoog. Daarmee worden problemen met volgordelijkheid van verwerking voorkomen. Een dialoog zou bijvoorbeeld kunnen bestaan uit het eerst versturen van een order header, gevolgd door het versturen van één of meer orderregels. Zonder de garantie die Service Broker biedt zou het kunnen zijn dat de eerste orderregel al wordt verwerkt door een tweede service instantie terwijl de eerste service instantie nog bezig is met de verwerking van de header. Berichten van verschillende dialogen kunnen wel parallel door verschillende service instanties worden verwerkt. In sommige situaties is de garantie van volgordelijke verwerking binnen een dialoog nog niet voldoende. Kijk b.v. eens naar de praktijk van de salarisverwerker, waar een planner twee keer achter elkaar een berekening voor dezelfde zorgverlener in dezelfde salarisperiode kan starten door twee wijzigingen achter elkaar uit te voeren op hetzelfde werkrooster. Elke berekening start een nieuwe dialoog met de rekenservice. De salarisverwerker moet er zeker van kunnen zijn dat alleen het rekenresultaat van de laatste situatie wordt bewaard. Dat is mogelijk met het Service Broker concept Conversation Group. Berichten van verschillende dialogen die onderdeel worden gemaakt van dezelfde Conversation Group, zullen gegarandeerd door één service instantie tegelijk worden afgehandeld. Dat werkt op basis van
28
MAGAZINE
SQLServer 2005 weet niets van het bestaan van LINQ, maar LINQ is niets anders dan een compiler truc die gewone CLR 2.0 compatible code oplevert met referenties naar een assembly uit het .Net 3.5 Framework. En de SQLServer CLR is compatible met de CLR 2.0. Dus het enige wat moet gebeuren om LINQ te ondersteunen vanuit de SQLServer CLR is het registreren van de betreffende .Net 3.5 Framework assembly in SQLServer. Listing 1 laat zien dat dit eenvoudig is. use master go
-- CLR ondersteuning aanzetten
EXEC sp_configure 'clr enabled', 1 RECONFIGURE go
IF @@VERSION NOT LIKE 'Microsoft SQL Server 2008%' BEGIN
-- Trustworthy setting is nodig om UNSAFE assemblies -- te mogen gebruiken.
-- Een assembly is UNSAFE als hij afhankelijk is
-- van een assembly die niet op de trusted list staat. ALTER DATABASE [$(TargetDB)] SET TRUSTWORTHY ON
END go
USE [$(TargetDB)] go
IF @@VERSION NOT LIKE 'Microsoft SQL Server 2008%' BEGIN
DATABASES .NET
-- Om LINQ in de database te kunnen gebruiken is -- onderstaande .NET 3.5 assembly nodig.
-- Omdat deze assembly nog niet op de trusted assembly -- list staat van SQL Server 2005,
-- is de enige optie om hem in UNSAFE mode te gebruiken. -- N.B. de $ parameter vereist dat SSMS in SQLCMD -- mode staat (zie de button
-- met het rode uitroepteken op de toolbar) IF NOT EXISTS( SELECT 1
FROM sys.assemblies
WHERE name = 'System.Core') BEGIN
CREATE ASSEMBLY [System.Core]
FROM '$(ProgramFiles)\Reference\Assemblies\ Microsoft\Framework\v3.5\System.Core.dll'
WITH PERMISSION_SET = UNSAFE
END
END
de namespace SDB.Mplus.Rekenserver.ObjectModel.Serializers. Het T-SQL type xml mapt op het .Net type System.Data.SqlTypes.SqlXml. Nadat deze “plumbing” code is geschreven kan de daadwerkelijke implementatie van de rekenlogica getypeerd worden uitgeschreven tegen het objectmodel dat op basis van het XML schema gemaakt is. using System;
using System.Data.SqlTypes; using System.Xml; using System.IO;
using System.LINQ;
using SDB.MPlus.RekenServer.ObjectModel; using Helper =
SDB.MPlus.RekenServer.ObjectModel.Serializers; public partial class StoredProcedures {
[Microsoft.SQL Server .Server.SqlProcedure]
public static void BerekenLoonfactoren(SqlXml vraag,
go
{
Listing1: Registreren van de assembly met LINQ Extension methods in SQLServer 2005
Listing 2 toont de C# stored procedure met de code om de XMLstream naar het object model te converteren en vice versa. Daarbij wordt gebruik gemaakt van de gegenereerde XML serializer classes in
// Creer het objectmodel uit de xml van de vraag XmlReader reader = vraag.CreateReader(); Helper.LoonfactorBerekeningVraag.
Ten eerste moet de SQLCLR worden aangezet. Vervolgens kan de assembly worden geregistreerd. De registratie van de .Net 3.5 assembly is niet nodig voor SQL Server 2008, omdat deze de benodigde assembly standaard op zijn vertrouwde lijst met assemblies heeft staan. Dit is niet het geval voor SQL Server 2005 en daarom moet de assembly als UNSAFE worden gemarkeerd. Een database administrator die daar huiverig voor is, moet zich realiseren dat deze settings alleen nodig zijn op de SQL Server instantie die de rekenservice host. En dat is natuurlijk een andere dan waarin alle productiedata zit. Die instantie draait zelfs op een andere server om de primaire database server niet te belasten met complexe berekeningen. Dus nog een extra SQL Server licentie erbij? Nee hoor, een gratis SQL Server Express Edition is voldoende mits de limiet van 1 CPU geen probleem is. SQL Service Broker werkt ook volledig op een SQL Server Express Edition, mits tenminste een Standard Edition in een dialoog betrokken is. En de SQL Server met de productiedata is dat. Vervolgens is de rekenservice in een aantal stappen gemaakt. 1) Startpunt is een XML Schema met de definitie van een drietal berichten. Ten eerste het vraagbericht met daarin alle data die nodig is voor een berekening. Ten tweede het antwoordbericht voor de rekenresultaten en ten derde een foutbericht mocht de berekening om een of andere reden niet lukken. 2) Op basis van het XML Schema is met behulp van het utility XsdObjectGen [ref1] een C# objectmodel gegenereerd. Alle berekeningen zullen geschreven worden op dit objectmodel. 3) De data wordt aangeleverd als een XML stream. Om de XML om te zetten in een instantie van een objectmodel en vice versa zijn XML (de)serializers nodig. Het .Net framework kan dynamisch XML (de)serializers genereren, maar de SQLCLR ondersteunt geen dynamisch gegenereerde code, dus in plaats daarvan zijn de serializers van te voren gegenereerd met behulp van de utility XgenPlus [ref2]. 4) Tenslotte is met behulp van Visual Studio 2008 een C# database project gemaakt. Dit project type bevat o.a. een template voor het maken van een CLR stored procedure. Daarbij moet bij de eigenschappen van het project gekozen worden voor .Net 3.5 als doelplatform; LINQ behoort dan tot de mogelijkheden.
out SqlXml antwoord)
LoonfactorBerekeningVraagSerializer vraagSerializer= new Helper.LoonfactorBerekeningVraag.
LoonfactorBerekeningVraagSerializer();
LoonfactorBerekeningVraag vraagObject = vraagSerializer.Deserialize(reader) as LoonfactorBerekeningVraag;
// Bereken het antwoord
LoonfactorBerekeningAntwoord antwoordObject =
BerekenLoonfactorenImplementatie(vraagObject);
// Converteer het antwoord naar xml
Helper.LoonfactorBerekeningAntwoord.
LoonfactorBerekeningAntwoordSerializer antwoordSerializer = new Helper. LoonfactorBerekeningAntwoord.
LoonfactorBerekeningAntwoordSerializer();
MemoryStream antwoordStream = new MemoryStream(); antwoordSerializer.Serialize(antwoordStream, antwoordObject);
antwoordStream.Flush();
antwoordStream.Position = 0; }
antwoord = new SqlXml(antwoordStream);
/// <summary>
/// Type-safe implementatie van de LoonfactorBerekening ///
private static LoonfactorBerekeningAntwoord BerekenLoonfactorenImplementatie
{
}
}
(LoonfactorBerekeningVraag vraag)
// Implementatie code weggelaten
return new LoonfactorBerekeningAntwoord();
Listing2: C# stored procedure met XML serializatie Om een idee te krijgen hoe vervolgens LINQ op het objectmodel leidt
magazine voor software development 29
DATABASES .NET
tot leesbare en dus onderhoudbare code is in listing 3 een code fragment weergegeven. Deze expressie had nog prima in T-SQL uitgeschreven kunnen worden, maar het gaat om het idee. int aantalDagenGewerkt =
(from d in vraag.DienstCollection.Cast()
from tb in d.TijdblokCollection.Cast<Tijdblok>() where (tb.DienstType.TeltMeeInGewerkteUren && !tb.IsZiek)
select d.Datum.Date).Distinct().Count();
Listing 3: Een berekening op setsgewijze data met behulp van LINQ Nadat de C# stored procedure is voltooid en tot een library assembly is gecompileerd, moet hij in SQL Server worden gedeployed. Listing 4 toont hoe dat moet. Eerst moet de assembly worden geregistreerd en vervolgens kan een T-SQL stored procedure wrapper om de C# static methode worden gemaakt. Omdat deze assembly afhankelijk is van de .Net 3.5 System.Core assembly en deze door SQL Server 2008 wel standaard wordt vertrouwd, wordt bij het registreren van de assembly onderscheid gemaakt tussen het benodigde permission level in SQL Server 2005 en 2008. IF @@VERSION LIKE 'Microsoft SQL Server 2008%' BEGIN
CREATE ASSEMBLY
[SDB.MPlus.RekenServer.LoonfactorBerekening]
FROM '$(RekenserverAssemblyDir)\
SDB.MPlus.RekenServer.LoonfactorBerekening.dll' WITH PERMISSION_SET = SAFE
END
ELSE IF @@VERSION LIKE 'Microsoft SQL Server 2005%' BEGIN
CREATE ASSEMBLY
[SDB.MPlus.RekenServer.LoonfactorBerekening]
FROM '$(RekenserverAssemblyDir)\
SDB.MPlus.RekenServer.LoonfactorBerekening.dll' WITH PERMISSION_SET = UNSAFE
END go
CREATE PROCEDURE dbo.BerekenLoonfactoren (
@Vraag xml
, @Antwoord xml OUTPUT )
-- [assembly name].[fully qualified type name]. --
[method name]
AS EXTERNAL NAME
[SDB.MPlus.RekenServer.LoonfactorBerekening].
go
[StoredProcedures].[BerekenLoonfactoren]
SELECT 1 FROM sys.service_message_types
WHERE name = '/Rekenserver/LoonfactorBerekening/Vraag')
CREATE MESSAGE TYPE
[/Rekenserver/LoonfactorBerekening/Vraag] VALIDATION = WELL_FORMED_XML
IF NOT EXISTS(
SELECT 1 FROM sys.service_message_types
WHERE name = '/Rekenserver/LoonfactorBerekening/Ant
woord')
CREATE MESSAGE
TYPE [/Rekenserver/LoonfactorBerekening/Antwoord] VALIDATION = WELL_FORMED_XML
IF NOT EXISTS(SELECT 1 FROM sys.service_message_types WHERE name = '/Rekenserver/Fout')
CREATE MESSAGE TYPE [/Rekenserver/Fout] VALIDATION = WELL_FORMED_XML
IF NOT EXISTS(
SELECT 1 FROM sys.service_contracts WHERE name =
'/Rekenserver/LoonfactorBerekening/DataContract')
CREATE CONTRACT (
[/Rekenserver/LoonfactorBerekening/DataContract] [/Rekenserver/LoonfactorBerekening/Vraag] SENT BY INITIATOR
, [/Rekenserver/LoonfactorBerekening/Antwoord] SENT BY TARGET
, [/Rekenserver/Fout] SENT BY TARGET
)
IF NOT EXISTS(
SELECT 1 FROM sys.service_queues
WHERE name = 'LoonfactorBerekeningRekenQueue')
CREATE QUEUE dbo.LoonfactorBerekeningRekenQueue -- Rekenserver moet berichten kunnen ontvangen
GRANT RECEIVE ON dbo.LoonfactorBerekeningRekenQueue TO RekenserverUser
-- lokaal service endpoint aanmaken IF NOT EXISTS(
SELECT 1 FROM sys.services WHERE name =
BEGIN
'/Rekenserver/LoonfactorBerekening/RekenService')
CREATE SERVICE
[/Rekenserver/LoonfactorBerekening/RekenService]
AUTHORIZATION RekenserverUser
Listing 4: Registratie van de assembly en de C# stored procedure in SQL Server Om deze stored procedure asynchroon via Service Broker te kunnen aansturen moet een aantal Service Broker artifacts worden gemaakt, zoals in de inleiding van dit artikel is beschreven. Listing 5 toont een gedeelte van de code aan de server zijde; het aanmaken van een aantal berichttypen, een contract, een queue en een service. Er is geen ruimte om alles te laten zien. Wat bijvoorbeeld ontbreekt, is het maken van een Service Broker endpoint waarmee SQL Server berichten zal ontvangen, alsmede de routes waarmee een bericht naar de juiste service wordt geleid. Dit is wel beschreven in de SQL Server Books Online [ref3]
30
IF NOT EXISTS(
MAGAZINE
ON QUEUE dbo.LoonfactorBerekeningRekenQueue ( )
[/Rekenserver/LoonfactorBerekening/DataContract]
END -- MPlus moet messages kunnen versturen naar -- de reken service GRANT SEND ON
SERVICE::[/Rekenserver/LoonfactorBerekening/RekenService] TO MPlusUser
Listing 5: Registratie van Service Broker artifacts
DATABASES .NET
Vervolgens moet er een stored procedure worden geschreven die geactiveerd wordt als er een bericht in de queue verschijnt. Listing 6 toont deze stored procedure. De stored procedure is zo geschreven dat hij niet voor elk afzonderlijk bericht hoeft te worden geactiveerd. Als hij eenmaal is opgestart, blijft hij berichten uit de queue lezen zolang die er nog zijn (WHILE @@ERROR = 0). Het lezen van een bericht gebeurt via het RECEIVE statement. Met het nieuwe WAITFOR statement kan de maximale wachttijd worden geregeld. In het voorbeeld wordt de stored procedure beëindigd zodra er gedurende één seconde geen nieuwe berichten meer in de queue verschijnen. Elk bericht wordt apart transactioneel afgehandeld. Het daadwerkelijk afhandelen van het bericht wordt overgelaten aan de C# stored procedure waarvoor in listing 4 een T-SQL wrapper gemaakt is. -- xml type methoden vereisen deze setting: SET QUOTED_IDENTIFIER ON go
N'/SDB/MPlus/Rekenserver/
LoonfactorBerekening/Vraag'
BEGIN
EXEC BerekenLoonfactoren @RequestMessage,
@ResponseMessage OUTPUT; SEND ON CONVERSATION @DialogHandle MESSAGE TYPE
[/SDB/MPlus/Rekenserver/
LoonfactorBerekening/Antwoord] (@ResponseMessage);
END
ELSE IF @MessageType =
N'http://schemas.microsoft.com/SQL/ ServiceBroker/EndDialog'
BEGIN
-- Service initiator heeft de dialoog beeindigd.
-- Deze stored procedure is bedoeld als -- "activating" stored procedure
-- voor de queue dbo.LoonfactorBerekeningQueue.
-- Op deze queue komen alle verzoeken binnen voor -- een loonfactorberekening.
CREATE PROCEDURE dbo.LoonfactorBerekeningRekenQueueReader AS
BEGIN
IF @@TRANCOUNT > 0 BEGIN
RAISERROR('dbo.LoonfactorBerekeningRekenQueueReader
mag niet vanuit een transactie gestart worden', 16, 1)
ROLLBACK TRANSACTION RETURN 1
END
WHILE (@@ERROR = 0) BEGIN
DECLARE @DialogHandle UNIQUEIDENTIFIER DECLARE @RequestMessage xml
DECLARE @ResponseMessage xml DECLARE @MessageType sysname BEGIN TRANSACTION BEGIN TRY WAITFOR (
-- Dan doen wij dat hier ook.
END CONVERSATION @DialogHandle;
END
COMMIT TRANSACTION
END TRY
BEGIN CATCH
IF XACT_STATE() = -1 BEGIN
-- huidige transactie is niet committable
-- Dit zou nooit mogen gebeuren. Gebeurt het
-- wel, dan wordt de foutmelding in de SQL Server -- log geschreven.
PRINT ERROR_PROCEDURE() PRINT ERROR_LINE()
PRINT ERROR_MESSAGE() ROLLBACK TRANSACTION
END
BEGIN TRANSACTION -- Verwerk de fout, desnoods buiten -- de oorspronkelijke transactie om DECLARE @FoutBericht xml;
WITH XMLNAMESPACES (DEFAULT
'http://www.sdbnieuws.nl/MPlus/Rekenserver/2008')
SELECT @FoutBericht = (
SELECT
RECEIVE TOP(1)
@DialogHandle = conversation_handle,
,
@RequestMessage = message_body,
@MessageType = message_type_name
,
), TIMEOUT 1000 BEGIN
-- Geen berichten meer in de queue ROLLBACK TRANSACTION BREAK;
END
-- Voor testdoeleinden:
-- SELECT @MessageType as RequestMessageType, --
@RequestMessage as RequestMessage
IF @MessageType =
, ,
FROM dbo.LoonfactorBerekeningRekenQueue
IF @@ROWCOUNT = 0
ERROR_NUMBER() 'ErrorNumber'
,
ERROR_SEVERITY() 'ErrorSeverity' ERROR_STATE() 'ErrorState'
ERROR_PROCEDURE() 'ErrorProcedure' ERROR_LINE() 'ErrorLine'
ERROR_MESSAGE() 'ErrorMessage'
FOR XML PATH ('RekenserverFout')
);
SEND ON CONVERSATION @DialogHandle
MESSAGE TYPE [/SDB/MPlus/Rekenserver/Fout] (@FoutBericht);
END CONVERSATION @DialogHandle; COMMIT TRANSACTION IF XACT_STATE() = 1 BEGIN
magazine voor software development 31
DATABASES .NET
-- huidige transactie is wel committable COMMIT TRANSACTION
END
END CATCH END;
END
BEGIN DIALOG CONVERSATION @RekenserverDialogId
FROM SERVICE [/Rekenserver/LoonfactorBerekening/DataService] TO SERVICE '/Rekenserver/LoonfactorBerekening/RekenService'
ON CONTRACT [/Rekenserver/LoonfactorBerekening/DataContract] WITH RELATED_CONVERSATION_GROUP = @ConversationGroupId;
-- WHILE
-- Stel de vraag samen
GO
EXEC Rekenserver.LoonfactorBerekening_VraagMaken @DienstverbandID = @DienstverbandID
GRANT EXECUTE
ON dbo.LoonfactorBerekeningRekenQueueReader
@PeriodeID = @PeriodeID
,
@Vraag = @Vraag OUTPUT
,
TO RekenServerUser go
,
@MessageID = @RekenserverDialogId
IF @Vraag IS NOT NULL
Listing 6: Stored procedure om berichten van de queue te verwerken
BEGIN
-- Verstuur de vraag
SEND ON CONVERSATION @RekenserverDialogId
MESSAGE TYPE [/Rekenserver/LoonfactorBerekening/Vraag]
Tenslotte moet de stored procedure aan de queue worden gekoppeld en dan is de rekenservice is klaar voor gebruik. Listing 7 laat zien hoe dat moet.
END
ALTER QUEUE dbo.LoonfactorBerekeningRekenQueue
BEGIN
WITH ACTIVATION (
MAX_QUEUE_READERS = 4,
EXECUTE AS 'RekenserverUser'
Listing7: Stored procedure aan de queue koppelen Daarbij kan worden gekozen om de stored procedure automatisch te laten starten zodra er een bericht op de queue verschijnt (ACTIVATION = ON). Als één instantie van de stored procedure het tempo waarmee berichten op de queue verschijnen niet kan bijhouden, zal Service Broker een extra instantie starten tot het aangegeven maximum (MAX_QUEUE_READERS). Omdat de stored procedure asynchroon worden uitgevoerd, is er geen client connectie en moet er expliciet voor een security context worden gekozen m.b.v. EXECUTE AS. De rekenserver implementeert de service, maar aan de initiërende kant is ook nog een aantal zaken vermeldenswaardig, zoals het initiëren van een nieuwe dialoog als onderdeel van een Conversation Group (zie listing 8). De dialoog wordt gestart WITH RELATED_CONVERSATION_GROUP. Zoals hiervoor al is beschreven is dit om te garanderen dat twee achtereenvolgende berekeningen van hetzelfde rooster in dezelfde volgorde worden afgehandeld, zodat altijd het resultaat van de meest recente berekening zal worden bewaard. DECLARE @RekenserverDialogId uniqueidentifier; DECLARE @ConversationGroupId uniqueidentifier; DECLARE @DienstverbandIDint;
-- niet getoond wordt hoe deze variabele een waarde krijgt DECLARE @PeriodeID int; DECLARE @Vraag xml;
-- idem
-- Maak een conversation group voor deze combinatie -- van periode en dienstverband. Dit voorkomt dat -- twee Rekenserver opdrachten voor dezelfde -- combinatie elkaar gaan 'inhalen'
EXEC Rekenserver.LoonfactorBerekening_ConversationGroupMaken @PeriodeID = @PeriodeID , ,
@DienstverbandID = @DienstverbandID
@ConversationGroupID = @ConversationGroupId OUTPUT
-- Maak een dialog voor het versturen van de vraag
32
MAGAZINE
ELSE
-- Er valt niks te versturen
END CONVERSATION @RekenserverDialogId
STATUS = ON,
PROCEDURE_NAME = [LoonfactorBerekeningRekenQueueReader], )
(@Vraag);
END
Listing 8: Start van een nieuwe dialoog als onderdeel van een conversation group
Er is nadrukkelijk aandacht nodig voor foutafhandeling bij een Service Broker applicatie Bij een Service Broker applicatie moet nadrukkelijk aandacht besteed worden aan foutafhandeling. Als het verwerken van een bericht van de queue vijf keer achter elkaar tot een rollback van de transactie leidt, zal Service Broker automatisch de queue disablen en dan stopt de verwerking volledig. Om dit te voorkomen moet de code zo geschreven worden dat het bericht linksom dan wel rechtsom kan worden verwerkt. En simpelweg committen van de transactie is niet altijd een optie; SQL Server staat niet toe dat de consistentie van de database in het gedrang komt. Listing 9 toont het foutafhandelingsgedeelte van een queue reader stored procedure. Met de functie XACT_STATE() wordt bepaald wat de mogelijkheden zijn. CREATE PROCEDURE
Rekenserver.LoonfactorBerekening_DataQueueReader
AS
BEGIN
IF @@TRANCOUNT > 0 BEGIN
RAISERROR(
'Rekenserver.LoonfactorBerekening_DataQueueReader
mag niet vanuit een transactie gestart worden', 16, 1)
ROLLBACK TRANSACTION RETURN 1
END
WHILE (@@ERROR = 0)
BEGIN
DECLARE @DialogHandle uniqueidentifier
DECLARE @ConversationGroupId uniqueidentifier DECLARE @Message xml
DECLARE @MessageType sysname
DATABASES .NET
DECLARE @Foutmelding varchar(255) BEGIN TRANSACTION BEGIN TRY WAITFOR (
RECEIVE TOP(1)
@ConversationGroupId = conversation_group_id, @DialogHandle = conversation_handle, @Message = message_body,
@MessageType = message_type_name
FROM Rekenserver.LoonfactorBerekeningDataQueue
), TIMEOUT 5000
-- code met verwerking van het bericht is weggelaten COMMIT TRANSACTION
END TRY
BEGIN CATCH
IF ERROR_NUMBER() IN (1205) BEGIN
PATH clause de meest flexibele variant. Een dataveld kan als ofwel een XML-attribuut dan wel als een XML-element worden opgenomen. Het wordt een attribuut als de alias met een @ begint. Het maken van geneste XML-elementen is met deze variant ook heel eenvoudig. Begin gewoon een geneste subselect op de plaats waar het subelement moet komen. De TYPE subclause in de SELECT zorgt ervoor dat de data als XML-data wordt gezien en niet als tekst. Als een XMLelement dieper genest moet worden dan de (sub)select die het dataveld selecteert, kan met een relatieve XPath-expressie de nesting expliciet gestuurd worden; vandaar de naam FOR XML PATH. Listing 10 toont beide technieken. Let vooral op het dieper geneste Salaris element. De XML-namespace wordt bepaald via de WITH XMLNAMESPACES clausule. DECLARE @Vraag xml, @MessageID uniqueidentifier; SET @MessageID = NewId(); WITH XMLNAMESPACES
(DEFAULT 'http://www.sdbgroep.nl/MPlus/Rekenserver/2008'),
SELECT @Vraag = (
SELECT @MessageID '@Id', CURRENT_TIMESTAMP '@Timestamp', (
SELECT DienstverbandID '@Id', (
-- Er is een herstelbare fout opgetreden;
-- Probeer hetzelfde bericht nogmaals te verwerken
, CaoCodeIs4Weken 'IsVierWeken'
ROLLBACK TRANSACTION
, CaoCodeNaam 'Naam'
CONTINUE
, CONVERT(int, CaoCodeWerkweekUren) 'WerkweekUren'
END
FROM dbo.tblCaoCode
IF XACT_STATE() = -1
WHERE CaoCodeID = dv.fkCaoCodeID
BEGIN
-- Huidige transactie is niet committable
)
SET @Foutmelding = 'Fout ' +
, CONVERT(char(10), dv.DienstverbandDatumInDienst, 120)
, dv.DienstverbandDagenPerWeek 'DagenPerWeek' 'DatumInDienst'
CONVERT(varchar(10), ERROR_NUMBER())
+ ' op regel ' + CONVERT(varchar(10),
, CONVERT(char(10), dv.DienstverbandDatumUitDienst, 120)
+ ERROR_PROCEDURE()
, ISNULL(NULLIF(dv.DienstverbandParttimeUren,0),
'DatumUitDienst'
ERROR_LINE()) + ' van procedure '
cao.CAOcodeWerkweekUren) 'ParttimeUren'
PRINT @Foutmelding
PRINT ERROR_MESSAGE()
, dv.DienstverbandGarantieToeslag
BREAK
, gb.CAOInpassingCodeBedrag
'Salaris/@GarantieToeslag'
ROLLBACK TRANSACTION
'Salaris/@GarantieTredeBedrag'
END
, dv.DienstverbandLoonToeslag
ELSE
'Salaris/@LoonToeslag'
BEGIN
-- Huidige transactie is wel committable;
, dv.DienstverbandSalaris
UPDATE Rekenserver.LoonfactorBerekeningLog
FROM …
-- leg de foutmelding vast
WHERE SsbDialogHandle = @DialogHandle -- breek de dialoog af
END CONVERSATION @DialogHandle; COMMIT TRANSACTION
END
END CATCH
-- WHILE
Listing 9: Queue reader stored procedure Tenslotte zal getoond worden hoe makkelijk het is sinds SQL Server 2005 om met XML-data te werken. Om XML-data te genereren is het SELECT statement met FOR XML
'Salaris/NominaalSalaris'
WHERE …
SET Foutmelding = ERROR_MESSAGE()
END
FOR XML PATH ('Cao'), TYPE
-- Dit zou nooit mogen gebeuren. Gebeurt het wel,
-- dan wordt foutmelding in SQL Server log geschreven
END;
SELECT CaoCodeID '@Id'
, CaoCodeCode 'Code'
-- N.B. Error 1205 = deadlock
),
FOR XML PATH ('Dienstverband'), TYPE
…
Listing 10: XML genereren met SELECT FOR XML PATH Listing 11 toont een bijpassend XML-fragment.
xmlns="http://www.sdbgroep.nl/MPlus/Rekenserver/2008" Id="300B830A-EA57-DD11-82FF-000C294CDAD3" Timestamp="2008-07-22T14:31:05.487">
xmlns="http://www.sdbgroep.nl/MPlus/Rekenserver/2008" Id="3062">
magazine voor software development 33
DATABASES .NET
xmlns="http://www.sdbgroep.nl/MPlus/Rekenserver/2008" Id="100">
4
0
CAO Ziekenhuizen
<WerkweekUren>36
4.00
<ParttimeUren>23.00
<Salaris GarantieToeslag="121.00" LoonToeslag="0.00"> 1234.5600
<Soort>PT
Listing 11: XML naar aanleiding van de SELECT uit listing 10 De gegenereerde XML wordt vervolgens naar de rekenservice gestuurd en die komt uiteindelijk met een XML-antwoord. Dat XMLantwoord kan vrij eenvoudig weer tot een resultset worden omgevormd met behulp van de ingebouwde functies op het XML-datatype (zie listing 12). Met de nodes() functie wordt een XML-nodelist gemaakt. Vervolgens worden de waarden uit elke node geselecteerd met de value() functie. De resultset kan tenslotte gewoon worden gejoined aan andere tabellen. DECLARE @Antwoord xml; SET @Antwoord = ' 5031 5 093 463 <Waarde>4 5031 5 094 463 <Waarde>1 '; WITH XMLNAMESPACES ('http://www.sdbgroep.nl/MPlus/Rekenserver/2008' as rs) SELECT vm.KaartSoort , vm.Waarde , vm.DienstverbandId , lf.LoonfactorID , vm.KostenPlaatsId , vm.KostenSoortId , vm.PeriodeId FROM ( SELECT vm.value('(rs:DienstverbandId)[1]', 'int') as DienstverbandId , vm.value('(rs:KaartSoort)[1]', 'int') as KaartSoort , vm.value('(rs:KostenSoortId)[1]', 'int') as KostenSoortId , vm.value('(rs:KostenPlaatsId)[1]', 'int') as KostenPlaatsId , vm.value('(rs:LoonfactorCode)[1]', 'varchar(3)')
34
MAGAZINE
as LoonfactorCode , vm.value('(rs:PeriodeId)[1]', 'int') as PeriodeID , vm.value('(rs:Waarde)[1]', 'numeric(7,2)') as Waarde FROM @Antwoord.nodes( '/rs:LoonfactorBerekeningAntwoord/rs:VariabeleMutatie') VariabeleMutaties(vm) ) as vm INNER JOIN dbo.Loonfactor lf ON lf.LoonfactorCode = vm.LoonfactorCode
Listing 12: Het omzetten van een XML-fragment tot een relationele resultset Conclusie In dit artikel hebben we laten zien dat SQL Server 2005 of 2008 een uitstekend platform is om services te bouwen die dicht tegen de database aanliggen. Service Broker heeft geen op webservice standaard gebaseerde interfaces zoals WCF, dus is niet geschikt als publieke service. Maar de transactionele, betrouwbare en zeer snelle services die gemaakt kunnen worden met Service Broker zijn uitstekend geschikt als service achter de schermen. De SQLCLR maakt het mogelijk om die services gewoon met het vertrouwde en krachtige .Net framework te realiseren. De nieuwste T-SQL mogelijkheden sluiten daar naadloos op aan. Referenties • [ref1] XsdObjectGen: hulpmiddel om op basis van een XML Schema een C# object model te genereren. Te downloaden via: http://www.microsoft.com/downloads/details.aspx?FamilyID= 89E6B1E5-F66C-4A4D-933B-46222BB01EB0&displaylang=en • [ref2] XgenPlus: hulpmiddel om XML Serializers te genereren op basis van een .Net object model. Te downloaden via: http://www.codeplex.com/xgenplus • [ref3] How To: Activate Service Broker networking: http://msdn.microsoft.com/en-us/library/ms166113.aspx •
Andre van Leeuwen André van Leeuwen is werkzaam als software engineer bij SDB Groep te Leidschendam en kan worden bereikt via e-mail op a.vanleeuwen@ sdbgroep.nl. André is specialist in salarisverwerking op het Microsoft platform.
Frans van der Geer Frans van der Geer is werkzaam als software architect bij VX Company in Baarn en kan worden bereikt via e-mail op [email protected]. Frans is specialist in op SQLServer en Biztalk Server gebaseerde oplossingen.
Advertentie Bergler Nederland b.v.
ld. Iede van d e reen zij P C h eb Waren n eigen ik mij e er oors werksta rtoe aa p ro n tion, m kelijke a (van de ngetrok aximalis llerlei vra k en IBM PC eer pro gen ove ) - die Micros ductivit je met r comp oft spe eit. F a li ti g b l al nie ht Simu iliteit va discuss t goed n klone lator ko ie tusse voor is n n te n sten (w ) -, vers Token R W indow aar een ing and cheen s en O e E r th S v /2 en d ernet n ervolge Pascal, etwerke e strijd ns de C++, V n, de str tussen isual Ba Was d d ijd tuss e s ic p rogram e CDG en Clip en meerta N desti per. Nederla len Turb jds nog nd, toc o d e h kunn basis v Clipper en we oor he is verlo n t u C w li re e p l zegge per-suc n. De g tal klein n dat d rote we ces in ere spe ie strijd reld is n lers in d 3-tier, m zeker d u .N e ET en J marge. oor ulti-tier, ava, me Maar h object code re t een aa ypes b oriente use, co li njv d e , n client b de gen … clien De SDN a s ed , w eration, t/serve is, als p r, e s e b rv b ice orie as ed , w rofessio te bijzo nted, c eb 2.0, nele org nderde loud co a n r is o atie, me m te zie mputin (Joop, R g, enz. t de tijd n dat e ob) nog en aan meege steeds den, he ta gaan. D l m a ctief zijn ensen v b meer es an het in de SD mensen blijven b eerste N. Ik he zien ko estaan. u u b r m z e ouder en en g Waren vaak bij zien wo aan, m de bestu iemand a ra r u e rs en vaste vergad thuis, to de deu eringen kern is ch heb r te verg in den b ben we aderen pen. Zo eginne m , oeten b omdat nog als die esluiten de disc keer in van he ussies om buit hotel O t restau n o en g u a d l eens h -Leusd rant na omdat en, toe oog op ar een de and n li e w a e fg d esloten ere gas oor de Ik ben b manag kamer ten zich enieuw e w r s e to rd o en ged d of he rden aa t nog s irigeerd n ons ‘g teeds z , espreks o hoog ’-volum kan op e. lopen …
. 43 t/m 84) Ad vd Lisdonk 1997 – 2005 (nrsgehe ten - kwam, kon je voor heel
nog CDGN … Toen ik als lid bij het SDN – destijds Alleen aanschaffen met wel 1 Mb geheugen. uter comp dure em extre veel geld een kon. eg overw ugen gehe el zove met a amm jammer dat standaard geen enkel progr er die Blink en pace ExoS als s linker op eren Nachtenlang zat ik dan ook te ploet de 640Kb. geheugen konden aanspreken boven a’s nden CD en DVD nog niet en programm besto n, bego … Toen ik bij de redactie Mb 1,44 er je kan dan t er een gaatje in boor leverde je uit op 720Kb diskettes (als je n in een zakelijke omgeving. Ik ben dan kome te op nnen bego erken opzetten!). Netw voor r mina geweest over het programmeren ook twee keer naar hetzelfde CDGN-se hil te begrijversc het g aardi ik t dach keer de een netwerkomgeving. Pas na de twee g. pen tussen optimistic en pessimistic lockin te winnen, kwam Delphi onze club verows Wind n bego … Toen ik in het bestuur zat, n lex voor de meeste Clipperaars. We krege sterken en bleek VO toch wel erg comp inde mij kan Ik n. ende verdi ork eigen netw kortom meer talen en technieken die een bijvoorbeeld nog herinneren. Een Micro ork Netw Office soft Micro het van troductie de over had wat last van gemor uit de zaal soft hotshot kwam zijn verhaal doen en t ng je geen programma opstart, crash “Zola toen: stabiliteit van Windows. Hij zei die a’s amm progr de Het probleem zit ‘m in Windows niet. Windows is dus stabiel. jaar geleden dat ik besloot om IT te gaan ig twint dan er mind is jullie schrijven”. Het zijn in die korte tijd! doen. D’r zal wel gewoon veel gebeurd
SD N
Ernst 1989 –Peter Tamm in 1995 (n Vanaf h rs. 1 t/ga et eers te m 29) momen gevoe t
GA
ZI SDN NE
ED IT IE
10 0 ED IT IE
MA GA ZI NE
MA
Ton Hofstede 1995
–
1997 (nrs. 30 t/m 1 Gb = 1.000.000 42) Kb Waar we nu met ee n gemiddelde 2.4 Gb verbinding gegeven sturen ging dat in de s over het internet oude dagen met 24 00 Kb via een inbelv (à 0.15 NLG per mi erbinding per mode nuut na 19:00 uur) m naar het HEKOM-BB Was het erg druk, en S. Als je geluk had dat was het meestal tenminste ... in de laag-tarief avo Kb of soms zelfs 30 nduren, dan viel je 0 Kb. Wilde je een ter ug op 1200 up da te downloaden, da lekker naar bed. Als n belde je ’s nachts je geluk had en de in en ging je ver bin din g was gedurende de broken door een str nacht niet spontaa oomdipje, dan had n verje de volgende ochte soort tijdseenhede nd je bestandje bin n waren er ook bij he nengekregen.Dat t compileren. Vanu anderhalf uur, waari it je Rmake-scriptfile n je mooi even naar duurde het zo’n de Albert Cuyp ma te halen. Een voord rkt kon lopen om ee eel was wel dat je n broodje haring lee rde programmeren. een enkele programm Even via trial-and-er aregel correct was, ror testen of liet je wel uit je hoofd. werken en zonodig De hele ochtend stu in de (papieren) hand g doorleid ing bladeren om pas na 1.5 uur compileren de middagpauze (ye ) te kunnen zien wa p, van ar je ge blu nd erd had. Als je tenmi meegelinkt en extra nste een Debugger testfuncties handma had tig had toegevoegd. Je middagfouten ko n je dan de volgend e ochtend weer be studeren …
Mark B
lomsm 2005 w a 2005 as het jaar va – 2005 niet. Te n .NET am Sy 2.0, Vis (nrs. 8 stem, W Unit Te ual Stu 5 t/m e b sten w Service d io 2 005 en aren ho 86) s, Click een nie Delphi t topics Once D uw jas 2005. . DotN eploym je gesto WCF, W e verwik tN e n uke be t, ASP.N ken en keld en PF en gon aa opgeze ET 2.0 WF be ik kan rd Langua ig (zonde t stonde o p m p basis opulair e herin ges’. G r AJAX n nog te v n a e e w n ), Mob re n o DNN 2 erics e n dat w hielden rd e n ile App e n anon .0. C# n in 20 en ASP e o p s en ymous de SD 05 is w en VB.N .NET d waren C een metho ww.sd evelop ET wa een co s d n e e s .n re s rs w s l in n ole, nie ie had waren aren nie in een zijn … den m zeer in uwe fe talenoo uwe fe Al met et als ature v hun no a rl tu o al een g re title ‘B s die .N an ASP pjes m mooi ja attle o et pers .NET 2 ET dev ar! f th o .0 e n e lo a . lization Zaken pers vo Remi , en me die inm lop bez Caron t name iddels ig Hoewe a masterp 2 lw 0 e er lang 0 l ik (pa 5 ages – s) sind e h n e b ken bij re d s 2006 ed ‘ge en (nr de org woon’ voorzit s. 87 t anisati ter ben moeilijk e. Toen / m v er een a n deze 1 m 0 ij 0 w te ) erd ge softwa kiezen Het be vraagd re-com , maar treft de munity om vo dan de SDC 2 Forte & o z ben ik r e 0 d 0 maar .. it num 2 (als ik Richard al wel . mer ee het mij heel la Campb ervoor n anekdo g ng betr o ell – zo ed heri met re te te le okgelmaa nner); ud en e ergens veren w twee v t gekla en Gee gedure a a a s n k g h o d N et n ight se nde de over h ze spre gestuu et eten ssie ve kers dag! J rd. “Ga rz w in a o ie n rg N n a v aar Mc ederlan e n . De an der nders 15 min dan Ste Donald d en m heren Graaff, uten na had d e s en ha et n am ve jaren la aanvan & Rich n in de a e l n g 2 h g et geb van de stuks v ard. D o n dagen z e rek aa hoofdre e here an iede sessie gewoo n h am n tone komt J re ham dacteu n blijft burgers n zich an met burger r, werd doorga sportie de ham die op op een an en a f, h b e v u erslind t menu missie rg ls vano e rs binnen en de staat”. uds do hambu en ove Ongev or het rhandig rgers o eer publiek t ze aa m beu hoog g n Steve rten … ewaard te eerd w rwijl de ordt. sessie
.NET C#
Bert Dingemans
Fun met WebParts in ASP.Net Deel 1: Basis Webparts Inleiding Webparts zijn een nieuw soort besturingselementen in webapplicaties, geïntroduceerd door Microsoft in ASP.Net sinds versie 2.0. Webparts zijn vooral bekend vanwege de toepassing binnen Sharepoint en MOSS. In deze twee platformen zijn webparts één van de mogelijkheden om eenvoudig toegang te krijgen tot legacy systemen. Echter ook in maatwerk ASP.Net applicaties zijn webparts “fun”. In een moderne webtoepassing is het meer en meer gebruikelijk dat gebruikers een eigen indeling kunnen maken van hun “eigen pagina”. Kijk naar sites als hyves.nl en de verschillende elementen lijken verdacht veel op webparts. In dit artikel gaan we in op een aantal basisaspecten van webparts en behandelen we naast de opzet ervan een manier om webparts te genereren op basis van een domeinmodel. De webparts zijn ontwikkeld in C#. Dat is voor mij niet mijn dagelijkse programmeertaal (dat zijn Vulcan.Net en VB.Net). Reden om hiervoor te kiezen is het feit dat binnen Sharepoint installatie van webparts die niet geschreven zijn in C#, lastig is. De webparts zijn op deze wijze in een handomdraai geschikt te maken voor Sharepoint. Basispagina Om webparts mogelijk te maken in een webpagina is het van belang een aantal extra besturingselementen op te nemen in een standaard pagina. In de onderstaande afbeelding een voorbeeld van een pagina met een aantal besturingselementen in ontwerpmodus.
waarop men een aantal ‘samenvatting’-webparts kan plaatsen. In onderstaande code een voorbeeld van een webpart-manager en een webpart-zone:
EmptyZonetext="Voeg hier een webpart toe" Height="100px"
Font-Names="Verdana" Padding="1"
ShowTitleIcons="true"
AllowLayoutChange="true" EditVerb-Visible="true" EditVerb-Enabled="true"
WebPartVerbRenderMode="TitleBar" LayoutOrientation="Vertical"
<EditVerb text="Bewerken"
>
Description="Pas de instellingen aan" />
Description="Verwijder het element" />
<MinimizeVerb text="Minimaliseren" Description="Verklein element"/>
Fig. 1
Opvallend is dat de pagina een aantal zones heeft waarin een webparts geplaatst zijn. Deze zones bieden de ontwikkelaar de mogelijkheid om de gebruiker van de toepassing een standaard indeling te geven. Door verschillende templates aan te bieden kun je gebruikers op eenvoudige wijze behulpzaam zijn bij het werken met zones. Standaard zijn een webpart-zone en een webpart-manager de minimum vereiste voor een pagina. De webpart-manager zorgt als een soort hub voor al het gedrag van de webpart-zones en de webpart-besturingselementen. Op een pagina met webparts moet één webpart-manager voorkomen. De webpart-zone is een container die gedrag toevoegt aan de webparts die binnen deze zone voorkomen. Zo kun je een zone maken
38
MAGAZINE
Description="Verbind element" /> Description="Maximaliseer element" />
Op basis van de eigenschappen is een zone-part op maat te maken met eigen opmaak en meldingen van de diverse opdrachten en gebeurtenissen. Aardig is dat ook de woorden en toelichtingen bij de verschillende opdrachten aangepast kunnen worden, zodat deze specifiek gemaakt kunnen worden voor de eigen toepassing. In de voorbeeldtoepassing is de pagina default2.aspx te vinden met daarin een compleet uitgewerkte pagina met drie zones.
.NET C# Naast de zones is het wenselijk dat een gebruiker van de toepassing de pagina kan gebruiken (bekijken) maar ook kan ontwerpen en beheren. Met de optie ontwerpen kun je webparts van de ene zone naar de andere verplaatsen om zo een logische indeling te krijgen. Bij bewerken komt er in het menu van een webpart de optie ‘bewerken’ te voorschijn. Hiermee kan een aantal instellingen van het webpart veranderd worden. In figuur 2 zie je een voorbeeld van de pagina.
Fig. 2 Als laatste is er de catalogus-modus. Hiermee is het mogelijk om webparts vanuit een lijst van beschikbare webparts toe te voegen aan één van de zones. Voor al deze verschillende pagina-functionaliteiten zijn zones aanwezig. In onderstaande code is één voorbeeld uitgewerkt; de andere zones zijn opgenomen in de voorbeeldapplicatie.
runat="server" id="CatalogZoneDRG" Height ="200px" Width="400px" BackColor="#F7F6F3"
BorderColor="#CCCCCC" BorderWidth="1px" Font-Names="Verdana" Padding="6">
runat="server" id="catalogDrg">
<WebPartsTemplate>
public partial class Default2 : System.Web.UI.Page { protected void Page_Load(object sender, EventArgs e) { if (!IsPostBack) { switch (Request.QueryString["instellen"]) { case "DesignDisplayMode": { WebPartManagerDRG.DisplayMode = WebPartManager.DesignDisplayMode; break; }; case "BrowseDisplayMode": { WebPartManagerDRG.DisplayMode = WebPartManager.BrowseDisplayMode; break; }; case "EditDisplayMode": { WebPartManagerDRG.DisplayMode = WebPartManager.EditDisplayMode; break; }; case "CatalogDisplayMode": { WebPartManagerDRG.DisplayMode = WebPartManager.CatalogDisplayMode; break; }; case "ConnectDisplayMode": { WebPartManagerDRG.DisplayMode = WebPartManager.ConnectDisplayMode; break; }; }; }; } }
Basis-webpart De voorbeeldtoepassing is zodanig opgezet dat de specifieke webparts zo weinig mogelijk gedrag bevatten, nl. door het instellen van een aantal eigenschappen en het definiëren van de besturingselementen die in het webpart getoond moeten worden. In het eerste codevoorbeeld is de definitie van de class en het instellen van een aantal eigenschappen opgenomen:
id="DRGMdw" runat="server" title="Mdw" />
using System;
id="DRGProj" runat="server" title="Project"/>
using System.Configuration;
id="DRGOrg" runat="server" title="Org." />
id="DRGPrs" runat="server" title="Prs" />
In het voorbeeld is te zien hoe van de catalogus verschillende eigenschappen in te stellen zijn voor de eigen toepassing. Daarnaast is in deze zone de lijst van beschikbare webparts opgenomen. Instellen van de verschillende scherm-modi is mogelijk door de webpart-manager aan te passen. In de voorbeeldtoepassing wordt dit eenvoudig opgelost door het scherm te openen met een bepaalde querystring.
using System.Data; using System.Web;
using System.Web.Security; using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts; using System.Web.UI.HtmlControls;
using System.Collections.Specialized; using System.Collections;
namespace DRG.Webparts.Controls {
public class DRGOrganisatie : DRGAbstractWebpart {
public DRGOrganisatie() {
this.insertcommand = "INSERT INTO [Organisatie] ( [bezoekadres],[bezoekplaats], [bezoekpostcode],[email],
magazine voor software development 39
.NET C# [organisatie_naam],[organisatie_soort],
organisatie_id,
[postadres],[postplaats],[postpostcode],
"valuecolumn", "displaycolumn", para);
[telefoon],[website] )
VALUES (
ControlFactoryHelper.CreateTextBox
'#bezoekadres#','#bezoekplaats#',
(this.Controls, "bezoekplaats", "",500);
'#bezoekpostcode#','#email#',
ControlFactoryHelper.CreateTextBox
'#postadres#','#postplaats#','#postpostcode#',
ControlFactoryHelper.CreateTextBox
'#organisatie_naam#','#organisatie_soort#', '#telefoon#','#website#' )";
this.updatecommand = "UPDATE [Organisatie] SET [bezoekadres] = '#bezoekadres#',
(this.Controls, "postadres", "",500);
…
[bezoekplaats] = '#bezoekplaats#',
"Organisatie", false, false, "Supply"));
[email] = '#email#',
this.toevoegenControlProperty(new
[organisatie_naam] = '#organisatie_naam#',
DRGControlProperty("organisatie_naam",
[organisatie_soort] = '#organisatie_soort#',
"Organisatie naam", false, false, "Modify"));
[postadres] = '#postadres#',
this.toevoegenControlProperty(new
[postplaats] = '#postplaats#',
DRGControlProperty("bezoekadres",
[postpostcode] = '#postpostcode#',
"Bezoekadres", true, false, "Modify"));
[telefoon] = '#telefoon#', [website] = '#website#'
this.deletecommand = "DELETE FROM [Organisatie] WHERE
[organisatie_id] = #organisatie_id# ";
this.selectcommand = "SELECT
[bezoekadres], [bezoekplaats], [bezoekpostcode], [email],
[organisatie_naam], [organisatie_soort], [postadres], [telefoon],
[postplaats], [postpostcode], [website]
FROM [Organisatie] }
}
WHERE
[organisatie_id] = #organisatie_id# ";
Het voorbeeld laat zien dat het webpart overerft van DRGAbstractWebpart. In deze abstracte klasse is de generieke functionaliteit opgenomen, waarover in de volgende paragraaf meer. In de constructor wordt gedefinieerd wat de verschillende SQL-statements zijn die bij dit specifieke webpart horen. In een volgende versie van het webpart schreeuwt dit vanzelf om een XML file dat deze instellingen opslaat en inleest in het webpart.
}
{
NameValueCollection para = new NameValueCollection();
DRGHelper objHelper;
objHelper = new DRGHelper();
this.creerStandaardControls(); DropDownList organisatie_id =
ControlFactoryHelper.CreateDropDownList(
this.Controls, "organisatie_id", "", 400);
objHelper.Sql2ListControl(
"SELECT ORGANISATIE.organisatie_id as valuecolumn , ORGANISATIE.organisatie_naam as displaycolumn FROM ORGANISATIE UNION
SELECT 0, '--Maak uw keuze--' FROM WebDefault ORDER BY 2",
40
MAGAZINE
ChildControlsCreated = true;
Eerst worden de verschillende besturingselementen aangemaakt; hiervoor is een helper klasse beschikbaar, waarover later meer. Daarna worden voor deze controls elementen van het type DRGControlProperty toegevoegd aan een arraylist binnen de abstracte klasse. Deze helper klasse wordt in de generieke klassen gebruikt om gedrag te activeren wanneer gewenst. Generieke en helper-classes De eerste helper-klasse is een factory voor besturingselementen welke ervoor zorgt dat een besturingselement op de juiste wijze wordt gecreëerd. Als voorbeeld is de methode opgenomen die een multlineedit aanmaakt: static public TextBox CreateMultiLineTextBox( System.Web.UI.ControlCollection controls,
{
String ID, String tooltip, Int16 width) TextBox control = new TextBox(); control.ID = ID;
control.ToolTip = tooltip; control.Width = width;
In de CreateChildControls-methode van het webpart worden de verschillende besturingselementen gedefinieerd voor dit webpart. In onderstaande sourcecode een voorbeeld (in de applicatie zijn een viertal webparts in detail uitgewerkt): protected override void CreateChildControls()
this.toevoegenControlProperty(new
DRGControlProperty("organisatie_id",
[bezoekpostcode] = '#bezoekpostcode#',
WHERE [organisatie_id] = #organisatie_id# ";
(this.Controls, "postpostcode", "",100);
control.Height = (width / 3);
control.TextMode = TextBoxMode.MultiLine; controls.Add(control); return control;
} De code is rechttoe rechtaan: er wordt een besturingselement van het juiste type aangemaakt en vervolgens wordt een aantal eigenschappen ingesteld en wordt deze toegevoegd aan de controls-collection van het webpart. Als returnwaarde wordt de aangemaakte control gebruikt. Dit maakt het mogelijk om specifiek gedrag aan een bepaalde control toe te voegen. In het voorbeeld van het basis besturingselement zie je hoe dit wordt gedaan bij een keuzelijst. Hierbij maakt de ControlFactory een keuzelijst aan en vervolgens zorgt de specifieke code ervoor dat het juiste SELECT-statement wordt gekoppeld aan de juiste keuzelijst. Een andere routine die in de ControlFactory-klasse is opgenomen, handelt af dat een dataset wordt omgezet naar de waardes in één of meer besturingselementen. In het codevoorbeeld hieronder is een deel van de routine opgenomen; in de voorbeeldtoepassing is de complete code te vinden.
.NET C# static public void Dataset2Controls(DataSet ds, System.Web.UI.ControlCollection controls) { String varType; if (ds.Tables.Count == 1) { DataTable objTable = ds.Tables[0]; if (ds.Tables[0].Rows.Count > 0) { DataRow objRow = objTable.Rows[0]; foreach (Control webcontrol in controls) { varType = webcontrol.GetType().ToString().ToUpper(); switch (varType) { case "SYSTEM.WEB.UI.WEBCONTROLS.TEXTBOX": { TextBox objTB = (TextBox)webcontrol; if (ColumnNameExists( objTable.Columns, objTB.ID)) { objTB.Text = objRow[objTB.ID].ToString(); }; break; }; case "SYSTEM.WEB.UI.WEBCONTROLS.DROPDOWNLIST": { DropDownList objTB = (DropDownList)webcontrol; if (ColumnNameExists( objTable.Columns, objTB.ID)) { objTB.Text = objRow[objTB.ID].ToString(); }; break; }; … }; }; }; }; }
Via de switch-case wordt de waarde van een rij in een datatable gekoppeld aan een waarde van een besturingselement. De foreach loop zorgt ervoor dat alle besturingselementen gecontroleerd worden. Het kan natuurlijk zijn, dan een besturingselement niet is gekoppeld aan een waarde uit de database. In dat geval zal de waarde niet gekoppeld worden, omdat de ColumnNameExists functie niet afgaat.
De code laat zien dat een SQL-statement wordt omgezet naar een dataset en dat de datatable afkomstig vanuit de aangemaakte dataset gebruikt wordt als vulling van een keuzelijst besturingselement. Reden om dit soort functionaliteit te plaatsen in een abstracte klasse is dat de toestand van een eigenschap kan veranderen via de eigenschap errorMelding. Naast het gebruik van een helper-klasse is het werken met overerving in het geval van webparts erg handig. Onder een helper-klasse versta ik trouwens een klasse met alleen maar static methods zodat die niet geïnstantieerd kan worden tot een object. Eigenlijk kan een helperklasse gezien worden als een half object. Een object bestaat normaal uit status en gedrag; een statische klasse bestaat enkel uit gedrag (dat de status van andere objecten kan aanpassen). Is wel de combinatie van status en gedrag gewenst, dan is het gebruik van overerving vaak een middel om hergebruik te introduceren. In figuur 3 hieronder is te zien hoe overerving en statische klassen gecombineerd gebruikt kunnen worden.
Fig. 3 In de abstracte klasse kan generieke code geplaatst worden welke de status van het webpart of de status van de besturingselementen binnen het webpart aanpassen. Ook kun je in een abstracte klasse events plaatsen die op generieke wijze reageren op acties van de gebruikers. Als voorbeeld de code hieronder: protected void verwerk_supply( {
NameValueCollection para = new NameValueCollection(); String sql = "";
DRGHelper objHelper = new DRGHelper();
DataSet ds;
In de onderstaande code is een voorbeeld van een methode te zien die in een andere helper-klasse geïmplementeerd wordt.
para = ControlFactoryHelper.Controls2Collection(
public void Sql2ListControl(string sql,
this.Controls);
ListControl control, string valueField,
{
Object sender, EventArgs e)
if (this.soortMutatie.Text == "Muteren")
string displayField, NameValueCollection colPara)
{
DataSet objDS;
sql = objHelper.ProcessStatement(sql, para); ds = objHelper.Statement2DataSet(sql);
sql = this.verwerkParameters(sql, colPara);
ControlFactoryHelper.Dataset2Controls(
objDS = this.Statement2DataSet(sql); if (objDS.Tables.Count > 0) {
control.DataSource = objDS;
control.DataValueField = valueField;
control.DataTextField = displayField; control.DataBind();
}
};
sql = this.selectcommand;
}
};
ds, this.Controls);
In onze webparts komt een aantal standaard besturingselementen voor (bijvoorbeeld soortMutatie) waarmee je instelt welke bewerking op de gegevens uitgevoerd wordt. In het voorbeeld is te zien hoe op basis van de helper-klasse de controls worden omgezet naar een naamwaarde collection. Deze wordt gebruikt om een SQL-statement te
magazine voor software development 41
.NET C# parametriseren met waarden zoals ingevuld in de besturingselementen. Vervolgens wordt het SQL(SELECT)-statement omgezet naar een dataset. De waarden uit deze dataset worden gekoppeld aan de eigenschappen van de controls. Hiermee is een round trip van controls naar database en weer terug naar de controls gerealiseerd. In de onderstaande afbeelding is een en ander te zien. Na het klikken op de knop kiezen gaat bovenstaande source code af. Omdat alleen de waarde van de eerste keuzelijst relevant is voor het SELECT-statement wordt op correcte wijze het juiste besturingselement gebruikt voor het instantieren van het SELECT-statement. Omdat de namen van de besturingselementen overeenkomen met de kolomnamen in de resultset van het SELECT-statement, worden vervolgens de juiste besturingselementen gevuld met waarden uit de resultset.
inconsistent objectmodel vrijwel niet mogelijk is. Is het domeinmodel gereed, dan kan er eerst een database-structuur aangemaakt worden voor SQL-Server of MS-Access. Daarna kan het webpart worden gegenereerd. Dit wordt gedaan op basis van een template zodat eigen gedrag later toegevoegd kan worden aan het webpart. In de afbeelding hieronder is te zien hoe een genereerscherm van een webpart er uitziet.
Fig. 6
Fig. 4 Genereren van webparts Uit dit artikel blijkt dat een webpart bestaat uit een groot generiek deel en een klein specifiek deel. Het specifieke deel wordt gebruikt om collecties van eigenschappen te instantiëren met specifieke waarden. We kunnen echter nog een stap verder gaan. Het specifieke deel kan vanuit een CASE tool gegenereerd worden, hierdoor kun je op basis van datamodellen zeer snel één of meerdere webparts genereren. Omdat de webparts in gedrag generiek zijn werken alle specifieke webparts op een zelfde wijze. De CASE tool DLA-Work in Process bevat de mogelijkheid om een domein- of datamodel om te zetten naar source code in een webpart. In figuur 5 is te zien hoe in een repository wordt opgegeven waarin specifieke eigenschappen aangepast worden.
Samenvatting In dit artikel is ingegaan op de basismogelijkheden van webparts. Webparts zijn niet alleen interessant binnen Sharepoint maar zeker ook binnen ASP.Net webtoepassingen. Bij het gebruik van webparts is het gebruik van helper-klasses en overerving een belangrijk hulpmiddel voor het implementeren van hergebruik en het zorgen dat specifieke sourcecode minimaal is en alleen gebruikt wordt voor het aanroepen van generieke functies binnen de generieke modulen. Door het gebruik van een CASE tool is het verder mogelijk om het specifieke deel van de code uit een repository te genereren. Hierdoor wordt het maken van webparts bijzonder eenvoudig. Een freeware versie van de CASE tool DLA Work in Process is te vinden op de website www.dlaos.nl. Bij het artikel hoort een voorbeeldtoepassing die gebruikt kan worden in Visual Studio 2008. In volgende artikelen over webparts zal ik ingaan op het koppelen van webparts aan elkaar en het gebruik van datagrids en andere besturingselementen voor Master-Detail webparts. •
Bert Dingemans Bert is als software architect werkzaam binnen de maatschap FreeIT, een maatschap van ICT professionals. Bert heeft een voorliefde voor Model Driven Development en het genereren van software. Zo heeft hij CASE tools ontwikkeld in Visual Objects als DLArchitect en DLA Work in Process. Er zijn freeware versies van deze tools beschikbaar op de dla-os website. Bert heeft een weblog op www.dla-os.nl.
UX
TIP:
Leren omgaan met Expression Blend?
Fig. 5 De CASE tool maakt het voor een gebruiker mogelijk om snel en eenvoudig door het domeinmodel te navigeren, maar nog belangrijker is het dat de tool structuur biedt en validaties uitvoert zodat een
42
MAGAZINE
Iedereen die met WPF of Silverlight begint zal vroeger of later met Expression Blend aan de slag gaan. Er zijn diverse manieren om het te leren maar één manier is een online cursus van bijna 4 uur van Lynda.com. Op de website is een complete gratis cursus Getting Started with Expression Blend te vinden. Zie http://movielibrary.lynda.com/html/modPage.asp?ID=384.
GENERAL
Interesting Things:
Why Newton was Agile and the Titanic was Not Let’s be perfectly clear about one thing: 2009 will not only be known as the year the financial crisis hit in hard, it will also be known as the year everything turned agile. Please allow me to explain. The times when banks, insurance companies, car industries and the likes could start up multi-million software development projects of titanic ambition, with dozens of stakeholders, never ending requirements sessions and five year deployment plans are passed. I mostly refer to such projects as titanic projects. They take forever to build, but sink at the first bump in the road. Newton’s first law Now, let’s add some physics. Back in 1687 Isaac Newton stated his three laws of motion. The first law nicely describes the current state of most titanic projects. It states: “A body continues to maintain its uniform motion unless acted upon by an external unbalanced force." Here, if you replace body by project, titanic projects will never stop unless something really drastic occurs. “We have already invested 25 million Euro’s in this project, we cannot afford to stop now!” prowls the eager program manager. Well, that was then, and this is now. We now do have an external unbalanced force. It’s called the financial crisis. For the majority, these titanic projects will be put on hold - or will shift to slow motion. Personally I consider putting such projects asleep to be one of the greatest benefits of the current worldwide economic situation. Newton’s third law So now what? This is where Newton’s third law of motion comes in handy. Without going into much technical detail it can be simplified as: “To every action there is an equal and opposite reaction." Doesn’t that sound good? Instead of running highly expensive, never ending, never delivering projects, there will an equal and opposite reaction. We all know what that reaction will look like: shorter, smaller, delivering projects. Agile projects. In 2009 and 2010 (late followers) nearly every respectable bank, insurance company and government agency will turn to look how agile practices can save their pet projects. Being a longtime agile evangelist you might say: “Sander, what are your complaining about? This is what you always dreamed of.” Well, yes and no. Of course I’m glad that agile practices finally become accepted. But there’s a dark side too. The sudden churn towards agile will trigger a huge demand for agile expertise in the marketplace, not only onshore as you might expect, but even offshore. A new breed As agile becomes hot, a whole new breed of agile practitioners will arise from the ashes of the titanic projects: the people that previously ran those projects. They all will be officially converted to agile, although deep down inside they very much believe that agile is not different from
the way they’ve been conducting (failing) projects for the last twenty years. And who will treat projects in exact the same way as they did for the last twenty years, only with a small speak. Thus we will see six month “iterations” - or longer, feature teams of over sixty members, projects where the first ten iterations are used to establish the requirements, the next ten iterations for doing design, and the remaining iterations to build the software, and finally if there’s still time left, will use the last iteration to test the software. Projects that are agile in name only. Please be aware of this new breed. Let me illustrate agile misuse with a small example I recently encountered. In the week before Christmas I received an interesting newsletter by a company that claims to be agile. Unfortunately, their newsletter writers are still left in the dark. A quote from this newsletter. “Government chooses agility in IT projects,” heads one of its articles. “As of 2010 all big IT projects run by government agencies are to be evaluated per stage on suitability for the next stage.” In my humble opinion there’s not much agile about this statement. In fact, it sounds like waterfall with even tighter milestones to me. But the newsletter concludes: “The Dutch government clearly chooses for an approach based on agility.” Eh? As I said before: we’re in the year everything turns agile. I even got new year’s greeting cards wishing me lots of agility in the new year. Agile popularity paradox Service oriented architecture is out. It’s just too complicated. And agile is in. In 2009 and 2010 the term agile will be used for anything that people want you to apply to your projects. Think of highly proprietary code generators that were sold five years ago as service oriented code generators, agile frameworks that previously were mash-up frameworks, agile project managers with only waterfall experience, agile requirements tools that require the creation of multiple Word documents per requirement, and projects who claim to be agile just because their weekly progress meeting is now done standing up. The risks involved form a true paradox. Yes, we know agile works. Hook up to its core principles, including collaborative teams, re-prioritization, short iterations, small units of work, and testing and delivering early and frequently. And then implement techniques to match these principles, such as daily (!) stand up meetings, agile dashboards, burn down charts, pair programming or retrospectives. Your projects will benefit. But due to the exploding popularity of anything agile we are going to have to wade through the mud. Newton was right. A body continues to maintain its uniform motion unless acted upon by an external unbalanced force. The thicker the mud, the slower the motion. Sander Hoogendoorn blog.sanderhoogendoorn.org • www.accelerateddeliveryplatform.com
magazine voor software development 43
DELPHI
Cary Jensen
Keystroke Combinations in
Delphi’s Code Editor There are many advantages to being a Delphi developer. Not only has the language kept pace with improvements added to other languages, such as C#, but it has maintained a remarkable consistency over its many years, first as Turbo Pascal, and later as Delphi. For example, routines originally written for Turbo Pascal in the 1980s often compile in the latest version of Delphi with little or no changes. There are not many languages you can say that about. Delphi developers have also benefited from Delphi’s strong IDE (integrated development environment). From Delphi 1 through RAD Studio 2009, Delphi’s IDE has provided developers with state-of-the-art features that support and improve the overall development experience.
Did you know that Delphi can record (Ctrl+Shift+R) and playback (Ctrl+Shift+P) a series of keystrokes?
44
MAGAZINE
Unfortunately, not all of the capabilities of Delphi’s IDE are well known. Consider this. Did you know that Delphi can record (Ctrl+Shift+R) and playback (Ctrl+Shift+P) a series of keystrokes? It’s my impression that at least half of Delphi developers do not know this. But it is a feature that has been available in Delphi since version 1. Part of the problem is that the various keystrokes supported by Delphi’s IDE have been notoriously hard to find in Delphi’s help. Some of them have never been documented. For example, one of my favorite keystroke combinations is Ctrl+<spacebar>, which invokes the Argument Value List drop down. Whenever you are entering the value of an expression, for example, the left side of an assignment statement or a parameter of a function, pressing Ctrl+<spacebar> displays a list of the possible symbols that would satisfy the expression. This feature, which was added in Delphi 3, appeared in the Delphi 4 help files only. It wasn’t in Delphi 3’s help, and has been missing in action since Delphi 5. Another interesting item to note is that, although Delphi’s IDE has undergone major revisions in the past few years with the introduction of the Galileo IDE, the keystrokes available in the IDE, and the editor specifically, have not changed much at all. In particular, nearly every keystroke combination that was available in Delphi 1 still works in Delphi 2009. But there is still the problem of documentation. When I inspected the most recent help that ships with RAD Studio 2007, I found that just over half of the editor’s keystroke combinations appeared in the help. The others were nowhere to be found. And this brings us to the essence of this article. I have attempted to collect here, in Table 1, many of the editor keystroke combinations that I am aware of (the complete list will be on the website-version of this article, because it would be too long in a magazine). These keystroke combinations are for the default key mapping, which is used by most Delphi developers. If you are using one of the other key mappings, some of these combinations will not work, but many will. I also want to admit that most of this table’s contents was derived from Delphi’s help files. In other words, while I compiled this table, I did not write most of the entries myself. Some entries appear exactly as they did in the help files, and I wrote some as well. Furthermore, I edited many of the help file entries, either to make corrections or to simplify the description. I also removed entries that were in the help files that either didn’t work correctly, or appeared to not work at all. Finally, I didn’t include many keystrokes that are not code editor specific. For instance, I did not include debugging related keystrokes. So here, for your consideration and enjoyment, is a comprehensive list of the keystrokes supported by Delphi’s editor. This list is presented in alphabetical order, by key. When two key or key combinations perform the same task, they both appear in the left column. The full list can be downloaded from www.sdn.nl •
DELPHI
Table 1. Delphi Editor Keystrokes for the Default Key Mapping Key/Key Combination
Action
Alt+Up Arrow - Ctrl+Left Click Click+Alt+Mouse Drag Ctlr+/ Ctrl+Alt+F12 Ctrl+Alt+Shift+End Ctrl+Alt+Shift+Home Ctrl+Alt+Shift+Left Arrow Ctrl+Alt+Shift+Page Down Ctrl+Alt+Shift+Page Up Ctrl+Alt+Shift+Right Arrow Ctrl+Backspace Ctrl+E Ctrl+O+A Ctrl+O+B Ctrl+O+C Ctrl+O+G Ctrl+O+K Ctrl+O+L Ctrl+O+O Ctrl+O+U Ctrl+P Ctrl+Q+[ Alt-[ Ctrl+Q+] Alt-] Ctrl+Q+A Ctrl+Q+B Ctrl+Q+F Ctrl+Q+K Ctrl+Q+P Ctrl+Q+R Ctrl+Q+S Home Ctrl+Q+T Ctrl+Q+U Ctrl+Q+X Ctrl+Q+Y Ctrl+Shift+<space bar> Ctrl+Shift+A Ctrl+Shift+D Ctrl+Shift+Down Arrow Ctrl+Shift+E Ctrl+Shift+End Ctrl+Shift+K+A Ctrl+Shift+K+E Ctrl+Shift+K+G Ctrl+Shift+K+M Ctrl+Shift+K+N Ctrl+Shift+K+O Ctrl+Shift+K+P Ctrl+Shift+K+R Ctrl+Shift+K+T Ctrl+Shift+P Ctrl+Shift+R Ctrl+Shift+Right Arrow Ctrl+Shift+Tab Ctrl+Shift-K+C Shift+Alt+Arrow Alt+Mouse Drag Shift+Alt+Arrow Click+Alt+Mouse Drag
Starts code browsing Selects column-oriented blocks Comments or uncomments the current line or selected block Displays list of open files Selects the column from the cursor position to the end of the current file Selects the column from the cursor position to the start of the current file Selects the column to the left of the cursor Selects the column from the cursor position to the top of the screen Selects the column from the cursor position to the bottom of the screen Selects the column to the right of the cursor Deletes the word to the right of the cursor Starts Incremental Search Triggers Open file at cursor or go to declaration Browses symbol at cursor Turns on column blocking Go to line number Turns off column blocking or line blocking Turns on line block mode Inserts compiler options and directives Toggles case of block Causes next character to be interpreted as an ASCII sequence Finds the matching delimiter (forward) Finds the matching delimiter (backward) Displays Replace dialog Moves to the beginning of a block Displays Find dialog Moves to the end of a block Moves to previous position Moves to the beginning of a file Moves to the beginning of a line Shifts editor so the current line is at the top of the window Shifts editor so the current line is at the top of the window, if possible Moves the cursor to the bottom of the window Deletes to the end of a line Displays Code Parameters pop-up window Displays Find unit dialog Displays the Declare Field dialog Jump between declaration and implementation Displays Rename refactoring dialog Selects from the cursor position to the end of the current file Expands all collapsed regions Collapses current region Collapses interface, implementation, initialization, and finalization sections Collapses all methods Collapses unit Enables/disables code folding Collapses nested procedures Collapse all custom regions Expands/collapses current region Playback a key macro Starts/stops recording a key macro Selects the word to the right of the cursor Moves to the previous code page (or file) Collapses all classes Selects column-oriented blocks Selects column-oriented blocks
magazine voor software development 45
ED IT IE
10 0
MA
GA
ZI SDN NE
ED IT IE MA GA ZI NE SD N
Maurice de Beijer
Smartmem, Zo Smart Als de Naam Zegt?
Dat was de titel van een artikel van Ernst Peter Tamminga in het eerste nummer van het SDN blad. Al moet ik eigenlijk CDGN zeggen want toen heette de gebruikersgroep nog de Clipper Developers Group Netherlands. En dat eerste nummer van het toenmalige CDGN blad kwam uit op 1 mei 1990. Er is veel veranderd sinds die tijd; zo zie je Clipper tegenwoordig nog wel een beetje terug als Visual Objects, maar wie het huidige SDN magazine doorleest zal veel meer over Delphi, .NET, Information Worker en Core Systems lezen. Softwareontwikkeling, verandering is de enige constante … Soms lijkt het wel of er geen eind komt aan de veranderingen in software land. We zien talen en technieken komen en gaan. Neem Clipper: 20 jaar geleden een populaire ontwikkelomgeving maar tegenwoordig hoor en zie je er niet veel van. Niet dat Clipper niet meer bestaat! Er is nog steeds een groep ontwikkelaars die hun brood verdient met het schrijven van Clipper code. Maar groot is deze groep zeker niet meer en er zullen weinig nieuwe mensen bijkomen. Maar zijn al die veranderingen nu eigenlijk echt of verpakken we oude problemen gewoon in een nieuwe TLA (three-letter acronym) en maken we onszelf wijs dat alles steeds verandert? Als ik het artikel uit 1990 doorlees krijg ik toch een beetje het idee dat we misschien nog met een aantal van dezelfde problemen bezig zijn. Zo gaat het artikel over Smartmem, een bibliotheek om binnen Clipper-applicaties het geheugengebruik te controleren en geheugen boven de 640Kb aan te spreken.
geheugen was dan ooit nodig zou zijn, 640Kb voor de programma’s en de rest voor MS-DOS zelf. Volgens de overleveringen zou Bill Gates deze beslissing zelf genomen hebben. Of Bill Gates dit echt gezegd heeft weet ik niet, maar één ding was zeker: in 1990 was 640Kb voor een programma al lang niet meer genoeg.
Fig. 2: Zo ziet het er tegenwoordig uit
Fig. 1: Zo keken we toen naar geheugengebruik De getallen zijn misschien heel anders geworden maar het probleem van “Wat doet mijn programma met zijn geheugen en hoe kan ik meer geheugen krijgen?” is nog steeds actueel. Toen het oorspronkelijke artikel geschreven werd, gebruikten de meesten van ons MS-DOS. Bij het ontwikkelen van MS-DOS is ooit bedacht dat 1 megabyte meer
46
MAGAZINE
Tegenwoordig denken we nauwelijks meer in kilo- of mega-bytes en is het allemaal giga. Maar het probleem bestaat nog steeds, vandaar de beweging van de 32 bit processor naar de 64 bit processor. Kocht ik een paar jaar geleden nog een nieuwe laptop met 1Gb intern geheugen, tegenwoordig denk ik er niet eens meer aan om er één met minder dan 4Gb te kopen en eigenlijk heb ik liever 8 Gb in mijn machine. En ook het probleem wat mijn programma met het geheugen doet, is niet minder geworden; alleen de technieken om het geheugengebruik te meten zijn een stuk beter geworden. Zo hebben we tegenwoordig memory profilers die we op onze .NET applicaties los kunnen laten. Een stuk makkelijker dan dit van tevoren in het programma te moeten inbouwen. Ook het vrijgeven van gebruikt geheugen blijft ons bezighouden. Als ik op GC.Collect() zoek, waarmee je binnen .NET geheugen vrij kan geven - iets wat eigenlijk binnen een virtuele omgeving helemaal automatisch zou moeten gaan -, kom ik toch nog ruim 66.000 hits tegen. Blijkbaar houdt geheugenbeheer ons nog flink bezig in 2008. Uiteraard zijn er genoeg dingen wel echt veranderd sinds 1990. Zo werken we al lang niet meer met MS-DOS maar is het nu Windows Vista en kijken we uit naar Windows 7. De procedurele talen als
0 10
ED IT IE
IE ED IT MA GA ZI NE SD N
ZI SDN NE GA
MA
Clipper zijn nu voor een groot deel verdrongen door object georiënteerde talen. En waar we met Clipper voornamelijk monolithische applicaties maakten, zijn het nu n-tier-, service georiënteerde- en gedistribueerde applicaties die het daglicht zien. Conclusie Er is veel veranderd sinds de CDGN opgericht is, dat is zeker. Maar soms lijkt het ook wel een beetje of we onszelf voor de gek houden en alleen de nummertjes veranderen. Eén ding is zeker, we werken in een industrie die volop verandert. Ik ben dan ook heel erg benieuwd naar wat er in nummer 200 van het SDN magazine, of hoe het dan ook zal heten, gaat staan als terugblik op dit nummer. •
Maurice de Beijer Maurice de Beijer is een zelfstandig software ontwikkelaar, Most Valuable Professional en bèta-tester voor Microsoft. Hij specialiseert zich in .NET, object oriëntatie en het oplossen van technisch moeilijke problemen. Hiernaast is Maurice ook de voorzitter van de UX track van het Software Development Netwerk, www.sdn.nl. Maurice is te bereiken via [email protected] of www.theproblemsolver.nl
Advertentie Aladdin
AGENDA 2009
VO DevFest, London (UK) . . .12-15 maart SDN Event . . . . . . . . . . . . . . . . .30 maart TechEd USA, Los Angeles . . . . .11-15 mei SDN Magazine Nr. 101 . . . . . . . . . .15 mei Microsoft DevDays 2009, Den Haag www.DevDays.nl . . . . . . . . . . . .28-29 mei SDN Event . . . . . . . . . . . . . . . . . . .26 juni SDN Magazine Nr. 102 . . . . . .28 augustus SDN: Software Developer Conference 2009 Papendal, Arnhem . . . . . .19 - 20 oktober Genoemde data onder voorbehoud
.NET ASP
Henry Cordes
ARCHITECTURE
ASP.NET Webapplicaties Eindelijk Unit-Testbaar Onder de bezielende leiding van Scott Guthrie heeft Microsoft ‘s ASP.NET team in december 2007 de eerste preview van het ASP.NET MVC-Framework uitgebracht. Met het ASP.NET MVC-Framework is het mogelijk een webapplicatie in ASP.NET te ontwikkelen, gebaseerd op het Model View Controller design pattern. Ontwerpprincipes Dit framework is, naast het MVC design pattern, opgebouwd volgens een aantal principes: • Convention over configuration: Het meest voorkomende patroon wordt gebruikt. Wil je iets anders bereiken, dan is dat te configureren. • DRY (Don’t Repeat Yourself): Filosofie die tracht duplicatie te reduceren. • Extensible: Het framework zal eenvoudig uitbreidbaar zijn. Kerncontracten zijn interface-gebaseerd. • Seperation of concerns: Het scheiden van belangen zorgt ervoor dat elk component zijn eigen verantwoordelijkheid heeft en niet afhankelijk is van een of meerdere andere componenten. • Facilitate Test Driven Development: Het framework moet het eenvoudig maken om applicaties die ermee ontwikkeld worden, unittestbaar te maken.
Voordelen Het MVC-Framework biedt de volgende voordelen: • Alle kerncontracten binnen MVC zijn interface-gebaseerd en daardoor uitwisselbaar (of mockable). Door het scheiden van belangen wordt een goede unit-testbaarheid verkregen. • Het framework is uitbreidbaar. Het is bijvoorbeeld mogelijk IOC containers zoals Windsor, Spring.Net, NHibernate, etc., in te zetten met het Framework, maar ook view engines en unit test frameworks zijn uitwisselbaar. • Door het introduceren van UrlRouting heb je veel controle over de URL’s van je applicatie. Je hoeft geen url rewriting meer in te zetten om URL’s te krijgen die afwijken van de WebForms manier. Page-request flow Figuur 2 toont een schematische weergave van het verloop van een page-request bij het MVC Framework:
DRY: Don’t Repeat Yourself Model View Controller MVC (Model View Controller) is een design pattern bedoeld om business-logica te scheiden van userinterface-logica. Het resultaat van een juiste toepassing van het MVC-pattern zal robuuste, onderhoudbare en (unit)testbare applicaties opleveren. • Model: Representeert de data en business-rules, verantwoordelijk voor handhaven van ‘State’. • View: Definieert hoe de data aan de gebruiker gepresenteerd wordt (UI). • Controller: Definieert hoe de UI reageert op ontvangen commando’s en requests. Deze componenten zijn verantwoordelijk voor de afhandeling van interactie met de eindgebruiker, het manipuleren van het Model en de uiteindelijke keuze van welke View aan de gebruiker wordt gepresenteerd.
Fig. 2: Verloop Page-request 1. Request komt vanaf de client naar de controller. 2. De controller roept het model aan om business-operaties uit te voeren (data ophalen, business-rules uitvoeren). 3. Het model retourneert het resultaat van de operatie aan de controller. 4. De controller beslist welke view getoond moet worden en stuurt deze view de benodigde data. 5. View toont de output en stuurt response naar de client. Unit-testen De consensus binnen de moderne developer community is dat het gebruik van unit-tests belangrijk is. Een unit-test moet autonoom, niet afhankelijk van externe resources of componenten, snel uit te voeren en herhaalbaar zijn. Dit is onder andere te bereiken door objecten die geen direct onderdeel uitmaken van de te testen unit te mocken en door het gebruik van “Inversion of Control”, ook wel het “Dependency Injection” design pattern genoemd. Het woord mocken betekent hier dat het gedrag en de eigenschappen van een ander object gesimuleerd worden.
Een unit-test moet autonoom, snel uit te voeren en herhaalbaar zijn Fig. 1: Schematische weergave Model View Controller
48
MAGAZINE
Het idee achter inversion of control is om externe afhankelijkheden naar objecten te faciliteren d.m.v. externe mechanismen in plaats van
.NET ASP
ARCHITECTURE
de objecten zelf de afhankelijkheden op te laten zetten, of zelfs de processen die de objecten gebruiken de benodigde afhankelijkheden op te laten zetten. Het einddoel is 'loose coupling', en het kan vrij eenvoudig bereikt worden door het parametriseren van de constructor met het object waarvoor je de aanroepende class verantwoordelijk laat zijn (zie listings 3, 4 en 5). Omdat het met deze hulpmiddelen eenvoudig is om een unit (methode) te isoleren, is de mogelijkheid om alleen het gedrag van de unit te testen binnen handbereik. Het MVCdesign pattern laat de controller de requests afhandelen en beslissen welk model er aan de view doorgegeven wordt om aan de gebruiker te tonen. Door deze scheiding van belangen is het unit-testen van een webapplicatie veel eenvoudiger. Het is mogelijk unit-tests te runnen zonder dat de webserver actief is, doordat de namespace System.Web.Abstractions, nadat het in het MVC framework zijn waarde bewezen heeft, is toegevoegd aan het .NET Framework. Deze namespace bevat HttpContextBase, waarmee je HttpContext kunt abstraheren en nabootsen, of mocken. Natuurlijk is het nog steeds van belang om principes als “een class heeft een verantwoordelijkheid” of “single responsibility principle” te volgen om een unit-testbare applicatie te krijgen. MVC-Framework en unit-testen Het ASP.NET MVC Web Application projecttype kan in Visual Studio toegevoegd worden door de installer te runnen; deze is te downloaden vanaf www.asp.net/mvc. Om duidelijk te maken hoe het MVCFramework omgaat met unit-tests maken we een nieuw project aan.
De eerste dialoog na het aanmaken van een ASP.NET MVC Web Application geeft de mogelijkheid tot het creëren van een unit-test project. Bij ‘Test Framework’ kan ieder unit-testframework gekozen worden dat integreert met het MVC-Framework. Wij maken gebruik van het Microsoft testframework dat standaard in Visual Studio 2008 aanwezig is.
Alternatief testframework is MbUnit, een open source initiatief Een veelgebruikt alternatief testframework is MbUnit, dit is een open source initiatief. Bij installatie van MbUnit versie 3.x zet MbUnit zichzelf in de lijst van beschikbare testframeworks. Tevens worden er MVC/MbUnit Test Project Templates voor C# en VB.NET geinstalleerd. Mocht het testframework van jouw keuze dit (nog) niet doen, dan kun je op de Visual Web DeveloperTeam Blog [3] lezen hoe dit werkt. Projectstructuur De wizard heeft twee projecten in de solution aangemaakt: de webapplicatie en een testproject. Zelf heb ik een class-library toegevoegd. In de webapplicatie zijn de models, views en controllers gescheiden door aparte mappen. In de View-map wordt voor iedere controller een bijbehorende subfolder verwacht. Doordat het convention over configuration principe gebruikt wordt, zal de controller direct werken wanneer men zich aan de conventie houdt. Hierdoor kun je snel aan de slag. Wil je ander gedrag, dan kun je dit configureren. In de sub folder worden de MVCASPX-views geplaatst. In de View-map staat ook een Shared-submap waarin de MasterPages en errorpagina’s geplaatst worden. De Content-map is voor de stylesheets en javascript–bestanden. De wizard plaatst een CSS-bestand en een MVC-variant van de ASP.NET AJAX Client library. De wizard maakt een Home- en een Account-controller aan met bijbehorende views. Home is voor de start pagina van het project en account voor de aanmeld pagina. Voor de controllers
Fig.: 3: Nieuw ASP.NET MVC Web Application project We geven de gewenste naam en locatie op en klikken op ‘OK’. Dan verschijnt het scherm in figuur 4.
Fig.4: Maak Unit Test Project aan
Fig. 5: Projectstructuur
magazine voor software development 49
.NET ASP
ARCHITECTURE
geldt het convention over configuration principe dat iedere publieke methode als action-methode gezien wordt. De controller baseclass zal de betreffende action-methode uitvoeren volgens de URL routing regels die in de GLOBAL.ASAX van de applicatie zijn geconfigureerd. In het testproject is een Controllers-map met een HomeControllerTest aangemaakt. In deze class zijn twee voorbeeldtests geplaatst, waarmee getest wordt of de controllers de juiste titel in de view plaatsen. Afhankelijkheden verwijderen We willen een unit, een zo klein mogelijk stuk functionaliteit, testen. In dit geval nemen we de Index-methode op de HomeController als voorbeeld . De Index-methode (zie lisiting 3) haalt data op uit een database via de HomeModel-class en geeft deze door aan de view. Zodra we met een database werken, wil je bij het unit-testen niet afhankelijk zijn van het bestaan van de database. Door met interfaces te werken wordt het eenvoudiger om de datalaag te abstraheren en de database na te bootsen. public interface IProductsDataSource { }
List GetProducts();
je totdat de test slaagt. Hierna start het proces weer van voren af aan. We starten met de test in listing 3. [TestMethod]
public void HomeController_Get_All_Products_Index() {
MockDatastore store = new MockDatastore();
HomeController controller = new HomeController(store); controller.Index(); Assert.IsTrue(
((List)controller.ViewData.Model).Count == 3, "ViewData.Model did not hold 3 Products");
}
Listing 3: Test HomeController Deze test instantieert de HomeController, geeft in de constructor een instantie van onze zojuist gemaakte MockDatastore-class mee en voert vervolgens de Index-methode uit. De test controleert dat er drie Producten in het model zitten. Na het uitvoeren van de test krijgen we het resultaat ‘Failed’, zoals getoond in figuur 6.
Listing 1: Interface IProductsDatasource In listing 1 is een simpel interface zichtbaar. Dit interface dicteert het implementeren van een methode die een List van het type Product teruggeeft. Door een interface te gebruiken maken we het mogelijk de methodes in deze interface uit te wisselen, waardoor we in onze unittest kunnen controleren wat deze retourneert. We maken een eigen class genaamd MockDataStore. Deze class implementeert het IProductsDataSource-interface en geeft altijd dezelfde List terug (zie listing 2). Deze class zal alleen voor unit-testdoeleinden ingezet worden. public class MockDatastore : IProductsDataSource {
public List GetProducts() {
List products = new List(); for (int i = 0; i < 3; i++) {
Product product = new Product();
product.Name = "Productname " + i.ToString(); product.Category = "Testproduct";
product.Description = "This is product no: " + i.ToString();
Fig. 6: Test failed Refactor Het Model krijgt een private read-only IProductsDatasource-property, die geïnstantieerd wordt met een nieuwe instantie van de datalaagclass genaamd Datastore. Dit is een object dat via de GetProductsmethode de producten uit de database haalt en retourneert. Door inversion of control te gebruiken kunnen we tijdens een test een ander object dat de IProductsDataSource interface implementeert, gebruiken om de data aan te leveren (zie listing 4). public class HomeModel {
private IProductsDataSource _DataSource; private IProductsDataSource DataSource {
} }
}
products.Add(product);
TEST / REFACTOR / TEST Test faalt Test Driven Development (TDD) schrijft voor om te starten met het schrijven van een test, deze uit te voeren en te zien falen. De volgende stap is de methode zo simpel mogelijk in te vullen, zodat de test slaagt. Vervolgens refactor je de code en voer je de test weer uit; dit herhaal
MAGAZINE
if (_DataSource == null) {
return products;
Listing 2: Mock Data class
50
get {
product.Price = (Decimal)i * 10;
} }
}
_DataSource = new Datastore();
return _DataSource;
public HomeModel() { } /// <summary>
/// Constructs an instance of HomeModel
/// inversing control of the datasource to the caller
.NET ASP
ARCHITECTURE
///
/// <param name="dataSource">
public HomeModel(IProductsDataSource dataSource) { }
_DataSource = dataSource;
public List GetProducts() { }
}
return DataSource.GetProducts();
Listing 4: Inversion of control op Model In listing 5 zien we dat de controller (HomeController) een private member ModelData heeft van het type HomeModel. De standaard constructor zonder parameter instantieert het HomeModel type met behulp van zijn standaard constructor. Op de controller maken we een extra constructor waarbij een IProductsDataSource als parameter wordt verwacht. In deze constructor wordt de waarde van de parameter aan de private member ModelData toegewezen. [HandleError]
public class HomeController : Controller {
HomeModel ModelData; public HomeController() { }
ModelData = new HomeModel();
public HomeController(IProductsDataSource productsData) { }
ModelData = new HomeModel(productsData);
public ActionResult Index() { }
}
return View("Index", ModelData.GetProducts());
Fig. 7: Test slaagt Conclusie Wanneer jouw volgende webapplicatie meer is dan alleen CRUD, als je de mogelijkheid wilt om je processing- en business-logica te testen, misschien zelfs TDD wilt adopteren … dan is het ASP.NET MVC Framework misschien iets voor jou. De aard van het MVC design pattern leent zich uitstekend voor unittesten doordat de afzonderlijke onderdelen gescheiden zijn. Voeg daar het feit aan toe dat de kerncontracten binnen het ASP.NET MVC Framework interface-gebaseerd zijn, en het wordt alleen maar beter. De onderhoudbaarheid van een MVC-applicatie zal beter zijn door de ‘loosely coupled’ aard ervan. Overigens kan ook bij gebruik van het ASP.NET MVC Framework een slecht onderhoudbare applicatie gemaakt worden … Ook als je het prettig vindt veel controle te hebben over bijvoorbeeld de HTML die in de browser gerenderd wordt, is MVC ideaal. Er wordt geen HTML gegenereerd; alleen datgene dat je zelf in de view plaatst wordt gebruikt. Voldoet de WebForms view-engine die standaard door het ASP.NET MVC-Framework gebruikt niet aan je wensen, gebruik dan de engine die je wel bevalt of maak er zelf een. Referenties Gebruikte URL’s: • ASP.NET MVC: http://www.asp.net/mvc • DRY: http://en.wikipedia.org/wiki/Don't_repeat_yourself • Visual WebDev team blog: http://blogs.msdn.com/webdevtools/archive/2008/03/06/asp-net-mvc-test-framework-integrationdemo.aspx • MVC-Framework Preview 5: http://www.codeplex.com/aspnet/Release/ProjectReleases.aspx?ReleaseId=16775 • Dit artikel is geschreven op basis van MVC-Framework Preview 5! •
Henry Cordes
Listing 5: Controller-class In de Index-methode, die een ActionResult retourneert, wordt de member ModelData gebruikt om GetProducts uit te voeren. Door deze werkwijze zorgen we ervoor dat standaard, wanneer de constructors zonder parameter gebruikt worden, de data uit de database gebruikt wordt. Tijdens onze test in listing 4 kunnen we echter een eigen Data-object gebruiken, MockDatastore uit listing 3, die we in de constructor van de HomeController injecteren. Vervolgens zal dit MockDatastore object aan de private member DataSource toegewezen worden via de geparameteriseerde constructor van het HomeController object, waarna het in de GetProducts methode de producten kan retourneren die wij in onze test verwachten. De class declaratie in listing 5 is met het attribuut HandleError gedecoreerd. Dit action-filter-attribuut is sinds preview 4 aan het MVC Framework is toegevoegd Test slaagt Na deze refactoring voeren we de test uit listing 4 nogmaals uit.In figuur 7 is te zien dat de test succesvol is uitgevoerd. We kunnen er nu vanuit gaan dat de methode Index op de HomeController-controller doet wat wij verwachten.
Henry Cordes is consultant bij Avanade(www.avanade.com), een samenwerkingsverband van Microsoft en Accenture. Voor vragen en opmerkingen is Henry te bereiken via [email protected] of zijn blog www.henrycordes.nl.
SDN
TIP:
.NET De SDN-site is vernieuwd. C# en VB.NET vind je nu samen onder de noemer .NET op www.sdn.nl/NET.
magazine voor software development 51
CORE SYSTEMS
Ulf Büchner
IBM Rational Business Developer with Enterprise Generation Language -
Java zonder Java In tijden waar IT organisaties onder steeds grotere kostendruk komen te staan worden stijgende productiviteit en hergebruik een vereiste om aan de verwachtingen van de gebruikersorganisatie te kunnen voldoen. Hergebruik doelt in dit opzicht echter niet alleen op het al langer toegepaste hergebruik van individuele softwarecomponenten, maar ook op hele applicatiesystemen en uiteindelijk zelfs op degenen die deze ooit maakten: de legacy ontwikkelaars. In dit artikel wordt uiteengezet hoe IBM’s Rational Business Developer (RBD) met de Enterprise Generation Language (EGL) hierbij kan helpen.
De gestegen productiviteit was door de overgang naar Java en de bijbehorende object-georiënteerde manier van programmeren ineens voor een deel verloren gegaan. Inmiddels is de productiviteit weer aan het groeien door strikter gebruik van frameworks en betere IDE’s, samen met aangepaste ontwikkelstraten en de uitstroom van een nieuwe generatie ontwikkelaars van de hogescholen en universiteiten die de nieuwe techniek met de paplepel ingegoten hebben gekregen.
Fig. 1: Productiviteitsverloop in de loop van de tijd Evolutie in de software-ontwikkeling In de laatste decennia van de vorige eeuw zijn programmeertalen uitgegroeid tot volledige ontwikkelomgevingen, met codegeneratie, patterns, overerving en andere vormen van abstractie en hergebruik. Code werd steeds meer logisch van aard en geschreven door business developers in plaats van door technici op byte niveau. Vaak was de software gerelateerd aan een bepaald platform, of een bepaalde technische infrastructuur (Synon, client/server). Overgang van het ene platform naar het andere of toepassing van een andere infrastructuur was niet of slechts door conversie of herbouw mogelijk. Voordeel was dat er met de nieuwe tools steeds sneller ontwikkeld kon worden: het aantal uren per functiepunt ging steeds verder omlaag. Halverwege de jaren ’90 begon de opmars van een nieuwe, object georiënteerde taal: Java. Door de platformonafhankelijkheid, een uitgebreide set libraries en de mogelijkheden om web applicaties te ontwikkelen werd de taal snel populair. Na verloop van tijd bleek echter dat Java grote problemen met zich mee bracht. Complexiteit van de taal en de libraries, het voor velen nieuwe object-oriëntatie concept, de steeds groter wordende hoeveelheid frameworks in allerlei versies en de samenwerking ertussen maakte de overstap voor zowel IT organisaties als de individuele – procedureel geschoolde – ontwikkelaar moeilijk. In veel gevallen zelfs onmogelijk.
Maar als ze nou Java zouden kunnen ontwikkelen zonder Java te kennen … 52
MAGAZINE
De vraag is echter of dit voldoende is. Bij veel bedrijven staan legacy systemen die al twee of meer decennia naar volle tevredenheid draaien en dermate groot en belangrijk zijn dat uitfasering nog lang op zich zal laten wachten. Herbouw of conversie is vaak geen optie door de enorme omvang. Daarnaast zijn er grote groepen ontwikkelaars van deze systemen die zich moeten richten op nieuwe uitdagingen. Voorbeelden hiervan zijn het toegankelijk maken van de legacy systemen voor andere toepassingen, zoals het web en uitwisseling van gegevens met derden via XML. Voor deze ontwikkelaars zal omscholing naar Java vanwege de complexiteit geen optie zijn, maar als ze nou Java zouden kunnen ontwikkelen zonder Java te kennen… RBD met EGL: Java zonder Java IBM heeft hiervoor sinds drie jaar een product, Rational Business Developer, kortweg RBD. Zoals de naam al doet vermoeden is dit product bedoeld om bedrijfskritische applicaties te laten ontwikkelen door mensen die business problemen oplossen en niet zozeer technische problemen. Zoals zoveel moderne IDE’s is RBD gebaseerd op het – oorspronkelijk door IBM ontwikkelde – Eclipse framework. Hierdoor wordt integratie met andere Eclipse gebaseerde tools ondersteund. Rational Business Developer biedt de mogelijkheid om vanuit een logische taal Java of COBOL code te genereren voor allerlei platforms. Denk hierbij aan webapplicaties die op een J2EE Application Server draaien (WebSphere, Tomcat, BEA), Windows en Linux (J2SE), de zSeries (mainframe) en de PowerSystems (voorheen i5, iSeries, AS/400).
CORE SYSTEMS Onderdeel van RBD is een generatietaal, de Enterprise Generation Language (EGL). Dit is een abstracte, declaratieve en procedurele taal die eenvoudig te leren is. Het leertraject is 1 tot 2 weken; na een maand is een ontwikkelaar volledig zelfstandig. Naast het feit dat EGL eenvoudig is en zoveel mogelijk de technologie verbergt, biedt het wel de ondersteuning die je van een moderne ontwikkeltaal mag verwachten. Zo kun je er allerlei soorten applicaties mee ontwikkelen (Web 1.0, Web 2.0, batch en SOA ondersteunende applicaties). Ook is er een geïntegreerde debugger. Als laatste redmiddel: als je er met EGL niet uitkomt, kun je altijd nog zelf Java of COBOL source code schrijven in zogenaamde external types en die zeer eenvoudig gebruiken vanuit EGL. EGL in een notendop Maar genoeg inleidende marketingpraat, laten we naar de opzet van EGL kijken. De beschrijving van de IDE laten we hier achterwege omwille van de ruimte. Iedereen (zeker hier?) die wel eens met een op Eclipse gebaseerde omgeving heeft gewerkt kent de werkwijze, met workspaces, projecten, packages, source objecten, perspectives en views. Om de omvang van het artikel te beperken zullen we ons verder hier vooral richten op het maken van webapplicaties, dus het genereren van J2EE code in combinatie met Java Server Pages (JSP’s) op een Application Server. Genereren van COBOL of het aanroepen van bestaande legacy applicaties is in de praktijk wel belangrijk, maar zal wellicht op een later moment worden besproken. EGL bestaat in de basis uit verschillende soorten source files. Deze hebben de extensie .egl en kunnen worden onderverdeeld in: • Programs – losstaande programma’s met een enkel entry point; • Libraries – groepen van programma’s in één file, dus meerdere entry points; • Services – groepen van operations in één (web) service, meerdere entry points; • JSFHandlers – event handling programma behorend bij een Java Server Page; • BIRTHandlers – event handling programma behorend bij een BIRT report; • BuildFiles – configuratiefile om instellingen vast te leggen voor het genereren van code (doelplatform, database, etc.). De source files worden in een logische structuur onderverdeeld in packages en hebben binnen een package een unieke naam. Vanuit deze EGL source files worden Java source files gegenereerd, die op hun beurt door Eclipse worden gecompileerd tot Java class files. EGL sources zijn op een bepaalde manier opgebouwd, afhankelijk van het soort source. Als voorbeeld is in listing 1 de code van een programma om een record uit de Customer tabel op te halen afgebeeld.
CustomerLib.GetCustomer(customerRecord, status); if(!status.succeeded)
SysLib.writeStderr("Error fetching record!");
end
end
end
Listing 1: Een eenvoudig EGL programma om een record op te halen Voor de Java kenners onder ons zal de structuur bekend voorkomen. Er zitten dan ook aardig wat overeenkomsten tussen Java en EGL voor wat betreft opbouw van sources. Maar ook voor een procedurele programmeur is de source goed leesbaar. Voor de verduidelijking lopen we de statements door vanaf het begin: • Package - geeft aan waar in de hiërarchische structuur van folders deze source staat; • Import statements - zorgen ervoor dat in het programma gebruikte objecten die niet in dezelfde package staan kunnen worden gevonden; • Program - dit is de definitie van het programma, met het interface (een veld customerID en een record van het type Customer, beide zowel in- als output); • Status - een variabele-definitie, waarbij status wordt gedefinieerd als zijnde van het type StatusRec (een record); • Function main() - het entry point van het programma. Binnen het main() entry point wordt de eigenlijke code geprogrammeerd. In dit geval wordt in het customerRecord het veld CUSTOMER_ID gevuld met het customerID dat via de call binnenkomt. Daarna wordt een functie GetCustomer aangeroepen in een library CustomerLib (hierover later meer). Het customerRecord is een dual parameter, waar dus het CUSTOMER_ID gevuld is. Als er een record voor dit CUSTOMER_ID bestaat, zal een volledig gevuld customerRecord worden geretourneerd. Het status record zal na de aanroep van de GetCustomer functie informatie bevatten of het aangeroepen programma succesvol is verlopen. Binnen het status record bevindt zich een boolean veld, succeeded. Als dit niet true is wordt een melding naar de console gestuurd via de ingebouwde EGL functie SysLib.writeStderr. Daarna eindigt het programma. Aan dit voorbeeld is te zien hoe eenvoudig EGL source in elkaar zit. De in EGL ingebouwde content assist, waarbij slechts delen van een statement hoeven te worden ingevuld waarna EGL zelf de statements aanvult, helpt enorm bij het snel programmeren. Om b.v. het statement CustomerLib.GetCustomer(…) in te voeren geeft de programmeur eerst een aantal beginletters in, bijvoorbeeld Cust, en drukt dan CTRL-spatie. Uit de lijst die dan wordt getoond kan CustomerLib worden gekozen.
package programs; import egldemo.StatusRec;
import egldemo.access.CustomerLib; import egldemo.data.Customer; // basic called program //
program myProgram type BasicProgram(customerID string, customerRecord Customer) {} // Variable Declarations status StatusRec; function main()
customerRecord.CUSTOMER_ID = customerID;
Fig. 2: Content assist Na het kiezen van de CustomerLib library geeft de programmeur een punt (.) in en drukt weer CTRL-spatie. Nu toont EGL alle functies die in de betreffende library beschikbaar zijn en kan hieruit een keuze worden gemaakt. Door beginletters in te geven kan de subset worden verkleind. Door het veelvuldig gebruik van content assist kan met grotere snelheid worden geprogrammeerd en worden fouten voorkomen.
magazine voor software development 53
CORE SYSTEMS
Erg krachtig is de manier waarop database-definities kunnen worden geïmporteerd Data access Erg belangrijk in een bedrijfsapplicatie is bijna altijd het benaderen van databases. EGL doet dit met SQL via JDBC. In principe is iedere database te gebruiken die een JDBC driver heeft. Erg krachtig is de manier waarop database-definities kunnen worden geïmporteerd, waarna automatisch libraries worden gegenereerd om de data in de betreffende database te kunnen benaderen. Nieuwe databases en tabellen kunnen worden gemaakt in de zogenaamde Data Perspective. Er kunnen dus zowel bestaande als nieuwe databases worden gebruikt. De database-import gebeurt met behulp van de Data Access Application Wizard. Deze wizard geeft de ontwikkelaar in achtereenvolgende stappen de mogelijkheid om: 1. De database te selecteren; 2. Al dan niet een filter te leggen op een bepaald schema in die database; 3. Tabellen te selecteren; 4. Eventueel wijzigingen aan te brengen in de sleuteldefinities die door EGL zullen worden gebruikt; 5. Eventueel ervoor te kiezen dat er na de import ook standaard JSP’s worden aangemaakt om de data in de database te bewerken. Na de import heeft EGL de volgende packages met daarin EGL source objecten aangemaakt (zie figuur 3): • Package egldemo.primitivetypes.data: bevat veldtypen die worden gebruikt; • Package egldemo.data: bevat record definities van de tabellen; • Package egldemo.access: bevat libraries die voor de database-toegang zorgen. De sources in deze packages maken gebruik van elkaar. Zo zijn record-definities van tabellen samengesteld uit veldtypen in de primitivetypes package. De libraries met database-toegang uit de access package maken weer gebruik van de record-definities uit de data package.
Zoals eerder gezegd verbergt EGL technische details waar mogelijk voor de ontwikkelaar. Een mooi voorbeeld daarvan is het commando om een record op te halen. Eerder in dit artikel gebruikten we de CustomerLib.GetCustomer functie om een record op te halen. Deze functie bekijken wij nu door de CustomerLib library te openen en naar de functie te scrollen. Een makkelijker alternatief hiervoor is om op het CustomerLib.GetCustomer statement te gaan staan en op F3 te drukken. EGL toont dan de functie in een nieuwe editor. Met behulp van deze functionaliteit is het dus erg eenvoudig om objecten binnen de applicatie te vinden. Listing 2 toont de GetCustomer functie. Function GetCustomer(searchRecord Customer inout, status StatusRec) try
get searchRecord with #sql{… };
if (SysVar.sqlData.sqlCode == 100)
HandleDBRecordNotFound(status, "CUSTOMER");
else
HandleSuccess(status);
end
SysLib.commit();
onException (exception SQLException)
HandleException(status, exception);
end
end
Listing 2: De GetCustomer functie We zien de functiedefinitie met parameter-interface en allerlei logica om af te handelen wat er moet gebeuren als iets fout gaat bij het ophalen van het record. Wat we ook zien is dat er één enkel statement is dat voor het lezen van het record zorgt: get searchRecord. Dit is een zogenaamd impliciet EGL statement, waaronder de eigenlijke SQL code is verborgen. Doordat searchRecord van het type Customer is, weet EGL dat een Customer record moet worden opgehaald. Naast ‘get’ zijn er de keywords ‘add’, ‘delete’ en ‘replace’, voor respectievelijk het toevoegen, verwijderen en wijzigen van records. We kunnen het SQL-statement zichtbaar maken door met rechts op het impliciete statement te klikken en Add te kiezen. EGL voegt dan de volledige syntax in, zoals te zien is in listing 3. Function GetCustomer(searchRecord Customer inout, status StatusRec) try
get searchRecord with #sql{
select
CUSTOMER.CUSTOMER_ID, CUSTOMER.FIRST_NAME, CUSTOMER.LAST_NAME, CUSTOMER.PASSWORD,
CUSTOMER.PHONE, CUSTOMER.EMAIL_ADDRESS, CUSTOMER.STREET, CUSTOMER.APARTMENT, CUSTOMER.CITY, CUSTOMER."STATE",
CUSTOMER.POSTALCODE, CUSTOMER.DIRECTIONS
from CUSTOMER where };
CUSTOMER.CUSTOMER_ID = :SEARCHRECORD.CUSTOMER_ID
if (SysVar.sqlData.sqlCode == 100)
HandleDBRecordNotFound(status, "CUSTOMER");
Fig. 3: Objecten aangemaakt door de database-import
54
MAGAZINE
else
HandleSuccess(status);
CORE SYSTEMS end
SysLib.commit();
onException (exception SQLException)
HandleException(status, exception);
end
end
Listing 3: Impliciet SQL statement volledig getoond We zien het SQL-statement dat wordt gebruikt om het record op te halen. Het expliciet maken van een SQL-statement heeft natuurlijk alleen nut als we er iets aan willen veranderen. Op die manier kan een record op een andere manier worden opgehaald door een kopie van de GetCustomer functie te maken, een andere naam te geven en in die kopie het SQL statement aan te passen. Overigens kunnen aangepaste definities eenvoudig worden veiliggesteld, zodat ze bij een nieuwe import van de database niet worden overschreven. Hiervoor is een speciale plek gereserveerd in iedere library. Het importeren van database-definities maakt het mogelijk om vanuit EGL bestaande databases te benaderen. De manier van werken met database libraries die de toegang verzorgen en de techniek verbergen houdt daarbij de programmatuur erg overzichtelijk. Webservices EGL ondersteunt zowel het publiceren als het consumeren van (web)services. Ook dit gebeurt op een abstracte manier, waarbij de techniek zoveel mogelijk wordt verborgen. Als eerste zullen we kijken naar het produceren van een webservice.
EGL ondersteunt zowel het publiceren als het consumeren van (web)services Zoals we eerder zagen is er een source type Service. Met behulp van een wizard maken we een nieuwe service, MyService.egl. We geven daarbij aan dat dit een webservice moet worden, waardoor EGL op de achtergrond allerlei zaken voor ons regelt. In de service voegen we een operatie toe die twee getallen bij elkaar optelt en het resultaat teruggeeft (zie listing 4). Merk op dat door middel van de keywords ‘in’ en ‘out’ kan worden aangegeven wat in- en output-parameters zijn. Daarnaast is er een keyword ‘inout’, dat aangeeft dat een parameter zowel in- als output is. Het gebruik ervan is echter triviaal, omdat parameters in EGL standaard al zo worden gegenereerd. package services; service MyService
function myOperation(number1 int in, number2 int in, result int out)
result = number1 + number2;
end
end
Listing 4: Een eenvoudige webservice Om de webservice te kunnen publiceren moeten we nu nog een zogenaamd WSDL-file maken. Deze bevat de locatie en het interface, waardoor derden de webservice kunnen aanroepen. Dit doen we eenvoudigweg door met rechts op de service te klikken en te kiezen voor Generate WSDL File. Via een wizard, waarin bijvoorbeeld de poort en server locatie voor de webservice kunnen worden ingevuld, maken we het WSDL-file aan. Zie als resultaat de (grafische weergave van) het WSDL-file in figuur 4. In dit geval hebben we als server localhost ingevuld met poort 9080.
Fig. 4: WSDL-file We kunnen deze webservice nu gebruiken vanuit andere software die webservices kan consumeren. Om te laten zien hoe we dit in EGL doen zullen we een webpagina maken die onze service gebruikt. Tevens wordt dan duidelijk hoe we een webpagina met bijbehorende eventhandler (JSFHandler) maken. Als eerste klikken we met rechts op het WSDL-file en kiezen Create EGL Client Interface. Dit zorgt ervoor dat er een EGL-library wordt aangemaakt die de toegang tot de webservice verzorgt. Met behulp van een wizard (New -> Web page) maken we een nieuwe JSP aan, callWS.jsp. Deze opent leeg in een nieuwe editor. Vanuit de palette, een view met componenten die we op de pagina kunnen toevoegen, kiezen we EGL->Service en slepen dit op de pagina (zie figuur 5).
Fig. 5: Aanmaken van de servicedefinities op een JSP We kiezen uit een lijstje van beschikbare services onze MyService service. Dit zorgt ervoor dat definities voor zowel het parameter-interface van de webservice als de functie om de webservice aan te roepen worden aangemaakt. Door met rechts op de pagina te klikken kiezen we Edit Page Code. Hierdoor opent de JSFHandler, dus de EGL eventhandler die bij de pagina hoort. We zien dat de code om de webservice aan te roepen al voor ons is ingevoegd doordat wij het service-component op de JSP hebben geplaatst. De code staat in listing 5. package jsfhandlers; import com.ibm.egl.jsf.*; import services.MyService; handler callWS type JSFHandler {onConstructionFunction = onConstruction, onPrerenderFunction = onPrerender, view = "callWS.jsp", viewRootVar = viewRoot} viewRoot UIViewRoot; number1 int; number2 int; result int; // Function Declarations function onConstruction() end function onPrerender() end function callWebService() myService MyService {@bindService}; myService.myOperation(number1, number2, result); end end
Listing 5: Aanroep van een webservice vanuit een JSFHandler
magazine voor software development 55
CORE SYSTEMS Naast de standaard code zoals die in een nieuwe JSFHandler automatisch staat is het volgende toegevoegd: • Drie variabele-definities type int voor de velden number1, number2 en result; • Een functie callWebService. In die laatste functie wordt eerst een locaal object myService aangemaakt. Dit is van het type MyService, dus het client-interface die de webservice kan aanroepen. Daarna wordt de operatie myOperation van de webservice aangeroepen, met de parameters die eerder als variabelen zijn gedefinieerd. Nu willen we in de JSP zorgen dat we de variabelen kunnen vullen en de callWebService functie kunnen aanroepen. Hiervoor gaan we terug naar de JSP en slepen uit de Page Data view (zie figuur 6 links onderin) de betreffende velden op de JSP. Wederom wordt een wizard gestart die ons vraagt wat voor velden (input of output, in ons geval input) dit moeten worden, waarna automatisch JSF componenten voor de velden op de pagina worden geplaatst. Daarnaast wordt meteen (naar keuze) een button op het scherm gezet.
AJAX, RichUI en Web 2.0 Naast de hier genoemde functionaliteit biedt EGL nog ondersteuning voor andere moderne technieken, zoals AJAX en Rich User Interfaces (RichUI) door middel van JavaScript-generatie en DOJO/Silverlightwidgets. Hiermee zijn o.a. mashups mogelijk om informatie uit verschillende bronnen te combineren. Hieraan wordt vaak gerefereerd met de term Web 2.0.
EGL biedt ook ondersteuning voor andere moderne technieken als AJAX en Rich User Interfaces Conclusie Met de Rational Business Developer en EGL heeft IBM een product dat het voor de klassieke ontwikkelaar mogelijk maakt om de nieuwe wereld van Java- en web-applicaties te betreden. Kennis van Java is in beperkte mate wenselijk, namelijk voor foutopsporing of het maken van zogenaamde external types, dus stukjes Java source code om eventuele extra ondersteuning te bieden waar EGL dit (nog) niet kan. In een team zal het vaak voldoende zijn om één iemand op te nemen die deze kennis heeft. Op een zeer eenvoudige manier kan een bestaande database-definitie worden ingelezen en kan de database daarna worden benaderd. Ook webservices kunnen makkelijk worden geproduceerd en geconsumeerd. Daarnaast worden steeds nieuwe technieken toegevoegd aan het product, zoals recentelijk Rich User Interfaces. Hierbij wordt in plaats van Java Server Pages JavaScript gegenereerd om webapplicaties te bouwen met processing in de browser. Voor ontwikkelaars die al bekend zijn met de Java-wereld kan EGL ook perspectieven bieden. Het bouwen van bedrijfskritische applicaties is veel eenvoudiger en er kan sneller resultaat worden geboekt, al zitten er aan een generatie-tool altijd beperkingen waar een Java-ontwikkelaar zich snel aan zal storen. •
Ulf Büchner Fig. 6: Opmaken van een JSP
Ulf werkt bij Synobsys als EGL Consultant. In het verleden werkte Ulf als 4GL application developer (CA Plex) op Windows/iSeries met C++/Java/ RPG. Ulf werkt met EGL sinds 2006.
Op deze manier hebben we onze variabelen als invoervelden op het scherm geplaatst, waarbij ook meteen de binding (de koppeling tussen de JSF-componenten en de EGL-variabelen) is geregeld. Nu willen we er nog voor zorgen dat de functie callWebService wordt aangeroepen als we op de Calculate button klikken. Dit doen we door de callWebService() functie, die in de Page Data view links onderin onder Actions staat, op de button te slepen. Hierdoor ontstaat een binding tussen de button en de functie. Als we nu de JSP opstarten op onze Application Server (die onderdeel is van de ontwikkelomgeving), dan kunnen we getallen invullen en de berekening uitvoeren.
IW
TIP:
Verloopdatum instellen op lijsten in SharePoint 2007? Vaak wil je een maximale datum stellen aan content. In lijsten en kolommen is deze functionaliteit standaard beschikbaar: zie de onderstaande link. Bron TechNet: http://blogs.technet.com/ collaboration/archive/2008/02/22/how-to-see-items-added-toa-sharepoint-list-library-in-the-last-x-days.aspx. Fig. 7: Aanroep van de service vanuit een webpagina
56
MAGAZINE
Jordy van Paassen - VX Company
Advertentie 4DotNet b.v.
Advertentie MI Consultancy BV
DOT NET NUKE
Brandon Haynes
LINQ to SQL and DotNetNuke: A Perfect Fit for Rapid Development Microsoft’s LINQ to SQL is a great choice for module development on the DotNetNuke platform. It allows for rapid development, is well-suited to smaller and focused architectures, and has a lightweight footprint. For many DotNetNuke modules – which often focus on performing one highly-focused task – this is a natural fit. In this article we explore the many advantages of using LINQ to SQL as a data tier within a DotNetNuke module, discuss the alternatives, and demonstrate a sample module using this technology.
LINQ to SQL is a great choice for module development on the DotNetNuke platform. LINQ to SQL for Rapid Development LINQ to SQL is a lightweight pseudo-ORM tool allowing for rapid development in connected, highly-focused architectures. It is a Microsoft SQL Server-specific technology. LINQ to SQL entities are easily updated as the underlying schema changes. Overall, its lightweight implementation offers “just enough” abstraction for such smaller, focused applications. Saliently, because DotNetNuke is predominately operated using Microsoft SQL Server, a more robust platform such as the Entity Framework (operable against a wide variety of data stores) introduces additional unnecessary overhead. Similarly, for most DotNetNuke modules, the database model is generally equivalent to its final conceptual structure. Such a module does not require the additional overhead (present in other ORMs such as the Entity Framework) that allows such separation. Isn’t LINQ to SQL Deprecated? No. It is true that LINQ to SQL was recently transferred to the SQL Data Programmability Team (home of the Entity Framework). There, Program Manager Tim Mallalieu has clearly communicated that LINQ to SQL is alive, well, and will be supported into .NET 4.0 as it is merged into the Entity Framework (Schwartz, 2008). The bottom line remains that it is both safe and prudent to move forward with LINQ to SQL development.
LINQ to SQL is alive, well, and will be supported into .NET 4.0 Why not the DAL? It is worth discussing why LINQ to SQL might be used in favor of the DotNetNuke-recommended data-layer solution, the Data Access Layer (DAL) and subsequently released DAL+. Both are designed to target any backing store (through the use of an abstract provider pattern). Access is generally achieved through the retrieval of IDataReader-implementing objects from the data tier. However, in practice, DotNetNuke is virtually always deployed using Microsoft SQL Server (Leupold, 2008). Given this reality, the DAL abstract provider pattern
58
MAGAZINE
adds additional complexity with little marginal utility. LINQ to SQL avoids many of these drawbacks given the context of a focused, nonenterprise module. Object exchange across tiers involves stronglytyped entity objects (instead of weakly-typed IDataReaderimplementing property bags). Hydration of distinct business entities from the already strongly-typed LINQ to SQL objects thereby becomes optional (and in many applications unnecessary). Finally, LINQ to SQL provides a footprint that is similar to or less than (when it is allowed to generate dynamic SQL) its DAL counterpart. A Drawback: Cross-Tier and Entity Context Lifetime There arguably exist some design disadvantages associated with the use of LINQ to SQL entity objects as an unmediated data layer. While such architectural caveats are generally beyond the scope of this article, it is worth mentioning that Microsoft LINQ Program Manager Dinesh Kulkarni (2007) noted that there exists no out-of-the-box multitier story within the LINQ to SQL framework. This, inter alia, makes change-tracking across tiers within LINQ to SQL particularly difficult. This difficulty, however, is offset to some length by the rapid, focused design constraints discussed above. In a stateless, web-oriented environment, this issue is of particular relevance with data context lifetime. LINQ to SQL requires all operations to be performed against a DataContext object, which is responsible for differential tracking. This context must, then, exist throughout a “unit of work” – which, for module development purposes is (in general) equivalent to the lifetime of any given ASP.NET request thread. One common solution to this problem involves the implementation of the Unit of Work (UoW) pattern (Fowler, 2002). This pattern is used in other ORM solutions such as NHibernate. A straightforward implementation utilizes the HttpContext.Current.Items collection to store a context over the lifetime of a request. This approach is demonstrated in listing 1, and is used for the DotNetNuke module demonstration that follows. This approach may be improved through the application of an Inversion of Control (IoC) pattern (to avoid the System.Web reference in the data tier), but such an enhancement is beyond the scope of this article (Johnson & Foote, 1988). static class Context { private const string key = "MyDataContext"; public static MyContext Current { get { if (!HttpContext.Current.Items.Contains(key)) HttpContext.Current.Items.Add(key,
DOT NET NUKE new MyContext(Config.GetConnectionString()));
}
}
return (MyContext)HttpContext.Current.Items[key]; }
within a given installation), we must associate this entity to something that contains the needed value. In this case we use a second entity, Module, which contains the desired identifier. A custom association (keyed on ModuleId) joins the two entities and allows parent and child references.
Listing 1: Unit of Work pattern for handling a LINQ to SQL data context LINQ to SQL in Action: A Sample Module A Search and Replace DotNetNuke Module We now turn to a concrete example demonstrating the use of LINQ to SQL in a DotNetNuke module. This module is designed to search and replace text across all instances of Text/HTML modules in a given DotNetNuke website. The module is developed using the web application project type (WAP) generally (but not exclusively) preferred by DotNetNuke developers. Note that, for purposes of brevity, we perform business-related functionality directly within the user control event handler and embed the LINQ to SQL entities and context directly within the UI assembly. While this allows for a more straightforward demonstration, any robust solution would certainly further separate these distinct architectural concerns. This module was tested against and deployed within DotNetNuke version 4.91 and 5.0. It assumes that ASP.NET 3.5 is deployed in the server environment (required for use of LINQ to SQL). Note that DotNetNuke now 5.0 supports auto-configuration of ASP.NET 3.5 through its UI. Project Structure and Discussion The module project structure contains three elements relevant to this discussion: a user control (and associated code-behind), a UoW context class (discussed above), and a LINQ to SQL database metamodel. The project also contains a module schema file (SearchAndReplace.dnn) used to install the module within a particular DotNetNuke installation. Figure 1 depicts the project structure as displayed in the Visual Studio solution explorer.
Fig. 2: LINQ to SQL Entity Model Sample Project Markup and Code Our markup, located in SearchAndReplace.ascx, is straightforward and requires little discussion. This markup is displayed in listing 2. <%@ Control Language="C#" Inherits="SearchAndReplace.Presentation.View" %> Search and replace text in all Text/HTML modules
Listing 2: Markup within SearchAndReplace.ascx module user control
Fig. 1: Project structure in Visual Studio Solution Explorer LINQ to SQL Model For purposes of simplicity, our model uses tables and views that exist across all DotNetNuke installations. This alleviates the need to create additional database objects during module installation. Note, however, that these techniques may be utilized against any custom database schema. Consult the DotNetNuke module development documentation (Washington, 2007) for more information about the creation and population of custom database objects during module installation. The entity model in this project contains two entities, as illustrated in figure 2. The first, HtmlInstance, contains the raw HTML data we desire to search (and replace). Because DotNetNuke identifies a given website by its unique portal identifier (many such portals may exist
Business Logic and Code-Behind The code-behind for our module, SearchAndReplace.ascx.cs, contains the code (listing 3) used to interface with the LINQ to SQL entity model discussed above. The first statement (lines 1–3) queries the data model for a set of HTML data instances, and is constrained by the current portal identifier (line 3). This is necessary because DotNetNuke supports multiple websites within the same installation, making it essential to ensure that the module only operates on the “current” website (as identified by this.PortalID). Next, the resultant entity enumeration is walked, during which string replacement is performed via a call to string.Replace (lines 5–7). Finally, the updates are committed (line 9). Note the use of the UoW-inspired Data.Context.Current instance throughout the method body. Additionally, despite the fact that an assignment is made to ALL entities (even those where no replacement is actually made), LINQ to SQL tracks and updates only those entities that were actually modified. 1: var htmlModules = Data.Context.Current.HtmlInstances 2: .Where(html => 3: html.Module.PortalID == this.PortalId); 4: 5: foreach (var htmlModule in htmlModules) 6: htmlModule.DesktopHtml = htmlModule.DesktopHtml 7: .Replace(SearchFor.Text, ReplaceWith.Text); 8: 9: Data.Context.Current.SubmitChanges();
Listing 3: Body of the SearchAndReplace_Click event handler in SearchAndReplace.ascx.cs
magazine voor software development 59
DOT NET NUKE Demonstrating the Sample Module The compiled module files may be zipped, uploaded to an active DotNetNuke installation, and added to a new page within DotNetNuke. The module will render in a manner similar to that depicted in figure 3. For demonstrative purposes, we will replace all instances of the word “DotNetNuke” with the replacement “DotNetNuke with LINQ to SQL.” In a freshly-installed DotNetNuke 5.0 site, this will result in changes to three module instances. Figures 4 and 5, respectively, display one such section both before and after the changes.
Fig. 3: Display of Sample LINQ to SQL Module Important: This module is intended as a LINQ to SQL sample for demonstrative purposes only. Because the module has the potential to affect markup across an entire DotNetNuke website, please ensure that only administrative users haveaccess to its functionality! Figure 6 illustrates the recommended permissions.
Fig. 4: Before Replacement
Fig. 5: After Replacement
A Note about Database Owners and Object-Qualifiers DotNetNuke allows an installation to be configured with a custom database owner and object qualifier (the latter being applied as a prefix on all database objects within a particular installation). This allows for myriad hosting configurations, including shared servers and multiple, isolated DotNetNuke installations within a database (each with a unique object qualifier). Additionally, the use of nondefault values for these attributes yields some protection against automated attacks (Connolly, 2006). An installation using non-default database owner and/or object qualifier introduces some difficulties when using LINQ to SQL. For those modules that must accommodate such scenarios – including all modules developed for commercial use – an alternative now exists in the form of a model adapter (Haynes, 2008). This adapter analyzes a LINQ to SQL meta-model at runtime and dynami-
cally produces an adapted model compatible with any installation (regardless of database owner and object qualifier specification). It is available free of charge and under a liberal BSD license at http:// codeplex.com/DNNLinqToSqlAdapter. The Bottom Line LINQ to SQL is not the panacea for the data tier of all DotNetNuke modules; no approach can be universally assigned such an accolade. Each project must carefully balance development complexity, level of abstraction, database targeting flexibility, performance, and many other factors. For modules requiring rapid development, performing a straightforward and focused task, and not requiring database agnosticism, LINQ to SQL remains an excellent choice. Strongly consider it for your next DotNetNuke module project. References • Connolly, C. (2006). Securing DotNetNuke: Hardening DotNetNuke Installations. Retrieved Dec 2008, from http://dotnetnuke.com/LinkClick.aspx?fileticket=qkVjRRDHNwU%3D • Fowler, M. (2002). Patterns of Enterprise Application Architecture. Boston, MA: Addison-Wesley Longman Publishing Co. • Haynes, B. (2008). DotNetNuke LINQ to SQL Model Adapter. Retrieved Dec 2008, Web site: http://codeplex.com/DNNLinqToSqlAdapter • Johnson, R., & Foote, B. (1988). Designing Reusable Classes. Journal of Object-Oriented Programming. 1, 22-35. • Kulkarni, D. (2007). LINQ to SQL: What is NOT in RTM (V1). Dinesh's Cyberstation. Retrieved Dec, 2008 from http://blogs. msdn.com/dinesh.kulkarni/archive/2007/10/15/linq-to-sql-what-isnot-in-rtm-v1.aspx • Leupold, S. (2008). Re: MySQL 5x and DotNetNuke 4.7x. Retrieved Dec 2008, from http://dotnetnuke.com/Default.aspx?ThreadId =223520&Scope=Posts&TabId=795 • Schwartz, J. (2008). Microsoft Says LINQ to SQL Not Dead. Redmond Developer News: Data Driver. Retrieved Dec 2008, from http://reddevnews.com/blogs/weblog.aspx?blog=3036 • Washington, M. (2007). DotNetNuke 4.0 Module Developers Guide (Part 1). Retrieved Dec 2008, from http://dotnetnuke.com/LinkClick.aspx?fileticket=s%2bGtSX0BJTM%3d •
Brandon Haynes Brandon Haynes (brandonhaynes.org) is chief executive officer at Everysport.net Inc., which delivers enterprise resource planning, webpresence, e-commerce, and integration-related functionality to recreational facilities. He is a member of the DotNetNuke Security Team. A graduate of the University of Illinois at Urbana-Champaign consistently ranked among the top-five computer-science programs worldwide - Brandon has a long history of intellectual curiosity and accomplishment. With more than twenty years of experience in software development, Brandon’s professional interests are currently focused on the nexus between intellectual property law, technology, and business. He is currently pursuing a graduate degree at Harvard University.
Fig. 6: Recommended Permissions – Deny on All Users
60
MAGAZINE
Advertentie Sira Holding BV
.NET C#
Gael Fraiteur
How Aspects Make Multithreaded WPF Easier It is sometimes claimed that 11 programmers out of 10 cannot implement multithreading properly. Although this statement is of course deliberately exaggerated, nobody would pretend it is not based on some truth: multithreading is difficult, especially when one has to work directly with the primitives provided by the .NET Framework. Fortunately, aspect-oriented programming comes to the rescue. This article demonstrates that multithreaded programming can be as easy as applying custom attributes. The recent popularity of multicore processors has turned the term multithreading into a buzz word. Still, it is hardly a new concept. Any non-trivial graphical application has to perform long operations such as reading and saving large files from disk, accessing the network, and carrying out expensive computations. However, implementing long operations without taking care of multithreading would utterly jeopardize user satisfaction. Indeed, the graphical subsystem of Windows (on the top of which both .NET WinForms and WPF are built) is intrinsically single-threaded. It is based on a message queue where individual actions (processing a button click or rendering the dialog box) are executed one after the other. Therefore, when a button event handler executes, the progress bar cannot be rendered, even if its value has been updated. Neither can the user interface react properly to a click of the Cancel button. Consequently, a golden rule for graphical programming is to never do anything long in the GUI thread. A few dozen milliseconds is the maximum we can afford to block the message queue, if we want users to be satisfied. And we do want this satisfaction. The commonly used solution is to execute long operations in a worker thread. In the .NET Framework, it is generally considered best practice not to create a new thread for every operation, but instead, to queue a work item into the thread pool. This operation used to be difficult, but C# 2.0 and anonymous methods have fortunately made it easier. The following piece of code handles clicks by using the Save button. It queues the I/O operation for asynchronous execution on a worker thread. void OnApplyClick(object sender, RoutedEventArgs e) {
ThreadPool.QueueUserWorkItem(
}
delegate { this.contact.Save(); });
Now, what if we want to display a message after the contact has been saved? Since the graphical subsystem is single-thread, we cannot invoke the MessageBox.Show method from the worker thread. Therefore, we have to dispatch it to the GUI thread. With WPF, we have to use a dispatcher object, as demonstrated in the following code sample:
62
MAGAZINE
void OnApplyClick(object sender, RoutedEventArgs e) { ThreadPool.QueueUserWorkItem( delegate { this.contact.Save(); this.Dispatcher.BeginInvoke( DispatcherPriority.Normal, new Action( delegate { MessageBox.Show( Window.GetWindow(this), "Saved!"); } )); } }
);
As can be seen, multithreading quickly makes the code unreadable and error-prone. Fortunately, however, there is a superior solution. What if we have the possibility to mark the affinity of methods directly to the worker thread, or GUI thread, and eliminate the plumbing code? It seems unrealistic, doesn’t it? But with PostSharp and aspect-oriented programming, it is not. So let’s dream on. What we want are two custom attributes: • the OnWorkerThreadAttribute, when the method should be executed asynchronously on a worker thread; • the OnGuiThreadAttribute, when the method should be executed on the GUI thread. The above piece of code would look like this: [OnWorkerThread] void OnApplyClick(object sender, RoutedEventArgs e) { this.contact.Save(); this.ShowMessage("Contact Saved!"); } [OnGuiThread] void ShowMessage(string message) { MessageBox.Show(Window.GetWindow(this), message); }
Implementing the OnWorkerThread and OnGuiThread Attributes If you are not already familiar with PostSharp, you may find it strange that custom attributes can actually change the behavior of methods.
.NET C# Indeed, these new custom attributes will have the effect of modifying the methods to which they are applied. But PostSharp is not an ordinary library: it is a tool that inserts itself in the build process and enhances the assemblies after the compiler did its job (figure 1).
namespace ContactManager.Aspects { [Serializable] public class OnGuiThreadAttribute : OnMethodInvocationAspect { public DispatcherPriority Priority { get; set; } public override void OnInvocation( MethodInvocationEventArgs eventArgs) { DispatcherObject dispatcherObject = (DispatcherObject) eventArgs.Delegate.Target;
Fig. 1: PostSharp inserts itself into the build process and enhances the assemblies after the compiler completes its job. So the first thing to do is to download PostSharp from http://www.post sharp.org/download and install it. Next, add Post Sharp.Public.dll and PostSharp.Laos.dll to your project references. We are now ready to develop our two aspects. Both custom attributes will be derived from the class PostSharp. Laos.OnMethodInvocationAspect. They will intercept calls to the method to which they are applied. Our first custom attribute, OnWorkerThreadAttribute, is trivial: using System;
using System.Threading; using PostSharp.Laos;
namespace ContactManager.Aspects {
[Serializable]
public class WorkerThreadAttribute: {
OnMethodInvocationAspect
public override void OnInvocation( {
}
}
}
MethodInvocationEventArgs eventArgs) ThreadPool.QueueUserWorkItem(
delegate { eventArgs.Proceed(); });
In this attribute, we implemented the method OnInvocation instead of the intercepted method. The statement eventArgs.Proceed() then proceeds with the invocation of the intercepted method. As can be seen, the implementation of this aspect simply queues the execution of the intercepted method into the thread pool.
Using these two simple aspects makes your code easier to read and less error-prone The implementation of the OnGuiThreadAttribute is a little more complex, because we first need to check if we are already on the GUI thread. If we are not, we need to invoke the intercepted method through Dispatcher.Invoke. using System;
using System.Windows.Threading; using PostSharp.Laos;
}
}
}
if (dispatcherObject.CheckAccess()) { // We are already in the GUI thread. Proceed. eventArgs.Proceed(); } else { // Invoke the target method synchronously. dispatcherObject.Dispatcher.Invoke( this.Priority, new Action(() => eventArgs.Proceed())); }
Note the use of the eventArgs.Delegate property. The eventArgs parameter contains everything we need to know about the intercepted method. Here, we are interested in the target instance of the method, because we need to cast it and retrieve its dispatcher. These two very simple custom attributes can have a significant impact on the way you think about multithreading. You can now forget about the thread pool and the WPF message dispatcher. All you have to think about is where the method should be executed: can it run asynchronously on a worked thread, or does it require a GUI thread? Using these two simple aspects makes your code easier to read and less error-prone. Synchronizing Access to Objects Putting operations on threads is only one side of the equation, and arguably the easiest one. What is much more difficult, is to avoid conflicts when accessing shared resources from different threads. Because of this, we need to address the following issues: • How do I ensure that an object is in a consistent state when a thread reads it? How can I be sure that another thread is not modifying it at that particular moment? • How do I avoid two threads concurrently modifying the same object and breaking its consistency? • How do I prevent deadlocks? In object-oriented programming, it often occurs that a significant part of the object model is a shared resource. This is typically the case with model objects in a Model-View-Controller. If the controller is allowed to modify the model from different threads, proper thread synchronization is necessary. The Design Pattern The first and most important thing to do when coping with thread synchronization is to identify good design patterns – there is no alternative to good design. The design pattern I chose here is based on reader-writer locks (see the class System.Threading.ReaderWriterLockSlim). These locks allow concurrent reader threads, but the writer must have exclusive access. That is, a writer must wait for other readers, or other writers, to finish before starting, and will prevent them
magazine voor software development 63
.NET C# public string FullName {
}
Fig. 2: As a part of the system design, it is essential to identify which parts of the object model should share multithreading consistency. (Note: Circles represent instances, rectangles represents groups of instances sharing the same lock) from starting until the writer finishes. Using these locks results in minimal thread contention (i.e. threads wait only minimally for each other) and deadlocks. Alas, they also result in an extensive amount of plumbing code. Our design pattern eventually associates a lock with each object instance. However, if instance-level consistency is not enough, many instances can share the same lock. This is typically the case when one object is aggregated into another one. For instance, one may want an order and its order lines to always be consistent. In that case, all instances forming together in the same order would share the same lock (see figure 2). As part of the design pattern, we decided that all synchronized objects should implement the IReaderWriterSynchronized interface: public interface IReaderWriterSynchronized { }
ReaderWriterLockSlim Lock { get; }
This interface will be useful when implementing the aspects. Additionally, since implementing IReaderWriterSynchronized is still writing plumbing code, we would prefer a custom attribute to do it for us. Let’s call it ReaderWriterSynchronizedAttribute. We further define custom attributes that, when applied to methods or property accessors, determine which kind of access to the object is required: ReaderAttribute, WriterAttribute or ObserverAttribute. Any method that modifies the object, should be annotated with the [Writer] custom attribute. Methods that read more than one field of the object should also be annotated with the [Reader] custom attribute (it is useless to synchronize methods or property getters performing a single read access, because the operation is always consistent). Let’s set aside the observer lock for the moment. The next piece of code illustrates a synchronized class: all its public members are guaranteed to perform consistently in a multithreaded environment. [ReaderWriterSynchronized] public class Person {
public string FirstName { get; [Writer] set; } public string LastName { get; [Writer] set; }
64
MAGAZINE
}
[Reader]
get { return this.FirstName + " " + this.LastName; }
Observer Locks In an MVC design, the view is bound to model objects. Model objects expose events that are raised when they are updated (typically the PropertyChanged event of the INotifyPropertyChanged interface). The view (for instance, a data-bound WPF control) subscribes to this event. In certain cases, it is crucial that the object does not get modified between the time the event is fired and the time it is processed by the view. One example is with observable collections in WPF (INotifyCollectionChanged interface). Since the NotifyCollectionChangedEventArgs object contains item indices, it is essential that these indices still refer to the same items when the event is processed. So at first sight, it seems that we need to invoke events inside the writer lock, doesn’t it? Wrong; this would cause a deadlock. Indeed, remember that the view is bound to the GUI thread. Therefore, the PropertyChanged event handler is dispatched to the GUI thread: [GuiThread]
void person_PropertyChanged( { }
object sender, PropertyChangedEventArgs e) this.label.Text = ((Person)e).FullName;
When you bind a control to a domain object using WPF Data Binding, the thread dispatching is completed transparently. When evaluating the FullName property, the GUI thread will require a read lock on the model object. However, the object is already locked by the writer! The GUI thread would therefore be required to wait for the worker thread to release the writer lock, but the worker thread has to wait until the GUI thread finishes the processing of the PropertyChanged event: we clearly would have a deadlock. Therefore, we need a locking level that would prevent any other writer, but would allow concurrent readers. This needed lock is called an upgradable reader lock: a reader lock that can be upgraded to a writer lock, then downgraded back to a reader lock. Upgradable readers allow concurrent readers, but forbid concurrent writers or upgradable readers. This is exactly what we need. Instead of acquiring a writer lock, WriterAttribute will always acquire an upgradeable read lock and upgrade it to a write lock. As a result, it will be possible to downgrade it into an ObserverAttribute. The following listing demonstrates how the Person class can be made observable, while ensuring multi-thread safety and avoiding deadlocks: [ReaderWriterSynchronized]
public class Person : INotifyPropertyChanged {
private string firstName; private string lastName;
public string FirstName { get { return this.firstName; } [Writer] set { this.firstName = value; this.OnPropertyChanged("FirstName"); }
.NET C# }
{
public string LastName { get { return this.lastName; }
}
[Observer] protected virtual void OnPropertyChanged( string propertyName) { if (this.PropertyChanged != null) this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } public event PropertyChangedEventHandler PropertyChanged;
We are now complete with our design pattern. Now, let’s look at the implementation of these custom attributes. Implementing the ReaderWriterSynchronizedAttribute This custom attribute must inject a new interface into the target type and implement this interface. This can be realized easily by deriving the CompositionAspect class. To accomplish this, two methods must be provided: • GetPublicInterface should return the type of the interface to be injected into the type. We simply return typeof(IReaderWriter Synchronized). • CreateImplementationObject should return an object implementing that interface. So we define a class ReaderWriterSynchronizedImpl and return an instance of it. The following code is a complete implementation of this procedure. using System; using System.Threading; using PostSharp.Laos; namespace Starcounter.Threading { [Serializable] public sealed class ReaderWriterSynchronizedAttribute: CompositionAspect { public override object CreateImplementationObject( InstanceBoundLaosEventArgs eventArgs) { return new ReaderWriterSynchronizedImpl(); } public override Type GetPublicInterface( Type containerType)
return typeof(IReaderWriterSynchronized);
private class ReaderWriterSynchronizedImpl : IReaderWriterSynchronized { private readonly ReaderWriterLockSlim @lock;
[Writer] set { this.lastName = value; this.OnPropertyChanged("LastName"); }
public string FullName { [Reader] get { return this.firstName + " " + this.lastName; } }
}
}
public ReaderWriterSynchronizedImpl() { this.@lock = new ReaderWriterLockSlim(); }
}
}
}
public ReaderWriterLockSlim Lock { get { return this.@lock; } }
Implementing ReaderAttribute, WriterAttribute and ObserverAttribute Before implementing an aspect, it’s good to ask oneself: how would we do it without aspects? What would the expanded code look like? To answer these questions, we would first have to determine if we already hold the lock and, if not, acquire it. We would have to enclose the whole method body in a try block and release the lock, if it was acquired, in the finally block. So our methods would look like this: void MyMethod() { bool acquire = !(this.myLock.IsWriteLockHeld || this.myLock.IsReadLockHeld || this.myLock.IsUpgradableReadLockHeld); if ( acquire ) this.myLock.EnterReadLock(); try { // Original method body. } finally { if ( acquire ) this.myLock.ExitReadLock(); } }
PostSharp provides an appropriate kind of aspect for this transformation: OnMethodBoundaryAspect. It wraps the original method body inside a try…catch…finally block and gives us the opportunity to execute code before the method, upon successful execution, upon exception, and in the finally block. This is exactly what we need. The following is the code for the Reader attribute. Note that the Writer and Observer attributes are similar. [Serializable]
[MulticastAttributeUsage(MulticastTargets.Method,
TargetMemberAttributes=MulticastAttributes.Instance)]
public class ReadLockAttribute : OnMethodBoundaryAspect {
public override void OnEntry( {
MethodExecutionEventArgs eventArgs) ReaderWriterLockSlim @lock =
((IReaderWriterSynchronized)eventArgs.Instance).Lock; if ([email protected] &&
[email protected] &&
{
[email protected])
magazine voor software development 65
.NET C# eventArgs.MethodExecutionTag = true; }
@lock.EnterReadLock();
else { }
}
eventArgs.MethodExecutionTag = false;
public override void OnExit( {
MethodExecutionEventArgs eventArgs) if ((bool) eventArgs.MethodExecutionTag) {
}
}
}
((IReaderWriterSynchronized) eventArgs.Instance). Lock.ExitReadLock();
Conclusions Multithreaded programming can be simplified by adequately raising the level of abstraction. But why should every programmer care about synchronization primitives? In an ideal world, it should be enough if he or she annotates methods with a custom attribute determining the thread affinity or the locking level required by the method. This is what we have demonstrated in this article by using six aspects: OnWorkerThread and OnGuiThread for thread affinity, and ReaderWriterSynchronized, Reader, Writer and Observer for the locking level. However, multithreading is just one possible field of application of aspect-oriented programming. Caching, transaction management, exception handling, performance monitoring, and data validation are other concerns where aspect-oriented programming can advantageously be applied. By providing a new way to encapsulate complexity, aspect-oriented programming results in shorter, simpler and more readable code, therefore being less expensive to write and maintain. •
Gael Fraiteur
In the preceding code, we implemented two handlers: OnEntry and OnExit. In order to obtain access to the ReaderWriterLockSlim object, we need to cast the target instance (available on the eventArgs. Instance property) to the IReaderWriterSynchronized interface and retrieve the Lock property. The OnEntry method needs to store the information somewhere, whether the lock was acquired by us or not. Indeed, this information will be required by the OnExit method. For this purpose, we can use the eventArgs.MethodExecutionTag property. Whatever a handler stores in this property will be available to the other handlers. Note the presence of the MulticastAttributeUsage custom attribute on the top of our class. It means that the aspect is to be used on instance methods only, so it is not to be used on constructors or on static methods.
Gael Fraiteur is the founder and project leader of PostSharp, the most popular aspect weaver for .NET. He is an occasional speaker at international conferences and user groups. Gael is Microsoft Certified Solution Developer for Microsoft.NET, and has an advanced knowledge of Oracle Server, Microsoft SQL Server and TIBCO EAI. He has experience in telecom and ISV industries. Gael speaks French, English, Czech and Flemish. He lives in Czech Republic.
Advertentie Twice IT
DELPHI
Pawel Glowacki
Getting Started with
Delphi DataSnap 2009 Introduction One of the main new features of RAD Studio 2009 is the DataSnap 2009 framework for building multitier database applications. In fact DataSnap 2009 is more than that. It is a component-based architecture for creating arbitrary applications that communicate over the network. It has never been easier to quickly create server and client applications in Delphi and C++Builder! The new DataSnap 2009 architecture extends the DBX4 database driver framework introduced in Delphi 2007 and C++Builder 2007. The DBX4 framework abstracted away the concept of a database driver and introduced the notion of extensible command types. DataSnap 2009 takes advantage of the DBX4 extensibility and adds to the portfolio of existing databases drivers the new ”DataSnap” driver that from the perspective of a client application - looks very much like a database, but in reality provides connectivity to a DataSnap server application. Similarly the new ”DSServerMethod” command type for invoking methods on server objects has been added to ”TDBX CommandTypes” class.
It has never been easier to quickly create server and client applications in Delphi and C++Builder! The DataSnap 2009 is part of the Visual Component Library (VCL) that is shared between Delphi and C++Builder. In this article I’m going to focus on Delphi 2009, however almost all of the functionality described in here is also applicable to C++Builder 2009. DataSnap 2009 ”Hello World” In this article I’m going to walk you through the steps for building a simple ”Hello World” DataSnap 2009 system consisting of a server and a client application. The server application will provide the ”Echo” function that accepts a string and returns a string that echoes the original value. The client application will provide means for entering an arbitrary string, call the ”Echo” function with the string provided and display the string returned from the server. Too simple? Yeah... but a good starting point for more complex things! Are you ready? Is your Delphi 2009 already installed and started? You do not have Delphi 2009 installed? Shame on you ;-) Grab the trial from http://www.codegear.com/products/delphi/win32 and give the
best version of Delphi ever a try! Steps for Building DataSnap 2009 Server Application Create a new Delphi (or C++Builder) ”VCL Forms Application”, click on ”Save All” from the ”Files” menu. Save the main form’s unit as ”uFormServer” and the project as ”DelphiDataSnapEchoServer”. In the Object Inspector change the ”Name” property of the form to ”FormServer” and ”Caption” property to ”Delphi 2009 DataSnap Echo Server”. Now we are going to transform this standard Delphi application into a DataSnap server. This is done with components from ”DataSnap Server” tab. It contains three different components and we need them all. Double-click on ”TDSServer”, ”TDSTCPServerTransport” and ”TDSServerClass” components in the Tool Palette to add them to the server form.
Fig. 1: DataSnap Server Application Main Form The ”DSServer1” component is the logical heart of the DataSnap server application. It contains ”Start” and ”Stop” methods for starting and stopping the server, and also a very handy ”AutoStart” property. By default it is set to ”True”, so the server starts automatically at the application startup. You only need one ”TDSServer” component per server application. The ”DSTCPServerTransport1” component contains the ”TIdTCPServer” Indy component that implements a multithreaded TCP server listening for incoming client connections on multiple threads. This component does not have any events, but it contains a ”Server” property that needs to be set to ”DSServer1”. It also has a ”Port” property that indicates the TCP port to be used. By default it is set to port 211. The ”DSServerClass1” component represents... yeah, you are right... a server class ☺. It also has the ”Server” property that needs to be set to ”DSServer1”. In this way all three components are linked together. The ”TDSServerClass” component contains ”OnGetClass” event that must be implemented by the programmer. If you fail to implement this event, the application will, immediately after a start, raise a TDBXError event with the message ”OnGetClass event not set or it did not provide a class reference”. The ”OnGetClass” event has the ”PersistentClass” argument that is passed by reference. In the event handler code the programmer needs to assign to ”PersistentClass” a class reference to a server class.
magazine voor software development 67
DELPHI
This is probably the one single most important concept to understand about the DataSnap2009 architecture. We are assigning to ”PersistentClass” a class reference and not an object reference.
The DataSnap server will automatically create and destroy instances of server classes The DataSnap server will automatically create and destroy instances of server classes. The instancing of a server class is controlled by the ”TDSServerClass.LifeCycle” property that can have one of the three possible values: ”Server”, ”Session” and ”Invocation”. The lifecycle set to ”Server” means that the DataSnapserver will create one instance of a server class that will be used by all clients connected to the server application. This represents a ”singleton” pattern. Be careful when using ”Server” lifecycle as your server class implementation needs to be thread-safe; it is possible that this singleton object will be accessed simultaneously from multiple threads. The default ”LifeCycle” value is ”Session”. This means that the DataSnap server will create one instance of a server class for every connected client. This is similar to the concept of a ”stateful” session bean in JEE. The third possible value for ”LifeCycle” property is ”Invocation”. A server class instance will be created and destroyed for every method call arriving from a client and the state of a server class will not be preserved between method calls. In order to implement ”OnGetClass” event we need to add to our DataSnap server project a server class. Select ”File | New | Other” from ”File” menu and double-click ”Server Module” icon from the ”Delphi Files” category.
Fig. 3: DSServerModule1 does not need to be auto-created It is also safe to comment out the global ”DSServerModule1: TDSServerModule” variable from the server module unit as it is never used. Now we are going to implement a simple ”Echo” function that will accept a string and return a string. I’m going for an enterprise strength echo functionality so my implementation will echo the argument not once, but twice;-). The resulting server module implementation looks like this: unit uServerModule; interface uses
SysUtils, Classes, DSServer;
type
TDSServerModule1 = class(TDSServerModule) private
{ Private declarations }
public
function Echo(s: string): string;
end; //var //
DSServerModule1: TDSServerModule1;
implementation {$R *.dfm} { TDSServerModule1 } function TDSServerModule1.Echo(s: string): string; begin
Result := ‘Delphi DataSnap 2009 is echoing ‘ +
end;
Fig. 2: New DataSnap 2009 ”Server Module” Item This will add a new server module to the project. Save the new unit as ”uServerModule”. At this stage I’m going to do one optional step that aims at highlighting the fact that it is the server that manages the lifecycle of server objects. The server module is ultimately derived from a data module and the Delphi designer automatically added it to a list of auto-created forms in our server application. This is unnecessary, so you can go to ”Project | Options” dialog and in the ”Forms” section remove the ”DSServerModule1” from the list of auto-created forms.
68
MAGAZINE
s + ‘ ... ‘ + s;
end.
Listing 1: DataSnap Echo server module implementation The last step is to implement the ”DSServerClass1.OnGetClass” event. In the server main form select the DSServerClass1 component and in the Object Inspector double-click on the ”OnGetClass” event to generate an empty handler. Add ”uServerModule” to the ”uses” clause of the main form and implement the event.
DELPHI implementation uses uServerModule; {$R *.dfm} procedure TFormServer.DSServerClass1GetClass( DSServerClass: TDSServerClass;
var PersistentClass: TPersistentClass); begin
PersistentClass := TDSServerModule1;
end; end.
Listing 2: DSServerClass1.OnGetClass event code Our DataSnap Echo server application is now complete. Client and Server Applications need a piece of shared knowledge to communicate If you want to have two arbitrary applications to communicate, there must be some form of a contract in place that describes what functionality a client can access from a server.
If you want to have two arbitrary applications to communicate, there must be some form of a contract in place that describes what functionality a client can access from a server
have access to a running instance of the server application. Doubleclick on the ”DelphiDataSnapEchoServer.exe” in the Project Manager to make it active and select ”Run | Run Without Debugging” from the main menu to run the server application. We need to have it running while developing the client. You can safely minimize the server application window. Double-click on the ”DelphiDataSnapEchoClient.exe” in the Project Manager to switch back to client application. As discussed earlier DataSnap 2009 extends the DBX4 database driver architecture by implementing a special ”DataSnap” driver that from the perspective of a client looks like a connection to a database, but in fact it provides connectivity to DataSnap servers. That is the reason that we start client development from a ”TSQLConnection” component. Make sure that the client form is opened in the IDE, go to Tool Palette and double-click on the ”TSQLConnection” component from the ”dbExpress” category to add it to the form. Go to Object Inspector, drop down the ”Driver” property and select ”Datasnap” driver. Note that the ”Driver” property can be now expanded and it contains DataSnap specific sub-properties. Make sure that the ”HostName” is set to ”localhost” (or the DSN name or the IP address of the machine where the server is running) and the ”Port” number is set to 211. The ”Port” property has to match the value of the ”DSTCPServerTransport1.Port” property in the server application. Additionally change the ”SQLConnection1.LoginPrompt” property to ”False” to prevent username and password dialog to popup every time we connect to the server. Right-click on the ”SQLConnection1” component on the form and select ”Generate DataSnap Client Classes” from the context-menu.
Different distributed computing technologies have different ways of solving this problem. In CORBA the contract is described in the form of an Interface Definition Language (IDL) document, in DCOM there is a Type Library. In Web Services we have a Web Service Description Language (WSDL) that is used by client applications to generate proxies. DataSnap 2009 does not have any static form of a service description. In order to create a DataSnap client you need to have access to a running server application at design time. This is similar to Web Services where the WSDL document can be typically obtained on-the-fly from a running Web Service application. Steps for Building DataSnap 2009 Client Application The DataSnap Server application is already opened in the Delphi IDE and it is the most convenient to add a client application to a project group so we could switch between two projects inside the IDE. Right-click on the ”ProjectGroup1” in the Project Manager, select ”Add New Project” from the context menu and go for the new Delphi ”VCL Forms Application”. Save all from the ”File” menu. Save the main form unit as ”uFormClient”, the project as ”DelphiDataSnapEchoClient” and the project group as ”DelphiDataSnapEchoGrp”. Click somewhere on the form to make sure it is selected in the Object Inspector. Set the ”Name” property to ”FormClient” and the ”Caption” property to ”Delphi 2009 DataSnap Echo Client”. You can have only one project active at any given point of time in RAD Studio IDE. The Project Manager indicates the active project by displaying the project name with bold font. You can also see the name of the active project in the title of the RAD Studio window itself. Double-click on the project name in the Project Manager to make the project active. During the development of a DataSnap client application we need to
Fig. 4: Generate DataSnap client classes from SQLConnection1 context menu This will add a new unit to the client project. Save it as ”uClientClasses”. The generated unit will contain the ”TDSServerModule1Client” class that can be used to communicate with the server as if it were a local object. DataSnap 2009 uses RTTI (Delphi ”reflection” mechanism) for discovering all public and published methods on server clas-
magazine voor software development 69
DELPHI
ses and generates proxy classes with matching method signatures. In order to call any method from our server it is necessary to create an instance of ”TDSServerModule1Client” and call its ”Echo” method that takes and returns a string. Switch to ”FormClient” and add to it ”TButton” and ”TEdit” components from the Tool Palette. Add ”uClientClasses” to the ”uses” clause of the ”FormClient” (for example using ”File | Use Unit” menu). Double click on the ”Button1” component and add the following code to the generated ”OnClick” event that creates an instance of a proxy class, calls its ”Echo” method passing the contents of the edit box and displays the result in a dialog box. uses uClientClasses; {$R *.dfm} procedure TFormClient.Button1Click(Sender: TObject); var proxy: TDSServerModule1Client; begin
The objective of this article was to provide step-by-step instructions for building the simplest possible ”Hello World” client and server DataSnap 2009 applications to get you up and running with this powerful new technology. But DataSnap 2009 is much more than that. In addition to returning simple types like ”string” it is possible to pass more complex types like array or ”TDataSet”. The new ”TSqlServerMethod” component can be used to call server methods without generating client classes, and new ”TDSProviderConnection” component make it easy to build database applications that can easily apply database updates from client back to server. With the ”ServerConnection” sub-property of the DataSnap ”TSQLConnection.Driver” it is even possible to access the server-side database connection directly from the client. This functionality is key for building Delphi Prism managed .NET DataSnap clients that communicate with native Delphi 2009 or C++Builder 2009 DataSnap servers. The electronic version of the application described in this article can be downloaded from the SDN-site. •
proxy := TDSServerModule1Client.Create( SQLConnection1.DBXConnection);
Pawel Glowacki
ShowMessage(proxy.Echo(Edit1.Text));
Paweł Głowacki is Senior Software Consultant, Presales Engineer, Technical Evangelist and blogger at Embarcadero Technologies - the makers of Delphi and C++Builder. You can reach Paweł at [email protected] or by visiting his blog at http://blogs. codegear.com/pawelglowacki
try
finally
proxy.Free;
end;
end; end.
Listing 3: DataSnap ”Echo” client implementation Note that the ”TDSServerModule1Client” constructor requires a ”TDBXConnection” argument that represents the non-visual DBX4 connection object. Luckily the ”TSQLConnection” class exposes its internal ”DBXConnection” as a property that we can use in our code. That’s it! Just run the client, enter something into the edit box and click the button to call the ”Echo” method on the server and display the result.
Delphi
TIP:
FillChar en Fill/ZeroMemory Delphi 2009 ondersteunt Unicode en een Char is niet langer een Char maar een 2-byte WideChar. Toch werkt FillChar nog met Bytes in plaats van met Char (vanwege backwards compatibility). Dus als we een string met iets willen vullen, dan moeten we niet het aantal Chars maar het aantal Bytes opgeven: var Buffer: array[0..255] of Char; begin FillChar(Buffer, Length(Buffer) * SizeOf (Buffer[0]), 0);
In plaats van SizeOf kunnen we ook de nieuwe functie StringElementSize gebruiken:
Fig. 5: DataSnap ”Echo” client in action Summary The DataSnap 2009 architecture for building multitier database applications is one of the most interesting and innovative new features introduced in RAD Studio 2009. It can be used not only for building database application but also for arbitrary client/server systems that communicate over the network using the TCP protocol. RAD Studio 2009 contains Delphi 2009, C++Builder 2009 for building high-performance, native Windows applications and the new Delphi Prism for building .NET applications. DataSnap 2009 technology is available in all of these environments.
70
MAGAZINE
var Buffer: array[0..255] of Char; begin FillChar(Buffer, Length(buffer) * StringElementSize(Buffer), 0);
Overigens kunnen we in dit specifieke voorbeeld ook SizeOf(Buffer) gebruiken in plaats van de vermenigvuldiging van het aantal elementen met de size van de individuele elementen. Omdat FillChar niet langer Chars maar Bytes vult, zou een betere naam FillByte of FillMemory zijn geweest. Toevallig is er nu ook een FillMemory, die intern FillChar aanroept. Net als een ZeroMemory, die FillChar aanroept met #0 als opvulteken.
Advertentie Sybase iAnywhere / ELMO ICT Systems
Advertentie Bridge Incubation Group b.v.