基于 WPF + Modern UI 的 公司OA小助手 开发总结
前言:
距离上一篇博客,整整一个月的时间了。人不能懒下来,必须有个阶段性的总结,算是对我这个阶段的一个反思。人只有在总结的过程中才会发现自己的不足。
公司每天都要在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.签到成功提示
五. 后话
在这个浮躁的世界里,我们不可以浮躁。人要有梦想,不然跟条咸鱼有什么分别。虽然我在追求自己喜欢的生活方式上有着各种阻碍,但是我还是认为,以自己喜欢的方式生活才是最开心的。人就一辈子,只要能承担责任,还有什么必要跟自己过不去呢?昨天我朋友从凤凰古城给我寄来一张明信片,我感慨万千,于是回复了一句:祝前程似锦,山明水秀,兄弟情谊,万古长青。切记,莫屈服,要以自己喜欢的方式生活。
也献给你们,亲爱的园友。