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