开发者

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.

Is it possible to bind a List to a ListView in WinForms?

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())
};
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜