即使使用锁,BindingList.Add()也不能跨线程工作

时间:2016-08-20 02:37:37

标签: c# multithreading winforms listbox bindinglist

我刚学习C#/ .NET,我遇到了这个问题。

所以在我的解决方案中,我有两个项目:winforms UI和带逻辑的dll。在dll中我有BindingList,它为UI中的listBox提供数据源。

UI:

public partial class Form1 : Form
{
    private Class1 _class1;

    public Form1()
    {
        InitializeComponent();
        _class1 = new Class1(); // logic class insatce
        listBox1.DataSource = _class1.BindingList;
    }

    private void button1_Click(object sender, EventArgs e)
    {
        _class1.Add();
    }

    private void button2_Click(object sender, EventArgs e)
    {
         _class1.Remove();
    }
}

逻辑课程:

public class Class1
{
    public BindingList<string> BindingList { get; set; } = new BindingList<string>() ;

    public void Add()
    {
        var th = new Thread(() =>
        {
            lock (BindingList)
            {
                BindingList.Add("1");
            }
        }) {IsBackground = true};
        th.Start();
        // works fine
        //BindingList.Add("1");
    }

    public void Remove()
    {
        if (BindingList.Count > 1)
        {
            BindingList.RemoveAt(0);
        }
    }
}

所以如果我只是运行解决方案(ctrl + F5)的问题一切正常,但在调试模式(F5)中按下按钮时没有任何反应。我发现的所有答案都说:“使用锁定”所以我使用了锁定,而列表框仍然没有对添加元素列表做出反应。请帮助我做错我或错过的地方。

对不起我的英语。

1 个答案:

答案 0 :(得分:4)

首先,要明确:您可能需要也可能不需要在此使用lock。这取决于是否实际上有两个或多个线程同时访问BindingList<T>对象 ,即字面上同时(例如,两个或多个线程将项添加到列表中,或者一个线程)添加项目而另一个尝试从列表中读取)。在您的代码示例中,似乎并非如此,因此没有必要。无论如何,lock语句与解决您所询问的特定问题所需的内容完全不同,并且在任何情况下仅在线程在同一对象上协同使用lock时才起作用(如果仅一个线程调用lock,这没有用。)


基本问题是ListBox无法响应来自BindingList的事件,而这些事件是在UI线程之外引发的。通常,对此的解决方案是调用Control.Invoke()或类似的操作来在UI线程中执行列表修改操作。但在您的情况下,拥有BindingList的类不是UI对象,因此自然无法访问Control.Invoke()方法。

恕我直言,最佳解决方案在所涉及的UI对象中保留UI线程知识。但是这样做需要让Class1对象将列表中的至少一些控件移交给该UI对象。其中一种方法是将事件添加到Class1对象:

public class AddItemEventArgs<T> : EventArgs
{
    public T Item { get; private set; }

    public AddItemEventArgs(T item)
    {
        Item = item;
    }
}

public class Class1
{
    public EventHandler<AddItemEventArgs<string>> AddItem;

    public BindingList<string> BindingList { get; set; }

    public Class1()
    {
        // Sorry, old-style because I'm not using C# 6 yet
        BindingList = new BindingList<string>();
    }

    // For testing, I prefer unique list items
    private int _index;

    public void Add()
    {
        var th = new Thread(() =>
        {
            string item = (++_index).ToString();

            OnAddItem(item);
        }) { IsBackground = true };
        th.Start();
    }

    public void Remove()
    {
        if (BindingList.Count > 1)
        {
            BindingList.RemoveAt(0);
        }
    }

    private void OnAddItem(string item)
    {
        EventHandler<AddItemEventArgs<string>> handler = AddItem;

        if (handler != null)
        {
            handler(this, new AddItemEventArgs<string>(item));
        }
    }
}

然后在Form1

public partial class Form1 : Form
{
    private Class1 _class1;

    public Form1()
    {
        InitializeComponent();
        _class1 = new Class1(); // logic class instance
        _class1.AddItem += (sender, e) =>
        {
            Invoke((MethodInvoker)(() => _class1.BindingList.Add(e.Item)));
        };
        listBox1.DataSource = _class1.BindingList;
    }

    private void button1_Click(object sender, EventArgs e)
    {
        _class1.Add();
    }

    private void button2_Click(object sender, EventArgs e)
    {
        _class1.Remove();
    }
}

此主题的变体是Class1中有两种不同的“添加”方法。第一个是你现在拥有的,最终使用一个线程。第二个是从UI线程调用 required 的那个,实际上会添加该项。在表单中的AddItem事件处理程序中,不是直接将项添加到列表中,而是调用第二个“add”方法来为表单执行此操作。

哪个最好取决于您在Class1中需要多少抽象。如果您试图将列表及其操作隐藏在其他类中,那么变化会更好。但是,如果您不介意从Class1代码以外的其他位置更新列表,则上面的代码示例应该没问题。

另一种方法是使您的Class1对象具有线程感知功能,类似于例如BackgroundWorker有效。您可以通过在创建SynchronizationContext对象时捕获线程的当前Class1来执行此操作(假设在要返回的线程中创建了Class1对象,添加项目)。然后在添加项目时,将该上下文对象用于添加。

看起来像这样:

public class Class1
{
    public BindingList<string> BindingList { get; set; }

    private readonly SynchronizationContext _context = SynchronizationContext.Current;

    public Class1()
    {
        BindingList = new BindingList<string>();
    }

    private int _index;

    public void Add()
    {
        var th = new Thread(() =>
        {
            string item = (++_index).ToString();

            _context.Send(o => BindingList.Add(item), null);
        }) { IsBackground = true };
        th.Start();
    }

    public void Remove()
    {
        if (BindingList.Count > 1)
        {
            BindingList.RemoveAt(0);
        }
    }
}

在此版本中,不需要更改Form1

这个基本方案有很多变化,包括一些将逻辑放入专用BindingList<T>子类的变体。例如(举几个例子):
Cross-Thread Form Binding - Can it be done?
BindingList<> ListChanged event

最后,如果你想真正一起破解,你可以强制整个绑定在列表更改时重置。在这种情况下,您无需更改Class1,但需要更改Form1

public partial class Form1 : Form
{
    private Class1 _class1;

    public Form1()
    {
        bool adding = false;

        InitializeComponent();
        _class1 = new Class1(); // logic class instance
        _class1.BindingList.ListChanged += (sender, e) =>
        {
            Invoke((MethodInvoker)(() =>
            {
                if (e.ListChangedType == ListChangedType.ItemAdded && !adding)
                {
                    // Remove and re-insert newly added item, but on the UI thread
                    string value = _class1.BindingList[e.NewIndex];

                    _class1.BindingList.RemoveAt(e.NewIndex);
                    adding = true;
                    _class1.BindingList.Insert(e.NewIndex, value);
                    adding = false;
                }
            }));
        };
        listBox1.DataSource = _class1.BindingList;
    }

    // ...
}

我并不是真的建议这种做法。但如果你无法改变Class1,那就是你能做的最好的事情。