Take the 2-minute tour ×
Code Review Stack Exchange is a question and answer site for peer programmer code reviews. It's 100% free, no registration required.

I'm in the midst of writing a custom ListView for our application.

In the process of getting this question answered, I realized I needed to separate my column-generation logic from the data population. This led to the need to preserve the ColumnDataBuilder<TDATA> builder object that I used to create the columns, so that I could keep track of the type of my data.

Ideally, usage will look something like this:

builder = MyListView.ColumnsFor<ReceiptHeaderForShipping>();
        mlvShippingList.createColumns(_builder.Create("Receipt", 60, x => x.receiptNumber),
                                      _builder.Create("Cust", 100, x => x.customer.LastName + ", " + x.customer.FirstName),
                                      _builder.Create("Total", 60, MyListView.ColumnType.Currency, x => x.subTotal)
);

And then later

mlvShippingList.populateData(_builder, data);

In addition, the MyListView will have to know what type of data it was built with, in order to know what type of ColumnDataBuilder the IMyColumnData comes from, so that it can cast to it in order to sort.


From MyListView:

public partial class MyListView : ListView
{
    public static ColumnDataBuilder<T> ColumnsFor<T>(IEnumerable<T> data)
    {
        return new ColumnDataBuilder<T>();
    }
    public void populateFromData<TDATA>(IEnumerable<TDATA> data, params ColumnDataBuilder<TDATA>.IMyListViewColumnData[] columns)
    {
        createColumns(columns);
        populateData(data, columns);
    }
    public void createColumns<TDATA>(params ColumnDataBuilder<TDATA>.IMyListViewColumnData[] columns)
    {
        Columns.Clear();
        foreach (var col in columns)
        {
            var ch = new ColumnHeader
                         {
                             Text = col.Name,
                             Width = col.Width,
                             Tag = col,
                         };
            // Other formatting goes here
            this.Columns.Add(ch);
        }
    }
    private void populateData<TDATA>(IEnumerable<TDATA> data, params ColumnDataBuilder<TDATA>.IMyListViewColumnData[] columns)
    {
        Items.Clear();
        var parsedData = data.Select(row => CreateListViewItem(columns, row));
        this.Items.AddRange(parsedData.ToArray());
    }

    public class ColumnDataBuilder<T>
    {
        internal List<IMyListViewColumnData> columns = new List<IMyListViewColumnData>(); 

        public interface IMyListViewColumnData
        {
            string Name { get; }
            int Width { get; }
            ColumnType Type { get; }
            string GetDataString(T dataRow);
        }

        public delegate TOut FormatData<out TOut>(T dataIn);

        public class MyListViewColumnData<TOut> : IMyListViewColumnData
        {
            public string Name { get; private set; }
            public int Width { get; private set; }
            public ColumnType Type { get; private set; }
            private readonly FormatData<TOut> _dataFormatter;

            public MyListViewColumnData(string name, int width, ColumnType type, FormatData<TOut> dataFormater)
            {
                _dataFormatter = dataFormater;
                Type = type;
                Width = width;
                Name = name;
            }

            public string GetDataString(T dataRow)
            {
                object data = _dataFormatter(dataRow);

                switch (Type)
                {
                    case ColumnType.String:
                    case ColumnType.Integer:
                    case ColumnType.Decimal:
                        return data.ToString();
                    case ColumnType.Date:
                        return ((DateTime)data).ToShortDateString();
                    case ColumnType.Currency:
                        return ((decimal)data).ToString("c");
                    case ColumnType.Boolean:
                        return (bool)data ? "Y" : "N";
                    default:
                        throw new ArgumentOutOfRangeException();
                }
            }
        }

        #region Factory Methods

        public IMyListViewColumnData Create<TOut>(string name, int width, ColumnType type, FormatData<TOut> dataFormater)
        {
            var col = new MyListViewColumnData<TOut>(name, width, type, dataFormater);
            columns.Add(col);
            return col;
        }

        public IMyListViewColumnData Create(string name, int width, FormatData<DateTime> dataFormater)
        {
            return Create(name, width, ColumnType.Date, dataFormater);
        }

        // More type-specific factories go here

        #endregion

    }

}

Am I totally off base at this point? Is this a reasonable track to go down?

share|improve this question
    
The ListView control is capable of binding to generic data to display them through properties, DataSource, ShowMember and ValueMember. –  Myron Nov 26 '13 at 10:13
    
@Myron - That's a proprietary component. The standard .NET ListView doesn't have a DataSource property. It's also on a broken website that you can't actually purchase it from (or even download the trial). –  Bobson Nov 26 '13 at 15:15
add comment

2 Answers

up vote 1 down vote accepted

I've done something like this a few times (a lot even), but most times it was a complete waste of time. You will probably not earn the time you save by not doing WebForms markup, even if it's terribly boring. If you have like 100-200 entity types to display in similar lists, it might be a useful path, but if it's 10 - go with good old aspx/ascx markup. If you don't need a lot of business rules and/or scalability in some form, go for markup only with SqlDataSource and WYSIWYG listviews. If you need a fancy UI, go with Telerik.

Given you actually have multiple customers with different list needs for the same entities in the same product, this might be "the only" way, though.

With regards to your code, it seems slim and clean enough. But beware when going further with this - you might get a lot of conflicting requirements. As you say, it's a good thing to separate responsibilities, so keep doing that if you really want to make something "re-usable". (It won't be, but still..)

share|improve this answer
    
It's a WinForm app, so markup isn't really an option. –  Bobson Dec 4 '12 at 14:28
    
Hmm, I see - but wysiwyg is still an option, as long as you separate out the fetching logic, you can use databinding and design time layout of the grids. –  Lars-Erik Dec 5 '12 at 8:18
    
ListView doesn't support data binding, and the class I'd be binding to uses fields instead of properties for legacy reasons, so it can't be bound anyway. Highly frustrating. I wish I could just databind directly. –  Bobson Dec 5 '12 at 17:03
    
OK, sorry for my rusty winforms. Really a web guy. Still, make absolutely sure you're using the right controls for the right job. Do you really really need to use the ListView with the limitations it imposes on you? Are there third party alternatives? –  Lars-Erik Dec 6 '12 at 9:52
    
No worries. Unfortunately, because of the legacy reasons, ListView is the right control. There are a few third party alternatives, but all the ones I've found fall short in one way or another. Rolling my own seemed easiest. –  Bobson Dec 6 '12 at 14:36
show 3 more comments

Not sure I would ever do smth like this for my own production, but here is the code that (IMHO) would clean things up...

Implementation of MyListView:

public interface IListViewDefinition<TData>
{
    IMyListViewColumn<TData>[] GetColumns(); //Just in case...
    IListViewDefinition<TData> AddColumn<TOut>(string name, int width, ColumnType type, Converter<TData, TOut> dataFormater);
    void PopulateData(IEnumerable<TData> data);
}

public interface IMyListViewColumn<in TData>
{
    string Name { get; }
    int Width { get; }
    ColumnType Type { get; }
    string GetDataString(TData dataRow);
}

public static class ListViewBuilderExtensions
{
    public static IListViewDefinition<TData> AddColumn<TData>(this IListViewDefinition<TData> instance, string name, int width, Converter<TData, DateTime> dataFormater)
    {
        return instance.AddColumn(name, width, ColumnType.Date, dataFormater);
    }
}

public partial class MyListView : ListView
{
    public IListViewDefinition<TData> SetupListViewFor<TData>()
    {
        return new ListViewDefinition<TData>(this);
    }

    private void CreateColumns<TData>(IEnumerable<IMyListViewColumn<TData>> columns)
    {
        Columns.Clear();
        foreach (var col in columns)
        {
            var ch = new ColumnHeader
            {
                Text = col.Name,
                Width = col.Width,
                Tag = col,
            };
            // Other formatting goes here
            this.Columns.Add(ch);
        }
    }

    private void PopulateData<TData>(IEnumerable<TData> data, IList<IMyListViewColumn<TData>> columns)
    {
        Items.Clear();
        var parsedData = data.Select(row => CreateListViewItem(columns, row));
        Items.AddRange(parsedData.ToArray());
    }

    private class MyListViewColumn<TData, TOut> : IMyListViewColumn<TData>
    {
        public string Name { get; private set; }
        public int Width { get; private set; }
        public ColumnType Type { get; private set; }
        private readonly Converter<TData, TOut> _dataFormatter;

        public MyListViewColumn(string name, int width, ColumnType type, Converter<TData, TOut> dataFormater)
        {
            //TODO: check compatibility of type and TOut
            _dataFormatter = dataFormater;
            Type = type;
            Width = width;
            Name = name;
        }

        public string GetDataString(TData dataRow)
        {
            object data = _dataFormatter(dataRow);

            switch (Type)
            {
                case ColumnType.String:
                case ColumnType.Integer:
                case ColumnType.Decimal:
                    return data.ToString();
                case ColumnType.Date:
                    return ((DateTime)data).ToShortDateString();
                case ColumnType.Currency:
                    return ((decimal)data).ToString("c");
                case ColumnType.Boolean:
                    return (bool)data ? "Y" : "N";
                default:
                    throw new ArgumentOutOfRangeException();
            }
        }
    }

    private class ListViewDefinition<TData> : IListViewDefinition<TData>
    {
        private readonly List<IMyListViewColumn<TData>> _columns = new List<IMyListViewColumn<TData>>();
        private readonly MyListView _myListView;

        public ListViewDefinition(MyListView myListView)
        {
            _myListView = myListView;
        }

        public IMyListViewColumn<TData>[] GetColumns()
        {
            return _columns.ToArray();
        }

        public IListViewDefinition<TData> AddColumn<TOut>(string name, int width, ColumnType type, Converter<TData, TOut> dataFormater)
        {
            _columns.Add(new MyListViewColumn<TData, TOut>(name, width, type, dataFormater));
            return this;
        }

        public void PopulateData(IEnumerable<TData> data)
        {
            _myListView.CreateColumns(_columns);
            _myListView.PopulateData(data, _columns);
        }
    }
}

The usage is like following

var viewDefinition = new MyListView().SetupListViewFor<ReceiptHeaderForShipping>();
viewDefinition.AddColumn("Receipt", 60, x => x.receiptNumber)
    .AddColumn("Cust", 100, x => x.customer.LastName + ", " + x.customer.FirstName)
    .AddColumn("Total", 60, MyListView.ColumnType.Currency, x => x.subTotal);

viewDefinition.PopulateData(/*your collection of ReceiptHeaderForShipping*/);
share|improve this answer
add comment

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.