前言:
距離上一篇博客,整整一個月的時間了。人不能懶下來,必須有個階段性的總結,算是對我這個階段的一個反思。人只有在總結的過程中才會發現自己的不足。
公司每天都要在OA系統上上班點擊簽到,下班點擊簽退,每天都要寫工作日志。有的時候頭腦不清醒或者忙過頭了(別說你們沒有過),就會忘記簽到或者簽退,有時候甚至忘記寫工作日志。這會直接導致扣人工啊有木有,所以我才有了這個想法。首先聲明,開發這個東西並不是博主對工作不認真不負責任,也並不是偷懶。相反,第一,可以避免因工作過忙忘記簽到扣工資;第二,在開發的過程中你學到的東西是快速的,有趣的,讓自己受益的。對於每個公司來說,OA系統都是他們的公司機密,所以博主並不會貼源碼,只在這里闡述一個開發流程與思想,讓你感覺到做一個自己覺得有趣的產品,思想的火花是多么不可思議。
一. 用到的模板與技術
1. WPF
相比傳統的WinForm,WPF真是太強大了,無論在UI還是在多線程的處理上,以及一些其它的改進,都預示着WinForm將被WPF取代(這只是理論上,事實上,因為很多產品都是多年前開發的,用的是WinForm,如果要整個框架移植到WPF將是一件痛苦的事,反正產品沒功能上的問題,這個移植就顯得沒必要了。所以目前,很多公司依然使用着WinForm的技術,開發者都在這個基礎上對產品縫縫補補,更沒有機會接觸WPF了,就算是會這門技術的人,也找不到這個職位。比如我的公司就是)。本軟件全面采用WPF技術,使用XAML布局以及做一些增強用戶體驗的動畫。
2. Modern UI
Modern UI 是基於WPF的一個開源項目,托管在 code plex 上。你可以參考以下方法把 Modern UI 的模板添加到你的 Visual Studio 上:
- 在Visual Studio 2012中,打開擴展管理器(工具 > 擴展和更新)
- 選擇在線 > Visual Studio庫和搜索“ 現代UI “
- 選擇現代為WPF的UI模板,然后單擊“ 下載“,下載並安裝。
關於 Modern UI 的介紹和使用,請參考http://mui.codeplex.com/,博主不再累贅。
3. 多線程
任何一個涉及到下載數據的程序都應該使用多線程編程,我們另開一個線程去下載數據的話,界面就不會有“假死”現象,用戶體驗顯著提升。在WPF中,如果要在子線程中獲取或者設置界面UI的值也是很簡單的事情,這個WPF都為我們處理好了,很方便使用。
4. Lambda表達式
Lambda表達式是一個匿名方法,你不必再為只使用一次的方法獨立寫成一個函數(比如委托)。在WPF中在子線程里獲取界面控件的值的時候就使用了Lambda表達式。Lambda表達式是委托的實現方法。
5. MVVM設計模式
MVVM 是 Model-View-ViewModel 的縮寫,看字面你就能想到是什么意思吧。使用它的好處是,如果綁定的數據上下文改變了,會自動通知UI做出相應的更改。這也是比WinForm進步的地方。對於開發人員來說相當方便。當然MVVM不只這些內容。
6. XML配置文件的操作
因為要保存用戶名密碼、是否開啟自動簽到動能、自動簽到的時間等等數據,就用到了App.config,實際上這是一個XML文件。
7. 系統托盤的處理
很多程序都有這個功能,主要是為了讓程序在后台繼續運行,以便時間到了就自動簽到或者簽退。
8. 開機自啟
早上一來開機就自動啟動,然后自動簽到,會很爽吧,都完全不用自己動手。主要是寫入注冊表操作。
9. 模擬瀏覽器請求(重點)
使用HttpWatch來抓包,使用HttpWebRequest和HttpWebResponse來模擬瀏覽器的行為,要理解HTTP請求協議,當然在asp.net下還要理解asp.net網站與普通網站的差異(asp.net的原理)。asp.net的網站,使用服務器控件開發的話,頁面上會有一大堆“垃圾代碼”,用來保存頁面狀態,控件狀態等等信息,這些信息在發送post報文的時候也需要發送過去,而且它的值的長度很長。
10. 正則表達式
軟件里面大量使用了正則表達式從服務器返回的html頁面來獲取我們需要的數據,比如我的工作日志列表,簽到記錄等等。關於正則表達式無非兩個類,Match/MatchCollection、Regex。有興趣的自己去了解一下正則表達式的使用,即便是做前端開發的,也需要掌握js下的正則表達式來做客戶端的表單驗證。
二. 核心代碼HtmlHelper

public static class HtmlHelper { private static string cookieHeader = string.Empty; /// <summary> /// 添加日志 /// </summary> /// <param name="strUrl">請求的url</param> /// <param name="param">參數</param> /// <returns></returns> public static string PostData(string strUrl, string param) { string strResult = ""; HttpWebRequest myHttpWebRequest = (HttpWebRequest)WebRequest.Create(strUrl); myHttpWebRequest.AllowAutoRedirect = true; myHttpWebRequest.KeepAlive = true; myHttpWebRequest.Accept = "image/gif, image/x-xbitmap, image/jpeg, imagepeg, applicationnd.ms-excel, application/msword, application/x-shockwave-flash, */*"; myHttpWebRequest.Timeout = 10000; myHttpWebRequest.UserAgent = "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; Maxthon; .NET CLR 2.0.50727)"; myHttpWebRequest.ContentType = "application/x-www-form-urlencoded"; myHttpWebRequest.Method = "POST"; myHttpWebRequest.Headers.Add("cookie:" + cookieHeader); Stream MyRequestStrearm = myHttpWebRequest.GetRequestStream(); StreamWriter MyStreamWriter = new StreamWriter(MyRequestStrearm, Encoding.ASCII); //把數據寫入HttpWebRequest的Request流 MyStreamWriter.Write(param); //關閉打開對象 MyStreamWriter.Close(); MyRequestStrearm.Close(); HttpWebResponse response = null; System.IO.StreamReader sr = null; response = (HttpWebResponse)myHttpWebRequest.GetResponse(); sr = new System.IO.StreamReader(response.GetResponseStream(), Encoding.GetEncoding("utf-8")); // //utf-8 strResult = sr.ReadToEnd(); return strResult; } /// <summary> /// 功能描述:模擬登錄頁面,提交登錄數據進行登錄,並記錄Header中的cookie /// </summary> /// <param name="strURL">登錄數據提交的頁面地址</param> /// <param name="strArgs">用戶登錄數據</param> /// <returns></returns> public static string PostLogin(string strURL, string strArgs) { string strResult = ""; HttpWebRequest myHttpWebRequest = (HttpWebRequest)WebRequest.Create(strURL); myHttpWebRequest.AllowAutoRedirect = true; myHttpWebRequest.KeepAlive = true; myHttpWebRequest.Accept = "image/gif, image/x-xbitmap, image/jpeg, imagepeg, applicationnd.ms-excel, application/msword, application/x-shockwave-flash, */*"; myHttpWebRequest.UserAgent = "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; Maxthon; .NET CLR 2.0.50727)"; myHttpWebRequest.ContentType = "application/x-www-form-urlencoded"; myHttpWebRequest.Method = "POST"; myHttpWebRequest.Headers.Add("cookie:" + cookieHeader); CookieContainer myCookieContainer = new CookieContainer(); myHttpWebRequest.CookieContainer = myCookieContainer; Stream MyRequestStrearm = myHttpWebRequest.GetRequestStream(); StreamWriter MyStreamWriter = new StreamWriter(MyRequestStrearm, Encoding.ASCII); //把數據寫入HttpWebRequest的Request流 MyStreamWriter.Write(strArgs); //關閉打開對象 MyStreamWriter.Close(); MyRequestStrearm.Close(); HttpWebResponse response = null; System.IO.StreamReader sr = null; response = (HttpWebResponse)myHttpWebRequest.GetResponse(); cookieHeader = myHttpWebRequest.CookieContainer.GetCookieHeader(new Uri(strURL)); sr = new System.IO.StreamReader(response.GetResponseStream(), Encoding.GetEncoding("utf-8")); // //utf-8 strResult = sr.ReadToEnd(); return strResult; } /**/ /// <summary> /// 功能描述:在PostLogin成功登錄后記錄下Headers中的cookie,然后獲取此網站上其他頁面的內容 /// </summary> /// <param name="strURL">獲取網站的某頁面的地址</param> /// <param name="strReferer">引用的地址</param> /// <returns>返回頁面內容</returns> public static string GetPage(string strURL) { string strResult = ""; HttpWebRequest myHttpWebRequest = (HttpWebRequest)WebRequest.Create(strURL); myHttpWebRequest.ContentType = "textml"; myHttpWebRequest.Method = "GET"; myHttpWebRequest.Headers.Add("cookie:" + cookieHeader); HttpWebResponse response = null; System.IO.StreamReader sr = null; response = (HttpWebResponse)myHttpWebRequest.GetResponse(); sr = new System.IO.StreamReader(response.GetResponseStream(), Encoding.GetEncoding("utf-8")); // //utf-8 strResult = sr.ReadToEnd(); return strResult; } /// <summary> /// 獲取隱藏控件的value /// </summary> /// <param name="loginHtml">HTML頁面代碼字符串</param> /// <param name="regex">要獲取的值的正則表達式</param> /// <param name="replaceLeft">左邊要刪除的字符串</param> /// <param name="replaceRight">右邊要刪除的字符串</param> /// <returns></returns> public static string GetHiddenValue(string loginHtml, string regex, string replaceLeft, string replaceRight) { string viewState = string.Empty; Match match = new Regex(regex).Match(loginHtml); if (match.Success) { viewState = match.Value.Replace(replaceLeft, ""); viewState = viewState.Replace(replaceRight, ""); } return viewState; }
吐槽一下自己,這個類其實可以優化,比如PostLogin和PostData其實可以合並,GetHiddenValue也可以寫得更好,只是工作這邊還比較忙,剛剛接手了一個任務,是把項目的結構全面改版,使得每一個功能就是一個小項目,這樣管理起來方便很多,所以沒有時間進行優化,做好了自己能用就用着先,還是工作比較重要。
很明顯,里面有4個方法,每個方法的作用以及調用方式都有很詳細的注釋,我就不重復了。至於更多的代碼我就不貼了,畢竟涉及到商業機密的問題。講講原理吧。
三. 原理
首先,我們知道http是無狀態連接,每次瀏覽器向服務器端發送請求,服務器返回數據之后就斷開了,你下一次請求的時候服務器並不知道你是否已經登錄,那么asp.net下服務器怎么知道你的登陸狀態呢?當你登陸之后,服務器會給瀏覽器發送一個cookie,用來標識你的登錄狀態,下一次請求的時候瀏覽器會把這個cookie一同發給服務器,服務器接收到之后驗證你發過來的cookie數據,然后就知道你是否已經登陸過。如果沒登陸,就不讓你請求別的頁面數據。我們可以使用HttpWebRequest和HttpWebResponse類來模擬瀏覽器的請求。
登陸之后,我們要寫工作日志,就要把日志內容拼成要提交的報文,然后post到服務器,這就是一個post請求過程。還有一種請求叫做get請求,這種請求是不提交報文的,直接發請求,然后服務器就會返回一個html頁面,然后我們就可以利用正則表達式來獲取我們需要的數據了。
這里有一個值得注意的地方,因為我們的程序要一個掛在電腦上,以便它可以到時間后自動簽退或者簽到,但是服務器為你保存的登錄狀態是有時間段的,如果過了一段時間你沒有請求操作,服務器認為你已經斷開連接,不再為你保持登錄狀態(我們公司的OA系統似乎是半個小時),所以我們進行一個請求之前要判斷一下登錄狀態是否還保持着,如果斷開了就重新登錄一下再進行請求。怎么判斷登錄狀態呢?我們公司的OA系統會彈出一個提示框提示身份驗證過期,而這個提示框當然是在html頁面上的,我們只需要請求一下主頁,看它返回的html頁面中是否包含身份驗證過期這個信息就行了(別說你不知道html頁面其實就是一個字符串)。
四. 曬圖
1.登陸(其實只是保存了用戶名密碼,並沒有真正登陸,到需要進行登陸操作的時候才登陸,比如提交日志、簽到、簽退、獲取日志列表、登錄信息等等,當然並不是每次這些操作都登陸,只要登錄狀態還保持着,就不需要重新登陸了)
2.主頁
3.添加工作日志
4.個性化(模板自帶)
5.用戶信息(用於登陸)
6.系統托盤
7.簽到成功提示
五. 后話
在這個浮躁的世界里,我們不可以浮躁。人要有夢想,不然跟條咸魚有什么分別。雖然我在追求自己喜歡的生活方式上有着各種阻礙,但是我還是認為,以自己喜歡的方式生活才是最開心的。人就一輩子,只要能承擔責任,還有什么必要跟自己過不去呢?昨天我朋友從鳳凰古城給我寄來一張明信片,我感慨萬千,於是回復了一句:祝前程似錦,山明水秀,兄弟情誼,萬古長青。切記,莫屈服,要以自己喜歡的方式生活。
也獻給你們,親愛的園友。