How to update a ListBox if an element was changed

2019-08-09 23:58发布

问题:

Hi,

I'm struggling a bit using the ListBox.DataSource and the INotifyPropertyChanged Interface. I checked several posts about this issue already but I cannot figure out, how to update the view of a ListBox if an element of the bound BindingList is changed.

I basically want to change the color of an IndexItem after the content has been parsed.

Here the relevant calls in my form:

btn_indexAddItem.Click += new EventHandler(btn_indexAddItem_Click);
lst_index.DataSource = Indexer.Items;
lst_index.DisplayMember = "Url";
lst_index.DrawItem += new DrawItemEventHandler(lst_index_DrawItem);

private void btn_indexAddItem_Click(object sender, EventArgs e)
{
    Indexer.AddSingleURL(txt_indexAddItem.Text);
}
private void lst_index_DrawItem(object sender, DrawItemEventArgs e)
{
    IndexItem item = lst_index.Items[e.Index] as IndexItem;
    if (item != null)
    {
        e.DrawBackground();
        SolidBrush brush = new SolidBrush((item.hasContent) ? SystemColors.WindowText : SystemColors.ControlDark);
        e.Graphics.DrawString(item.Url, lst_index.Font, brush, 0, e.Index * lst_index.ItemHeight);
        e.DrawFocusRectangle();
    }
}

Indexer.cs:

class Indexer
{
    public BindingList<IndexItem> Items { get; }
    private object SyncItems = new object();

    public Indexer()
    {
        Items = new BindingList<IndexItem>();
    }

    public void AddSingleURL(string url)
    {
        IndexItem item = new IndexItem(url);
        if (!Items.Contains(item))
        {
            lock (SyncItems)
            {
                Items.Add(item);
            }

            new Thread(new ThreadStart(() =>
            {
                // time consuming parsing
                Thread.Sleep(5000);
                string content = item.Url;

                lock (SyncItems)
                {
                    Items[Items.IndexOf(item)].Content = content;
                }
            }
            )).Start();
        }
    }
}

IndexItem.cs

class IndexItem : IEquatable<IndexItem>, INotifyPropertyChanged
{
    public int Key { get; }
    public string Url { get; }
    public bool hasContent { get { return (_content != null); } }

    private string _content;
    public string Content {
        get
        {
            return (hasContent) ? _content : "empty";
        }
        set
        {
            _content = value;
            ContentChanged();
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;
    private void ContentChanged()
    {
        if (this.PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs("Content"));
        }
    }

    public IndexItem(string url)
    {
        this.Key = url.GetHashCode();
        this.Url = url;
    }

    public override bool Equals(object obj)
    {
        return Equals(obj as IndexItem);
    }
    public override int GetHashCode()
    {
        return Key;
    }
    public bool Equals(IndexItem other)
    {
        if (other == null) return false;
        return (this.Key.Equals(other.Key)) ||
            ((hasContent || other.hasContent) && (this._content.Equals(other._content)));
    }
    public override string ToString()
    {
        return Url;
    }
}

Any ideas what went wrong and how to fix it? I'll appreciate any hint...

回答1:

It seems to me that the control should redraw when it raises the ListChanged event for that item. This will force it to do so:

lst_index.DrawItem += new DrawItemEventHandler(lst_index_DrawItem);
Indexer.Items.ListChanged += Items_ListChanged;

private void Items_ListChanged(object sender, ListChangedEventArgs e)
{
    lst_index.Invalidate(); // Force the control to redraw when any elements change
}

So why doesn't it do that already? Well, it seems that the listbox only calls DrawItem if both DisplayMember changed, and if the INotifyPropertyChanged event was raised from the UI thread. So this also works:

lock (SyncItems)
{
    // Hacky way to do an Invoke
    Application.OpenForms[0].Invoke((Action)(() =>
    {
        Items[Items.IndexOf(item)].Url += " "; // Force listbox to call DrawItem by changing the DisplayMember
        Items[Items.IndexOf(item)].Content = content;
    }));
}

Note that calling PropertyChanged on the Url is not sufficient. The value must actually change. This tells me that the listbox is caching those values. :-(

(Tested with VS2015 REL)