Drag ‘n drop - en lille hjælper skal man da have.

Skrevet - Wednesday, October 8th, 2008 kl. 21:21 | Kategori - Kodning.

Lad det være sagt med det samme. Jeg har aldrig været et stort fan af at implementere træk-og-slip funktionalitet. Dels har der traditionelt været meget spaghetti kode involveret med timingen af events, meget boiler-plate kode og et generelt buggy miljø. Men… en gang imellem må man føje brugerne og implementere skidtet. Efter at have arbejdet med BDD i et par måneder, prøver jeg meget rigidt at overholde S.O.L.I.D principperne, hvor de giver mening. Og de fleste implementationer af Drag and Drop overtræder et par stykker… der kommer til at lægge en masse kode sovset ind i UI, som er næsten umuligt at teste på en automatiseret måde.

Efter at have læst Beatriz Costa’s indlæg om emnet, gik jeg igennem hendes løsning med en statisk hjælpeklasse. Jeg er ikke vild med statiske klasser af mange forskellige årsager. Derudover skete der en masse ‘magiske’ ting med en ekstra adorner, og den kunne heller ikke løse alle de scenarier som jeg gerne ville kunne understøtte. Ikke desto mindre virkede hendes kode, så den fik lov til at være udgangspunkt.

Det rigtige dejlige ved hendes løsning var at alt blev sat som attached properties på ekisterende kontroller - dvs. at der ikke skulle nogen opsætning til på kontrollerne for at den virkede. Hun benytter attached DependencyProperties. Jeg har valgt en lidt anden vej… de krav som jeg havde til hjælperen var følgende:

Det kræver nok lidt forklaring. Min hjælpeklasse skal kunne understøtte arbitrært vanskellige koblinger. Fx at afsenderobjektet (DragSource) skal sættes på en property fire lag nede i objektgrafen på modtagerobjektet (DropTarget) eller at afsenderobjektet skal puttes i en helt tredje liste og registreres som låst osv. osv. Derfor har jeg brug for at kende typen af de objekter jeg flytter rundt med - så klassen endte op med to generiske argumenter. Deruodver vil jeg kunne understøtte to forskellige scenarier i det samme skærmbillede, så jeg kan have brug for to forskellige instanser, som opfører sig forskelligt. Derudover skal mere end en kontol kunne være henholdsvis modtager og afsender af objekter. Med live-preview mener jeg at det element jeg vil ramme i DropTarget skal markeres inden jeg slipper knappen, så jeg har en chance for at ramme rigtigt.

For at håndtere run-time koblingen, har jeg valgt at implementere et Observer-lignende pattern. Hjælpeklassen har fire funktioner til at styre registrering/afregistrering af ListBoxe. Et sæt til DragSources og et sæt til DropTargets. Det hjælpeklassen egentlig gør, er at lytte på de relevante events på de registrerede kontroller og internt holde styr på afsender og modtager. Det vil sige at der er lidt Mediator over vores hjælpeklasse.

For at håndtere selve koblingen, har jeg opfundet endnu en generisk interface ICombineCommand med to generiske elementer (de samme som ligger på hjælpeklassen), som publicerer en enkelt metode (Execute) som sørger for den logik der skal ske imellem afsender og modtager. Denne ICombineCommand injecter jeg så i hjælpeklasssens constructor.

Live-preview har jeg tyv-stjålet fra Beatriz’ kode. Den er simplificeret en del, fordi jeg ikke ønsker at supportere indsætning mellem elementer eller efter.

For at illustrere løsningen har jeg lavet en lille implementation, hvor jeg viser nogle af de features som er mulige. Jeg har lavet en lille applikation, som kan koble et køretøj sammen med en chauffør. For eksemplets skyld har jeg to DragSources. Læg mærke til hvor få linjers kode, der ligger i de forskellige klasser. Desuden har jeg prøvet at vise styrken ved ObjectDataProviders ved at anvende dem overalt.

Der er en enkelt begrænsning i implementationen - en given kontrol kan kun agere DragSource for en DragAndDropHelper. Det skyldes, at når DragNDrop initieres, bliver alle normale mouseevents skiftet ud me MouseDragEvents - og det vil sige at den hjælper som først registrerer trækket vil effektivt deaktivere de andre - og det kan være tilfældigt (især i et multitrådet miljø som WPF) hvilken der aktiverer. Det giver en meget ustabil følelse…

Hele projektet kan hentes her.

ObjectDataProvider - et must for seriøs databinding

Skrevet - Saturday, October 4th, 2008 kl. 15:02 | Kategori - Kodning.

DataBinding i WPF er kraftfuldt. Alt kan bindes på kryds og tværs - hvilket er lidt et tve-ægget sværd. Hvis man først begynder at være afhængigt af det, kan man brænde nallerne noget så eftertrykkeligt, når man nu får Bindingen til at holde op med at fungere.

For at nævne et par typiske scenarier:

Hvis for eksempel du har bundet en TextProperty op på en given property i detail-delen af et master-detail setup og den aktuelt valgte værdi i master-delen skifter, forsvinder bindingen samme sted hen som det tidligere valgte objekt… og pludselig bliver detail-delen ikke opdateret længere.

*stor fanfare* ObjectDataProvider (ODP) er din ven. Den kan ikke hjælpe dig, hvis du får overskrevet bindingen, men især i master-detail scenarier spiller den max. Ideen med ODP er at man har en mellemstation imellem det aktuelle objekt og det der skal bindes fra. TextBoxen peger dermed altid på det samme objekt, mens objekt instansen skiftes på ODP (.ObjectInstance = _newValue;). Det beskytter bindingen fra at pege på et null-objekt eller et out-of-date objekt.

Derudover implementerer ODP INotifyCollectionChanged, hvilket vil sige, at hvis man har en samling af objekter, som man tilføjer, sletter og omarrangerer vil disse ‘boble’ op i UI uden at man i den sammenhæng skal gøre noget aktivt.

Tricket til at få ODP til at virke er, at de objekter man putter i ObjectInstance helst skal implementere INotifyPropertyChanged (enkelt instanser) og INotifyCollectionChanged for samlinger. Hvis det er kontrollen selv, der tilføjer, fjerner osv. er dette dog ikke nødvendigt. Derudover skal man sørge for at Binde på ODP’en og ikke objekt instansen, da man ellers er tilbage ved punkt et.

En anden pointe er, at jo færre ODP’er man har i spil, jo nemmere er alle opdateringsscenarier. Husk på at den kan håndtere vilkårligt dybe Path’s ned i objektgrafen og INotifyPropertyChanged vil stadig virke. I vores løsning på arbejde er vi nede i 5te niveau fra hovedobjektet uden problemer. Derudover kan man Binde dem til hinanden, så man ikke engang behøver håndtere events, når master-delen i master-detail scenarier skifter.

Dokumentationen på ODP er omfangsrig - start på MSDN og læs bagefter Beatriz Costa’s indlæg (www.beacosta.com). Det giver et rigtig godt udgangspunkt.

BDD: Metoden der kan håndtere alt vs. strategy pattern

Skrevet - Saturday, September 6th, 2008 kl. 17:26 | Kategori - Kodning.

Version 0.12 af EconomyDeluxe er online her. Der er sket en del refaktorering (efterhånden en fast bestanddel) - alle behaviour projekterne er nu samlet i et projekt og EconomyDeluxe er splittet op i to - en .exe/.xbap og en .dll, som indeholder alt logikken. Der er to grunde til opsplitningen - Castle Windsor kan ikke udlede classer fra en .exe og jeg vil nu kunne tilføje en web-style wpf applikation med samme logik.

Primært er der nu tilføjet den første MVP - jeg har i den forbindelse lavet en hjælpeklasse (Property2WPFControlMapper) som skal sørge for at bygge propertygrids. Det er ikke første gang jeg bygger sådan en klasse, så det gik forholdsvis hurtigt med at bygge den. Jeg kan bygge et fuldstændig databundet view med textboxe, comboboxe osv. med en linje kode og en hel bunke reflection. I vores projekt på arbejde har jeg lavet samme klasse, bare noget mere sofistikeret og mere funktionalitet. Her rendte vi ind i, at alle andre end mig havde svært ved at gennemskue den. Problemet består i at der er forholdsvis mange scenarier for en given property - og den control, der kommer ud af det, har meget forskellig adfærd - det resulterede i en del if-then-else-if og switch-case.

Vores projektleder bad mig anvende strategy-specification pattern i en refactorering. Jeg må indrømme, at min kode er blevet noget mere læsbar. Der er dybest set 4 forskellige scenarier (strategies), som giver mening i vores domæneobjekter:

Jeg har oprettet en IControlStrategySelector, som fungerer som min specification, dvs. den der skal sørge for logikken i hvilken strategi, der bliver valgt. Derefter har jeg lavet 4 IControlStrategy-classer, som alle implementerer 2 simple metoder - CreateControl() og IsSatisfiedBy(). Den første laver controllen ud fra en PropertyInfo, mens den anden bestemmer hvorvidt strategien kan håndtere den givne PropertyInfo.

Eftersom jeg kun skal bruge en strategi per PropertyInfo, har jeg lavet selector’en temmelig simpel - jeg har lavet en IList, som jeg itererer henover, indtil jeg finder en strategi, der opfylder mine behov. Derfor er rækkefølgen vigtig - default-strategien er textboksen, så den ligger til sidst i IList’en. Strategien for komplekse ValueTypes er lavet generisk fordi jeg skal have en datasource med, som ikke er ens for de forskellige typer. På nuværende udviklingsstadie har jeg kun en type, der skal ligge i en combobox - ILedgerAccount, men der vil komme flere.

Resultat: Min oprindelige hjælpeklasse var på ca. 150 linjer med mange nestede if-then-else strukturer. Nu er den splittet op i 2 interfaces (10 linjer tilsammen), 4 implementationer (ca 20 linjer/stk) + 1 abstract klasse(25 linjer) for IControlStrategy samt 1 implementation (23 linjer) af IControlStrategySelector - og den oprindelige klasse er nede på 43 linjer. Netto er der altså tilføjet ca. 30 linjer kode og 8 filer. Til gengæld… overskueligheden er steget med mange grader og kompleksiteten for andre end mig er faldet markant.

EDIT: Jeg er klar over, at man normalt ville lade Specifications og Strategies være to forskellige klasser for bedre at overholde SoC og SRP (se evt. her). I den her sammenhæng virker det dog som needless complexity eftersom det ikke vil udvide sig voldsomt med tiden (det er trods alt en gennemprøvet rutine, der er refactoreret til pattern). Lad os kalde det Compact Specification/Strategy pattern…

« Forrige indlæg · Næste indlæg »