記筆記
俗話說:好記性不如爛筆頭。當(dāng)然,這并不是說我們的腦子不好使,也不是叫我們不要用腦子記東西,而是提醒我們解放腦力,讓大腦從事更有價值的思考。因此,這節(jié)課我們將會創(chuàng)建一個筆記本,用來記錄課堂重點,但是,我們需要什么樣的筆記本呢?我曾經(jīng)在《你的燈亮著嗎?》里讀到這樣一句話:如果某人能夠解決這個問題,但是他本人卻不會遇到這一問題時,那么你們首先要做的就是讓他也感受到這個問題。最近公司來了一批校招生,我找了個機會混進(jìn)去聽了一節(jié)入職前的技術(shù)培訓(xùn),我想知道在課堂上把手機掏出來記筆記是一種什么樣的感覺。
在課堂上,每當(dāng)我想記點什么時,就會不自覺地拿起紙筆而不是手機,而且,用手機記筆記遠(yuǎn)沒用紙筆來得隨意自如。隨后,我找了一些大學(xué)生和中學(xué)生,分別了解一下他們記筆記的情況,結(jié)果發(fā)現(xiàn),他們記筆記的方式真是多種多樣,有的直接記在書上,有的記在專門的筆記本上,有的記在練習(xí)冊上,有的記在卷子上,有的甚至用手機把老師的板書直接拍下來……不難看出,他們的做法是怎么方便就怎么記,就目前而言,企圖用一個手機應(yīng)用來取代他們現(xiàn)有的做法顯然是不現(xiàn)實的,也沒必要,用戶有權(quán)選擇他們認(rèn)為適合的做法,而我們的職責(zé)只是提供必要的幫助和支持。
那么,我們可以提供什么樣的幫助和支持呢?想想看,現(xiàn)有的自由零散的做法會導(dǎo)致什么問題呢?最直接的影響是很難快速找到想要的內(nèi)容,因為它們可能遍布各處,這種時候要是有個索引或者目錄什么的就好了……Bingo!我們可以創(chuàng)建一個應(yīng)用,幫助用戶建立這個索引,雖然用戶也可以另外找本小冊子建立索引,但我們可以通過一個標(biāo)簽系統(tǒng)幫助用戶快速找到相關(guān)的內(nèi)容。這樣,用戶既可以保留現(xiàn)有的自由的記筆記習(xí)慣,又可以獲得新的有序的管理效果。那么,用戶應(yīng)該在何時以及如何建立這個索引呢?
當(dāng)然是越早越好!比如說,用戶可以在每晚做完作業(yè)之后稍稍整理一下筆記,然后為它們創(chuàng)建一些條目并貼上標(biāo)簽。用戶不必為所有筆記創(chuàng)建條目,可以挑選重要的來創(chuàng)建,這個過程本身就可以加深對知識的理解和鞏固對知識的記憶。至于條目的內(nèi)容,用戶可以引用課本或者老師板書的原話,也可以用自己的話來概括復(fù)述,還可以直接引用課本或者練習(xí)冊的頁碼和位置(段落、題號或者標(biāo)記)等等,這個過程可以幫助用戶熟悉如何根據(jù)條目的內(nèi)容找到對應(yīng)的筆記。
現(xiàn)在,用Visual Studio打開項目,在Models文件夾里創(chuàng)建一個Note類,并讓它繼承NotificationObject類:
代碼 1
根據(jù)前面的討論,Note類應(yīng)該包含以下三個屬性:
屬性名字 | 屬性類型 | 屬性描述 |
Id | Guid | 唯一標(biāo)識 |
Course | string | 課程名稱 |
Content | string | 筆記內(nèi)容 |
Tags | string | 筆記標(biāo)簽 |
那么,我們?nèi)绾斡|發(fā)這些動畫?前面說過,當(dāng)用戶單擊Application Bar上的按鈕時,ListBox將會顯示,這個比較簡單,只需在按鈕的事件處理程序里調(diào)用ShowTagsStoryboard的Begin方法就可以了:
代碼 4
而當(dāng)用戶選好標(biāo)簽之后,ListBox將會隱藏,這個可以通過Expression Blend提供的ControlStoryboardAction來實現(xiàn)。打開Assets面板,選擇Behaviors,然后把ControlStoryboardAction拖到Objects and Timeline面板的ListBox上:
圖 12
此時,Objects and Timeline面板將會變成這樣:
圖 13
確保ControlStoryboardAction處于選中狀態(tài),在Properties面板上把EventName和Storyboard兩個屬性的值分別改為MouseLeftButtonUp和HideTagsStoryboard:
圖 14
這樣,當(dāng)用戶在ListBox里選好標(biāo)簽并松開手時就會觸發(fā)HideTagsStoryboard。看到這里,你可能會問,為什么前面不直接在Application Bar的按鈕上使用ControlStoryboardAction?這是因為Application Bar并非Silverlight的一部分,你不可以把它和我們平時接觸到的Silverlight對象等同起來。事實上,如果你試圖把ControlStoryboardAction拖到ApplicationBarIconButton上,Expression Blend會提示你"Not a valid drop target":
圖 15
接下來,我們將會為兩個ListBox定制數(shù)據(jù)模板。
首先,通過Data面板導(dǎo)入以下兩個XML文件:
代碼 5
代碼 6
此時,Data面板將會變成這樣:
圖 16
把Data面板上的NoteCollection拖到顯示筆記的ListBox上,然后進(jìn)入列表項模板的編輯狀態(tài),把StackPanel的Margin屬性、TextBlock的FontSize屬性和TextBlock的TextWrapping屬性分別設(shè)為PhoNETouchTargetOverhang、PhoneFontSizeNormal和Wrap:
圖 17
好了之后退出列表項模板的編輯狀態(tài)。接著,把TagCollection拖到顯示標(biāo)簽的ListBox上,然后進(jìn)入列表項模板的編輯狀態(tài),把StackPanel的Margin屬性、TextBlock的FontSize屬性和TextBlock的TextWrapping屬性分別設(shè)為PhoNETouchTargetOverhang、PhoneFontSizeLarge和Wrap:
圖 18
接下來干嘛?你懂的!
打開MainPage.xaml,添加一個菜單項,并讓它導(dǎo)航至NoteBookPage頁:
圖 19
好了,按F5吧:
圖 20
單擊筆記本:
圖 21
單擊Application Bar上的按鈕:
圖 22
啊!忘記把ListBox的Background設(shè)成不透明了!
退出應(yīng)用程序。把ListBox的Background改為PhoneBackgroundBrush。而動畫方面,0.5秒感覺有點長,我們可以把它改為0.25秒:
代碼 7
此外,還有一個地方值得改善的,我希望ListBox出來的時候能夠有一種逐漸慢下來的感覺,而離開的時候則相反,逐漸快起來。這可以通過緩動函數(shù)(easing function)來實現(xiàn)。單擊Objects and Timeline面板上的Open a Storyboard按鈕,然后選擇ShowTagsStoryboard:
圖 23
展開ListBox節(jié)點,確保下面的RenderTransform處于選中狀態(tài):
圖 24
然后在Properties面板上把EasingFunction設(shè)為Cubic Out:
圖 25
好了之后仿照上述步驟把HideTagsStoryboard的EasingFunction設(shè)為Cubic In。現(xiàn)在,你可以按F5看看修改后的效果了。
接下來,是時候考慮一下筆記本的相關(guān)操作了。我們知道,課程表通常都是整個創(chuàng)建的,而作業(yè)通常也一次過把一門課當(dāng)天要做的都記下來,對于這種集中式批量操作來說,提供保存所有更改和撤銷所有更改是有必要的,但筆記本就不同了,里面的內(nèi)容很可能分散在不同的時間點記錄,沒有太明顯的集中式批量操作,如果我們遵循課程表和作業(yè)本的套路,要么用戶不得不在每次記筆記時都額外執(zhí)行一次保存操作,要么用戶不得不承擔(dān)最后可能忘記統(tǒng)一保存而丟失數(shù)據(jù)的風(fēng)險。因此,我打算把五項操作簡化為新建、編輯和刪除三項,并在用戶執(zhí)行每項操作之后自動保存數(shù)據(jù),其中,新建將會以ApplicationBarIconButton的方式放在Application Bar上:
圖 26
而編輯和刪除則放在上下文菜單里:
代碼 8
根據(jù)之前的經(jīng)驗,我們需要一個新的頁面來處理新建和編輯操作:
圖 27
那么,我們應(yīng)該如何設(shè)計這個頁面呢?
想想看,Note類的哪些屬性和用戶無關(guān)?Id。哪些屬性無需勞煩用戶處理?CourseName。那么剩下的Content和Tags兩個屬性就應(yīng)該出現(xiàn)在這里了。毫無疑問,TextBox完全能夠勝任顯示和編輯這兩個屬性的工作:
圖 28
值得注意的是,這里不再通過普通的Button控件來提供"確定"和"取消"兩項功能,而是通過Application Bar上的按鈕來提供,為什么呢?當(dāng)用戶輸入完畢之后,軟鍵盤可能處于開啟狀態(tài),它會遮蓋普通的Button控件,這意味著用戶就不得不先單擊一下頁面上的空白處關(guān)閉軟鍵盤,再單擊Button控件關(guān)閉頁面,而Application Bar不會被軟鍵盤遮蓋,這意味著用戶可以在輸入完畢之后直接單擊上面的按鈕關(guān)閉頁面。你知道嗎,這個小小的簡化可以極大地提升用戶體驗,之前測試課程表和作業(yè)本的時候,這個不必要的步驟曾多次讓我誤觸TimePicker/DatePicker控件的有效范圍,導(dǎo)致新的頁面被打開,我對此深感厭惡,你知道,當(dāng)用戶對軟件的操作感到厭惡時,后果將會很嚴(yán)重!
現(xiàn)在,回到NoteBookPage頁,為Application Bar上的新建按鈕添加一個事件處理程序:
代碼 9
然后按F5:
圖 29
從上圖可以看出,軟鍵盤和Application Bar是并列一起的,所以我可以在任何時候單擊Application Bar上的按鈕。此外,當(dāng)我在TextBox里輸入較長的內(nèi)容時,TextBox還會自動調(diào)整自身的高度,以便完整顯示我輸入的內(nèi)容。當(dāng)我單擊第二個TextBox進(jìn)行輸入時,整個頁面將會稍稍向上平移,這樣做的好處非常明顯——避免軟鍵盤遮蓋TextBox!從這里不難看出,WP7在用戶體驗上的設(shè)計確實很體貼!
連接前端和后端
接下來,我們要為這些用戶界面創(chuàng)建對應(yīng)的ViewModel類。我們知道,整個筆記本就是一個Pivot控件,而每門課程的筆記則對應(yīng)一個Pivot項,這個結(jié)構(gòu)和上節(jié)課的作業(yè)本類似,于是,我們可以仿效上節(jié)課的做法,分別創(chuàng)建NoteBookViewModel和NoteListViewModel兩個類:
代碼 10
代碼 11
由于每個Pivot項都包含了一個標(biāo)題和一組筆記,NoteListViewModel類自然需要提供兩個對應(yīng)的屬性:
代碼 12
值得注意的是,這里不直接使用ObservableCollection集合,而是和第一節(jié)課的課程表一樣,通過CollectionViewSource來提供集合視圖,這樣做的好處是我們只需指定過濾條件,剩下的事情CollectionViewSource會代為處理,而無需我們親自出手:
代碼 13
而NoteBookViewModel類則和上節(jié)課的作業(yè)本一樣,直接使用ObservableCollection集合:
代碼 14
并根據(jù)課程表里的數(shù)據(jù)創(chuàng)建對應(yīng)的NoteListViewModel對象:
代碼 15
有了ViewModel類,我們就可以著手處理數(shù)據(jù)綁定了。
現(xiàn)在,打開NoteBookPage.xaml文件,切換到XAML模式,在頁面的資源字典里添加兩個數(shù)據(jù)模板:
代碼 16
接著,把它們應(yīng)用到Pivot控件上:
代碼 17
把LayoutRoot的DataContext屬性去掉,然后在代碼隱藏文件的構(gòu)造函數(shù)里設(shè)置DataContext屬性:
代碼 18
好了,按F5吧。在打開筆記本之前,我們得先新建一些課程,否則Pivot控件不會創(chuàng)建任何Pivot項的:
圖 30
好了之后就可以打開筆記本了:
圖 31
當(dāng)然,現(xiàn)在的筆記本既沒有筆記也不能創(chuàng)建筆記,因為這部分功能還沒實現(xiàn)呢!
新建和編輯筆記的工作是由NewOrEditNotePage頁負(fù)責(zé)的,根據(jù)上兩節(jié)課的經(jīng)驗,我們需要創(chuàng)建NewOrEditNoteViewModel、NewNoteViewModel和EditNoteViewModel三個類,但我已經(jīng)厭倦了每次都要手工創(chuàng)建這么多類,而且還有這么多重復(fù)的代碼,所以這次我要對這部分進(jìn)行重構(gòu)。在ViewModels文件夾里創(chuàng)建一個NewOrEditItemViewModel泛型類,并讓它繼承NotificationObject類:
代碼 19
仔細(xì)觀察NewOrEditCourseViewModel和NewOrEditAssignmentViewModel兩個抽象類,不難發(fā)現(xiàn),它們的有效成分是頁面標(biāo)題、Model類的實例和提交數(shù)據(jù)的方法。頁面標(biāo)題很好處理,一個普通的類型為string的Title屬性:
代碼 20
Model類的實例有點棘手,因為我們有3個不同的Model類,怎么處理?我們有兩個選擇,一個是把屬性的類型聲明為object,另一個就是這里采用的做法——泛型:
代碼 21
這也正是我把NewOrEditItemViewModel類聲明為泛型類的緣由。我們知道,每個Model類的數(shù)據(jù)都會提交到不同的地方,我們顯然不能把這部分代碼固化在NewOrEditItemViewModel類里,所以我決定通過委托把代碼外包出去:
代碼 22
最后,我們需要在構(gòu)造函數(shù)里初始化這三個成分:
代碼 23
那么,我們?nèi)绾问褂眠@個類呢?
打開NewOrEditNotePage.xaml.cs文件,重寫OnNavigatedTo方法:
代碼 24
這個是我們根據(jù)查詢字符串初始化DataContext屬性的基本套路。當(dāng)action的值是new時,初始化DataContext屬性的代碼應(yīng)該是這樣的:
代碼 25
而當(dāng)action的值是edit時,初始化DataContext屬性的代碼應(yīng)該是這樣的:
代碼 26
這樣,下次我們再有新的Model類就可以直接創(chuàng)建ViewModel類的實例了。
看到這里,你可能會問,代碼24那個套路每次都是一樣的,應(yīng)該可以處理一下吧?嗯,可以的。我們有兩種處理方案,第一種方案是創(chuàng)建一個NewOrEditItemViewModelFactoryBase抽象類,并在里面使用模板方法模式(Template Method Pattern)處理那個套路,這樣做的代價是我們需要為每個Model類創(chuàng)建一個對應(yīng)的工廠類。如果你不喜歡這種做法,你可以選擇第二種方案,創(chuàng)建一個NewOrEditItemPage類,按照代碼24重寫OnNavigatedTo方法,然后應(yīng)用模板方法模式,這樣做的代價是我們需要讓每個新建/編輯頁面繼承這個類。無論我們選擇哪種方案,有一點是可以肯定的,那就是NewOrEditItemViewModel類的三個成分似乎無法避免,因為這些工作始終要做,而我們從一種方案改成另一種方案只不過是把這些工作從一個地方挪到另一個地方罷了。如果你確實希望減輕這些工作,(理論上)也不是不可能,不過你得做好心理準(zhǔn)備,因為你需要創(chuàng)建一個足夠靈活的子系統(tǒng),并且提供充足的元數(shù)據(jù),這些數(shù)據(jù)包括每個Model類在不同狀態(tài)下分別對應(yīng)的頁面標(biāo)題、新建Model類的實例需要初始化哪些屬性以及這些屬性的數(shù)據(jù)來自哪里或者如何計算、克隆現(xiàn)有Model類的實例的方法是哪個、數(shù)據(jù)提交到哪里以及調(diào)用哪個方法、提交數(shù)據(jù)的是否需要同時保存到獨立存儲區(qū)等等等等。當(dāng)我寫到這里的時候,我已經(jīng)隱約感覺得出這將是個很復(fù)雜的子系統(tǒng),如果你真的打算實現(xiàn)這個子系統(tǒng),那么請你先把右手抬起來,捂在左邊胸口,然后問問自己:
- 會有人愿意負(fù)責(zé)提供這些數(shù)據(jù)嗎?如果有,會是誰呢?
- 會有人愿意負(fù)責(zé)維護(hù)這個子系統(tǒng)嗎?如果有,會是誰呢?
- 當(dāng)你的程序用上這個子系統(tǒng)之后,你能得到什么實質(zhì)的好處?
- 這些好處能否抵消提供數(shù)據(jù)和維護(hù)子系統(tǒng)的付出?
如果你沒有自欺,你的心將會告訴你這個決定是否值得。
創(chuàng)建好ViewModel類之后,我們就可以著手處理它和頁面之間的關(guān)聯(lián)了。首先是設(shè)置數(shù)據(jù)綁定,需要設(shè)置的控件以及對應(yīng)的綁定表達(dá)式如下表所示:
描述 | 類型 | 屬性 | 綁定表達(dá)式 |
頁面標(biāo)題 | TextBlock | Text | {Binding Title} |
筆記內(nèi)容 | TextBox | Text | {Binding Item.Content, Mode=TwoWay} |
筆記標(biāo)簽 | TextBox | Text | {Binding Item.Tags, Mode=TwoWay} |
我們知道,NewOrEditNotePage頁的入口點有兩個,而且都在NoteBookPage頁上,一個是Application Bar上的新建按鈕,另一個是上下文菜單里的編輯菜單項。當(dāng)用戶單擊新建按鈕時,我們需要告訴NewOrEditNotePage頁當(dāng)前的課程是什么,但是,我們從哪里獲取這個信息?辦法有很多種,其中一種是在NoteBookViewModel類里創(chuàng)建一個SelectedNoteList屬性:
代碼 28
并把它綁到Pivot控件的SelectedItem屬性:
代碼 29
然后在新建按鈕的事件處理程序里通過SelectedNoteList的Header屬性獲取課程名稱:
代碼 30
當(dāng)用戶單擊編輯菜單項時,我們需要告訴NewOrEditNotePage頁筆記的Id是什么。我們知道,上下文菜單是嵌在列表項里的,這意味著它能從列表項那里繼承DataContext屬性的值,而這個值正是當(dāng)前選中的筆記,所以我們可以通過菜單項的DataContext屬性獲取筆記的Id:
代碼 31
類似地,刪除操作也是通過相同的方式獲取當(dāng)前選中的筆記,然后把它刪除:
代碼 32
好了,不知不覺又到看效果的時候了!打開筆記本:
圖 32
單擊新建按鈕:
圖 33
輸入筆記內(nèi)容和筆記標(biāo)簽,然后單擊確定返回:
圖 34
接著,長按筆記打開上下文菜單:
圖 35
選擇編輯:
圖 36
嗯?我剛才沒有輸入標(biāo)簽?還是我現(xiàn)在眼花看錯?為什么標(biāo)簽沒有保存?現(xiàn)在,重新輸入標(biāo)簽,單擊頁面上的空白處,單擊確定返回,然后再次編輯筆記:
圖 37
哈!這次有了!怎么回事?!
這個時候,我的腦子里突然蹦出一個問題,當(dāng)TextBox處于編輯狀態(tài)時,單擊確定按鈕會不會觸發(fā)TextBox的LostFocus事件?為了回答這個問題,我做了一個試驗,結(jié)果發(fā)現(xiàn),當(dāng)TextBox處于編輯狀態(tài)時,單擊頁面上的按鈕或者空白處都能觸發(fā)TextBox的LostFocus事件,而單擊Application Bar上的按鈕卻不會,在這種情況下,TextBox的內(nèi)容不會提交!這個問題不難解決,我們只需在調(diào)用Submit方法之前讓TextBox失去焦點就行了,要實現(xiàn)這個效果,最簡單的辦法是讓頁面獲得焦點:
代碼 33
這等效于單擊頁面上的空白處。不幸的是,這個辦法只在新建筆記時有效,我不知道為什么,看來我們只能走別的路子了。在Silverlight里,TextBox的Text屬性在兩種情況下會更新綁定源,第一種情況我們剛才試過了,結(jié)果你也知道了,第二種是手動更新,這里的手動更新并不是指把數(shù)據(jù)從Text屬性手動復(fù)制到Note對象的對應(yīng)屬性,而是告訴Text屬性的綁定表達(dá)式把當(dāng)前數(shù)據(jù)更新回綁定源。我們知道,當(dāng)用戶單擊Application Bar上的確定按鈕時,最多只有一個TextBox獲得焦點,其它TextBox會因為失去焦點自動更新,因而沒有必要對每個TextBox進(jìn)行手動更新。那么,如何才能得到獲得焦點的控件?FocusManager類提供了一個GetFocusedElement靜態(tài)方法,它返回獲得焦點的控件,如果這個控件是TextBox,我們就告訴Text屬性的綁定表達(dá)式把當(dāng)前數(shù)據(jù)更新回綁定源:
代碼 34
重新運行應(yīng)用程序,這次沒問題了。雖然這個問題我們自己也可以解決,但我個人認(rèn)為這是系統(tǒng)應(yīng)該考慮到的問題,即使普通按鈕和Application Bar上的按鈕有著本質(zhì)的區(qū)別,容許這種行為上的不一致會為開發(fā)者帶來不便和困惑。
標(biāo)簽
到目前為止,我們的筆記本只不過是一個很普通的筆記本,雖然我們提供了與標(biāo)簽相關(guān)的用戶界面,但這部分功能還沒真正實現(xiàn)出來。那么,如何實現(xiàn)這部分功能?
我們知道,當(dāng)用戶單擊Application Bar上的顯示標(biāo)簽按鈕時,顯示標(biāo)簽的ListBox將會滑出來,里面列出當(dāng)前課程的標(biāo)簽,用戶可以從中選擇一個標(biāo)簽,此時,ListBox將會滑出去,筆記本的內(nèi)容也將根據(jù)選中的標(biāo)簽進(jìn)行篩選。從這里不難看出,我們需要在NoteListViewModel類里添加兩個屬性,一個用于存放當(dāng)前課程的標(biāo)簽,另一個用于存放當(dāng)前選中的標(biāo)簽:
代碼 35
它們分別綁到ListBox的ItemsSource和SelectedItem兩個屬性:
代碼 36
那么,當(dāng)用戶選好標(biāo)簽之后,我們?nèi)绾魏Y選筆記?還記得我們是如何根據(jù)課程名稱篩選筆記的嗎?我們直接把課程篩選條件告訴CollectionViewSource,當(dāng)數(shù)據(jù)源發(fā)生改變時,CollectionViewSource將會自動篩選,因此,我們不妨考慮把標(biāo)簽篩選條件整合進(jìn)去,讓CollectionViewSource一并處理:
代碼 37
需要說明的是,當(dāng)用戶還沒選擇任何標(biāo)簽時,SelectedTag屬性的值為null,此時,我們應(yīng)該按照不做標(biāo)簽篩選的情況處理,否則看看筆記的標(biāo)簽是否包含用戶選中的標(biāo)簽。看到這里,你可能會問,當(dāng)用戶選擇一個標(biāo)簽時,數(shù)據(jù)源并未發(fā)生任何改變啊,CollectionViewSource應(yīng)該不會自動篩選吧?這個問題問得好!事實上,它不會自動篩選,我們需要手動刷新一下它生成的視圖:
代碼 38
那么,如何初始化標(biāo)簽列表?
想想看,什么時候需要初始化標(biāo)簽列表?每次打開筆記本的時候肯定需要初始化標(biāo)簽列表,但除此之外呢?當(dāng)用戶新建或編輯筆記時,可能引入新的標(biāo)簽;當(dāng)用戶編輯或刪除筆記時,可能刪除現(xiàn)有標(biāo)簽,這些都會導(dǎo)致重新計算標(biāo)簽列表。既然計算標(biāo)簽列表的代碼需要在這么多地方使用,我們當(dāng)然應(yīng)該把它提取到一個單獨的方法里:
代碼 39
計算標(biāo)簽列表的思路非常簡單,首先,選取當(dāng)前課程的筆記(忽略沒有標(biāo)簽的),接著,從中提取標(biāo)簽,為了避免前/后空格的影響,這里使用Trim方法做了處理,然后,去掉空字符串以及重復(fù)的標(biāo)簽,最后,把標(biāo)簽添加到Tags屬性。
現(xiàn)在,請思考一下,我們應(yīng)該在哪調(diào)用ComputeTags方法?有些同學(xué)可能會說,這還不簡單,分別在NoteListViewModel類的構(gòu)造函數(shù)和三個操作的事件處理程序里調(diào)用不就行了?在NoteListViewModel類的構(gòu)造函數(shù)和刪除操作的事件處理程序里調(diào)用是沒問題的,但在新建和編輯兩個操作的事件處理程序里調(diào)用就有問題了,為什么呢?舉個例子吧:
代碼 40
你覺得上面代碼的最后一行是在NewOrEditNotePage頁顯示之前還是之后執(zhí)行呢?答案是之前,這意味著標(biāo)簽列表的計算在用戶編輯筆記之前就完成了,這顯然不是我們期望的效果。怎么辦?
想想看,每次從NewOrEditNotePage頁返回都會發(fā)生什么事呢?觸發(fā)NoteBookPage頁的Loaded事件!于是,我們可以把代碼39里的最后一行放在Loaded事件處理程序里,不過,這樣做會導(dǎo)致一個問題,每次從主菜單打開NoteBookPage頁時,當(dāng)前課程的標(biāo)簽列表會被計算兩次,第一次是在NoteListViewModel類的構(gòu)造函數(shù)里,第二次是在Loaded事件處理程序里,因為NoteBookPage頁每次顯示都會觸發(fā)Loaded事件。怎么處理這個問題?最簡單的辦法是通過一個bool變量區(qū)分NoteBookPage頁是否已經(jīng)打開過。不過,既然我們的目的只是為了在標(biāo)簽發(fā)生改變時做些事情,為什么不直接監(jiān)聽Note對象的PropertyChanged事件呢?要實現(xiàn)這個效果,有三個事兒需要我們做的:
- 監(jiān)聽現(xiàn)有Note對象的PropertyChanged事件。
- 監(jiān)聽App.NoteStore.Items的CollectionChanged事件,一旦有新的Note對象添加進(jìn)來就監(jiān)聽它的PropertyChanged事件。
- 監(jiān)聽App.NoteStore.Items的CollectionChanged事件,一旦現(xiàn)有的Note對象被刪除就移除PropertyChanged事件的監(jiān)聽。
我們可以在PropertyChanged事件處理程序里調(diào)用ComputeTags方法。
雖然這種做法聽起來有點復(fù)雜,但它避免了第一種做法的問題。本質(zhì)上,這兩種做法是等效的,只是一個在前臺處理,另一個在后臺處理,而正是這個角度的轉(zhuǎn)變讓我們對它們有了更進(jìn)一步的了解。想想看,用戶并非每次新建/編輯筆記之后都會單擊Application Bar上的顯示標(biāo)簽按鈕,一個比較常見的使用情景用戶把當(dāng)天的筆記都輸入了,然后通過標(biāo)簽的篩選來復(fù)習(xí)特定的內(nèi)容,這樣的話,在用戶每次新建/編輯筆記之后重新計算標(biāo)簽列表顯然沒有必要。事實上,如果用戶沒有單擊Application Bar上的顯示標(biāo)簽按鈕,我們根本沒有必要計算標(biāo)簽列表,換句話說,我們可以把代碼39里的最后一行放在顯示標(biāo)簽按鈕的Click事件處理程序里,從而實現(xiàn)按需計算:
代碼 41
需要注意的是,當(dāng)用戶單擊Application Bar上的顯示標(biāo)簽按鈕時,如果用戶還沒創(chuàng)建任何課程,直接調(diào)用ComputeTags方法將會引發(fā)異常,所以我們需要在調(diào)用之前判斷一下。不過,這種做法也有個問題,試想一下,如果用戶多次單擊Application Bar上的顯示標(biāo)簽按鈕,其間沒有新建/編輯任何筆記,那么,除了第一次計算標(biāo)簽內(nèi)容是必要的,后面幾次都是多余的。那么,如何才能避免多余的計算?看到這里,你可能會說,為什么不把后兩種做法結(jié)合起來試一下呢,比如說,我們可以通過一個bool變量標(biāo)識是否需要計算,然后在PropertyChanged事件處理程序里把它的值設(shè)為true,而在ComputeTags方法里,僅當(dāng)這個變量的值為true時才執(zhí)行計算,執(zhí)行完畢之后把它的值設(shè)為false。嗯,這個主意不錯,我就把它留給你當(dāng)課后作業(yè)吧。
好了,不知不覺又到看效果的時候了!按F5運行應(yīng)用程序,新建一條筆記:
圖 38
單擊Application Bar上的顯示標(biāo)簽按鈕:
圖 39
單擊頁面空白處收回標(biāo)簽列表。再新建一條筆記:
圖 40
這次,我們給它兩個標(biāo)簽,其中一個標(biāo)簽是現(xiàn)有的,另一個是新的,并且分隔符后面有個空格。單擊確定返回,然后單擊Application Bar上的顯示標(biāo)簽按鈕:
圖 41
再新建一條筆記:
圖 42
現(xiàn)在,單擊Application Bar上的顯示標(biāo)簽按鈕:
圖 43
選擇buying behavior:
圖 44
再次單擊Application Bar上的顯示標(biāo)簽按鈕,選擇sales strategy:
圖 45
非常好!不過,現(xiàn)在有個問題,我想顯示所有筆記怎么辦?
沒問題,我們可以在計算標(biāo)簽列表的時候加插一個"特殊"的標(biāo)簽:
代碼 42
并在篩選的時候進(jìn)行"特殊"處理:
代碼 43
好了,重新運行應(yīng)用程序,分別為兩個課程新建一些筆記:
圖 46
圖 47
現(xiàn)在,單擊Application Bar上的顯示標(biāo)簽按鈕:
圖 48
選擇disposition effect:
圖 49
現(xiàn)在,切換到sales psychology課程,單擊Application Bar上的顯示標(biāo)簽按鈕,選擇sales strategy:
圖 50
再次單擊Application Bar上的顯示標(biāo)簽按鈕,選擇(全部):
圖 51
現(xiàn)在,切換回behavioral finance課程:
圖 52
怎么回事?!我們剛才已經(jīng)做了篩選啊!
原來,當(dāng)我們切換課程時,ListBox的ItemsSource屬性發(fā)生改變,導(dǎo)致ListBox的SelectedItem屬性被重設(shè)為null,而NoteListViewModel對象的SelectedTag屬性和ListBox的SelectedItem屬性是雙向綁定的,因而也被重設(shè)為null了。ListBox的SelectedItem屬性被重設(shè)為null是對的,因為新的數(shù)據(jù)源不一定包含SelectedItems屬性的值,但把NoteListViewModel對象的SelectedTag屬性也重設(shè)為null就不對了,因為同一個NotelistViewModel對象的Tags屬性肯定包含SelectedTags屬性的值,因此,SelectedTag屬性的set訪問器應(yīng)該忽略這個重設(shè):
代碼 44
重新運行應(yīng)用程序,重新執(zhí)行一次上面的測試,嗯,這次沒問題了。
命令與行為
我們知道,WPF和最新的Silverlight 4都支持命令綁定,比如說,Button控件有一個Command屬性和一個CommandParameter屬性,前者用于綁定實現(xiàn)ICommand接口的對象,后者用于綁定傳給前者的參數(shù),但SL for WP卻只有一個ICommand接口,這意味著我們無法為按鈕設(shè)置命令,而Application Bar上的按鈕這種異類就更不用說了。幸虧Prism為我們帶來了ApplicationBarButtonCommand(使用之前請先引用Microsoft.Practices.Prism.Interactivity.dll類庫),它能讓我們?yōu)锳pplication Bar上的按鈕設(shè)置命令。下面,我們拿NewOrEditNotePage頁來示范它的用法。
在設(shè)置命令之前,我們得先有個命令,而命令通常是由ViewModel類提供的。打開NewOrEditItemViewModel.cs,在NewOrEditItemViewModel類里添加一個SubmitCommand屬性:
代碼 45
那么,我們應(yīng)該如何初始化它?一般的做法是創(chuàng)建一個SubmitCommand類,并讓它實現(xiàn)ICommand接口,然后在NewOrEditItemViewModel類的構(gòu)造函數(shù)里把SubmitCommand類的實例賦給SubmitCommand屬性。如果你不嫌麻煩的話,你可以這樣做,不過,由于創(chuàng)建命令對象的需求非常普遍,Prism為我們帶來了DelegateCommand泛型類,我們只需把提交數(shù)據(jù)的代碼傳給它的構(gòu)造函數(shù)就可以了:
代碼 46
接著,把_submit私有字段以及在構(gòu)造函數(shù)里初始化它的代碼刪除,因為我們不再需要它了。刪除之后,把Submit方法改成這樣:
代碼 47
換句話說,原來的_submit私有字段被現(xiàn)在的SubmitCommand屬性取代了。
現(xiàn)在,打開NewOrEditNotePage頁,從Assets面板上把ApplicationBarButtonCommand拖到Objects and Timeline面板的PhoneApplicationPage上:
圖 53
此時,Objects and Timeline面板將會變成這樣:
圖 54
接著,在Properties面板上把ButtonText屬性的值設(shè)為"確定":
圖 55
單擊CommandBinding右邊的小正方形,并選擇Custom Expression:
圖 56
在彈出的Custom expression對話框里輸入"{Binding SubmitCommand}"并按回車:
圖 57
用同樣的辦法把CommandParameterBinding設(shè)為"{Binding Item}"。
那么,用戶提交數(shù)據(jù)之后如何返回?這個時候就輪到ApplicationBarButtonNavigation上場了。從Assets面板上把ApplicationBarButtonCommand拖到Objects and Timeline面板的PhoneApplicationPage上,此時,Objects and Timeline面板將會變成這樣:
圖 58
接著,在Properties面板上把ButtonText和NavigateTo兩個屬性的值分別設(shè)為"確定"和"#GoBack":
圖 59
需要說明的是,"#GoBack"是一個硬性規(guī)定的特殊值,當(dāng)我們把NavigateTo屬性的值設(shè)為"#GoBack"時,ApplicationBarButtonNavigation會調(diào)用NavigationService.GoBack方法返回,而當(dāng)我們把NavigateTo屬性設(shè)為XXX.xaml時,它會調(diào)用NavigationService.Navigate方法導(dǎo)航至對應(yīng)的頁面。
那么,Text屬性更新綁定源的問題呢?難道Prism也提供了相應(yīng)的組件?沒有,這次我們得親自出手了。右擊Utils文件夾,然后選擇Add New Items,在彈出的New Item對話框里選擇Behavior,并把它命名為AppBarButtonUpdateSource:
圖 60
我們知道,Application Bar上的按鈕并非Silverlight的一部分,因此Behavior無法直接作用于它,而Silverlight里只有PhoneApplicationPage類提供了訪問Application Bar的方法,因此我們需要把AppBarButtonUpdateSource的目標(biāo)類型改為PhoneApplicationPage:
代碼 48
這也正是我們把ApplicationBarButtonCommand和ApplicationBarButtonNavigation拖到PhoneApplicationPage上的原因。那么,如何才能找到Application Bar上的按鈕?Prism為我們帶來了FindButton擴展方法,可以通過按鈕的文字來查找,因此,我們需要創(chuàng)建一個ButtonText屬性和一個_button私有字段,前者用于指定待查找按鈕的文字,后者用于保存找到的按鈕:
代碼 49
FindButton擴展方法需要一個實現(xiàn)IApplicationBar接口的對象,而能夠提供這個對象的只有PhoneApplicationPage對象,后者可以通過Behavior的AssociatedObject屬性訪問。于是,我們可以在OnAttached方法里初始化_button私有字段:
代碼 50
找到按鈕之后,我們需要把更新綁定源的代碼關(guān)聯(lián)到按鈕上,而做到這點的唯一辦法就是為按鈕創(chuàng)建一個Click事件處理程序:
代碼 51
最后,在OnDetaching方法里解除它們之間的關(guān)聯(lián):
代碼 52
現(xiàn)在,重新編譯項目,然后從Assets面板上把AppBarButtonUpdateSource拖到Objects and Timeline面板的PhoneApplicationPage上,并把ButtonText屬性的值設(shè)為"確定"。此時,Objects and Timeline面板將會變成這樣:
圖 61
不過,這個順序是不對的,AppBarButtonUpdateSource應(yīng)該排在其它兩個的前面,但這個順序在Expression Blend里無法調(diào)整,因此我們需要手動修改XAML。
至于取消按鈕,由于它只是簡單地返回,我們只需為它添置一個ApplicationBarButtonNavigation就行了。添置好后,Objects and Timeline面板將會變成這樣:
圖 62
現(xiàn)在,我們可以把這兩個按鈕的Click事件處理程序去掉了。
命令綁定是MVVM模式的重要組成部分,它不但可以進(jìn)一步降低View和ViewModel之間的耦合度,還可以簡化單元測試的工作。目前我們通過Behavior來實現(xiàn)命令綁定只是權(quán)宜之計,希望微軟可以在將來的版本里把這部分功能補完了。還有的就是希望微軟能夠進(jìn)一步完善Application Bar,包括處理焦點問題以及提供更豐富的訪問方式。
下課了……
it知識庫:WP7有約(三):課堂重點,轉(zhuǎn)載需保留來源!
鄭重聲明:本文版權(quán)歸原作者所有,轉(zhuǎn)載文章僅為傳播更多信息之目的,如作者信息標(biāo)記有誤,請第一時間聯(lián)系我們修改或刪除,多謝。