|
作業(yè)本
上節(jié)課布置的作業(yè)有做嗎?沒人吭聲啊,看來大家都忘了哦,沒事,我們這次弄個(gè)作業(yè)本出來,大家就有地方記作業(yè)了。在開始設(shè)計(jì)應(yīng)用程序之前,我們先來看看通常的作業(yè)本是怎樣記作業(yè)的:
圖 1
從上圖可以看到,作業(yè)本有點(diǎn)像日記本,每次記錄時(shí)都會(huì)寫下當(dāng)天的日期,每天的作業(yè)又會(huì)根據(jù)課程進(jìn)行歸類。慢著!我怎么知道這些作業(yè)什么時(shí)候交?一般情況下,中小學(xué)生的作業(yè)都是第二天上課時(shí)交的,但大學(xué)生就不同了,他們的作業(yè)可能第二天交,也可能一周之后交,有時(shí)甚至幾周之后才交,更重要的是,不同的作業(yè)可能在不同的時(shí)間交。換句話說,我們的應(yīng)用程序還需要支持記錄交作業(yè)的時(shí)間。此外,每當(dāng)完成一項(xiàng)作業(yè),我們可以在旁邊做個(gè)記號(hào),這樣,當(dāng)我們打開作業(yè)本時(shí),即使作業(yè)再多也能馬上知道哪些還沒做完。
現(xiàn)在,用Visual Studio打開項(xiàng)目,在Models文件夾里創(chuàng)建一個(gè)Assignment類,和上節(jié)課的Course類一樣,它也需要實(shí)現(xiàn)INotifyPropertyChanged接口。由于我們有很多類都需要實(shí)現(xiàn)INotifyPropertyChanged接口,為了避免不必要的重復(fù),你可以考慮創(chuàng)建一個(gè)類專門實(shí)現(xiàn)這個(gè)接口,然后讓有需要的類繼承這個(gè)類。這個(gè)需求似乎比較常見,因此Prism提供了一個(gè)NotificationObject類,我們只需繼承它就行了:
代碼 1
繼承之前別忘了引用Bin/Phone/Microsoft.Practices.Prism.dll類庫和Microsoft.Practices.Prism.ViewModel命名空間哦。根據(jù)前面的討論,Assignment類應(yīng)該包含以下屬性:
屬性名字 | 屬性類型 | 備注 |
Id | Guid | 唯一標(biāo)識(shí) |
CourseName | string | 課程名稱 |
StartDate | DateTime | 創(chuàng)建日期 |
DueDate | DateTime | 截止日期 |
Content | string | 作業(yè)內(nèi)容 |
IsCompleted | bool | 完成狀態(tài) |
定制數(shù)據(jù)模板
首先是定制分組標(biāo)題的數(shù)據(jù)模板,右擊LongListSelector控件里的任何地方,選擇Edit Additional Templates/Edit GroupHeaderTemplate/Create Empty:
圖 5
在彈出的Create DataTemplate Resource對(duì)話框里輸入模板名字,然后按OK關(guān)閉對(duì)話框:
圖 6
進(jìn)入模板的編輯狀態(tài)之后,你會(huì)看到一個(gè)空的Grid,從Tools面板把一個(gè)TextBlock拖到Grid里,確保TextBlock處于選中狀態(tài)(而不是編輯狀態(tài)),單擊Text屬性右邊的小正方形,并選擇Data Binding:
圖 7
在彈出的Create Data Binding對(duì)話框里選中Use a custom path expression,并在旁邊的編輯框里輸入Key:
圖 8
為什么輸入Key呢?因?yàn)橥ㄟ^LINQ的group XXX by YYY創(chuàng)建的分組對(duì)象實(shí)現(xiàn)了IGrouping<TKey, TElement>接口,而這個(gè)接口有個(gè)Key屬性保存了分組的依據(jù)——創(chuàng)建日期,也就是這里需要的分組標(biāo)題了。
當(dāng)你按OK關(guān)閉對(duì)話框之后,你將會(huì)看到:
圖 9
奇怪了!我們明明提供了示例數(shù)據(jù)啊,而且數(shù)據(jù)綁定也沒弄錯(cuò)啊,為什么TextBlock沒有任何顯示?仔細(xì)觀察Text屬性下面的DataContext屬性:
圖 10
此時(shí)的值應(yīng)該是分組對(duì)象而不是AssignmentListViewModel對(duì)象??!我懷疑LongListSelector控件沒有正確處理DataContext在設(shè)計(jì)時(shí)的傳遞(bug?),導(dǎo)致Expression Blend無法獲取正確的數(shù)據(jù)。既然這樣,我們只好再弄點(diǎn)示例數(shù)據(jù)了,單擊Text屬性右邊的編輯框,選擇Reset,然后把Text屬性的值改為"2010/11/29"。接著,在Objects and Timeline面板上選中Grid,單擊Background屬性右邊的小正方形,并選擇System Resource/PhoneAccentBrush:
圖 11
此時(shí),你的Artboard應(yīng)該是這樣的:
圖 12
退出模板的編輯狀態(tài),保存所有修改,然后重新編譯項(xiàng)目,好了之后就能看到分組標(biāo)題了:
圖 13
不要奇怪分組標(biāo)題都是"2010/11/29",這是我們剛才為了編輯的方便硬編碼上去的結(jié)果,暫時(shí)忍耐一下吧。
接下來是列表項(xiàng)的數(shù)據(jù)模板,右擊LongListSelector控件里的任何地方,選擇Edit Additional Templates/Edit ItemTemplate/Create Empty,在彈出的Create DataTemplate Resource對(duì)話框里輸入模板名字(itemTemplate),然后按OK關(guān)閉對(duì)話框?,F(xiàn)在,我們要思考的問題是,如何更好地顯示作業(yè)數(shù)據(jù)呢?回顧表1,Id屬性為了便于應(yīng)用程序搜索Assignment對(duì)象而創(chuàng)建的,用戶并不需要知曉它的存在,所以我們不必把它呈現(xiàn)在用戶面前,Pivot項(xiàng)的標(biāo)題已經(jīng)顯示了CourseName屬性,分組標(biāo)題也顯示了StartDate屬性,剩下的就是DueDate、Content和IsCompleted三個(gè)屬性了,那么我們應(yīng)該如何顯示這三個(gè)屬性?此時(shí),我的腦子里浮現(xiàn)出的第一個(gè)想法是這樣的:
圖 14
整個(gè)Grid分為兩個(gè)Column,左邊是作業(yè)內(nèi)容,自動(dòng)換行,右邊從上到下分別是截止日期的月、日和完成狀態(tài),一般情況下,創(chuàng)建日期和截止日期的年份都是一樣的,所以我們沒有必要提供重復(fù)的信息,即使碰到跨年的情況,用戶也不會(huì)因?yàn)槿鄙倌攴荻械揭苫螅怯袀€(gè)老師布置了一個(gè)跨越兩年或以上的作業(yè)。想到這里,我的腦子里突然閃出一個(gè)問題,表示完成狀態(tài)的TextBlock能否去掉,并以其它方式表達(dá)這個(gè)信息呢?此時(shí),我的腦子里迅速浮現(xiàn)出各種各樣的圖標(biāo),但是,還有更好的方式嗎?顏色,突然這個(gè)詞兒從我的腦子里掠過,一般而言,與文字相比,我們的大腦對(duì)顏色的反應(yīng)更快更準(zhǔn)。有鑒于此,我把列表項(xiàng)的模板改成這樣:
圖 15
右邊部分將會(huì)根據(jù)作業(yè)的不同狀態(tài)顯示不同底色。退出模板的編輯狀態(tài),保存所有修改,然后重新編譯項(xiàng)目,好了之后就能看到效果了:
圖 16
顯然,字體的大小、控件之間的間距還不能讓人滿意,我們需要調(diào)整一下,這個(gè)過程可能有點(diǎn)反復(fù)和枯燥,但這卻是我們體貼用戶的重要途徑,我們不但要讓用戶的眼睛感到滿意,還要讓用戶的手指感到滿意(別忘記我們開發(fā)的是觸屏應(yīng)用程序哦),下面是我調(diào)整之后的效果:
圖 17
現(xiàn)在,我們可以再次進(jìn)入模板的編輯狀態(tài),為對(duì)應(yīng)的控件設(shè)置數(shù)據(jù)綁定了,做法和前面為分組標(biāo)題設(shè)置數(shù)據(jù)綁定的一樣(圖7和圖8),各個(gè)控件對(duì)應(yīng)的自定義路徑表達(dá)式如下圖所示:
圖 18
好了之后就可以看到我們前面準(zhǔn)備的示例數(shù)據(jù)了:
圖 19
噢,分組標(biāo)題!我希望只顯示日期,而且是符合中國區(qū)域設(shè)置的短日期格式,還有月份的顯示,我希望是"十一月"而不是"11"。
這個(gè)時(shí)候又輪到轉(zhuǎn)換器出場了。首先,切換到Visual Studio,在Utils文件夾里創(chuàng)建下面兩個(gè)類:
代碼 12
代碼 13
需要說明的是,因?yàn)槲覀兊慕壎ㄊ菃蜗虻?,所以沒有必要實(shí)現(xiàn)ConvertBack方法。接著,在AssignmentBookPage.xaml的資源字典里創(chuàng)建它們的實(shí)例:
代碼 14
看到這里,你可能會(huì)問,這兩個(gè)轉(zhuǎn)換器的Convert方法都使用了culture這個(gè)參數(shù),但我們沒有直接調(diào)用Convert方法啊,那我們?cè)趺窗堰@個(gè)參數(shù)傳給它?這可以通過設(shè)置綁定表達(dá)式的ConverterCulture屬性做到,現(xiàn)在,把那兩個(gè)TextBlock的Text屬性的綁定表達(dá)式改為"{Binding Key, Converter={StaticResource dateConverter}, ConverterCulture=zh-CN}"和"{Binding DueDate.Month, Converter={StaticResource monthNameConverter}, ConverterCulture=zh-CN}"。
剩下的就是截止日期的底色了,既然轉(zhuǎn)換器可以把DateTime對(duì)象轉(zhuǎn)換成字符串,它也應(yīng)該可以把Assignment對(duì)象轉(zhuǎn)換成SolidColorBrush對(duì)象,不過,在創(chuàng)建這個(gè)轉(zhuǎn)換器之前,我們得先弄清楚什么狀態(tài)對(duì)應(yīng)什么底色。前面我們說過,作業(yè)本的主要目的是讓學(xué)生對(duì)要做哪些作業(yè)一目了然,而"未完成"的作業(yè)里可能存在一些已經(jīng)過了截止日期的,這類作業(yè)需要馬上處理,所以我們應(yīng)該單獨(dú)為這類作業(yè)設(shè)置一種底色,以便用戶及時(shí)知曉并采取行動(dòng)。假設(shè)這三種狀態(tài)及其對(duì)應(yīng)的底色如下表所示(你也可以換成其它底色):
狀態(tài) | 底色 |
已逾期 | Red |
未完成 | #FF1BA1E2 |
已完成 | Green |
插曲 #1
究竟發(fā)生了什么事?示例數(shù)據(jù)和綁定表達(dá)式應(yīng)該都沒問題啊,否則Expression Blend和Visual Studio的設(shè)計(jì)器也不會(huì)正常顯示,那么問題到底出在哪里呢?突然,一個(gè)想法在我的腦子里閃過,如果我在DateConverter類的Convert方法里設(shè)個(gè)斷點(diǎn),你覺得會(huì)怎么樣?試一下吧……結(jié)果是,沒有到達(dá)這個(gè)斷點(diǎn),換句話說,Convert方法根本沒被調(diào)用!這種情況有點(diǎn)像數(shù)據(jù)綁定找不到分組對(duì)象的Key屬性,比如說,我故意把綁定表達(dá)式的Key改為Key1,結(jié)果Expression Blend的設(shè)計(jì)器就變成這樣了:
圖 24
我們知道,分組對(duì)象實(shí)現(xiàn)了IGrouping<TKey, IElement>接口,因此Key屬性肯定存在,否則編譯器會(huì)報(bào)錯(cuò),那么,什么情況下這個(gè)屬性是不可見的,或者說,有什么辦法可以讓它不可見?想到這里,一個(gè)詞兒突然在我的腦子里冒出來——顯式接口實(shí)現(xiàn)!如果Key屬性是顯式實(shí)現(xiàn)的,僅當(dāng)變量的類型是IGrouping<TKey, IElement>時(shí)Key屬性才是可見的??吹竭@里,你可能會(huì)說,Silverlight不可能直接調(diào)用分組對(duì)象的Key屬性,它應(yīng)該是通過反射獲取這個(gè)屬性的。沒錯(cuò),當(dāng)我們?cè)诮壎ū磉_(dá)式里以字符串的形式給出屬性路徑,PropertyPathConverter對(duì)象將會(huì)把這個(gè)字符串轉(zhuǎn)換成PropertyPath對(duì)象,那么,PropertyPath對(duì)象又是如何找到對(duì)應(yīng)的屬性呢?在微軟公開的.NET Framework 4.0源代碼里,我找到了PropertyPath類的實(shí)現(xiàn),里面有個(gè)GetPropertyHelper方法負(fù)責(zé)獲取指定的屬性:
代碼 17
如果Key屬性是顯式實(shí)現(xiàn)的話,GetProperty方法就會(huì)返回null!換句話說,數(shù)據(jù)綁定和顯式實(shí)現(xiàn)的屬性一起工作的話會(huì)出問題。那么,group XXX by YYY返回的分組對(duì)象是不是顯示實(shí)現(xiàn)Key屬性的呢?我們知道,使用group XXX by YYY實(shí)質(zhì)上就是調(diào)用Enumerable類的GroupBy方法,經(jīng)過一番查找,我發(fā)現(xiàn)它返回的分組對(duì)象就是Lookup類內(nèi)部的Grouping類的實(shí)例,但Grouping類的Key屬性是隱式實(shí)現(xiàn)的,有趣的是,Key屬性上方有一段注釋:
代碼 18
除了Key屬性之外,Grouping類的其它屬性都是顯式實(shí)現(xiàn)的,我猜Key屬性原來也是顯式實(shí)現(xiàn)的,后來由于數(shù)據(jù)綁定的問題才改為隱式實(shí)現(xiàn)。
這些代碼是WPF 4.0的,而Key屬性上面的注釋也明確提到了WPF,這是不是說Key屬性的值在WPF里可以正確顯示?我們可以設(shè)計(jì)一個(gè)簡單的實(shí)驗(yàn)來驗(yàn)證一下:
- 創(chuàng)建一個(gè)ListBox。
- 定制ListBox的ItemTemplate,里面只放一個(gè)TextBlock。
- 把TextBlock的Text屬性設(shè)為"{Binding Key}"。
- 通過GroupBy方法創(chuàng)建分組對(duì)象的集合,并把它綁到ListBox的ItemsSource屬性。
- 按F5。
我分別在WPF 4.0、SL 4.0和SL for WP7上執(zhí)行這個(gè)實(shí)驗(yàn),發(fā)現(xiàn)只有WPF 4.0能夠正確顯示Key屬性的值,其它兩個(gè)的ListBox是一片空白的。我懷疑SL的分支是在這個(gè)問題得到修復(fù)之前創(chuàng)建的,但我沒有代碼證實(shí)這個(gè)猜想。
還有一個(gè)問題我沒弄明白的,為什么設(shè)計(jì)器能夠正確顯示而程序真正運(yùn)行的時(shí)候卻不能?難道設(shè)計(jì)器對(duì)顯式實(shí)現(xiàn)的屬性有什么特別的照顧?為了驗(yàn)證這個(gè)猜想,我又做了一個(gè)實(shí)驗(yàn),我不直接返回分組對(duì)象,而是通過下面這個(gè)Grouping類包裝一下再返回:
代碼 19
結(jié)果,設(shè)計(jì)器也不顯示了……我不知道為什么設(shè)計(jì)器能夠正確顯示GroupBy方法返回的分組對(duì)象的Key屬性,這里面肯定有些東西是我不知道的,如果你知道原因,或者先我一步找到原因,那你一定要告訴我哦!
連接前端和后端
既然顯式實(shí)現(xiàn)的屬性會(huì)對(duì)數(shù)據(jù)綁定造成不良影響,那我們就換成隱式實(shí)現(xiàn)吧。首先,在ViewModels文件夾里創(chuàng)建AssignmentGroupViewModel類,并讓它繼承ObservableCollection<Assignment>類:
代碼 20
為什么要繼承ObservableCollection<Assignment>類呢?前面說過,LongListSelector控件硬性規(guī)定分組對(duì)象至少實(shí)現(xiàn)IEnumerable接口,不過,要想獲得更好的效果,僅僅實(shí)現(xiàn)IEnumerable接口是不夠的,LongListSelector控件通過內(nèi)部的GetItemsInGroup方法來獲取分組內(nèi)容:
代碼 21
從上面代碼不難看出,如果分組對(duì)象實(shí)現(xiàn)了IList接口,那么每次獲取分組內(nèi)容時(shí)都會(huì)免掉一次遍歷。此外,我們還希望當(dāng)分組內(nèi)容發(fā)生改變時(shí),比如新建/刪除一項(xiàng)作業(yè),分組對(duì)象能夠自動(dòng)通知LongListSelector控件做出相應(yīng)的更新,為了實(shí)現(xiàn)這個(gè)效果,分組對(duì)象需要實(shí)現(xiàn)INotifyCollectionChanged接口。毫無疑問,能夠一次過滿足我們所有要求的最簡單做法就是繼承ObservableCollection<Assignment>類了。
看到這里,你可能會(huì)問,IGrouping<TKey, TElement>接口不用實(shí)現(xiàn)嗎?不用,LongListSelector控件沒有規(guī)定分組對(duì)象必須實(shí)現(xiàn)這個(gè)接口,我們只需簡單地創(chuàng)建一個(gè)Key屬性,配合綁定表達(dá)式里的屬性路徑就行了:
代碼 22
需要說明的是,ObservableCollection<Assignment>類也實(shí)現(xiàn)了INotifyPropertyChanged接口,所以我們可以直接使用它的OnPropertyChanged方法。
接下來是分組對(duì)象的初始化,這個(gè)過程的主要任務(wù)有兩個(gè):
- 查詢數(shù)據(jù)源,把滿足條件的作業(yè)內(nèi)容添加到自身。
- 監(jiān)聽數(shù)據(jù)源,把滿足條件的內(nèi)容更改反映到自身。
執(zhí)行這兩個(gè)任務(wù)的前提是有個(gè)可用的數(shù)據(jù)源,我們可以仿效課程表的做法,在App類里通過靜態(tài)屬性提供JsonDataStore<Assignment>對(duì)象:
代碼 23
有了數(shù)據(jù)源我們就可以著手執(zhí)行第一個(gè)任務(wù)了:
代碼 24
需要說明的是,這里把判斷條件單獨(dú)提取出來了,因?yàn)閳?zhí)行第二個(gè)任務(wù)時(shí)還要用到:
代碼 25
需要說明的是,e參數(shù)的NewItems和OldItems兩個(gè)屬性看起來好像可能包含多個(gè)元素,但事實(shí)上它們只會(huì)包含一個(gè),因?yàn)镹otifyCollectionChangedEventArgs類的構(gòu)造函數(shù)限制了這個(gè)可能,不過這個(gè)限制僅存在于Silverlight的現(xiàn)有版本(SL3、SL4、SL for WP7)。另外,這里使用了Lambda語句來創(chuàng)建CollectionChanged事件的處理程序,雖然你也可以通過一個(gè)單獨(dú)的方法做到,但使用Lambda語句可以利用閉包的特點(diǎn)重用前面的判斷條件,當(dāng)然,使用匿名方法的語法也是可以的。
還差什么呢?噢,對(duì)了,LongListSelector控件內(nèi)部會(huì)調(diào)用分組對(duì)象的Equals方法進(jìn)行判等,我們可以重寫AssignmentGroupViewModel類的Equals和GetHashCode兩個(gè)方法,使之根據(jù)Key屬性來判等以及獲取哈希值。這個(gè)任務(wù)留給你當(dāng)課后作業(yè)吧。
既然分組對(duì)象的類型改了,那AssignmentListViewModel類的AssignmentGroups屬性也得做出相應(yīng)的調(diào)整吧:
代碼 26
由于AssignmentListViewModel類對(duì)應(yīng)用戶界面上的Pivot項(xiàng),我們還需要給它創(chuàng)建一個(gè)Title屬性:
代碼 27
有了這些準(zhǔn)備,我們就可以著手實(shí)現(xiàn)AssignmentListViewModel類的構(gòu)造函數(shù)了:
代碼 28
看到這里,你可能會(huì)說,這條LINQ語句看起來有點(diǎn)復(fù)雜嘛!其實(shí)不然,想想看,我們的最終目的是什么?創(chuàng)建分組對(duì)象并把它們添加到AssignmentGroups屬性。那創(chuàng)建分組對(duì)象需要什么條件?課程名稱和創(chuàng)建日期。課程名稱已經(jīng)有了,創(chuàng)建日期來自哪里?來自數(shù)據(jù)源。那我們對(duì)創(chuàng)建日期有些什么要求?我們只要和指定課程相關(guān)的,而且不要重復(fù)的?,F(xiàn)在,你再看看上面這條LINQ語句,從上往下看,有沒有覺得它像下面這條"流水線"?
圖 25
前面我們說過,當(dāng)用戶新建一項(xiàng)作業(yè)時(shí),它會(huì)自動(dòng)添加到"今天"的分組里,但如果"今天"的分組還沒創(chuàng)建出來呢?那AssignmentListViewModel類就應(yīng)該為這項(xiàng)新的作業(yè)創(chuàng)建"今天"的分組,并把它添加到AssignmentGroups屬性:
代碼 29
當(dāng)用戶刪除一項(xiàng)作業(yè)時(shí),如果這項(xiàng)作業(yè)是所屬分組的唯一一項(xiàng)作業(yè),LongListSelector控件會(huì)自動(dòng)隱藏這個(gè)分組。而當(dāng)用戶撤銷所有更改時(shí),AssignmentListViewModel類得把AssignmentGroups屬性清空。
到目前為止,AssignmentBookPage頁里的每個(gè)組成部分都有對(duì)應(yīng)的ViewModel類了,現(xiàn)在是時(shí)候?yàn)樗鼊?chuàng)建一個(gè)了。在ViewModels文件夾里創(chuàng)建一個(gè)AssignmentBookViewModel類,并創(chuàng)建一個(gè)AssignmentLists屬性:
代碼 30
AssignmentBookViewModel類的任務(wù)是讀取課程表的數(shù)據(jù),然后創(chuàng)建對(duì)應(yīng)的AssignmentListViewModel對(duì)象:
代碼 31
看到這里,你可能會(huì)問,為什么這里不用監(jiān)聽數(shù)據(jù)源的更改?如果你要編輯課程表,一定要進(jìn)入課程表的用戶界面,一旦離開課程表的用戶界面,課程表的數(shù)據(jù)就會(huì)凍結(jié)下來,換句話說,在AssignmentBookViewModel對(duì)象的整個(gè)生命周期里,課程表的數(shù)據(jù)是穩(wěn)定的。
現(xiàn)在,我們可以著手處理數(shù)據(jù)綁定了。打開AssignmentBookPage.xaml文件,切換到XAML模式,在頁面的資源字典里添加兩個(gè)數(shù)據(jù)模板:
代碼 32
接著,把現(xiàn)有的Pivot項(xiàng)刪除,并在Pivot控件上設(shè)置數(shù)據(jù)模板和數(shù)據(jù)綁定:
代碼 33
最后在AssignmentBookPage的構(gòu)造函數(shù)里創(chuàng)建一個(gè)AssignmentBookViewModel對(duì)象,并它把賦給DataContext屬性:
代碼 34
好了,不知不覺又到看效果的時(shí)候了!按F5運(yùn)行應(yīng)用程序:
圖 26
單擊"課程表"菜單項(xiàng)進(jìn)入課程表,新建兩個(gè)課程,保存,然后按Back鍵返回主菜單:
圖 27
在主菜單里單擊"作業(yè)本"菜單項(xiàng)進(jìn)入作業(yè)本,此時(shí),你會(huì)看到作業(yè)本已經(jīng)為剛才創(chuàng)建的兩個(gè)課程準(zhǔn)備了兩個(gè)Pivot項(xiàng):
圖 28
只是作業(yè)本上沒有任何內(nèi)容,也沒有任何途徑可以添加內(nèi)容……
編輯作業(yè)本
作業(yè)本支持的操作和課程表一樣,包括新建、編輯、刪除、保存所有更改和撤銷所有更改,其中,新建和保存以ApplicationBarIconButton的方式放在Application Bar上,撤銷所有更改以ApplicationBarMenuItem的方式放在Application Bar上,而編輯和刪除則放在上下文菜單里:
圖 29
為什么這樣安排?當(dāng)老師布置作業(yè)時(shí),我們會(huì)掏出作業(yè)本記下作業(yè),下課之后,當(dāng)我們要做作業(yè)時(shí),我們會(huì)掏出作業(yè)本看看要做哪些作業(yè),換句話說,新建、保存和顯示作業(yè)內(nèi)容這三個(gè)功能已經(jīng)可以滿足用戶絕大多數(shù)的需求了。新建和保存作為最常用的兩個(gè)操作自然應(yīng)該放在最顯眼的位置,刪除和撤銷所有更改這兩個(gè)操作基本上不會(huì)用到,至于編輯,一般情況下我們只是用來修改作業(yè)的完成狀態(tài),由于編輯和刪除是針對(duì)特定作業(yè)的,我們把它們放在上下文菜單里,當(dāng)用戶長按某項(xiàng)作業(yè)時(shí)將會(huì)顯示出來,而撤銷所有更改則隱藏在Application Bar的菜單里。
接著,創(chuàng)建一個(gè)Windows Phone Page,并把它命名為NewOrEditAssignmentPage.xaml,這個(gè)頁面會(huì)在用戶單擊Application Bar上的新建按鈕或者上下文菜單上的編輯菜單項(xiàng)時(shí)顯示。完了之后把ApplicationTitle的Text屬性值改為"作業(yè)本",但PageTitle保留原樣:
圖 30
那么,這個(gè)頁面應(yīng)該放些什么控件呢?想想看,創(chuàng)建一個(gè)完整的Assignment對(duì)象需要哪些數(shù)據(jù)?Id是自動(dòng)生成的,課程名稱可以從上下文獲取,創(chuàng)建日期可以從DateTime的Today屬性獲取,剩下的就是截止日期、作業(yè)內(nèi)容和完成狀態(tài)了。截止日期可以使用SL for WP Toolkit的DatePicker控件,作業(yè)內(nèi)容可以使用TextBox控件(上面的標(biāo)題需要額外添置TextBlock控件),而完成狀態(tài)則可以使用CheckBox控件:
圖 31
看到這里,你可能會(huì)問,為什么不把其它信息也顯示出來呢?你可以這樣做,但是,請(qǐng)注意,這個(gè)頁面的主要目的是收集而不是顯示信息,我們應(yīng)該盡可能簡化用戶的輸入過程,在這里放置控件顯示其它信息,尤其是可編輯的控件,可能會(huì)耗費(fèi)用戶額外的注意力,比如說,有些用戶會(huì)下意識(shí)地檢查所有數(shù)據(jù)是否輸入正確。創(chuàng)建作業(yè)的過程應(yīng)該是既簡單又快速的,而我們也希望用戶能有這樣的感受,但耗費(fèi)用戶額外的注意力意味著增加整個(gè)操作過程的時(shí)間,從而可能導(dǎo)致用戶的感受和我們期望的剛好相反,這是我們不希望看到的。
ViewModel類方面,我們將會(huì)仿效課程表的做法,創(chuàng)建NewOrEditAssignmentViewModel、NewAssignmentViewModel和EditAssignmentViewModel三個(gè)類:
圖 32
我們知道,NewOrEditAssignmentPage頁有兩個(gè)模式,一個(gè)是新建模式,另一個(gè)是編輯模式,前者對(duì)應(yīng)NewAssignmentViewModel類,而后者則對(duì)應(yīng)EditAssignmentViewModel類。當(dāng)用戶新建一項(xiàng)作業(yè)時(shí),NewAssignmentViewModel類可以從DateTime的Today屬性獲取創(chuàng)建日期,但它沒法獲取課程名稱,所以我們需要通過參數(shù)傳給它:
代碼 35
為什么DueDate屬性也要設(shè)置呢?想想看,如果我們不給它設(shè)置一個(gè)值,由于DateTime是值類型,將被自動(dòng)初始化為"1/1/0001",當(dāng)用戶看到頁面上的DatePicker控件顯示這樣一個(gè)日期可能會(huì)感到不友好,再者,老師布置下來的作業(yè)一般不會(huì)當(dāng)天交(課堂作業(yè)除外),而第二天交的情況則比較常見(當(dāng)然,計(jì)算下一個(gè)"上課日"可能更加合理)。而當(dāng)用戶編輯一項(xiàng)作業(yè)時(shí),EditAssignmentViewModel類將會(huì)從數(shù)據(jù)源里查找這項(xiàng)作業(yè)的數(shù)據(jù),但前提是我們把作業(yè)的Id告訴它:
代碼 36
需要說明的是,Assignment類的Id屬性是只讀的,而Assignment類原來的構(gòu)造函數(shù)會(huì)在每次調(diào)用時(shí)創(chuàng)建一個(gè)新的Id,這導(dǎo)致了我們無法使用現(xiàn)有的Id,所以我們需要在Assignment類里添加下面這個(gè)構(gòu)造函數(shù):
代碼 37
創(chuàng)建好ViewModel類之后,我們就可以著手處理它們和NewOrEditAssignmentPage頁之間的關(guān)聯(lián)了。首先是設(shè)置數(shù)據(jù)綁定,需要設(shè)置的控件以及對(duì)應(yīng)的綁定表達(dá)式如下表所示:
描述 | 類型 | 屬性 | 綁定表達(dá)式 |
頁面標(biāo)題 | TextBlock | Text | {Binding Title} |
截止日期 | DatePicker | Value | {Binding Assignment.DueDate, Mode=TwoWay} |
作業(yè)內(nèi)容 | TextBox | Text | {Binding Assignment.Content, Mode=TwoWay} |
完成狀態(tài) | CheckBox | IsChecked | {Binding Assignment.IsCompleted, Mode=TwoWay} |
插曲 #2
究竟發(fā)生了什么事?是數(shù)據(jù)沒有添加進(jìn)去?是事件通知沒有發(fā)出?還是出現(xiàn)線程安全的問題?我調(diào)試了一下,數(shù)據(jù)已經(jīng)正確添加進(jìn)去了,事件通知也正確發(fā)出去了,所有操作都在UI線程里執(zhí)行,而且沒有出現(xiàn)并發(fā)問題,那么問題到底出在哪里呢?
帶著這個(gè)疑問,我從codeplex.com上下載了SL for WP Toolkit的最新代碼(Change Set 57505),然后調(diào)試進(jìn)去看看。在調(diào)試的過程中,我發(fā)現(xiàn)每次從NewOrEditAssignmentPage頁返回AssignmentBookPage頁時(shí),LongListSelector控件都會(huì)調(diào)用Balance方法,但每次都會(huì)"跳過"本應(yīng)執(zhí)行的大部分代碼,一開始我沒怎么留意,覺得這個(gè)方法一下子就返回實(shí)在太神奇了,仔細(xì)觀察,原來它是通過第一個(gè)if里的return悄悄返回的:
代碼 43
難怪LongListSelector控件什么也沒顯示,因?yàn)锽alance方法后面那些負(fù)責(zé)調(diào)整顯示的代碼一句都沒執(zhí)行。為什么會(huì)這樣?關(guān)鍵在于IsReady方法,因?yàn)樗看味挤祷豧alse。當(dāng)我單步進(jìn)入IsReady方法時(shí),發(fā)現(xiàn)_itemsPanel和ItemsSource都不為null,但ActualHeight的值卻為0.0,從而導(dǎo)致IsReady方法返回false:
代碼 44
為什么會(huì)這樣?這是因?yàn)?,?dāng)我們打開NewOrEditAssignmentPage頁時(shí),由于AssignmentBookPage頁暫時(shí)無需顯示,Silverlight會(huì)把它從主對(duì)象樹移除,于是ActualHeight會(huì)被"清零",當(dāng)我們從NewOrEditAssignmentPage頁返回時(shí),Silverlight需要重新測量每個(gè)控件的大?。ò撁姹旧恚?,并安排它們的位置,ActualHeight的值為0.0意味著Silverlight還沒完成布局處理的工作,換句話說,LongListSelector控件還沒準(zhǔn)備好,IsReady方法返回false是正確的。奇怪的是,每次我們從NewOrEditAssignmentPage頁返回時(shí),Balance方法里的IsReady方法沒有一次返回true的,這可能意味著Balance方法的調(diào)用時(shí)機(jī)不對(duì),那什么時(shí)候調(diào)用才對(duì)呢?控件加載完畢的時(shí)候,即Loaded事件觸發(fā)的時(shí)候,那么,LongListSelector控件在Loaded事件觸發(fā)的時(shí)候做了些啥呢?其實(shí)沒什么,只是簡單地把_isLoaded設(shè)為true,然后調(diào)用EnsureData方法:
代碼 45
這么看來,問題的關(guān)鍵就在于EnsureData方法有沒有正確調(diào)用Balance方法了。我們來看看EnsureData方法的代碼:
代碼 46
FlattenData和Balance是兩個(gè)很重要的方法,前者負(fù)責(zé)從ItemsSource把數(shù)據(jù)初始化到_flattenedItems,而后者則負(fù)責(zé)確定哪些數(shù)據(jù)需要顯示以及如何顯示。顯然,當(dāng)我們從NewOrEditAssignmentPage頁返回時(shí),如果我們創(chuàng)建了作業(yè),if里面的語句是不可能執(zhí)行的,因?yàn)開flattenedItems里面包含了我們的作業(yè)???這聽起來很別扭,不是嗎?毫無疑問,LongListSelector控件沒有考慮我們的情況,即打開一個(gè)另一個(gè)頁面操作數(shù)據(jù)源,這是不應(yīng)該的,你不可能指望我們把所有事情都放在同一個(gè)頁面里處理吧?
既然知道了原因,問題就不難解決了,把LongListSelector控件的Loaded事件處理程序改成下面這樣:
代碼 47
看到這里,你可能會(huì)問,_isLoadedRaisedBefore是干嘛的?我們知道,第一次進(jìn)入AssignmentBookPage頁和從NewOrEditAssignmentPage頁返回時(shí)都會(huì)觸發(fā)Loaded事件,這是兩種需要區(qū)別處理的情況,因?yàn)锽alance方法里包含了重設(shè)_resolvedFirstIndex和_resolvedCount的代碼(參見代碼43),如果我們?cè)诤竺婺欠N情況下執(zhí)行這行代碼,LongListSelector控件的顯示就會(huì)亂掉,因?yàn)樗?jì)算不出正確的顯示索引,_isLoadedRaisedBefore的存在就是為了防止這種情況的發(fā)生。接著,在Balance方法里用if把重設(shè)_resolvedFirstIndex和_resolvedCount的那行代碼包圍起來:
代碼 48
值得提醒的是,每次調(diào)用FlattenData方法都會(huì)重設(shè)_flattenedItems,這對(duì)于從NewOrEditAssignmentPage頁返回的情況來說是沒有必要的,所以Loaded事件處理程序里的FlattenData方法需要放在if里,否則,使用ObservableCollection就會(huì)變得毫無意義了。
改好之后,編譯一下。注意,如果你是通過MSI安裝SL for WP7 Toolkit的話,你需要先在項(xiàng)目屬性里修改一下版本再編譯,否則待會(huì)重新添加引用的時(shí)候Visual Studio會(huì)自作聰明的引用原來那個(gè)dll文件,因?yàn)镸SI在注冊(cè)表里做了手腳。
一切準(zhǔn)備就緒之后就可以按F5了。單擊Application Bar上的新建按鈕打開NewOrEditAssignmentPage頁:
圖 36
輸入作業(yè)內(nèi)容,然后按確定返回:
圖 37
噢,終于看到我的作業(yè)啦!
編輯作業(yè)本·續(xù)
回到作業(yè)本的操作,接下來我們要實(shí)現(xiàn)編輯和刪除兩個(gè)操作。前面提到,我打算把它們放在上下文菜單里,那么,如何創(chuàng)建上下文菜單?非常簡單,我們可以使用SL for WP Toolkit的ContextMenu控件:
代碼 49
正如你所看到的,ContextMenu控件只需嵌入目標(biāo)對(duì)象就能工作了,非常方便。
接下來的問題是如何實(shí)現(xiàn)它們的事件處理程序。我們知道,這兩個(gè)操作有一個(gè)共同點(diǎn),就是要獲取用戶當(dāng)前選中的作業(yè),怎么獲取呢?有些同學(xué)可能會(huì)建議,在AssignmentListViewModel類里添加一個(gè)SelectedAssignment屬性,并為它和LongListSelector控件的SelectedItem屬性設(shè)置雙向綁定,這樣,一旦用戶選中某項(xiàng)作業(yè),我們就可以通過SelectedAssignment屬性獲取作業(yè)的Id了。你可以這樣做,不過,這個(gè)做法會(huì)帶來一個(gè)小小的問題,就是用戶在長按某項(xiàng)作業(yè)之前得先單擊一下。什么意思?我們知道,手機(jī)沒有鼠標(biāo)右擊的概念,我們是通過長按(Touch and Hold)打開上下文菜單的,但從觸摸手勢的角度來看,長按和單擊(Tap)是兩個(gè)不同的觸摸手勢。LonglistSelector控件只會(huì)在單擊的時(shí)候設(shè)置SelectedItem屬性,它不處理長按,所以當(dāng)我們通過長按打開上下文菜單時(shí),SelectedItem屬性可能為null或者之前選中的其它作業(yè),前者會(huì)引發(fā)異常,而后者則會(huì)為用戶帶來困擾。為了避免這些問題,要么我們?cè)俅涡薷腖ongListSelector控件的代碼,要么用戶不得不執(zhí)行一步額外的操作,顯然,這都不是什么好辦法,還有沒有別的選擇?
當(dāng)然有!你知道嗎,DataContext屬性是一個(gè)很特別的屬性,子元素可以從父元素那里繼承這個(gè)屬性的值,對(duì)照代碼49來看,這意味著MenuItem的DataContext和Grid的有著相同的值,而這個(gè)值正是我們苦苦尋找的作業(yè)!換句話說,只要我們獲取到用戶單擊的MenuItem對(duì)象,就可以通過它的DataContext屬性獲取用戶想要操作的作業(yè)。我們知道,事件處理程序的第一個(gè)參數(shù)就是引發(fā)該事件的對(duì)象,于是我們可以通過這個(gè)參數(shù)來訪問MenuItem對(duì)象:
代碼 50
這樣,我們既不需要在AssignmentListViewModel類里添加一個(gè)SelectedAssignment屬性,也不需要修改LongListSelector控件的代碼,更不需要委屈用戶執(zhí)行額外的操作,真是一舉三得啊!
現(xiàn)在只剩一個(gè)操作了——撤銷所有更改,我相信這對(duì)于你來說不是問題,所以我決定把它留給你當(dāng)課后作業(yè)。
好了,又到看效果的時(shí)候了!按F5運(yùn)行應(yīng)用程序,新建三項(xiàng)作業(yè):
圖 38
長按第三項(xiàng)作業(yè),你會(huì)看到這項(xiàng)作業(yè)以外的所有東西都縮小了,給人一種向后移動(dòng)的感覺,這個(gè)動(dòng)畫生動(dòng)地突出了正在操作的作業(yè)以及上下文菜單,不過,不知道是不是動(dòng)畫的bug,第三項(xiàng)作業(yè)的截止日期上面有個(gè)瑕疵(試了幾次都是這樣):
圖 39
單擊編輯將會(huì)打開NewOrEditAssignmentPage頁,修改一下截止日期:
圖 40
然后按確定返回,你會(huì)看到剛才修改的截止日期:
圖 41
接著,長按第二項(xiàng)作業(yè)(Textbook. P20. Ex 2),并選擇刪除:
圖 42
作業(yè)成功刪除。但是,如果你嘗試刪除(剩下的)第二項(xiàng)作業(yè),你會(huì)發(fā)現(xiàn)它還在那里!為什么!?我調(diào)試了一下,發(fā)現(xiàn)此時(shí)MenuItem對(duì)象的DataContext屬性的值居然是已故的前任第二項(xiàng)(Textbook. P20. Ex 2),而不是我們期望的現(xiàn)任第二項(xiàng)(Textbook. P21. Ex 3)!因?yàn)榍叭蔚诙?xiàng)已被刪除,所以Remove方法不會(huì)觸發(fā)CollectionChanged事件,LongListSelector控件自然不會(huì)更新顯示。如果你現(xiàn)在嘗試刪除第一項(xiàng)作業(yè)(既是前任也是現(xiàn)任),你會(huì)成功的,但是,在刪除之后,如果你再次嘗試刪除剩下的唯一一項(xiàng)作業(yè),你會(huì)發(fā)現(xiàn)此時(shí)MenuItem對(duì)象的DataContext屬性的值變成剛故的前任第一項(xiàng)(Textbook. P10. Ex 9, 10)!從此以后,剩下的唯一一項(xiàng)作業(yè)就再也刪除不了了,除非你返回MainPage頁重新打開AssignmentBookPage頁。由于編輯操作采用了相同的實(shí)現(xiàn)思路,如果一項(xiàng)作業(yè)刪除不了,那么它也編輯不了。
究竟發(fā)生了什么事?是ContextMenu控件的bug嗎?我另外創(chuàng)建了一個(gè)新的項(xiàng)目,在同等條件下,分別在ListBox和LongListSelector上測試了ContextMenu,結(jié)果,Listbox一方表現(xiàn)正常,而LongListSelector一方問題依舊,有趣的是,即使不用打開新的頁面,結(jié)果還是一樣。這讓我不得不再一次懷疑是LongListSelector控件的問題。
我重新運(yùn)行應(yīng)用程序,然后單步執(zhí)行第一次刪除的整個(gè)過程。在這個(gè)過程里,我發(fā)現(xiàn)一個(gè)很奇怪的事情,當(dāng)我刪除第二項(xiàng)時(shí),LongListSelector控件先把第三項(xiàng)的ContentPresenter和Assignment分離開來,并把分離出來的ContentPresenter推入內(nèi)部的_recycledItems(類型為Stack<ContentPresenter>),接著對(duì)第二項(xiàng)做相同的事,然后把第二項(xiàng)從_flattenedItems里刪除,最后重新關(guān)聯(lián)第三項(xiàng)的ContentPresenter和Assignment,問題就出現(xiàn)在最后一步,它居然直接使用_recycledItems頂部的ContentPresenter,換句話說,它把第二項(xiàng)的ContentPresenter和第三項(xiàng)的Assignment關(guān)聯(lián)了!見鬼!此時(shí),如果我刪除第一項(xiàng)的話,它會(huì)把第一項(xiàng)的ContentPresenter和第三項(xiàng)的Assignment關(guān)聯(lián)!從這里不難看出,它應(yīng)該在關(guān)聯(lián)之前把_recycledItems頂部那個(gè)垃圾扔掉!既然知道了原因,問題就不難解決了,在OnRemove方法的相應(yīng)地方加上紅框里面那句:
代碼 51
重新編譯所有東西,然后運(yùn)行應(yīng)用程序,這次沒問題了。
寫完這篇文章之后,我的第一感覺是LongListSelector控件遠(yuǎn)未達(dá)到產(chǎn)品級(jí)別的質(zhì)量,它的問題導(dǎo)致我無法專注于應(yīng)用程序本身的功能設(shè)計(jì)和實(shí)現(xiàn),如果你是本著學(xué)習(xí)和研究的態(tài)度去用它,那沒問題,如果你想用它來做產(chǎn)品,那你就要做好心理準(zhǔn)備了。不管怎樣,這次我還是學(xué)到了不少東西。LongListSelector控件的補(bǔ)丁我已經(jīng)提交到codeplex.com了,在官方發(fā)布修正版本之前,我只能使用自己修改的版本了→_→
下課了……
it知識(shí)庫:WP7有約(二):課后作業(yè),轉(zhuǎn)載需保留來源!
鄭重聲明:本文版權(quán)歸原作者所有,轉(zhuǎn)載文章僅為傳播更多信息之目的,如作者信息標(biāo)記有誤,請(qǐng)第一時(shí)間聯(lián)系我們修改或刪除,多謝。