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?
2 Answers 2
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..)
-
\$\begingroup\$ It's a WinForm app, so markup isn't really an option. \$\endgroup\$Bobson– Bobson2012年12月04日 14:28:24 +00:00Commented 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\$Lars-Erik– Lars-Erik2012年12月05日 08:18:54 +00:00Commented 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\$Bobson– Bobson2012年12月05日 17:03:43 +00:00Commented 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\$Lars-Erik– Lars-Erik2012年12月06日 09:52:21 +00:00Commented 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\$Bobson– Bobson2012年12月06日 14:36:16 +00:00Commented Dec 6, 2012 at 14:36
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*/);
DataSource
property. It's also on a broken website that you can't actually purchase it from (or even download the trial). \$\endgroup\$