Downloads til denne artikel finder du her ! |
Demo applikationen viser 7 forskellige måder at tilføje indhold til en listboks på. Fordele og ulemper ved disse forskellige tilgangsvinkler kan du læse om i artiklen. |
ItemsAdderDemo_vb.zip Items-adder demo i VB version |
ItemsAdderDemo_cs.zip Items-adder demo i C# version | |
ToString-funktionen er en stakkels misforstået funktion, som ofte bliver misbrugt på det groveste. For at illustrere artiklens emne vil jeg se på et ofte forekommende Windows brugergrænseflade-element nemlig ListBox-kontrollen.
ListBox-kontrollen er som bekendt i stand til at præsentere en række elementer, hvert element visuelt repræsenteret ved en tekststreng. Ud over den synlige tekststreng er man også ofte interesseret i at få koblet en usynlig (men programmeringsmæssig tilgængelig) identifikation til hvert af ListBoxens elementer. Denne identifikation har for mit vedkommende ofte været værdien af et database-felt i form af en primærnøgle - typisk en autocounter/autonumber eller hvad man nu foretrækker at kalde det.
I de følgende kode-eksempler indsættes data baseret på typen Person:
Class Person
Public PersonId As Integer
Public PersonNavn As String
Public FirmaNavn As String
End Class
class Person
{
public int PersonId;
public string PersonNavn;
public string FirmaNavn;
}
"per" er i kode-eksemplerne en variabel af typen Person instansieret og initialiseret på passende vis.
Lad os starte med lidt VB6 nostalgi, der demonstrerer, hvordan man i et sådan scenarium ofte fyldte data i en ListBox:
VB6:
MinVB6ListBox.AddItem(per.PersonNavn)
MinVB6ListBox.ItemData(MinVB6ListBox.NewIndex) = per.PersonId
Det principielle i VB6s brug af ListBoxe er, at man tilføjede tekster (v.h.a. AddItem-metoden) med samhørende heltal (v.h.a. ItemData-metoden).
Man kunne så efterfølgende tilgå PersonId værdien for det i ListBoxen valgte element med:
VB6:
id = MinVB6ListBox.ItemData(MinVB6ListBox.ListIndex)
Ny platform - nye muligheder
Med .NET har man fået mulighed for at tilføje vilkårlige objekter til en ListBox. Teksten, der vises, kan bestemmes på to måder: Enten via ListBoxens DisplayMember-property eller ved det indsatte elements ToString-metode.
DisplayMember og ValueMember
DisplayMember og ValueMember er to properties på en .NET ListBox, der ved databinding kan benyttes til at definere, hvilke properties der skal benyttes dels til den visuelle præsentation af ListBoxens elementer dels til lagring af den til teksten samhørende nøgleværdi. Bemærk at når der i denne artikel omtales properties på forretningsobjekter så som Person-objekter, kan der lige så godt være tale om fields.
Følgende kode sætter ListBoxen op:
ListBox1.DisplayMember = "PersonNavn"
ListBox1.ValueMember = "PersonId"
ListBox1.DisplayMember = "PersonNavn";
ListBox1.ValueMember = "PersonId";
Følgende kode laver en databinding af f.eks. en ArrayList til ListBoxen (og PersonNavn-feltets værdi vises i ListBoxen for hvert af arrayets Person-objekter):
ListBox1.DataSource = Personer
ListBox1.DataSource = Personer;
Ønsker man senere at kende PersonId-feltets værdi for et af brugeren valgt element i ListBoxen (når brugeren f.eks. dobbeltklikker på elementet), kan det f.eks. findes med følgende kode:
Dim personId As Integer
personId = CType(ListBox1.SelectedValue, Integer)
int personId;
personId = (int) ListBox1.SelectedValue;
Og værdien af PersonNavn feltet (tilsvarende kode vil gælde for en persons øvrige properties) for det valgte element kan f.eks. findes v.h.a. følgende kode:
Dim personNavn As String
personNavn = (CType(ListBox1.SelectedItem, Person).PersonNavn
string personNavn;
personNavn = ((Person) ListBox1.SelectedItem).PersonNavn;
eller v.h.a.:
Dim personNavn As String
personNavn = CType(ListBox1.Items(ListBox1.SelectedIndex), _
Person).PersonNavn
string personNavn;
personNavn = ((Person) ListBox1.Items(ListBox1.SelectedIndex)).
PersonNavn;
I artiklens kodeeksempler tilgår jeg ikke ListBoxens tekststrenge direkte. I stedet refereredes det i ListBoxen indsatte elements properties. Herved fastholdtes adskillelsen mellem objektets præsentation i ListBoxen og den øvrige kode.
|
Man bør heller ikke i VB6 tilgå tekststrengene i en ListBox direkte v.h.a. ListBoxens List-property. List-propertyen i VB6 var udelukkende (eller burde udelukkende være) beregnet til at specificere en streng til visning i ListBoxen. Har man f.eks. i et event brug for den valgte persons PersonNavn property bør man ikke tilgå den direkte via ListBoxens List-metode, men i stedet ved at bruge elementets samhørende PersonId-værdi (aflæst via ItemData-metoden) til at finde selve objektet i en Collection eller lignende samt derefter aflæse objektets PersonNavn-property ud fra det fundne objekt. Det lyder måske lige lovligt bøvlet (men det kan let pakkes ind og burde i så fald ikke tage længere tid at lave end den mere hackede version). Den markante fordel vil til gengæld være, at ens event-kode i så fald vil være fuldstændig uafhængig af, hvad man vælger at vise i ListBoxen. | |
At benytte DisplayMember og ValueMember kan måske nok være en OK løsning i nogle situationer - men det er dog en løsning, der har en akilleshæl: Hvad nu hvis man ønsker at vise en streng, der ikke kan returneres som en allerede eksisterende property på klassen - måske ønsker man en specifik formatering af en eksisterende property eller en kombination af en række eksisterende properties - f.eks.: PersonNavn " " FirmaNavn. En hurtig og oplagt løsning på dette problem kunne selvfølgelig være at lave en ny property. Men får man brug for flere forskellige måder at vise tekster på fra samme klasse i forskellige sammenhænge, ender man hurtigt op med en lang række tilsvarende properties. Det er ikke et kønt - eller særligt overskueligt - syn, hvis Person-klassen på denne måde efterhånden "forurenes" med properties.
Dong! Overskriv ikke metoden - der kommer ToString!
Når objekter tilføjes en ListBox vises indholdet af den property på objektet, som er specificeret i ListBoxens DisplayMember-property, dersom denne har en værdi. Er der ikke angivet et navn på en property i ListBoxens DisplayMember-property vises i stedet den streng, der returneres fra objektets ToString-metode. Implementationen for System.Object-klassens ToString-metode returnerer blot klassens navn; og laver vi vores egen klasse (direkte/indirekte, implicit/eksplicit nedarvet fra System.Object-klassen) vil den jo naturligvis som udgangspunkt benytte den samme - i praksis ikke specielt nyttige - implementation. Mere nyttig kan ToString dog gøres ved i vores Person-klasse at overskrive den med en passende implementation som eksempelvis den følgende:
Public Overrides Function ToString() As String
Return Me.PersonNavn + " " + Me.FirmaNavn
End Function
public override string ToString()
{
return this.PersonNavn + " " + this.FirmaNavn;
}
Ved brug af denne måde at håndtere data i ListBoxen på kan vi igen finde Person-objekternes properties v.h.a. kode á la den vi så på for DisplayMember-tilgangsvinklen.
Igen har vi problemstillingen med, at vi i forskellige sammenhænge formentlig ønsker at kunne præsentere Person-objekter på mange forskellige måder. I meget simple eksempler kunne dette evt. løses ved, at vi på PersonKlassen lavede en view-property, der angav, hvilket "view" ToString-funktionen skulle returnere. Men hvis vores Person-objekt samtidigt indgår i flere forskellige sammenhænge, bliver det umiddelbart problematisk (og ihvertfald ikke særligt kønt) at få præsenteret forskellige views i disse forskellige sammenhænge.
Udover problemstillingen med "forurening" af Person-objektets interface ved brug af DisplayMember er der også et andet principielt problem ved de netop skitserede løsninger. Både DisplayMember- (de aller simpleste scenarier undtaget) og ToString-løsningerne tager udgangspunkt i, at den kode, der er bestemmende for, hvilken tekst der præsenteres i ListBoxen, ligger i Person-klassen. Tilhængere af rene flerlags-arkitekturer vil finde det stærkt problematisk, at hvad der i denne brug af f.eks. ToString må opfattes som værende præsentationslogik er blandet sammen med forretningslogik (selve Person-klassen). En sammenblanding der gør det mere end almindeligt svært at ændre brugergrænsefladen uden samtidig at skulle forholde sig til forretningslogikken.
Jo men - hva´ gør vi så?
En mulig løsning på problemet er at lave en klasse, hvis egentlige formål det er at kunne præsentere data i en ListBox (eller i andre lignende sammenhænge). En sådan klasse kan skrues sammen på mange forskellige måder. Jeg vil til inspiration præsentere to forslag til implementationer.
En præsentationsklasse
Aller først en generel klasse kaldet GenericListItem til visning af data i f.eks. en ListBox:
Public Class GenericListItem
Private _displayText As String
Private _displayValue As Object
Public Sub New(ByVal displayValue As Object, _
ByVal displayText As String)
_displayValue = displayValue
_displayText = displayText
End Sub
Public Property DisplayText() As String
Get
Return _displayText
End Get
Set(ByVal Value As String)
_displayText = Value
End Set
End Property
Public Property DisplayValue() As Object
Get
Return _displayValue
End Get
Set(ByVal Value As Object)
_displayValue = Value
End Set
End Property
Public Overrides Function ToString() As String
Return _displayText
End Function
End Class
public class GenericListItem
{
private string _displayText;
private object _displayValue;
public GenericListItem(object displayValue, string displayText)
{
_displayValue = displayValue;
_displayText = displayText;
}
public string DisplayText
{
get
{
return _displayText;
}
set
{
_displayText = value;
}
}
public object DisplayValue
{
get
{
return _displayValue;
}
set
{
_displayValue = value;
}
}
public override string ToString()
{
return _displayText;
}
}
Et person-objekt kan nu tilføjes og præsenteres med f.eks. følgende kode:
ListBox1.Items.Add( _
New GenericListItem(per, _
per.PersonNavn + " " + per.FirmaNavn))
ListBox1.Items.Add(
new GenericListItem(per,
per.PersonNavn + " " + per.FirmaNavn));
eller:
ListBox1.Items.Add( _
New GenericListItem(per.PersonId, _
per.PersonNavn + " " + per.FirmaNavn))
ListBox1.Items.Add(
new GenericListItem(per.PersonId,
per.PersonNavn + " " + per.FirmaNavn));
alt efter om man foretrækker at opfatte hele person-objektet eller blot PersonId-propertyen som værdien af elementet i ListBoxen.
GenericListItem er, som navnet på klassen jo også understreger, en generel løsning (og det er jo som udgangspunkt godt), der baserer sig på en typesvag tilgang til de i ListBoxen lagrede elementer (og det er jo som udgangspunkt skidt). Man kan om ønsket rimeligt let lave en mere typestærk udgave af GenericListItem. En sådan klasse kunne have følgende udseende:
Public Class SpecificListItem
Private _displayText As String
Private _comp As EifosComponent
Public Sub New(ByVal comp As EifosComponent, _
ByVal displayText As String)
_comp = comp
_displayText = displayText
End Sub
Public Property DisplayText() As String
Get
Return _displayText
End Get
Set(ByVal Value As String)
_displayText = Value
End Set
End Property
Public Property Component() As EifosComponent
Get
Return _comp
End Get
Set(ByVal Value As EifosComponent)
_comp = Value
End Set
End Property
Public Overrides Function ToString() As String
Return _displayText
End Function
End Class
public class SpecificListItem
{
private string _displayText;
private EifosComponent _comp;
public SpecificListItem(EifosComponent comp,
string displayText)
{
_comp = comp;
_displayText = displayText;
}
public string DisplayText
{
get
{
return _displayText;
}
set
{
_displayText = value;
}
}
public EifosComponent Component
{
get
{
return _comp;
}
set
{
_comp = value;
}
}
public override string ToString()
{
return _displayText;
}
}
Et alternativ til GenericListItem kunne være en klasse, hvis implementering er specifik for en række af views af Person relaterede data eller måske endog specifik for ét bestemt view. Et eksempel på et sådan kunne være:
Public Class PersonFirmaView
Private _per As Person
Public Sub New(ByVal per As Person)
_per = per
End Sub
Public Property Personen() As Person
Get
Return _per
End Get
Set(ByVal Value As Person)
_per = Value
End Set
End Property
Public Overrides Function ToString() As String
Return _per.PersonNavn + " " + _per.FirmaNavn
End Function
End Class
public class PersonFirmaView
{
private Person _per;
public PersonFirmaView(Person per)
{
_per = per;
}
public Person Personen
{
get
{
return _per;
}
set
{
_per = value;
}
}
public override string ToString()
{
return _per.PersonNavn + " " + _per.FirmaNavn;
}
}
Fordelen ved denne løsning er, at den for implementation af viewet nødvendige kode ligger placeret i en selvstændig klasse - en klasse hvis eneste formål netop er at kunne præsentere data fra et Person-objekt. Til gengæld kræver det jo i sagens natur desværre en vis mængde specifik kode.
ToString - den misforståede funktion.
Men hvad bruger man så egentlig ToString-funktionen til? Og hvad er det, der gør den misforstået? Brugen af ToString-funktionen bør generelt begrænse sig til udviklings-orienterede situationer, hvor man til brug for debugning ønsker at få en tekstuel repræsentation af et objekts data. Desværre ser man ofte eksempler på at forretningsklasser overskriver ToString-funktionen til brug i forbindelse med brugergrænseflader. Et eksempel på en pæn flerlags-arkitektur er dette langt fra.
Vil I undgå mareridt om horder af klasser, der i ét væk overskriver ToString-funktionen, bør I ikke springe over, hvor gærdet nogle gange måtte syne lavest, men i stedet satse på en holdbar løsning, hvor forretningsspecifik kode og user interface kode er skarpt adskilt. Et eksempel på en mulig implementation af denne adskillelse kunne være i form af GenericListItem, som du sammen med en lille demo kan downloade - se sidebaren først i artiklen.