5

I have a list of classes, but different children have different properties that need to be displayed.

What I want to achieve is to have a listbox-type control in the gui which enables each child to display it's properties the way it wants to - so not using the same pre-defined columns for every class.

I envisage something like the transmission interface (below), where each class can paint it's own entry, showing some text, progress bar if relevant, etc.

enter image description here

How can this be achieved in C#?

Thanks for any help.

Michael Paulukonis
  • 9,020
  • 5
  • 48
  • 68
Mark
  • 1,759
  • 4
  • 32
  • 44

2 Answers2

18

Let your list items implement an interface that provides everything needed for the display:

public interface IDisplayItem
{
    event System.ComponentModel.ProgressChangedEventHandler ProgressChanged;
    string Subject { get; }
    string Description { get; }
    // Provide everything you need for the display here
}

The transmission objects should not display themselves. You should not mix domain logic (business logic) and display logic.

Customized ListBox: In order to do display listbox items your own way, you will have to derive your own listbox control from System.Windows.Forms.ListBox. Set the DrawMode property of your listbox to DrawMode.OwnerDrawFixed or DrawMode.OwnerDrawVariable (if the items are not of the same size) in the constructor. If you use OwnerDrawVariable then you will have to override OnMeasureItem as well, in order to tell the listbox the size of each item.

public class TransmissionListBox : ListBox
{
    public TransmissionListBox()
    {
        this.DrawMode = DrawMode.OwnerDrawFixed;
    }

    protected override void OnDrawItem(DrawItemEventArgs e)
    {
        e.DrawBackground();
        if (e.Index >= 0 && e.Index < Items.Count) {
            var displayItem = Items[e.Index] as IDisplayItem;
            TextRenderer.DrawText(e.Graphics, displayItem.Subject, e.Font, ...);
            e.Graphics.DrawIcon(...);
            // and so on
        }
        e.DrawFocusRectangle();
    }
}

You can let your original transmission class implement IDisplayItem or create a special class for this purpose. You can also have different types of objects in the list, as long as they implement the interface. The point is, that the display logic itself is in the control, the transmission class (or whatever class) only provides the information required.

Example: Because of the ongoing discussion with Mark, I have decided to include a full example here. Let's define a model class:

public class Address : INotifyPropertyChanged
{
    private string _Name;
    public string Name
    {
        get { return _Name; }
        set
        {
            if (_Name != value) {
                _Name = value;
                OnPropertyChanged("Name");
            }
        }
    }

    private string _City;
    public string City
    {
        get { return _City; }
        set
        {
            if (_City != value) {
                _City = value;
                OnPropertyChanged("City");
                OnPropertyChanged("CityZip");
            }
        }
    }

    private int? _Zip;
    public int? Zip
    {
        get { return _Zip; }
        set
        {
            if (_Zip != value) {
                _Zip = value;
                OnPropertyChanged("Zip");
                OnPropertyChanged("CityZip");
            }
        }
    }

    public string CityZip { get { return Zip.ToString() + " " + City; } }

    public override string ToString()
    {
        return Name + "," + CityZip;
    }

    #region INotifyPropertyChanged Members

    public event PropertyChangedEventHandler PropertyChanged;

    private void OnPropertyChanged(string propertyName)
    {
        var handler = PropertyChanged;
        if (handler != null) {
            handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }

    #endregion
}

Here is a custom ListBox:

public class AddressListBox : ListBox
{
    public AddressListBox()
    {
        DrawMode = DrawMode.OwnerDrawFixed;
        ItemHeight = 18;
    }

    protected override void OnDrawItem(DrawItemEventArgs e)
    {
        const TextFormatFlags flags = TextFormatFlags.Left | TextFormatFlags.VerticalCenter;

        if (e.Index >= 0) {
            e.DrawBackground();
            e.Graphics.DrawRectangle(Pens.Red, 2, e.Bounds.Y + 2, 14, 14); // Simulate an icon.

            var textRect = e.Bounds;
            textRect.X += 20;
            textRect.Width -= 20;
            string itemText = DesignMode ? "AddressListBox" : Items[e.Index].ToString();
            TextRenderer.DrawText(e.Graphics, itemText, e.Font, textRect, e.ForeColor, flags);
            e.DrawFocusRectangle();
        }
    }
}

On a form, we place this AddressListBox and a button. In the form, we place some initializing code and some button code, which changes our addresses. We do this in order to see, if our listbox is updated automatically:

public partial class frmAddress : Form
{
    BindingList<Address> _addressBindingList;

    public frmAddress()
    {
        InitializeComponent();

        _addressBindingList = new BindingList<Address>();
        _addressBindingList.Add(new Address { Name = "Müller" });
        _addressBindingList.Add(new Address { Name = "Aebi" });
        lstAddress.DataSource = _addressBindingList;
    }

    private void btnChangeCity_Click(object sender, EventArgs e)
    {
        _addressBindingList[0].City = "Zürich";
        _addressBindingList[1].City = "Burgdorf";
    }
}

When the button is clicked, the items in the AddressListBox are updated automatically. Note that only the DataSource of the listbox is defined. The DataMember and ValueMember remain empty.

Olivier Jacot-Descombes
  • 104,806
  • 13
  • 138
  • 188
  • I don't think I quite understand. So would you have a separate class that handles the displaying, is bound to a list item and can be added to the listbox control? So would it be possible to have different items showing different properties? Do you have any links or examples to explain this further? Thanks! – Mark Nov 17 '11 at 10:17
  • Thank you for the updates. I will give it a go! It will certainly give me a starting point to learn from.. – Mark Nov 18 '11 at 09:12
  • By reviewing my example code, I noticed, that it is wrong. No loop is required, since OnDrawItem is called once for each item. I edited the example and replaced the loop by an index check. – Olivier Jacot-Descombes Nov 18 '11 at 13:26
  • Thank you for the update. I am nearly there. May I ask, how am I best to draw the progress bar on the entry? – Mark Nov 21 '11 at 08:51
  • 1
    In WinForms you trigger the Repaint of a control by calling the Invalidate() method (or one of overloads). `myListBox.Invalidate();`. The `IDisplayItem` interface could have a `float Progress { get; }` property which exposes the current state of the transmission. At each change of a property exposed by `IDisplayItem` you will have to invalidate the listbox. Another way to go is to use data binding. Bind your listbox to an object-binding source and implement `INotifyPropertyChanged` in your displayed items. The magic of data binding will then let the listbox display the changes automatically! – Olivier Jacot-Descombes Nov 21 '11 at 14:19
  • Thanks for the continued support! I have 2 more questions if that is ok.. 1) If I implement `INotifyPropertyChanged`, the listbox only updates if I have set `DisplayMember`/`ValueMember` to the name of the property being updated - so if I am painting using several properties, then it will only update when the bound property is changed, not the others. How best to overcome this? Perhaps also call `PropertyChanged("RePaint")` or similar for all properties and bind to that? – Mark Nov 22 '11 at 11:08
  • 2) In `OnDrawItem`/`DrawItem`, I am just checking for the type of object to see how to paint it. Is there a more elegant way of doing this? I know that you should keep the display logic separate, but when they are each so different, it seems that there should be a better way.. Also, for the progress bar, I am currently creating a `Bitmap` and a `ProgressBar`, setting the progress bar properties from the object being updated and then using `ProgressBar.DrawToBitmap(..)` & `e.Graphics.DrawImage(bitmap, location)`. Is this the best way to do this? – Mark Nov 22 '11 at 11:13
  • 1
    I made some tests. It seems to work better if you use a `System.ComponentModel.BindingList`. In my test I converted the List into a binding list: `var bindingList = new BindingList
    (addressList); addressBindingSource.DataSource = bindingList;`. I did not set the Display or ValueMember.
    – Olivier Jacot-Descombes Nov 22 '11 at 21:30
  • I already have a `BindingList`as the listbox DataSource, but no `BindingSource` - what is the benefit of having this two stage binding? Adding the `BindingSource` still does not work though. the `ListChanged` event is being raised, but `BindingComplete` is not. Is there another step that I need to perform? – Mark Nov 23 '11 at 10:13
  • 1
    OK, I based my previous statements on a code, which did other things as well, what caused confusion. I decided to make a full example in order to create clear facts (see EDIT #2). Mark you are right, if a `BindingList` is used, no `BindingSource` is required. The example uses solely a `BindingList`. – Olivier Jacot-Descombes Nov 23 '11 at 14:26
  • Thank you very much for the additional details! – Mark Nov 28 '11 at 09:12
  • Wow - can't believe Mark never gave you an up-vote! Very helpful stuff. – Aaron C Mar 11 '12 at 03:38
1

yes, if you use WPF it is quite easy to do this. All you have to do is make a different DataTemplate for your different types.

MSDN for data templates
Dr. WPF for Items Control & Data Templates

Muad'Dib
  • 28,542
  • 5
  • 55
  • 68
  • Unfortunately, I am on WinForms. Sorry for not saying in my original post. But the links look great for WPF - exactly the sort of thing I am trying to do. – Mark Nov 17 '11 at 10:14