Praktijk | Programmeren met C#
Rudolf Huttary, Arne Schäpers, Pieter-Paul Spiertz
Patroonherkenning Programmeren met C#, deel 7: componenten, exceptions en design patterns Net zo belangrijk als de syntax van C# is al het gedoe eromheen. Hoe ga je netjes om met runtime-errors? Hoe maak je je software beschikbaar als component voor bijvoorbeeld de toolbox? En wat zijn die assembly's nou eigenlijk precies?
I
n zekere zin is deze C#-cursus natuurlijk een prettige vorm van heiligschennis: vergeleken met de traditionele, didactisch verantwoorde methoden zappen we in hoog tempo door de leerstof. Helaas kunnen we je daardoor van allerlei zaken niet meer dan een indruk geven. Toen c't eind 1999 een inleiding tot Java gaf, ontkwamen we daar ook niet aan. Tijd om in dit eenna-laatste deel op diverse hakken en takken te springen en zo een flink aantal losse eindjes aan elkaar te knopen.
Huiswerkbespreking Eerst komen we terug op de oplossing van het huiswerk 142
uit deel 6 [1]. Daarbij was het de bedoeling om een eigen klasse Rubberband te implementeren, die de functionaliteit uit deel 5 [2] inkapselt. Zo kun je die gebruiken voor willekeurige andere Control-objecten. We hielden de taakomschrijving bewust kort, maar je kon er lang over nadenken: - waar kun je de code het beste implementeren, in een eigen klasse of op de plaats waar je andere controls instantieert? - welke mechanismen zijn er nodig om het vereiste gedrag te realiseren? - wat moet de klasse precies weten, en hoe komt die informatie binnen? - welke output geeft de klasse,
en hoe wordt die gecommuniceerd? Op deze vragen bestaat geen eenduidig antwoord: meestal is het een kwestie van persoonlijke stijl. In principe is een implementatie binnen een bestaande klasse het meest realistisch, want in een applicatie zal immers nooit meer dan één rubberband tegelijk gebruikt worden (tenzij er na de dualcore processor ook een dubbelmuis wordt uitgevonden). Puur om een zo algemeen mogelijk voorbeeld te geven, hebben wij in listing 3 de voorkeur gegeven aan een instantieerbare, eigen klasse. Omdat een Rubberband-object via een Control moet wer-
ken, ligt het voor de hand om dit Control-object meteen in de constructor aan te vragen. De functionaliteit zelf werkt via afhandelroutines voor de events MouseDown, MouseMove en MouseUp van het Control-object. Dit object kan de rubberband aan- en uitschakelen door de eigenschap Enabled. Dat aanen uitschakelen kost dankzij delegates niet meer werk dan het toevoegen van eventhandlers aan de control. Is er met de rubberband eenmaal een keuze gemaakt, dan genereert het object een Selected-event, waarvoor we in de .NET-klassenbibliotheek een bijbehorend type hebben opgezocht: het delegatetype InvalidateEventc’t 2006, Nr. 12
Praktijk | Programmeren met C#
Handler en de eventcontextklasse InvalidateEventArgs. Dankzij deze blijft de publieke interface van Rubberband beperkt tot de constructor, die een pointer naar een Control-object opeist, de eigenschap Enabled en het event Selected. Als testcode voor de klasse kun je een simpel form gebruiken, zoals de listing onderaan. Dat form maakt een Rubberband-object aan met een referentie naar zichzelf (dat mag, want ook Form heeft Control als basisklasse), verbindt Selected met een gepaste handler en toont het interactief omschakelen van Enabled. Dat een Form een Control en dus ook een Component is, zien we helaas niet terug in de toolbox; gek genoeg kiest Visual Studio ervoor om forms zelf niet in de toolbox op te nemen.
Er was eens… COM Hiermee heeft de klasse Rubberband zelf echter nog geen entreekaartje voor de toolbox. Dat krijgt hij pas nadat je hem tot component hebt gepromoveerd. Daarmee belanden we in de rijke Windows-geschiedenis, omdat een klasse zich daarvoor moet schikken in het .NET Component Model, dat op zijn beurt af en toe wat houterig aandoet omdat het compatibel probeert te blijven met COM uit 1993 (het Component Object Model). Het enige dat je er voor hoeft te doen is je klasse direct of indirect af te leiden van Component en meer dan de impliciete standaardconstructor heb je ook niet nodig. Maar het is technisch gezien een wereld van verschil. Tot nu toe regelde de compiler onze methodeoproepen, dat heet early binding. Bij componenten kan alle externe code System.Reflection gebruiken om een methode-aanroep tijdens runtime tot stand te brengen. Dat heet late binding. COM was destijds een baanbrekende, maar complexe uitvinding en het model laat nog overal in Windows zijn sporen na. Toen C++ werd uitgevonden, miste het belangrijke features voor het schrijven van componenten, zoals eigen geheugenmanagement voor objecten (reference counting), een binary interface (ABI) en een manier om te achterc’t 2006, Nr. 12
Het testprogramma voor de klasse Rubberband inverteert de kleur van het geselecteerde gebied. halen welke functionaliteit een object heeft. Bovendien was er een soort bibliotheek nodig die vanuit elke programmeertaal aan te roepen was. Daarom ontwikkelde Microsoft COM en begonnen anderen aan CORBA. (Om de tijdlijn even voort te zetten: COM+ breidde COM in 1997 uit met transacties en object pooling. Microsofts antwoord op de netwerkfuncties van CORBA was in 1995 het veel te moeilijke DCOM, dat in het .NET Framework is vervangen door .NET Remoting en SOAP.) Maar de concurrentie heeft ondertussen niet stilgezeten. In Java is (Enterprise) JavaBeans momenteel de vergelijkbare techniek; moderne Unixvarianten zijn Bonobo, KParts en D-BUS [6]. Sommige programmeurs noemen COM een 'rommeltje bovenop C++ virtual function tables', anderen stellen dat OO-talen niet compleet zijn zonder een COM-achtig systeem. COM-componenten
(ook coclasses genoemd) hebben meestal de extensie .exe of .dll. Je kunt ze beschouwen als speciale klassebibliotheken in een voorgedefinieerd binair formaat. Daarmee is COM een taalonafhankelijke voorganger van .NET. Je kon COM-componenten ook al uitwisselen tussen verschillende hosts (bijvoorbeeld de Google Toolbar in IE). Methoden uit deze COMcomponenten roep je aan via interface-pointers. COM-klassen en -interfaces zijn identificeerbaar via hun GUID's (Globally Unique Identifiers) en dát zijn weer de tekenreeksen (eigenlijk 128-bit getallen) waar het hele Windows-register vol mee staat.
Stadsreiniging Terug naar het heden. Een Component in C# berust op de abstracte basisklasse MarshalByRefObject. Deze voorziet je component van de nodige know-how om te communiceren met componenten buiten je eigen applicatie (preciezer gezegd: buiten je AppDomain, dat is een term uit .NET Remoting). Ook implementeert Component de interfaces IContainer en IDispose. Je component mag zelf namelijk ook weer componenten gebruiken en via deze interfaces worden deze gedefi-
1 public partial class Form1 : Form 2 { 3 Rubberband rub; 4 5 public Form1() 6 { 7 InitializeComponent(); 8 rub = new Rubberband(this); 9 rub.Selected += rubber_Selected; 10 } 11 12 private void button1_Click(object sender, EventArgs e) 13 { 14 rub.Enabled = !rub.Enabled; 15 if(rub.Enabled) 16 button1.Text = "Deactiveer Rubberband"; 17 else 18 button1.Text = "Activeer Rubberband"; 19 } 20 21 private void rubber_Selected(object sender, InvalidateEventArgs e) 22 { 23 ControlPaint.FillReversibleRectangle(e.InvalidRect, Color.Red); 24 } 25 }
Listing 1: de testcode voor de klasse Rubberband schakelt de Enabled-eigenschap in en begint met kleuren als het Selectedevent optreedt.
nieerd en na gebruik ook weer opgeruimd. Nu begrijp je ook de code beter die de designer in Visual C# voor je genereert als je een form ontwerpt. Die code (typisch het bestand Xxx.Designer.cs) werkt met een private container-dataveld met de naam components. Dat wordt aan ingevoegde componenten meegegeven bij hun instantiëring als die daarvoor een passende constructor hebben: public Rubber(IContainer container) { container.Add(this); } Typerend voor COM-compatibele objecten is dat de component zelf beslist of hij beheerd wil worden door de component die hem aanroept. Als het Add-commando ontbreekt of er slechts een parameterloze constructor is, dan is er overigens geen man overboord, zolang de component en zijn eigen subcomponenten het Dispose-mechanisme niet nodig hebben. Via Dispose() krijgt een component te horen dat clientcode die hem gebruikt hem niet meer nodig heeft en dat hij alle verkregen resources moet vrijgeven. Daarmee worden ook alle subcomponenten bedoeld die hij gebruikt, zoals handles voor vensters, bestanden, GDIobjecten, subcomponenten, noem maar op. Om al die resources tegelijk aan te kunnen spreken is de components-container erg handig, zoals je ziet in deze door een formsjabloon gegenereerde code: protected override void Dispose (bool disposing) { if (disposing && (components != null)) { components.Dispose(); } base.Dispose(disposing); } De methode components.Dispose roept de Dispose-methoden aan van alle ondergeschikte componenten die zich bij de instantiëring vrijwillig hebben geregistreerd.
Componenten maken Het ombouwen van de klasse Rubberband naar een component 143
Praktijk | Programmeren met C#
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
[ToolboxBitmap(typeof(Rubber))] public class Rubber : Component { Rubberband rubber = new Rubberband(null); public event InvalidateEventHandler Selected; private Control parent; public Control CtrlParent { get { return parent; } set { parent = value; rubber = new Rubberband(parent); rubber.Selected += Selected; } }
}
public bool Enabled { get { return rubber.Enabled; } set { if (parent == null && value == true) throw new Exception("Geen parent gedefinieerd"); else rubber.Enabled = value; } }
De component Rubber verschijnt na compilatie in de toolbox. Je kunt hem via drag&drop op een form plaatsen en initialiseren via het eigenschappenvenster.
Listing 2: de klasse Rubber verpakt een Rubberband-object en maakt diens interface geschikt als component. is eigenlijk vrij eenvoudig. Probeer het gerust alsnog zelf voordat je onze oplossing bekijkt. In de volledige versie van Visual Studio 2005 bestaat er een sjabloon 'Component Class', waarmee je in één keer een nieuwe component kunt toevoegen aan een bestaand project, inclusief het hierboven beschreven codegeraamte. Gebruik je de Express Edition, kies dan het sjabloon 'Windows Form' of 'User Control', en voer vervolgens deze stappen uit: - verander in het codegeraamte de basisklasse in Component - voeg de ontbrekende constructor toe - wis in de methode InitializeComponent() alle regels achter de components-instantiëring. Om puur de toegang tot de toolbox te forceren, hoef je zelfs niks anders te doen dan een nieuwe klasse te maken, zonder constructor, op basis van Component: public class Rubber : Component Onze Rubber-component (listing 2) definieert een dataveld van het type Rubberband en definieert daarvoor accessors. Indien nodig kun je Rubberband.cs importeren met het snelmenucommando 'Project / Add Existing Item' of kun je het via drag&drop vanuit een Verkenner-venster naar de Solution Explorer slepen. Daarna moet 144
je wel de namespace aanpassen. Om het Rubberband-object rubber nu te kunnen instantiëren, is eigenlijk een Control-object in diens constructor nodig. Puur omdat de designer een constructor met die parameter zou negeren, hebben we de eigenschap CtrlParent toegevoegd. Dat zorgt wel voor een extra risico: clientcode kan de Enabled-eigenschap op true zetten zonder eerst een doelobject te kiezen; het resultaat is in dat geval een vage runtime-error. Je kunt dit op twee manieren oplossen. Je component kan Enabled gewoon op false houden zolang parent nog de waarde null heeft en verder niks doen, dan ziet de set-accessor er als volgt uit: if (parent == null) rubber.Enabled = false; else rubber.Enabled = value;
Maar netjes is dit niet: de programmeur aan de clientkant moet de juiste volgorde maar net weten om je component te laten werken! Het is dan ook beter om clientcode via een exception te vertellen dat het misgaat (zie het kader). Hoe dat gaat zie je in listing 2 vanaf regel 22. Een vergelijkbaar probleem kan trouwens ook in de klasse Rubberband optreden, als je diens constructor een nullpointer in de maag splitst. Juist bij componenten zijn runtime-errors tijdens hun initialisatie onplezierig, want dan knalt de designer van Visual Studio eruit en laat hij het hele formdesign niet meer zien (zie afbeelding). Meestal komt dit door iets onschuldigs en kun je het verhelpen door handmatig xxx.Designer.cs te editen. Zoiets gebeurt namelijk doordat de designer de initialisatiecode voor je formeigenschappen in alfabetische volgorde genereert en zo ook Is de Designer er eenmaal uitgeknald, dan laat hij het formdesign niet meer zien. Je kunt hier alleen nog uitkomen door een handmatige aanpassing van het bijbehorende bestand xxx. Designer.cs.
de uitvoervolgorde ervan bepaalt. Had de nieuwe eigenschap Parent geheten in plaats van CtrlParent, dan krijg je deze volgorde en laat de designer het afweten: // // rubber1 // this.rubber1.Enabled = true; this.rubber1.Parent = this; We zijn nog twee stappen verwijderd van ons doel: de rubber-component in de toolbox te krijgen. Eerst plaatsen we de code los van de testapplicatie in een eigen project met het sjabloontype 'Class library' en maken we er een DLL van. Nu kiezen we in het toolbox-snelmenu 'Choose elements / Browse', selecteren we 'Rubber' en dat was het. Om de component nog te voorzien van een eigen toolbox-icon, hebben we een 16×16 pixels grote bitmap aan het project toegevoegd, zijn eigenschap 'Build Action' op de waarde 'embedded resource' gezet en hebben we hem met een klassenattribuut aan de component toegewezen: [ToolboxBitmap(typeof(Rubber))] public class Rubber : Component Hoe de hier gebruikte overloading van de ToolboxBitmapAttribute-constructor werkt, is lastig uit te leggen: de typeof-operator levert een typeobject voor de klasse Rubber en neemt hierbij c’t 2006, Nr. 12
Praktijk | Programmeren met C#
meteen de eraan toegewezen resource rubber.bmp mee vanwege zijn naam. Essentieel is dan wel (en helaas is dit echt slecht gedocumenteerd) dat de 'default namespace', die je vindt bij de projecteigenschappen, overeenkomt met de klassennaam. Het gewenste effect is vervolgens door een fout in de toolbox-implementatie pas te zien als je de component blijvend in de toolbox zet, of als je een instantie van de Rubber-component op een form plaatst.
Verantwoord blunderen Het zou wel lekker sensationeel klinken, maar helaas: een fout in computersoftware veroorzaakt technisch gezien nooit een chaotische toestand. Met een crash in de klassieke zin, waarbij hardware het laat afweten of de processor probeert om willekeurige bytes in het geheugen als instructies uit te voeren, heb je op een stabiel systeem met de .NET-runtime-omgeving sowieso niet meer te maken. Dankzij de CLR heb je de zekerheid dat zelfs als je programma afbreekt met een ongecorrigeerde runtime-error, dit gebeurt op een volledig gedefinieerde en reproduceerbare manier. Stel dat een methode een fatale foutconditie detecteert, dan zorgt deze ervoor dat er een speciaal soort event optreedt: een exception. Deze genereert een zogenaamde foutcontext en verlaat onmiddellijk de huidige methode, tenminste als de exception niet ter plekke wordt opgevangen. Het exception-mechanisme werkt iets anders dan het eventsysteem, maar het idee erachter gaat al terug tot de interrupts van de Univac I-computer uit 1951 [5]. Aan de ene kant verleent een exception een met recht ‘uitzonderlijke’ terugsprong naar de oproepende code, inclusief een gestructureerde foutcontext. Aan de andere kant kan de oproepende code zelf zo’n exception ‘opvangen’ en de foutcontexten naar wens evalueren. Gebeurt
c’t 2006, Nr. 12
Nieuw huiswerk De klasse Rubberband bevat een fout die wij met opzet niet hebben gecorrigeerd: als je van een Rubberband-object de eigenschap Enabled herhaaldelijk op true zet, registreert het object zijn routines ook meerdere keren (Hoe kan dit, en welk effect heeft het?). Je zou op dit soort fouten in clientcode kunnen reageren door doublures te negeren (hoe?), maar je kunt het ook serieus aanpakken en een exception
genereren, om zo de schrijver van de clientcode op zijn slordige logica te wijzen. Bouw en test het optreden van zo'n exception. Implementeer deze reactie en schrijf een testprogramma dat de exception gestructureerd afhandelt.
Assembly's In het vorige deel sneden we de term assembly's al aan: zo heten de .EXE- en .DLL-eindproducten die de .NET-compiler van je programma's maakt.
class MyException : Exception { } class Program { static void Main(string[] args) { Console.Title = "TestException: Demo van exception-afhandeling"; try { Console.WriteLine("Main: try"); SubRoutine(); } catch (MyException e) { Console.WriteLine("Main: catch MyException"); } catch (Exception e) { Console.WriteLine("Main: catch Exception"); } catch { Console.WriteLine("Main: catch"); } finally { Console.WriteLine("Main: finally"); } Console.WriteLine("Main: Code na try/catch/finally"); Console.ReadKey(); } static void SubRoutine() { try { Console.WriteLine("SubRoutine: try"); throw new MyException(); } catch { Console.WriteLine("SubRoutine: catch"); throw new Exception(); } finally { Console.WriteLine("SubRoutine: finally"); } Console.WriteLine("test: Code na try/catch/finally"); } }
dat niet, dan wordt de exception weer doorgestuurd naar de dààrvoor draaiende aanroepende code etc., tot aan Main() toe. Er zijn dus allerlei kansen om een fout op te vangen, maar als dat nergens gebeurt, volgt er onherroepelijk een runtimeerror. Op taalniveau in C# (en Java) weerspiegelt dit foutmechanisme zich in de vier keywords throw, try, catch en finally. .NET kent een aantal standaardexceptions, maar je kunt ook je eigen exceptiontypes definiëren. Je genereert zo’n exception met throw myException;. In dit geval is dat je eigen exception MyException, die is afgeleid van de klasse Exception en daarmee profiteert van de gestructureer-
de foutcontext die deze construeert. In het algemeen vang je een exception als volgt op: try { code_block; } catch ( af_te_handelen_exception ) { catch_code_block; } finally { finally_code_block; } Het try-block bevat normale commando’s van het programma, die mogelijk tot een exception leiden. Op een try-blok moet in ieder geval een catch- of een finally-blok volgen; beide mag ook. Als het catch-blok de opgetreden exception kan afhandelen, gebeurt dat in zijn codeblock. Het optionele finally-blok wordt daarna altijd uitgevoerd, of het catch-blok nu is uitgevoerd
Ze bevatten geen processorinstructies, maar tussencode (Intermediate Language ofwel IL) die weer geïnterpreteerd wordt door de .NET-runtime interpreter (de CLR), net zoals Java-bytecode door de JVM. Voordat de CLR een assembly laadt, wordt deze grondig onderzocht. Om snelheid te winnen, wordt de assembly nog vóór het uitvoeren gecompileerd naar platformafhankelijke code door een zogenaamde Just-In-Time (JIT) compiler. Een tweede verschil met ou-
De uitvoer van het programma TestException demonstreert de uitvoeringsvolgorde bij gestructureerde exception-afhandeling. of niet. Typisch is finally bedoeld voor opruimcode, bijvoorbeeld het sluiten van een file of databaseverbinding. Om exceptions gestructureerd af te handelen, mag je willekeurig veel catch-blokken definiëren, die elk op de afhandeling van één specifiek type exception zijn gespecialiseerd. Let op de volgorde: de meest algemene exception specificeer je als laatste. Stel dat de klasse BException is afgeleid van AException en die weer net als A1Exception is afgeleid van Exception, dan ziet de afhandeling er als volgt uit: try { ... } catch (BException e) { ... } catch (AException e) { ... } catch (A1Exception e) { ... } catch (Exception e) { ... } catch { ... } finally { ... } Uiteraard kun je de catch-blokken van hetzelfde afleidingsniveau omwisselen (hier AException en A1Exception). Binnen elk catch-blok bevat de variabele e het exception-object. Hiermee kun je verdere details over de plaats van de fout en de oorzaak daarvan achterhalen, bijvoorbeeld via e.StackTrace.
145
Praktijk | Programmeren met C#
Zoals ildasm kenbaar maakt, bevat de DLL van de Rubber-component onder andere de versienummers van alle assembly's waarop hij gebaseerd is.
derwetse 'native' executables zit 'm erin dat een assembly naast de IL-code ook nog metadata moet bevatten, met de volledige type-informatie over de datatypen die erin te vinden zijn. Deze metadata wordt het manifest genoemd. In het manifest genereert de compiler altijd een eigen versienummer (bij Visual C# Express 2.0.50727) en een lijst die omschrijft welke andere assembly's je code nodig heeft tijdens het uitvoeren. Deze is heel precies; het is namelijk de
Design patterns Naarmate je meer programmeert, merk je dat bepaalde objectgeoriënteerde oplossingen goed werken, ook in een andere context. Vergelijk het met het plot van een boek of een tv-serie, waarin ook regelmatig dezelfde patronen terugkeren. Sinds een beroemd, maar enigszins saai vakboek van de 'Gang of Four' [8] heten dit soort patronen in software ook wel 'design patterns'. Er zijn er in de literatuur inmiddels zo’n 100 bekend, waarvan zo'n twintig er echt toe doen. Typisch heeft een design pattern deze kenmerken: - Een korte, goede naam, die de gehele aanpak beschrijft - Een probleem of context waarvoor het typisch een oplossing vormt - Een oplossing, die het globale
146
enige informatiebron voor een .NET-compiler die van je gecompileerde klasse een afleiding wil maken. Al deze informatie kun je bekijken met de IL-disassembler (ildasm.exe), die bij de .NET SDK hoort, maar die verscholen staat in je map 'Program Files\Microsoft Visual Studio 8\SDK\v2.0\bin'. De andere versienummers zijn wat moeilijker te verklaren, omdat ze uit twee verschillende bronnen afkomstig zijn. Het versienummer van de Rubber-component kun je zelf idee beschrijft - Consequenties: een patroon past niet altijd perfect en heeft soms nadelen Design patterns verbeteren vooral de interfaces en samenhang van componenten. Als je ze slim toepast, verminderen de afhankelijkheden en verkrijg je 'separation of concerns'. Dat zijn basisvereisten voor herbruik bare en schaalbare software. De meeste design patterns brengen wat overhead met zich mee, bijvoorbeeld door polymorfie of indirectie. Maar dat weegt ruim op tegen de tijdwinst die je boekt bij het latere opsporen van bugs en performance-problemen. Goed software-ontwerp loont dus. Maar don't overdo it; teveel patterns maken je software alleen maar complexer. Ook moet je opletten dat je niet te eenvoudige patterns gebruikt
kiezen. Hiervoor klik je rechts in de Solution Explorer op de entry Rubber, kies je 'Properties' uit het snelmenu en beland je in het tabblad 'Application' (dat net zo heet bij een DLL-project). Hier kun je ook zien welk output-type je voor Rubber hebt gekozen. De button 'Assembly Information' brengt je tenslotte in een dialoogvenster met invoeropties. In plaats van deze dialog te gebruiken kun je ook het bestand AssemblyInfo.cs direct editen, dit is bereikbaar via de submap Properties van de Solution Explorer. De overige versienummers in het manifest komen uit de References, die ook al in de Solution Explorer te vinden zijn. Als je een nieuw project aanmaakt, krijg je voor defaults als System automatisch de versienummers mee van de assembly's die al op je ontwikkelsysteem staan. In Visual Studio 2005 is dat dus iets als 2.0.x.y. In de toekomst kan dit versienummer meeveranderen met de .NET-versie die je gebruikt. Oudere .NET-bibliotheken kunnen vreedzaam met nieuwe samenleven én naast elkaar bestaan (anders dan bij COM), zoals ook de verschillende CLR-versies. Kijk maar eens met Verkenner in de map Windows\assembly. De forse mappenhiërarchie hieronder heet ook wel de 'Global Aspublic class Singleton { private static Singleton deInstantie = null; private Singleton() { } public static Singleton GeefInstantie { get { if (deInstantie == null) deInstantie = new Singleton(); return deInstantie; } } }
op plaatsen waar de complexere patterns beter geschikt zouden zijn voor de situatie (pattern abuse). Dankzij design patterns zijn diverse begrippen normaal programmeursjargon geworden. Een eenvoudig pattern is de singleton, dat je gebruikt als een klasse maar één instantie mag hebben. Het null objectpattern bespaart je checks op nullpointers. Ook nog simpel is
sembly Cache' (GAC). Deze is is gestructureerd op basis van deze versienummers. Klik je in het References-snelmenu op op 'Add reference', dan kun je op een iets mooiere manier zien welke assembly's beschikbaar zijn en daar kun je bepalen welke versie je wilt gebruiken. Ook in deze lijst kan een assembly met meerdere versienummers of opslagplaatsen voorkomen, afhankelijk van je installatie. Dankzij deze flexibiliteit kan .NET bijvoorbeeld een applicatie of component dwingen om precies versie 9.0.242.0 van ExceptionMessageBox te gebruiken. Biedt een systeem deze assembly niet aan of alleen in een andere versie, dan kun je ervoor zorgen dat de runtimeomgeving in dat geval dienst weigert. Dit is wel een hele rigide oplossing voor het 'DLL Hell'-probleem, zoals bijvoorbeeld bleek uit de betafase van DirectX 9: als je daarvoor een programma schreef, kon het slechts met de grootste moeite naar de definitieve versie worden geporteerd.
Deployment Als je zelfgeschreven componenten als reference opneemt in een applicatie, zul je meestal een lokale kopie maken naar de map van je applicatie en dit pad toevoegen aan je Reference de command; hierbij definieer je een interface met bijvoorbeeld een Execute()- of Undo()-method, die vervolgens door allerlei commandoklassen wordt geïmplementeerd. Het composite-pattern implementeert een interface en zorgt ervoor dat je deze kunt gebruiken voor een hele hiërarchie van objecten tegelijk. Je gebruikt het factory-pattern als je objecten wilt aanmaken zonder de concrete klasse te specificeren, zodat subklassen ze nog nader kunnen typeren. Het decorator-pattern is bedoeld om individuele objecten van extra functies te voorzien, als alternatief voor overerving. Hierbij definieer je bijvoorbeeld een uitgebreidere subklasse die je slechts voor één object gebruikt. Meer en diepgaandere informatie over dit onderwerp vind je in de literatuur.
c’t 2006, Nr. 12
Praktijk | Programmeren met C#
onder andere voorzien zijn van een 'strong name', dat wil zeggen dat je een PKI-gesigneerde hash meekrijgt, zodat de authenticiteit ervan vaststaat. Hoewel je assembly's met beheerderrechten gewoon via drag&drop in de GAC kunt installeren (de .NET SDK bevat hiervoor een Verkenner-plug-in), is automatische installatie [4] duidelijk lastiger dan copy deployment.
De laatste sprint
Via de projecteigenschappen kom je in een dialoog om versienummers te kiezen, in de code zie je het effect. Deze screenshot is een fotomontage, normaal kun je beide vensters niet tegelijkertijd zien. Paths. Dit heet ook wel 'copy deployment'; je verspreidt je applicatie dus door de benodigde mappenhiërachie gewoon erbij te kopiëren. Voor producenten als Microsoft en Borland is dat
geen optie; zij leveren componenten die in één versie voor alle applicaties op het systeem beschikbaar moeten komen en plaatsen deze daarom in de GAC. Dergelijke assembly's moeten
1 class Rubberband 2 { 3 private Control parent; 4 private bool enabled; 5 Rectangle rectRubber; 6 7 public event InvalidateEventHandler Selected; 8 protected void OnSelected(InvalidateEventArgs e) 9 { 10 if (Selected != null) 11 Selected(this, e); 12 } 13 14 public bool Enabled 15 { 16 get { return enabled; } 17 set 18 { 19 enabled = value; 20 if (parent == null) 21 return; 22 if (value) 23 { 24 parent.MouseDown += MouseDown; 25 parent.MouseMove += MouseMove; 26 parent.MouseUp += MouseUp; 27 } 28 else 29 { 30 parent.MouseDown -= MouseDown; 31 parent.MouseMove -= MouseMove; 32 parent.MouseUp -= MouseUp; 33 } 34 } 35 } 36 37 public Rubberband(Control c) 38 { 39 parent = c; 40 } 41 42 private void MouseDown(object sender, System.Windows.Forms. MouseEventArgs e) 43 {
c’t 2006, Nr. 12
Als je de cursus tot hier hebt gevolgd, is dat een hartelijke felicitatie waard: wat praktische C#-kennis betreft hebben we nu de top bijna bereikt. De weg was soms bochtig en steil en ook was hij niet altijd optimaal voorzien van wegwijzers. Vooral van bibliotheken en toepassingen hebben we helaas maar weinig voorbeelden kunnen geven. Ook was er geen ruimte voor het buildsysteem (MSBuild, NAnt, CruiseControl.NET), het genereren van documentatie (Ndoc) en de coverage-tool Ncover. De volgende keer volgt de klim naar de top. Dan laten we zien welke nieuwe features er in C# 2.0 in oktober 2005 zijn
geïntroduceerd (o.a. generieke datatypes) en werpen we een korte blik op de toekomst van de taal (LINQ, Aspect#). Literatuur [1] Rudolf Huttary, Arne Schäpers, Pieter-Paul Spiertz; Testsignaal (programmeren met C#, deel 6): c’t 11/06, p. 130 [2] Rudolf Huttary, Arne Schäpers, Pieter-Paul Spiertz; Action painting (programmeren met C#, deel 5): c't 10/06, p. 134 [3] http://en.wikipedia.org/wiki/ Component_Object_Model [4] http://en.wikipedia.org/wiki/ Windows_Installer [5] http://www.cs.clemson.edu/ ~mark/interrupts.html [6] http://en.wikipedia.org/wiki/ Software_componentry [7] Eric Freeman & Elisabeth Freeman; Head First Design Patterns; O'Reilly, 2004 [8] Gamma 'Helm, Johnson, Vlissides', Design Patterns; Elements of Reusable ObjectOriented Software, AddisonWesley, 1995 [9] http://en.wikipedia.org/wiki/ Design_pattern_(computer_ science) [10] CFF Explorer, ntcore.com/ exsuite.php c
44 if ((e.Button & MouseButtons.Left) > 0) // linker muisknop? 45 { 46 rectRubber.Location = Control.MousePosition; 47 } 48 } 49 50 private void MouseMove(object sender, System.Windows.Forms. MouseEventArgs e) 51 { 52 if ((e.Button & MouseButtons.Left) > 0) // linker muisknop? 53 { 54 if (parent.ClientRectangle.Contains(new Point(e.X, e.Y))) 55 { 56 ControlPaint.DrawReversibleFrame( 57 rectRubber, parent.BackColor, FrameStyle.Dashed); 58 rectRubber.Size = 59 (Size)Control.MousePosition - (Size)rectRubber.Location; 60 ControlPaint.DrawReversibleFrame( 61 rectRubber, parent.BackColor, FrameStyle.Dashed); 62 } 63 } 64 } 65 66 private void MouseUp(object sender, System.Windows.Forms. MouseEventArgs e) 67 { 68 if ((e.Button & MouseButtons.Left) > 0) // linker muisknop? 69 { 70 ControlPaint.DrawReversibleFrame( 71 rectRubber, parent.BackColor, FrameStyle.Dashed); 72 InvalidateEventArgs iva = new InvalidateEventArgs(rectRubber); 73 OnSelected(iva); 74 rectRubber.Size = new Size(0, 0); 75 } 76 } 77 }
Listing 3: de klasse Rubberband tekent in het clientgebied van een control-object een keuzerechthoek en genereert een Selected-event nadat de keuze is gemaakt.
147