2
\$\begingroup\$

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?

asked Dec 3, 2012 at 19:42
\$\endgroup\$
2
  • \$\begingroup\$ The ListView control is capable of binding to generic data to display them through properties, DataSource, ShowMember and ValueMember. \$\endgroup\$ Commented Nov 26, 2013 at 10:13
  • \$\begingroup\$ @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). \$\endgroup\$ Commented Nov 26, 2013 at 15:15

2 Answers 2

1
\$\begingroup\$

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..)

answered Dec 4, 2012 at 0:33
\$\endgroup\$
8
  • \$\begingroup\$ It's a WinForm app, so markup isn't really an option. \$\endgroup\$ Commented Dec 4, 2012 at 14:28
  • \$\begingroup\$ 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. \$\endgroup\$ Commented Dec 5, 2012 at 8:18
  • \$\begingroup\$ 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. \$\endgroup\$ Commented Dec 5, 2012 at 17:03
  • \$\begingroup\$ 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? \$\endgroup\$ Commented Dec 6, 2012 at 9:52
  • \$\begingroup\$ 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. \$\endgroup\$ Commented Dec 6, 2012 at 14:36
1
\$\begingroup\$

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*/);
answered Dec 4, 2012 at 12:56
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.