Drag ‘n drop – en lille hjælper skal man da have.
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:
- Generisk og instans. Dvs. ingen statisk klasse.
- Kontroller skulle kunne sættes til run-time at agere som modtager eller afsender af objekter.
- Skulle kunne understøtte en arbitrær kobling mellem modtager og afsender.
- En form for live-preview.
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.