19

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 don't know how I should bind if the Items property is readonly?

5 Answers 5

13

The ListView class does not support design time binding. An alternative is presented in this project.

Sign up to request clarification or add additional context in comments.

Comments

6

I use the following technique to bind data to a ListView.

enter image description here

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; }
    }
}

8 Comments

Very useful! and really much better than use DataGridView for binding! Although Initialization is not so practical...
Tips for anyone that has any problem in adapt to regular WinForms: 1) In the Form's .cs file do: private List<(string ColumnName, Func<Person, object> ValueLookup, Func<Person, string> DisplayStringLookup)> _columnMappingForPerson;
2) Do all columnMapping initialization BEFORE InitializeComponent(); 3) In the designer.cs private void InitializeComponent() do: this.listViewPerson = new ListViewEx<Person>(_columnMappingForPerson);
But, of course, it is no real DataSource. For minor projects, it is Ok. For larger projects, I'm using @Petr Havlicek indication
I wrote some code to auto-map the columns: stackoverflow.com/a/70946578/740639 . @Fidel : It didn't seem appropriate to edit your answer with my code, but feel free to add it to your answer if you'd like,
|
3

Nice binding implementation for ListView

http://www.interact-sw.co.uk/utilities/bindablelistview/source/

Comments

3

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; }
}

2 Comments

Hi @ozgur, one question about your code. Where do you specify headers in the datagrid, i mean, how do you indicate that column1 is Barcode, column 2 is Price and so on?
I was using a DataGridView, but I switched to this because the DataGridView was super slow to populate/update. My data source only had 52 items.
2

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

Comments

Your Answer

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.