實現 INotifyPropertyChanged 接口可以在屬性更改后通知數據的使用者,這個相信大伙兒都知道。於是,有朋友會問:對於要實時顯示進度的情況,比如更新進度條,能用這個實現嗎?
當然是可以的,也很簡單,定義一個類,實現 INotifyPropertyChanged 接口,然后公開表示處理進度的屬性,並且在屬性更改后引發通知事件。
然后把該類的實例與進度條進行綁定即可,和一般的綁定差不多。不過,有一點需要強調:通常是把屬性更改通知發送給UI對象的,多數情況下,我們在處理一些耗時操作都會在另一個線程上執行,這就使得在實現這個接口時,引發PropertyChanged 事件的時候容易發生錯誤。為了避免錯誤發生,在實現接口時,應當用Dispatcher來引發事件,確保能在UI線程上執行。
一個比較不錯的方法,是先弄個抽象類,讓它實現 INotifyPropertyChanged 接口,后面你所定義的各種 Model 類就可以從這個抽象類派生,這樣會相當省事。
比如這樣:
public abstract class NotifyPropertyChangedBase : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; protected async void OnPropertyChanged([CallerMemberName] string propName = "") { await Window.Current.Dispatcher.RunAsync(CoreDispatcherPriority.High, () => { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName)); }); } }
因為在引發屬性更改通知時需要指定屬性名稱,咱們使用一個技巧,在 OnPropertyChanged 方法的參數上加一個 CallerMemberNameAttribute,這么一來,只要在被更改的屬性的set方法中調用這個方法,就會自動把屬性的名字傳給參數了,也不用我們手動輸,既免去繁雜工作,也不容易弄錯。注意參數在附加CallerMemberNameAttribute后一定要給它一個默認值,說白了就是讓這個參數變成可選參數,以方便運行時庫在運行階段修改它。
調用 Window.Current.Dispatcher.RunAsync 方法確保事件引發的代碼是在UI線程上執行的,就不會出現因交叉線程更改用戶界面而導致的異常。
好,上面說了一車的廢話,下面我們來定義一個表示進度數據的類,主要包括進度的最大值、最小值,以及當前進度,這個類從剛才定義的 NotifyPropertyChangedBase 類派生。
public sealed class ProgressData : NotifyPropertyChangedBase { private int _max, _min, _currvalue; public int Max { get { return _max; } set { if (value != _max) { _max = value; OnPropertyChanged(); } } } public int Min { get { return _min; } set { if (value != _min) { _min = value; OnPropertyChanged(); } } } public int CurrentValue { get { return _currvalue; } set { if (value != _currvalue) { _currvalue = value; OnPropertyChanged(); } } } }
在用戶界面上聲明 ProgressBar 控件,然后綁定到上面類型的屬性。
<ProgressBar Name="pb" Height="25" Margin="3,30,5,2" Maximum="{Binding Max}" Minimum="{Binding Min}" Value="{Binding CurrentValue}" SmallChange="1.0"/>
如何讓 ProgressData 實例與 ProgressBar 控件關聯呢,這好辦,因為有一個 DataContext 屬性,它可以賦任何類型的值,然后控件中的綁定會從該屬性的值中尋找數據。
下面我們來關聯一下。
m_progressdata = new ProgressData(); m_progressdata.Max = 100; m_progressdata.Min = 0; m_progressdata.CurrentValue = 0; this.pb.DataContext = m_progressdata;
注意看最后一行,不解釋。
然后定義一個基於 Task 的異步方法,來模擬在新線程上處理數據。
async Task TestSomethingAsync() { while (m_progressdata.CurrentValue < m_progressdata.Max) { m_progressdata.CurrentValue++; await Task.Delay(20); } }
隨后可以進行測試了。
private async void OnClick(object sender, RoutedEventArgs e) { m_progressdata.CurrentValue = m_progressdata.Min; Button btn = sender as Button; btn.IsEnabled = false; await TestSomethingAsync(); btn.IsEnabled = true; }
至此,就完成進度的綁定了,當你在線程中處理進度時,你可以不用管UI上用什么來顯示進度了,哪怕是用文本顯示,或用進度條來顯示,都無所謂,數據與界面分離。
看看效果,還是挺不錯的。
這種方法除了在UWP中可用,同樣適用於WPF項目,因為都是一脈相承的,所以老周一直堅持:只要WPF學好了,別的都好辦。