分享几个asp.net开发中的小技巧
下面这几个,是在实际开发或阅读中发现的一些问题,有些甚至是有很多年开发人员写出的代码,也是很多人经常犯的错误。各位可以看看,你有没有躺着中枪。
第一个,对整型变量进行非null判断。
// a 是int型 (不是int?) if(a != null){ //操作 }
个人点评:无意义判断,值类型永远不可能为null。
第二个,用static来保持页面回发
static int id; protected void Page_Load(object sender, EventArgs e) { if (Request.QueryString["ID"] != null && Request.QueryString["ID"].ToString() != "") { id = Convert.ToInt32(Request.QueryString["ID"].ToString()); } }
个人点评:这个不解释,不知道怎么说。但最近还真就遇到了,而且也不是什么小项目,WebForm无服务器控件开发。
第三个,用编程方式绑定数据控件时,数据源为DataSet时判断null而不判读DataSet内的Tables数。
DataSet ds = bll.GetList(); if (ds != null) { Repeater1.DataSource = ds; Repeater1.DataBind(); }
个人点评:当bll.GetList()返回的DataSet非null但里面没有包含数据表时,执行DataBind()方法时会报HttpException异常(IListSource 不包含任何数据源)。正确写法应该是
DataSet ds = bll.GetList(); if (ds != null && ds.Tables.Count > 0) { Repeater1.DataSource = ds; Repeater1.DataBind(); } //或 DataSet ds = bll.GetList()??new DataSet(); if (ds.Tables.Count > 0) { Repeater1.DataSource = ds; Repeater1.DataBind(); }
第四个,用编程方式绑定数据控件时,数据源为DataTable或List<T>时判断null。
DataTable dt = bll.GetList(); if (dt!=null) { Repeater1.DataSource = dt; Repeater1.DataBind(); }
个人点评:无意义判断,下面的写法没有任何问题,即使dt=null
DataTable dt = bll.GetList(); Repeater1.DataSource = dt; Repeater1.DataBind();
第五个
Model m = new Model(); m = bll.GetModel(id); m.name;
个人点评:以为只要声明时不为null,后面就不需要做非空非null判断了。万一第二步BLL层返回的model就为null呢?
第六个,在Repeater1_ItemDataBound中写这样的代码
Label lblPMID = (Label)e.Item.FindControl("lblPMID"); if (lblPMID.Text != "") { //操作 }
个人点评:低效,无意义判断,很可能出现NullReferenceException(未将对象引用设置到对象的实例)异常。
正确写法:
Label lblPMID = e.Item.FindControl("lblPMID") as Label; if (lblPMID!=null && lblPMID.Text != "") //视里面使用情况决定是否判断lblPMID.Text为“”或空白 { //操作 }
第七个
string txtName = Request["txtName"] == null ? "" : Request["txtName"].ToString(); string strWhere += "and ID=" + userId + ""; //userId是int if (txtName != "") { strWhere += " and NAME='" + txtName + "'"; } strWhere += " order by id desc"; //项目本身都是采用参数化查询的,这里是一些暴露给Web层的高级查询条件。
个人点评:1、值类型和字符串拼接会隐式装箱,2、SQL注入危险。正确做法是userId.ToString()并且过滤txtName中特殊字符,限制字符串长度。
注意,拼接SQL时过滤字符串并不能完全防止SQL注入,但很多时候在高级查询时拼接SQL是最简单也是最方便的,这时候过滤不应该只过滤一些指定的特殊字符,
比如只过滤单引号,等号,大于/小于/等于,空格,括号之类的危险字符。应该对除中文字符、英文字母、和数字外的所有字符全部过滤掉(视情况而定)。
并且严格限制字符串长度,一般查询时输入的关键字不会太长,如果用户输入的有空格,就拆分成多个条件。这样能尽可能的减小SQL注入的机会。
最后,给大家分享几个小经验,虽说有些只是语法糖,但却可以帮助我们更高效编写或阅读代码。
一、引用类型的null值很麻烦,因为类型为null时使用点运算符 (.)会报异常,所以经常要做非null判断,可以用?? null 合并运算符减少代码量。例如:
//写法一 int ID; if (Request.Form["ID"] != null && Request.Form["ID"].ToString() != "") { ID = Convert.ToInt32(Request.Form["ID"].ToString()); } //写法二 int id; if (int.TryParse(Request.Form["ID"]??"",out id)) { } //方法一 string userName2=string.Empty; if (Session["userName"]!=null) { userName2 = Session["userName"].ToString(); } //方法二 string userName1 = Session["userName"] == null ? "" : Session["userName"].ToString(); //方法三 string userName = (Session["userName"] ?? "").ToString();
二、Web项目中的所有Session或cookie最好统一放到一个类中管理。最重要的目的是把Session中索引名独立出来管理,也就是除了本类外的所有页面都不要输入Session名。
可能用语言表达不够直白,直接上代码。
看到很多人是这样,包括网上流行的一些很常见的辅助类库。
1 /// <summary> 2 /// Session 操作类 3 /// 1、GetSession(string name)根据session名获取session对象 4 /// 2、SetSession(string name, object val)设置session 5 /// </summary> 6 public class SessionHelper 7 { 8 /// <summary> 9 /// 根据session名获取session对象 10 /// </summary> 11 /// <param name="name"></param> 12 /// <returns></returns> 13 public static object GetSession(string name) 14 { 15 return HttpContext.Current.Session[name]; 16 } 17 /// <summary> 18 /// 设置session 19 /// </summary> 20 /// <param name="name">session 名</param> 21 /// <param name="val">session 值</param> 22 public static void SetSession(string name, object val) 23 { 24 HttpContext.Current.Session.Remove(name); 25 HttpContext.Current.Session.Add(name, val); 26 } 27 /// <summary> 28 /// 检测session是否存在 29 /// </summary> 30 /// <param name="name"></param> 31 /// <returns></returns> 32 public static bool CheckSession(string name) 33 { 34 try 35 { 36 if (GetSession(name) == null) 37 { 38 return false; 39 } 40 else 41 { 42 return true; 43 } 44 } 45 catch 46 { 47 return false; 48 } 49 } 50 }
我想知道,这样做有什么实际意义么?而且HttpContext.Current还不做null检查。
项目中还到处都是SessionHelper.SetSession("name"),SessionHelper.GetSession("name")这样编译器无法找到具体引用的代码,当你有很多页面用到这个会话后,
你再想更改会话名称或删除这个会话那将是一场灾难,而且这样的代码多了还可能出现多个会话重名造成冲突,名称写错造成会话丢失。
要克服以上问题,Web项目的Session会话你应该这样写
1 /// <summary> 2 /// 系统中Session会话管理 3 ///<para>注意:该类使用Session,若在一般处理程序中调用该类方法必须实现 IRequiresSessionState 接口</para> 4 /// </summary> 5 public class SessionManager 6 { 7 private const string USER_LOGIN_MARK = "userModel"; //用户登录 8 private const string USER_REGISTER_MARK = "registerModel"; //用户注册 9 private const string CHECKSUM_MARK = "checksumModel"; //校验码 10 private const string CAPTCHA_MARK = "captchaModel"; //验证码 11 12 #region 用户登录会话 13 /// <summary> 14 /// 添加用户登录标识 15 /// </summary> 16 /// <param name="cardModel"></param> 17 internal static void AddUserLoginMark(UserModel model) //UserModel是用户对象 18 { 19 if (model == null) 20 { 21 throw new ArgumentNullException("参数不能为Null"); 22 } 23 24 HttpContext context = HttpContext.Current; 25 if (context != null) 26 { 27 context.Session[USER_LOGIN_MARK] = model; 28 } 29 } 30 31 /// <summary> 32 /// 移除用户登录标识 33 /// </summary> 34 internal static void RemoveUserLoginMark() 35 { 36 HttpContext context = HttpContext.Current; 37 if (context != null) 38 { 39 context.Session.Remove(USER_LOGIN_MARK); 40 } 41 } 42 43 /// <summary> 44 /// 获取当前登录用户对象 45 /// </summary> 46 /// <returns></returns> 47 internal static UserModel GetUserLogin() 48 { 49 HttpContext context = HttpContext.Current; 50 if (context != null) 51 { 52 return context.Session[USER_LOGIN_MARK] as UserModel; 53 } 54 55 return null; 56 } 57 #endregion 58 59 #region 用户注册会话 60 /// <summary> 61 /// 添加用户注册会话标识 62 /// </summary> 63 /// <param name="cardModel"></param> 64 internal static void AddUserRegisterMark(RegisterModel model) 65 { 66 if (model == null) 67 { 68 throw new ArgumentNullException("参数不能为Null"); 69 } 70 71 HttpContext context = HttpContext.Current; 72 if (context != null) 73 { 74 context.Session[USER_REGISTER_MARK] = model; 75 } 76 } 77 78 /// <summary> 79 /// 移除用户注册会话标识 80 /// </summary> 81 internal static void RemoveUserRegisterMark() 82 { 83 HttpContext context = HttpContext.Current; 84 if (context != null) 85 { 86 context.Session.Remove(USER_REGISTER_MARK); 87 } 88 } 89 90 /// <summary> 91 /// 获取当前注册会话对象 92 /// </summary> 93 /// <returns></returns> 94 internal static RegisterModel GetUserRegister() 95 { 96 HttpContext context = HttpContext.Current; 97 if (context != null) 98 { 99 return context.Session[USER_REGISTER_MARK] as RegisterModel; 100 } 101 102 return null; 103 } 104 #endregion 105 106 #region 校验码会话(手机和邮件) 107 /// <summary> 108 /// 添加一个校验码会话 109 /// </summary> 110 internal static void AddChecksumMark(ChecksumModel model) 111 { 112 if (model == null) 113 { 114 throw new ArgumentNullException("参数不能为Null"); 115 } 116 117 HttpContext context = HttpContext.Current; 118 if (context != null) 119 { 120 context.Session[CHECKSUM_MARK] = model; 121 } 122 } 123 124 /// <summary> 125 /// 移除当前用户的校验码会话 126 /// </summary> 127 internal static void RemoveChecksumMark() 128 { 129 HttpContext context = HttpContext.Current; 130 if (context != null) 131 { 132 context.Session.Remove(CHECKSUM_MARK); 133 } 134 } 135 136 /// <summary> 137 /// 获取当前用户的校验码会话 138 /// </summary> 139 internal static ChecksumModel GetChecksum() 140 { 141 HttpContext context = HttpContext.Current; 142 if (context != null) 143 { 144 return context.Session[CHECKSUM_MARK] as ChecksumModel; 145 } 146 147 return null; 148 } 149 #endregion 150 151 #region 验证码会话 152 /// <summary> 153 /// 添加一个验证码会话 154 /// </summary> 155 internal static void AddCaptchaMark(string c) 156 { 157 if (c == null) 158 { 159 throw new ArgumentNullException("参数不能为Null"); 160 } 161 162 HttpContext context = HttpContext.Current; 163 if (context != null) 164 { 165 context.Session[CAPTCHA_MARK] = c; 166 } 167 } 168 169 /// <summary> 170 /// 移除当前用户的验证码会话 171 /// </summary> 172 internal static void RemoveCaptchaMark() 173 { 174 HttpContext context = HttpContext.Current; 175 if (context != null) 176 { 177 context.Session.Remove(CAPTCHA_MARK); 178 } 179 } 180 181 /// <summary> 182 /// 获取当前用户的验证码会话 183 /// </summary> 184 internal static string GetCaptcha() 185 { 186 HttpContext context = HttpContext.Current; 187 if (context != null) 188 { 189 return context.Session[CAPTCHA_MARK] as string; 190 } 191 192 return null; 193 } 194 195 /// <summary> 196 /// 检查验证码是否和服务器端一致(不区分大小写) 197 /// </summary> 198 /// <param name="code">用户输入的验证码</param> 199 /// <returns></returns> 200 public static bool ValidateCaptcha(string code) 201 { 202 if (string.IsNullOrWhiteSpace(code)) 203 { 204 throw new ArgumentNullException("参数不能为Null"); 205 } 206 string code2 = GetCaptcha() ?? ""; 207 if (code.ToUpper() == code2.ToUpper()) 208 { 209 return true; 210 } 211 return false; 212 } 213 #endregion 214 }
这样写,项目中使用时:
登录成功就添加会话SessionManager.AddUserLoginMark(当前登录用户对象);
页面登录检查时判断 SessionManager.GetUserLogin();返回是否为null就行。
退出登录时SessionManager.RemoveUserLoginMark();即可。
这样就只管调用就行,不用再去管Session名是什么,删除、更改也更方便,当然也不会出现Session重名现象了(如果这样都能整成会话重名的话,那你真成人才了)。
当然,这样写也不是一点缺点都没有,和前一种相比,这种方法可能就不能做到一次书写,到处使用了,需要跟据当前项目具体灵活改动相应代码,但好处是很明显的。这样的方法同样适用于Cache和Cookie。
三、最好不要用Request[]代替Request.Form[]和Request.QueryString[]。
如果页面有很多Request.Form[]、Request.QueryString[]、Session[]最好在页面Page_Load中就把所有值取出来存放在变量中,并转换成需要的类型。
满篇的Request.Form[]、Request.QueryString[]、Session[]编译器没法检查[]中的字符串,容易出错,影响阅读,还可能同一参数需要多次类型转换(这一条针对WebFrom无服务器控件开发时特别有用)。
四、尽量使用 as 代替引用类型间转换(见上面第六个)。
这个大家都知道,但还是发现很多人在用强制转换,包括一些优秀的开源项目。
五、RegisterClientScriptBlock、RegisterClientScriptInclude、RegisterStartupScript、RegisterOnSubmitStatement、RegisterClientScriptResource等方法要求前台页面必须有form服务器控件(<form runat="server"></form>)。
也就意味着在WebForm无服务器控件开发时,这几个没什么用了(同样的还有Page.IsPostBack要小心了)。
六、微软不推荐直接在后台使用Response.Write()输出JS,并且有的浏览器的确会造成页面变形。
但发现很多人在用,包括一些很优秀的开源项目。我暂时用在前台加入<%= strMsg %>来接收后台传过来的消息,不知道各位都有什么好的方法。
七、最后向大家分享一段自己写的小代码,为Repeater控件添加EmptyDataTemplate模板(EmptyDataTemplate在FooterTemplate之前)。
原理很简单,默认添加的模板会出现在FooterTemplate之后,在RenderChildren()方法中给它们换下位置就行。
代码如下:
1 using System; 2 using System.ComponentModel; 3 using System.Web.UI; 4 5 namespace MyRepeater 6 { 7 public class Repeater : System.Web.UI.WebControls.Repeater 8 { 9 [PersistenceMode(PersistenceMode.InnerProperty), Browsable(false), TemplateContainer(typeof(TemplateControl))] 10 public ITemplate EmptyDataTemplate { get; set; } 11 12 protected override void OnDataBinding(EventArgs e) 13 { 14 base.OnDataBinding(e); 15 16 this.Controls.Clear(); 17 this.ClearChildViewState(); 18 19 this.CreateControlHierarchy(true); 20 this.ChildControlsCreated = true; 21 22 if (EmptyDataTemplate != null) 23 { 24 if (this.Items.Count == 0) 25 { 26 EmptyDataTemplate.InstantiateIn(this); 27 } 28 } 29 } 30 31 protected override void RenderChildren(HtmlTextWriter output) 32 { 33 if (HasControls()) 34 { 35 for (int i = 0; i < Controls.Count; i++) 36 { 37 if (this.FooterTemplate != null && this.Items.Count == 0 && EmptyDataTemplate != null) 38 { 39 if (i == Controls.Count - 2) 40 { 41 Controls[i + 1].RenderControl(output); 42 continue; 43 } 44 if (i == Controls.Count - 1) 45 { 46 Controls[i - 1].RenderControl(output); 47 continue; 48 } 49 } 50 Controls[i].RenderControl(output); 51 } 52 } 53 } 54 55 protected override void Render(HtmlTextWriter output) 56 { 57 RenderChildren(output); 58 } 59 } 60 }
暂时能想到的只有这么多,都是自己的经验之谈,当然也属于一家之言,如果有什么错误或不合理的,可以在下边留言给我或狠狠的踩上几脚。同时也希望大家能把自己的一些开发经验或技巧分享出来,供大家学习学习。
2014-01-04补充
错误修正
第二段、第七个、为Repeater控件添加EmptyDataTemplate模板会导致Repeater的ItemDataBound事件处理执行两次。
请注释掉下面这几行代码
//this.Controls.Clear(); //this.ClearChildViewState(); //this.CreateControlHierarchy(true); //this.ChildControlsCreated = true;
本来我的代码就参考了kdalan的这篇文章,他的文章中本来没有这几句的,但在看见msdn中的示例有这几句,当时也没多想就加上了,也不一在用,没出什么毛病。但前两天在调试的时候发现ItemDataBound事件会执行两次。为了查找为什么ItemDataBound事件为什么会多次执行,费了好大一番功夫,最后才排确定是this.CreateControlHierarchy(true)这句导致的。因为这几句是在MSDN上的示例中看到的,所有一直没怀疑它的正确性,在排错的过程中放到最后,耽误不少时间。其实最后想想,也大概知道了其中的原因,.net中的事件可以多次订,基类和子类都调用this.CreateControlHierarchy(true);会导致多次订阅ItemDataBound事件。为了证实我的猜想,专门建立了项目,对自定义的Repeater再次继承重写,在调用时ItemDataBound事件会被执行三次。测试项目我放到google code上,有兴趣的朋友可以下载看看,http://my-repeater.googlecode.com/svn