Is it possible to bind a List to a ListView in WinForms?
I'd like to bind a ListView to a List<string>
. I'm using this code:
somelistview.DataBindings.Add ("Items", someclass, "SomeList");
I'm getting this exception: Cannot bind to property 'Items' because it is read-only.
I d开发者_如何学Goon't know how I should bind if the Items property is readonly?
The ListView class does not support design time binding. An alternative is presented in this project.
I use the following technique to bind data to a ListView.
It supports proper (not text-based) sorting. In the case above, by string, DateTime and integer.
The ListView above was generated with this code:
var columnMapping = new List<(string ColumnName, Func<Person, object> ValueLookup, Func<Person, string> DisplayStringLookup)>()
{
("Name", person => person.Name, person => person.Name),
("Date of birth", person => person.DateOfBirth, person => $"{person.DateOfBirth:dd MMM yyyy}"),
("Height", person => person.HeightInCentimetres, person => Converter.CentimetresToFeetInchesString(person.HeightInCentimetres))
};
var personListview = new ListViewEx<Person>(columnMapping)
{
FullRowSelect = true,
View = View.Details,
Left = 20,
Top = 20,
Width = 500,
Height = 300,
};
var people = new[]
{
new Person("Cathy Smith", DateTime.Parse("1980-05-15"), 165),
new Person("Bill Wentley", DateTime.Parse("1970-10-30"), 180),
new Person("Alan Bridges", DateTime.Parse("1990-03-22"), 190),
};
personListview.AddRange(people);
Controls.Add(personListview);
In the column mapping, you'll notice that you have to specify how to get the item's value (for sorting) as well as a string (for displaying).
Full source:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
namespace GUI
{
public class ListViewEx<T> : ListView
{
public ListViewEx(IList<(string ColumnName, Func<T, object> ValueLookup, Func<T, string> DisplayStringLookup)> columnInfo) : base()
{
ColumnInfo = columnInfo;
DoubleBuffered = true;
//Create the columns
columnInfo
.Select(ci => ci.ColumnName)
.ToList()
.ForEach(columnName =>
{
var col = Columns.Add(columnName);
col.Width = -2;
});
//Add the sorter
lvwColumnSorter = new ListViewColumnSorter<T>(columnInfo);
ListViewItemSorter = lvwColumnSorter;
ColumnClick += ListViewEx_ColumnClick;
}
IList<(string ColumnName, Func<T, object> ValueLookup, Func<T, string> DisplayStringLookup)> ColumnInfo { get; }
private readonly ListViewColumnSorter<T> lvwColumnSorter;
public void Add(T item)
{
var lvi = Items.Add("");
lvi.Tag = item;
RefreshContent();
}
public void AddRange(IList<T> items)
{
foreach (var item in items)
{
Add(item);
}
}
public void Remove(T item)
{
if (item == null) return;
var listviewItem = Items
.Cast<ListViewItem>()
.Select(lvi => new
{
ListViewItem = lvi,
Obj = (T)lvi.Tag
})
.FirstOrDefault(lvi => item.Equals(lvi.Obj))
.ListViewItem;
Items.Remove(listviewItem);
RefreshContent();
}
public List<T> GetSelectedItems()
{
var result = SelectedItems
.OfType<ListViewItem>()
.Select(lvi => (T)lvi.Tag)
.ToList();
return result;
}
public void RefreshContent()
{
var columnsChanged = new List<int>();
Items
.Cast<ListViewItem>()
.Select(lvi => new
{
ListViewItem = lvi,
Obj = (T)lvi.Tag
})
.ToList()
.ForEach(lvi =>
{
//Update the contents of this ListViewItem
ColumnInfo
.Select((column, index) => new
{
Column = column,
Index = index
})
.ToList()
.ForEach(col =>
{
var newDisplayValue = col.Column.DisplayStringLookup(lvi.Obj);
if (lvi.ListViewItem.SubItems.Count <= col.Index)
{
lvi.ListViewItem.SubItems.Add("");
}
var subitem = lvi.ListViewItem.SubItems[col.Index];
var oldDisplayValue = subitem.Text ?? "";
if (!oldDisplayValue.Equals(newDisplayValue))
{
subitem.Text = newDisplayValue;
columnsChanged.Add(col.Index);
}
});
});
columnsChanged.ForEach(col => { Columns[col].Width = -2; });
//AutoResizeColumns(ColumnHeaderAutoResizeStyle.ColumnContent);
//AutoResizeColumns(ColumnHeaderAutoResizeStyle.HeaderSize);
}
private void ListViewEx_ColumnClick(object sender, ColumnClickEventArgs e)
{
if (e.Column == lvwColumnSorter.ColumnToSort)
{
if (lvwColumnSorter.SortOrder == SortOrder.Ascending)
{
lvwColumnSorter.SortOrder = SortOrder.Descending;
}
else
{
lvwColumnSorter.SortOrder = SortOrder.Ascending;
}
}
else
{
lvwColumnSorter.ColumnToSort = e.Column;
lvwColumnSorter.SortOrder = SortOrder.Ascending;
}
Sort();
}
}
public class ListViewColumnSorter<T> : IComparer
{
public ListViewColumnSorter(IList<(string ColumnName, Func<T, object> ValueLookup, Func<T, string> DisplayStringLookup)> columnInfo)
{
ColumnInfo = columnInfo;
}
public int Compare(object x, object y)
{
if (x == null || y == null) return 0;
int compareResult;
var listviewX = (ListViewItem)x;
var listviewY = (ListViewItem)y;
var objX = (T)listviewX.Tag;
var objY = (T)listviewY.Tag;
if (objX == null || objY == null) return 0;
var valueX = ColumnInfo[ColumnToSort].ValueLookup(objX);
var valueY = ColumnInfo[ColumnToSort].ValueLookup(objY);
compareResult = Comparer.Default.Compare(valueX, valueY);
if (SortOrder == SortOrder.Ascending)
{
return compareResult;
}
else if (SortOrder == SortOrder.Descending)
{
return -compareResult;
}
else
{
return 0;
}
}
public int ColumnToSort { get; set; } = 0;
public SortOrder SortOrder { get; set; } = SortOrder.Ascending;
public IList<(string ColumnName, Func<T, object> ValueLookup, Func<T, string> DisplayStringLookup)> ColumnInfo { get; }
}
}
Nice binding implementation for ListView
http://www.interact-sw.co.uk/utilities/bindablelistview/source/
Alternatively, you can use DataGridView if you want data binding. Using BindingList and BindingSource will update your DataGrid when new item is added to your list.
var barcodeContract = new BarcodeContract { Barcode = barcodeTxt.Text, Currency = currencyTxt.Text, Price = priceTxt.Text };
list.Add(barcodeContract);
var bindingList = new BindingList<BarcodeContract>(list);
var source = new BindingSource(bindingList, null);
dataGrid.DataSource = source;
And data model class
public class BarcodeContract
{
public string Barcode { get; set; }
public string Price { get; set; }
public string Currency { get; set; }
}
Adding to the answer by @Fidel
If you just want quick auto-mapped columns, add this code to the ListViewEx
class:
using System.Reflection;
public ListViewEx() : this(AutoMapColumns()) { }
private static List<(string ColumnName, Func<T, object> ValueLookup, Func<T, string> DisplayStringLookup)> AutoMapColumns()
{
var mapping = new List<(string ColumnName, Func<T, object> ValueLookup, Func<T, string> DisplayStringLookup)>();
var props = typeof(T).GetTypeInfo().GetProperties();
foreach (var prop in props)
{
mapping.Add((
prop.Name,
(T t) => prop.GetValue(t),
(T t) => prop.GetValue(t)?.ToString()
));
}
return mapping;
}
Alternative to Reflection
After some testing, I discovered that using Reflection like in the code above is much slower than direct property access.
In my test, I did 100,000,000 iterations of each. Reflection took 8.967 seconds, direct access took 0.465 seconds.
So I wrote this method to generate the code for the ListViewEx ColumnMapping.
// Given an object, generate columnMapping suitable for passing to the constructor of a ListViewEx control
// Usage: AutoMapColumns_CodeGen(new Person());
private static string AutoMapColumns_CodeGen<T>(T source)
{
var info = typeof(T).GetTypeInfo();
var props = info.GetProperties();
var columns = new List<string>();
foreach (var prop in props)
columns.Add($"\t(\"{prop.Name}\", o => o.{prop.Name}, o=> o.{prop.Name}?.ToString())");
string code = string.Join("\n",
$"var columnMapping = new List<(string ColumnName, Func<{info.Name}, object> ValueLookup, Func<{info.Name}, string> DisplayStringLookup)>() {{",
string.Join(",\n",columns),
"};"
);
return code;
}
Output
var columnMapping = new List<(string ColumnName, Func<Person, object> ValueLookup, Func<Person, string> DisplayStringLookup)>() {
("Name", o => o.Name, o=> o.Name?.ToString()),
("DateOfBirth", o => o.DateOfBirth, o=> o.DateOfBirth?.ToString()),
("HeightInCentimetres", o => o.HeightInCentimetres, o=> o.HeightInCentimetres?.ToString())
};
精彩评论