[廚餘回收] 在 Windows Forms 做控制項的資料綁定後,發生「跨執行緒作業無效」的錯誤。

如果我們的 Data Source 是非同步更新的話,那麼我們就很容易收到下面的錯誤訊息。

跨執行緒作業無效: 存取控制項 'xxx' 時所使用的執行緒與建立控制項的執行緒不同。(Cross-thread operation not valid: Control 'xxx' accessed from a thread other than the thread it was created on.)

一般遇到這個情況,我們通常就是判斷 Control.InvokeRequired 屬性,然後改用 Control.Invoke()Control.BeginInvoke() 方法來修改控制項的屬性,如果是在有資料綁定的情況呢?怎麼解決這個跨執行緒的問題?

ISynchronizeInvoke

Control.Invoke() 及 Control.BeginInvoke() 是實作 ISynchronizeInvoke 這個介面而來的,意謂著只要實作了 ISynchronizeInvoke 的元件,就能在有需要的時候建立委派,進行同步呼叫。

接下來,我們就延續前一篇文章的範例,改寫一下綁定用的資料模型的程式碼,把 ISynchronizeInvoke 在建構式傳進去,並且修改 OnPropertyChanged() 方法。

public class MyData : INotifyPropertyChanged
{
    private readonly ISynchronizeInvoke synchronizeInvoke;
    private int id;
    private string name;

    public MyData(ISynchronizeInvoke synchronizeInvoke = null)
    {
        this.synchronizeInvoke = synchronizeInvoke;
    }

    public event PropertyChangedEventHandler PropertyChanged;

    public int Id
    {
        get => this.id;
        set
        {
            if (value == this.id) return;

            this.id = value;
            this.OnPropertyChanged();
        }
    }

    public string Name
    {
        get => this.name;
        set
        {
            if (value == this.name) return;

            this.name = value;
            this.OnPropertyChanged();
        }
    }

    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        if (this.PropertyChanged == null) return;

        if (this.synchronizeInvoke != null && this.synchronizeInvoke.InvokeRequired)
        {
            this.synchronizeInvoke.BeginInvoke(this.PropertyChanged, new object[] { this, new PropertyChangedEventArgs(propertyName) });
        }
        else
        {
            this.PropertyChanged.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

BindingList

單一個資料模型會發生「跨執行緒作業無效」的問題,BindingList 一樣也會,只不過它會出現的可能不是「跨執行緒作業無效」,而是「集合已修改; 列舉作業可能尚未執行」,或是異動的資料沒有出現在畫面上。

這個情況我們一樣需要 ISynchronizeInvoke 物件,並且建立委派,進行同步呼叫,下面我們就繼承 BindingList,新增一個 DelegableBindingList,覆寫 OnListChanged() 方法。

public class DelegableBindingList<T> : BindingList<T>
{
    private readonly ISynchronizeInvoke synchronizeInvoke;

    public DelegableBindingList(ISynchronizeInvoke synchronizeInvoke)
    {
        this.synchronizeInvoke = synchronizeInvoke;
    }

    protected override void OnListChanged(ListChangedEventArgs e)
    {
        if (this.synchronizeInvoke != null && this.synchronizeInvoke.InvokeRequired)
        {
            this.synchronizeInvoke.BeginInvoke(new Action<ListChangedEventArgs>(base.OnListChanged), new object[] { e });
        }
        else
        {
            base.OnListChanged(e);
        }
    }
}

這樣做之後,BindingList 裡面的資料就能順利地更新到畫面上。

參考資料

C# 指南 ASP.NET 教學 ASP.NET MVC 指引
Azure SQL Database 教學 SQL Server 教學 Xamarin.Forms 教學