WM有約(三):下一次是什么時候?
Written by Allen Lee
不要留戀過去
怎樣才能約束用戶,不讓其選擇過去的日期呢?有一個很傻的辦法,就是每次啟動應用程序的時候,自動把MonthCalendar控件的MinDate屬性的值設為今天。這樣雖然禁止了用戶選擇過去的日期,卻帶來另外一些問題:
- 當月之前的日期無法查看。
- 和選項窗體的Min Date設置相沖。
有鑒于此,我們采用另一種辦法,就是在用戶選中某個日期時,判斷這個日期是否已經過去,若是,則禁用Pin菜單項,若否,則啟用Pin菜單項。那么,如何獲知用戶選中了某個日期?最簡單的辦法就是使用MonthCalendar控件的DateChanged事件:
代碼 1
運行應用程序,你會發現,當我選中今天或者將來的日期時,Pin菜單項是啟用的(圖1),而當我選中過去的日期時,Pin菜單項則是禁用的(圖2):
圖 1
圖 2
這(幾)天不要選
在繼續之前,我們有必要搞清楚,"排除某(幾)天"究竟是什么意思。在這里,"排除某(幾)天"并不是指禁止用戶選中那(幾)天,而是指那(幾)天不在計劃中,但我們很清楚,計劃趕不上變化,或許那(幾)天真正到來的時候又可以選了。
和之前的"釘住日期"相比,"排除日期"除了無需在MonthCalendar控件上有所反映之外,其它部分基本上是一樣的,它支持排除某天、連續的幾天和某個周末,用來保存被排除的日期的文件和應用程序放在同一個文件夾里,應用程序在啟動的時候會檢查這個文件是否存在,如果不存在就創建一個空白的文件。從上面這些描述來看,"排除日期"和"釘住日期"在很大程度上共享著相同的代碼,于是,接下來就是考慮如何重用現有的代碼并實現新的功能。
首先要處理的是LoadPinnedDates和SavePinnedDates兩個方法(參見《WM有約(一):你好,CF》的代碼5和代碼6),我們提取這兩個方法的代碼,并創建兩個新的方法:
代碼 2
代碼 3
這樣,LoadPinnedDates和SavePinnedDates兩個方法就可以簡化為分別對LoadDates和SaveDates兩個方法的調用了,而LoadExcludedDates和SaveExcludedDates兩個方法也可以如法炮制了。在著手實現這些方法之前,我們還需要提供一個東西,那就是文件的路徑,也是我們接下來需要做的事情——改造GetFilePath方法(參見《WM有約(一):你好,CF》的代碼4),改造后的GetFilePath方法將會用來映射文件路徑:
代碼 4
有了這些準備,我們就可以著手實現LoadExcludedDates和SaveExcludedDates兩個方法了:
代碼 5
至于LoadPinnedDates和SavePinnedDates兩個方法的新版本就留給讀者自行處理了。
接著就是"排除日期"的核心功能——ExcludeWeekend和ExcludeRange兩個方法了,它們與PinWeekend和PinRange兩個方法(參見《WM有約(一):你好,CF》的代碼1和代碼2)的最大區別就是不需要把操作結果反映在MonthCalendar控件上,而它們的共同之處是都要計算具體的日期并把它們添加到對應的集合里。我們先來看看計算具體的日期這部分功能,它分為兩種情況,一種是計算周末的,另一種是計算兩個日期之間的,如果這兩個日期相同,則視為一天,于是,我們可以創建CalculateWeekend和CalculateRange兩個方法來分別負責這兩種情況:
代碼 6
有了這些準備,我們就可以著手實現ExcludeWeekend和ExcludeRange兩個方法了:
代碼 7
至于PinWeekend和PinRange兩個方法的新版本就留給讀者自行處理了。
還差什么呢?對,用戶界面,沒有這個,我們辛苦了這么久就白干了:
圖 3
還有Exclude菜單項的相關代碼:
代碼 8
噢,別忘了在InitializeFile方法(參見《WM有約(一):你好,CF》的代碼12)里添加檢查文件是否存在的代碼,以及在適當的地方添加保存數據的代碼,否則……
運行應用程序,選中2009年1月17日到2009年1月31日之間的日期,然后單擊Exclude菜單項:
圖 4
通過資源管理器找到ExcludedDates.txt文件,然后用Word Mobile查看里面的內容,結果發現只有下面3天!
圖 5
問題出在哪里?原來,我選中的那幾天的開始日期恰好是星期六,于是應用程序"自作聰明"地把它視為一般周末!如何解決這個問題?回到代碼8,我們知道,ExcludeWeekend方法的調用需要滿足兩個條件,第一個是用戶只選中了一天,另一個則是這天是星期六。要知道用戶是否只選中了一天,我們只需要看看SelectionStart和SelectionEnd兩個屬性是否同一天:
代碼 9
再次運行應用程序,這次就正常了:
圖 6
需要提醒的是,Pin菜單項的相關代碼由于應用了相同的邏輯,于是也存在相同的問題,不過解決方法是一樣的,所以這里就不說了。另外,因為"排除日期"不像"釘住日期"那樣會在用戶界面上有所反映,所以當我們單擊Exclude菜單項時,一切都在后臺完成,如果用戶不知情的話,感覺起來就像什么也沒干一樣,為了增強用戶體驗,最好就顯示一個消息框告訴用戶日期已被排除。
這(幾)天應該選
由于"包含日期"和"排除日期"極其相似,再加上我們在實現"排除日期"時提取的公共代碼也適用于"包含日期",于是,我們可以用非一般的速度來實現"包含日期"的內部邏輯:
代碼 10
至于用戶界面,我們同樣為它添加一個Include菜單項:
圖 7
而這個菜單項的相 關代碼如下:
代碼 11
其它東西,例如應用程序啟動的時候檢查用來保存日期的文件是否存在、讀取保存的日期和在適當的時候保存日期,和前面的實現大同小異,這里就不細說了。
運行應用程序,選中2009年2月14日,然后單擊Include菜單項:
圖 8
由于這天剛好是星期六,所以應用程序執行了包含周末的邏輯,這也是預期的行為:
圖 9
到了這里,你可能會認為"排除日期"和"包含日期"也是時候告一段落了,但事實上我們還有一個問題需要處理。試想一下,如果我對同一個日期先后執行包含和排除操作,那么應用程序是否應該分別在m_IncludedDates和m_ExcludedDates兩個集合里登記這個日期?我們知道,"排除日期"和"包含日期"都是用來反映計劃的調整,比起分別在兩個地方登記同一個日期,執行抵消操作或許更有意義。舉個例子,剛才我包含了2009年2月14日,現在我要排除這個日期,那么應用程序應該從m_IncludedDates里刪除這個日期而不是向m_ExcludedDates添加這個日期。怎么樣?是不是很簡單?然而,這個東西實現起來一點都不容易,因為我們通常操作的是一組日期而不是單個日期,如果我們足夠好運,那么要抵消的日期集合會是被抵消的日期集合的子集,如果我們不夠運氣,那么……一般地,如果我們要包含一組日期,那么我們要先檢查m_ExcludedDates是否包含了這些日期的部分或全部,如果是,則從m_ExcludedDates里刪除相同部分,剩下的才添加到m_IncludedDates。以IncludeWeekend方法(參見代碼10)為例,從最初的monthCalendar1.SelectionStart到最后的m_IncludedDates.AddRange需要經過如下四步:
圖 10
其中,第三步的Subtract方法是解決這個問題的關鍵,那么,如何實現這個方法呢?我們知道,List本身沒有提供這個方法,要想達到這樣的效果就得使用C# 3.0的擴展方法了。下面來看看我的實現:
代碼 12
對于second里的每個日期,Subtract方法試圖從first里刪除,并通過Remove方法的返回值判斷刪除操作是否成功,如果不成功,就意味著這個日期應該添加到m_IncludedDates里,于是返回這個日期。有了這些準備,我們就可以著手修改IncludeWeekend方法:
代碼 13
另外,IncludeRange、ExcludeWeekend和ExcludeRange等方法也需要修改,不過都是大同小異,所以就不一一列舉了。
下一次是什么時候?
下一次……在MonthCalendar控件下面……
圖 11
通常,這種可預測的"下一次"都意味著計算周期的存在,對于這個應用程序,這個周期是兩周,以星期六為計算基準,比如說,假設上圖的5、6和7三天已被釘住,那么 下次應該被釘住的日期將會是19、20和21三天,于是"Next time:"下面的Label就應該顯示"2008年12月19日"。這個計算過程的一般形式如下圖所示:
圖 12
有了這些分析,我們就可以著手實現CalculateNextTime方法了:
代碼 14
故事到此結束了嗎?當然不是,前面我們花了這么多精力來實現"排除日期"和"包含日期",如果僅僅用來保存一些日期,那么我也未免太無聊了。
首先,我們來看看"包含日期"將會如何影響"下一次"的計算,還是借用圖11,假設19、20和21三天已被釘住,今天是23號,那么"Next time:"下面的Label應該顯示"2009年1月2日",但如果26、27和28三天已被包含,那么"Next time:"下面的Label就應該顯示"2008年12月26日"了。簡而言之,在時間軸上排在前面的"包含日期"將會取代使用默認算法計算出來的日期:
代碼 15
接著,我們再來看看"排除日期"將會如何影響"下一次"的計算,假設2、3和4三天已被釘住,今天是6號,那么"Next time:"下面的Label應該顯示"2009年1月17日",但如果17到31之間的日期已被排除,那么"Next time:"下面的Label就應該顯示"2009年2月7日"了。
圖 13
簡而言之,"排除日期"會導致使用默認算法計算出來的日期逐周往后推,直到計算出來的日期沒被排除為止:
代碼 16
由此可見,完整的CalculateNextTime方法應該包含如下四步:
圖 14
其中,第一步和第四步是從代碼14里分解出來的:
代碼 17
有了這些準備,我們就可以著手實現完整的CalculateNextTime方法了:
代碼 18
最后,終于到最后了,我們要把計算結果顯示在應用程序主窗體的"Next time:"下面,那么,我們應該在什么時候顯示呢,又應該在什么時候更新呢?用Activated事件!你可能會問,為什么不用Load事件呢?這是因為當用戶單擊應用程序右上角的關閉按鈕時,應用程序實際上只是最小化到后臺,當用戶通過菜單或者其它方式再次啟動應用程序時,實際上只是把應用程序"還原"到前臺,而在這個過程里Load事件并不會被觸發。事不宜遲,讓我們完成本集的最后一段代碼吧:
代碼 19
運行應用程序,終于看到下一次是什么時候了:
圖 15
你還想要什么?
在這本集里,我們花費巨大精力實現"下一次"的計算,然而,"除了'現在',你永遠不能生活在任何其他時刻,你所能得到的只是現在的時光,未來在到來時也只不過是另一個現在"([美]韋恩·W·戴爾,《你的誤區》),好好把握每一個"現在",你將會得到一個滿意的軌跡。
到目前為止,應用程序的用戶界面都是為"垂直"屏幕設計的,有沒有想過,假如用戶旋轉設備的屏幕,使之變成"水平"的,將會發生什么事情呢?下一集,我們將會探討這個問題及其解決方案,我們還會嘗試創建用戶控件來封裝MonthCalendar控件、"下一次"Label以及相關的代碼,如果"時間"允許的話,我們還會看看如何在這個用戶控件上實現數據綁定。
相關文章: