WPF中如何通過數據綁定使用ProcessBar控制項?

剛開始學WPF,啥都不懂。

我想處理一大批文件,然後用一個進度條指示文件的處理的進度。竟然要用進度條的任務,肯定是很慢的,所以肯定要用一個線程來處理,以防窗體假死。然後我想用剛學的數據綁定,於是我寫了一下代碼:

public class ViewModel : DependencyObject
{
/// & /// 當前已處理的文件數(自定義依賴屬性)
/// &
public int CurrentNum
{
get { return (int)GetValue(CurrentNumProperty); }
set { SetValue(CurrentNumProperty, value); }
}

// Using a DependencyProperty as the backing store for CurrentFileNum. This enables animation, styling, binding, etc...
public static readonly DependencyProperty CurrentNumProperty =
DependencyProperty.Register("CurrentFileNum", typeof(int), typeof(DataVM), new PropertyMetadata(0));

public void Parse()
{
Thread th = new Thread(() =&>
{
for (int i = 0; i &< filesPath.Length; i++) { // Do Something this.CurrentNum++; } }); th.Start(); } }

然後,我new了一個ViewModel實例,並放在了窗體的上下文中。並且將CurrentNum屬性綁定到了ProcessBar控制項的Value上。

但是運行之後,拋出異常:調用線程無法訪問此對象,因為另一個線程擁有該對象。

後來查了相關資料,說得用Dispatcher.BeginInvoke(),所以我把上面那句改寫為:

Dispatcher.BeginInvoke(() =&>
{
for (int i = 0; i &< filesPath.Length; i++) { // Do Something this.CurrentNum++; } });

然而這麼寫後,根本就不是非同步的,窗體依然假死了!(Invoke()也試過了,Task也試過了)感覺好無助……

另外,書上說依賴屬性是線程安全的,這是什麼意思?難道依賴屬性不許允兩個線程調用嗎?


題主你的DependencyProperty的名字和你的屬性名怎麼不一樣。還有,不要在UI線程以外訪問你的DependencyProperty。但是如果你需要假裝它是非同步的話,你要把你的循環改成假遞歸,然後每一次this.CurrentNum++之後把自己再射進Dispatcher:譬如說

void Fuck(int i, string[] filePaths)
{
if(i&
{
this.CurrentNum++;
ThreadPool.QueueUserWorkItem(()=&>
{
i++;
Fuck(i, filePaths);
});
});
}
}

void Fuck(string[] filePaths)
{
ThreadPool.QueueUserWorkItem(()=&>
{
Fuck(0, filePaths);
});
}

其實這是一個在async-await出現之前,任何語言的合格的程序員都知道的一個手法(譬如說古時候的前端們)。現在的年輕人啊。


只需要把currentNum++這一個操作放到Dispatcher,Dispatcher就是把裡面的操作調度到Dispatcher所在的線程執行,如果你裡面的doSomething很耗時的話ui線程就會阻塞了。


先說問題,因為你的currentNum綁定到界面上了,因此currentNum++ 會觸發 RaisePropertyChanged() 事件,該事件會觸發UI更新。這裡補充一點綁定的說明。在currentnum變化時會觸發也就是RaisePropertyChanged()事件,該事件的具體實現是操作ui對象更新。bind就是把具體實現註冊到事件上。需要注意的是該事件就是同步調用。

如果 currentNum++ 是在非UI線程調用,那麼就是你在該線程中觸發了UI更新,因此運行時,會拋異常調用線程無法訪問此對象,因為另一個線程擁有該對象。注意:此處異常中的對象指的是UI對象,你不能通過非UI線程操作UI對象,雖然其實你沒想操作。這個異常的問題你可以通過在彈出異常時查看調用堆棧來驗證。查看currentNum++ 後的調用堆棧。

解決辦法有兩種,第一種主動刷新,第二種被動委託UI線程。

第一,被動委託:既然不能非UI線程操作UI對象,那麼我們可以通過委託UI線程做,因此,委託UI線程去做的是刷新界面的代碼而非你的全部業務代碼。你問題裡面更改為委託之後的問題就在這裡,你把所有事情都委託UI線程做了。記住:UI線程同一時間也只能做一件事,你如果委託它去做你的事情,他就不能刷新UI,因此就會出現假死情況。所以正確的方式是:在你的非同步線程中,所有可能導致UI操作的代碼部分採取委託。所以至少應該改成如下代碼。

for (int i = 0; i &< filesPath.Length; i++)

{

// Do Something

//處理你的文件,注意,處理文件的過程不能委託ui線程,因此放在外面。

Dispatcher.BeginInvoke(() =&>

{

//此處應只放界面操作代碼,或引發界面操作的代碼.

this.CurrentNum++;

});

再說說主動刷新。

我之前做過一個數據處理,平均每1s對一個int變數賦值1000次以上,我採用的是timer定期刷新,因為不能直接在每次變數變化時觸發RaisePropertyChanged() 讓界面刷新,即時是非同步委託UI線程去刷新,那1s 刷新1000次 UI是受不了的。

因此我在vm層定義timer,定期RaisePropertyChanged() 從model層取數刷新UI。 而數據處理線程操作model層。這樣,timer是UI線程創建的,因此可以調用UI對象。如果需要具體代碼請留言,我可能得過倆天寫給你。

最後說說 invoke begininvoke 還有task。

首先,我更推薦使用task,因為線程能夠更安全的操作以及退出。另外注意你之前的thread並沒有設置background ,並且沒有安全退出以及異常退出等操作,這些也是不好的習慣。

invoke和begininvoke的區別在於對於調用invoke和begininvoke的線程來說,invoke需要等待返回值,也就是卡在invoke這句話處,而調用begininvoke會直接執行下一句代碼,它的返回值是通過回調函數來獲取的。這個特點你可以通過調試就能發現。但是值得注意的是,調用了begininvoke委託UI線程執行代碼,並不一定是立刻生效的。就像我剛才說我那個例子,1s刷新1000次,UI線程也是要一個一個執行的。因此不是立刻生效,持續時間過長後會導致有延遲甚至崩潰。

在你的問題當中,我當然推薦使用begininvoke,因為不能讓UI的更新影響處理文件的效率,但是如果你的更新速度太快,還是推薦我首先提到的主動更新。

期待更好的解決方式。


說一下問題所在。Dispatcher.BeginInvoke()這個方法的意義是將一個委託丟給dispatcher所屬的線程去處理,而你題目描述里的寫法中,Do Something放到了委託里,亦即工作還是在UI線程里進行的,所以會假死。

跨線程訪問UI時需要注意,把耗時任務放在線程里,而把那些需要訪問UI的動作放在BeginInvoke/Invoke委託里以避免訪問不到。Invoke的意義只是讓你給UI線程一個任務,依舊不允許其他線程訪問UI。

順帶一提,BeginInvoke和Invoke的區別僅僅在於前者立即返回,後者會等待返回。


推薦閱讀:

託管應用程序的性能實際上超過了非託管應用程序?
如何學習C#編程?
有哪些原因會導致HRESULT:0x800704A6提示已經計劃系統關機,除了更新系統或瀏覽器?
如何看待 Unity 與 Xamarin 均僅支持 C# 而不支持 VB.NET?
entity framework中怎麼通過lambda表達式生成sql語句的?

TAG:編程 | C# | WindowsPresentationFoundationWPF | 線程 | 數據綁定 |