Introduction
When I found out that a ListBox
is only able to select one property (through DisplayMember
) from an object or use the ToString()
method, I thought I would create a general class that solves the problem.
Background
When adding items to a ListBox
, you usually want them displayed to the user. However, there are only two options for selecting what to display:
- Select a public property from the item (through
DisplayMember
) - Let the
ListBox
call ToString()
in the item
To have the item display nicely in the ListBox
, we can simply implement the ToString()
method on the corresponding class.
At times, we might want to use a domain object in several different GUIs and have it displayed in different ways according to the GUI. Now, we could add several properties to the domain class to have it suit the different GUIs. But it is probably already clear to most readers that amending the domain code to have the GUI display correctly is not good design.
So, what do we do? What do we do in order to avoid amending the domain model in order to display the correct information in the GUI.
Well, I implemented a ObjectsToStringAdapter
class.
This class has an internal List<>
to which objects can be added. When the objects have been added, you need to give a format string. After this is done, you (or a ListBox
) can call ToString()
and the class will return a string formatted according to your format string and the objects in the internal List<>
.
So, the formatting string is of the format:
!sequence!.PropertyName, !sequence!.PropertyName ... !sequence!.PropertyName
where sequence is the (zero based) sequence of the object in the List
, i.e., the sequence in which it was added to the class. An example could be:
Firstname: {0}.FirstName, Lastname: {0}.Lastname {1}.ToString {2}.ToString {0}.Email
where a Person
object, two value types, or then strings has been added to the object. So, if a Person
object with data from me, an int
with value '31', and a string
object with the value "Testing" was added to the object, the output would be:
Firstname: Klaus, Lastname: Hebsgaard 31 Testing spam@spam.com
This is the code for the class:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
namespace Krifa.UI.Util {
public class ObjectsToStringAdapter {
List<SYSTEM.OBJECT> listedObjects;
List<SYSTEM.TYPE> typeOfListedObjects;
List<STRING> formattedValue;
List<STRING> propertyNames;
private string _format = string.Empty;
private string output = string.Empty;
public ObjectsToStringAdapter() {
listedObjects = new List<SYSTEM.OBJECT>();
typeOfListedObjects = new List<TYPE>();
formattedValue = new List<STRING>();
propertyNames = new List<STRING>();
}
public List<STRING> PropertyNames {
get {
return propertyNames;
}
}
public string Format {
get {
return _format;
}
set {
_format = value;
}
}
public void Add<T>(T objectToBeListed) {
Debug.Assert(objectToBeListed != null);
listedObjects.Add(objectToBeListed);
typeOfListedObjects.Add(objectToBeListed.GetType());
}
public override string ToString() {
Parse();
return output;
}
public string[] ToStringArray() {
Parse();
return (string[])formattedValue.ToArray();
}
private void Parse() {
Debug.Assert(listedObjects.Count > 0);
Debug.Assert(typeOfListedObjects.Count > 0);
formattedValue.Clear();
output = Format;
string pattern = @"{(?<objectSEQUENCE>.*)}.(?<PROPERTYNAME>.*)";
string[] splittedFormats = Format.Split(',', ' ', ';');
Debug.Assert(splittedFormats.Length > 0);
foreach (string splittedFormat in splittedFormats) {
Match parsed = Regex.Match(splittedFormat, pattern);
if (parsed.Success) {
int objectSequence;
Int32.TryParse(parsed.Groups["objectSequence"].Value,
out objectSequence);
string propertyName = parsed.Groups["propertyName"].Value;
string replString = "!" + objectSequence.ToString() + "!." +
propertyName;
Debug.Assert(objectSequence <= listedObjects.Count);
propertyNames.Add(propertyName);
if (typeOfListedObjects[objectSequence].IsValueType ||
typeOfListedObjects[objectSequence].Name == "String") {
ParseValueTypeAndString(objectSequence, propertyName, replString);
}
else {
ParseReferenceType(objectSequence, propertyName, replString);
}
}
}
}
private void ParseReferenceType(int objectSequence,
string propertyName, string replString) {
try {
string propValue = typeOfListedObjects[objectSequence].GetProperty(
propertyName).GetValue(listedObjects[objectSequence],
null).ToString();
formattedValue.Add(propValue);
output = output.Replace(replString, propValue);
}
catch (Exception) {
try {
FieldInfo field = typeOfListedObjects[objectSequence].GetField(
propertyName,
BindingFlags.Instance |
BindingFlags.NonPublic | BindingFlags.Public);
string propValue =
field.GetValue(listedObjects[objectSequence]).ToString();
formattedValue.Add(propValue);
output = output.Replace(replString, propValue);
}
catch (Exception) {
FormatError(replString);
}
}
}
private void ParseValueTypeAndString(int objectSequence,
string propertyName, string replString) {
if (propertyName == "ToString") {
formattedValue.Add(listedObjects[objectSequence].ToString());
output = output.Replace(replString,
listedObjects[objectSequence].ToString());
}
else {
FormatError(replString);
}
}
private void FormatError(string replString) {
string errMessage = "Error in parsing";
formattedValue.Add(errMessage);
output = output.Replace(replString, errMessage);
}
public System.Object this[int index] {
get {
return listedObjects[index];
}
set {
listedObjects[index] = value;
}
}
}
}
Why didn't I inherit the ListBox
class and implement this functionality on the ListBox
? Well, I wanted this functionality to be very general, so by making it a class of its own, it can be used for multiple purposes. The class can be extended to other uses where objects need to be formatted as strings.
I have already added a function ToStringArray
, which can be used for adding the formatted code to a DataGridView
.
Please note, however, that when adding to a DataGridView
, you do not actually add an object, so by using this method, you need to do a lot of manual work in order to find the corresponding object.
Please also note that if you plan on adding many properties from many objects to one or more ListBox
es, you might get better performance by implementing a more specific adapter. However, when doing this, you might also add a lot more code to your project.
Using the code
Please note that when using reference types, Reflection is done per property.
Here is the code for adding objects to a ListBox
:
Person per = new Person(textBox1.Text, textBox2.Text, emailTextBox.Text);
ObjectsToStringAdapter otsa = new ObjectsToStringAdapter();
otsa.Format = Firstname: {0}.FirstName, Lastname: {0}.Lastname
{1}.ToString {2}.ToString {0}.Email;
otsa.Add<PERSON>(per);
otsa.Add<double>(31.7);
otsa.Add<string>("test" + " string");
theListBox.Items.Add(otsa);
Here is the code for retrieving code from a ListBox
:
ObjectsToStringAdapter otsa = (ObjectsToStringAdapter)theListBox.SelectedItem;
string dlgStr = otsa.ToString();
MessageBox.Show(dlgStr);
Of course, it is possible to use an indexer to get objects from the ObjectsToStringAdapter
and then cast it back to the original type.