如果我們的 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 裡面的資料就能順利地更新到畫面上。