一步一步使用Ext JS MVC与Asp.Net MVC 3开发简单的CMS后台管理系统之登录窗口
完成配置后,要做的是完成登录页面。因为要实现登录之后写入认证信息到Cookie,因而必须做一次跳转。当然,不做跳转,或不写入认证信息也行,但问题比较复杂,在这里还是做简单处理比较合适。还有就是写入认证信息的目的是为了在控制器通过特性控制方法的权限。
既然要跳转一次,就有两种思路了,一种是为了快速显示登录页,可不加载Ext JS,而是使用传统的页面,显示一个登录页,这样页面加载快,用户感受也好很多。第二种方式就是用Ext JS窗口做一个登录窗口,不过要加载Ext JS库,显示时间会久点,体验不是太好。不过加载过Ext JS,后面显示主框架页的时候就可以利用缓存数据。采用那种方式,根据自己喜好或项目要求定吧。本文是演示Ext JS的,因而会采用第二种方式,写一个登录窗口。
登录窗口作为常用组件,在其它项目也会用到,因而很适合写成一个扩展。这里还要考虑一个问题,是写成单例模式的扩展,还是单纯是扩展?单例模式的好处是在页面中直接用show方法显示就行了,而纯扩展的好处的是可以通过配置项自定义标题、图标这类的东西。笔者更喜欢单例模式,原因是直接在扩展中修改标题之类的这些东西,比使用配置项来定义来得方便,毕竟是Javascript写的扩展,修改起来比使用C#这些语言的扩展容易得多了。
目标明确后,就可以动手了,在解决方案资源管理器中选择Scripts\ExtJS\ux目录,单击右键选择添加,新建项,在弹出窗口中如图6那样选择Jscript文件,并将名称修改为login.js(以后的项目的可直接将该文件复制到该目录),单击添加按钮创建文件。这里要注意,文件名不能用类的全名做文件名,因为动态加载会根据类名自动找到目录并加载文件,类名中最后一个小数点后的名称就是文件名,例如,登录窗口的类全称为Ext.ux.Login,而login就是文件名。
如果想要在脚本中使用ExtJS的提示信息,可将书附带的资源包中的Ext.js文件复制到ExtJS目录中,复制后,在解决方案资源管理器将Ext.js拖到到login.js文件中,就会生成以下代码:
/// <reference path="../Ext.js" />
这样,就可以在脚本中使用Ext JS的提示信息,在文件中输入“Ext.cr”,将看到如图7所示的效果。
现在,先把类的定义写好,包括父类、单例模式、窗口标题、宽度和高度。窗口的标题为“简单的CMS后台管理系统后台登录窗口”。宽度和高度暂定为400,到时候再调整。最终代码如下:
Ext.define("Ext.ux.Login", { extend: "Ext.window.Window", singleton: true, title: '简单的CMS后台管理系统后台登录窗口', width: 400, height: 400 });
接下来,要考虑窗口应该包含那些配置项了,窗口应是模态的,不能关闭,不能调整大小,关闭模式为隐藏,隐藏模式为偏移等,因而加入以下代码:
modal: true, closable: false, resizable: false, closeAction: 'hide', hideMode: 'offsets',
好了,现在要在窗口的initComponent方法内定义登录用的控件了。一般的登录窗口都包含用户名、密码和验证码3个文本输入框,还包含有显示验证码的图片、登录和重置按钮。因而需要用到的ExtJS控件包括表单面板、图片、工具栏、按钮和文本字段。下面要做的是先定义好表单,在扩展内加入以下代码:
initComponent: function () { var me = this; me.form = Ext.create(Ext.form.Panel, { }); me.callParent(arguments); }
代码中,me的作用是将外部作用域中的this对象保存为本地变量,这样的好处包括,一是,如果this是window等全局变量,就可以将全局变量变成本地变量,提高访问效率,二是可以让闭包访问该对象。这写法在Ext JS文件中始终贯穿其中,本着拿来主义的精神,好东西应该学一下。
注意create方法中的对象名称,笔者并没有使用字符串,这样就可以直接使用对象,而不需要再去转换表中找对象,可以提高速度,笔者建议这样写。
调用callParent方法是必须的,不然组件运行会出问题,达不到预期效果,具体请参考笔者书中的讲解。
接下来是定义表单的配置项了,笔者习惯的配置项有以下几个:border: false, bodyPadding: 5, bodyStyle: "background:#DFE9F6",
代码中,第一句表示不要边框,如果喜欢带有边框的表单,可以把这项去掉或者修改为true。第二句表示将表单面板向内压缩5像素,这样表单内的组件就不会和窗口的内边框粘在一起,这个可根据个人喜好设置。第三句的作用就是让表单面板的背景颜色和窗口融合在一起,而不是默认的白色,这还是个人喜好问题。
接着加入表单面板的提交地址,这里定为Account/Login,就是Account控制器的Login方法,代码如下:url: "Account/Login",
因为表单内使用的都是文本字段,因而可以统一做一些定义,如标签宽度为80,标签的分隔符为中文冒号,锚固为0,都不允许为空等,代码如下:
defaultType: "textfield", fieldDefaults: { labelWidth: 80, labelSeparator: ":", anchor: "0", allowBlank: false },
接下来是定义字段了,这个简单,因为默认设置已经定义了几个配置项,因而余下的就只有字段标签和名称。验证码特殊点,必须是6位字符,代码如下:
items: [ { fieldLabel: "用户名", name: "UserName" }, { fieldLabel: "密码", name: "Password", inputType: "password" }, { fieldLabel: "验证码", name: "Vcode", minLength: 6, minLength: 6 } ]
现在要考虑怎么显示验证码图片,如果直接在表单内加入Image控件,会很难控制图片的位置,因为最好的方式是先套一个容器。因为Img对象的实例在刷新图片的时候还要用到,因而最好用一个属性来指向对象实例,这样就可以通过该属性在类的内部访问到实例了。在创建表单的前面添加以下创建Img对象实例的代码:
me.image=Ext.create(Ext.Img,{ src: "/VerifyCode" });
千万不要在创建表单后面创建,不然在表单内插入图片的时候就找不到对象了。
代码中,验证码图片将VerifyCode控制器生成,这个暂时放下,会在后面讨论。
还要实现的是单击图片刷新验证码,但是查API发现Img对象居然没单击事件。没关系,在4.1版本的Ext JS中,修改了事件的定义方式,可以直接为对象生成的HTML元素绑定事件了,只要在监听事件中加入element配置项就行了,这相当方法。因而可在创建Img实例的配置对象中加入以下代码:
listeners:{ click:me.onRefrehImage, element:"el", scope:me }
代码中,element配置项中的el就表示要在对象生成的HTML元素中绑定事件,绑定事件为click事件,事件将调用onRefrehImage方法。方法只是简单的刷新图片,因而使用Img对象的setSrc方法就可以,使用以下代码顺便完成onRefrehImage方法:
onRefrehImage: function () { this.image.setSrc("/VerifyCode?_dc=" + (new Date()).getTime()); }
代码很简单,使用setSrc方法刷新图片的src就行了,加上时间戳可防止显示缓存图片。
好了,可以在表单items里加入验证码图片了,代码如下:
{ xtype: "container", height: 80, anchor: "-5", layout: "fit", items: [me.image] }
从代码可以看到,使用容器的作用就是可以使用fit布局来限制图片的尺寸,这样布局就容易多了。
还要加入一段提示信息,告知用户验证码不区分大小写,且如果看不清楚验证码图片,可单击图片刷新验证码,代码如下:{ xtype: "container", anchor: "-5", html: "**验证码不区分大小写,如果看不清楚验证码,可单击图片刷新验证码。" }
表单余下的就是添加登录和重置按钮了,代码如下:
dockedItems: [{ xtype: 'toolbar', dock: 'bottom', ui: 'footer', layout: { pack: "center" }, items: [ { text: "登录", width: 80, disabled: true, formBind: true, handler: me.onLogin, scope: me }, { text: "重置", width: 80, handler: me.onReset, scope: me } ] }]
在这里使用了dockedItems配置项,目的一是因为介绍Ext JS 4的新功能,二是因为使用这个确实挺方便。代码中定义了一个工具栏,停靠位置由dock配置项决定,在这里是底部(bottom),工具栏的样式使用了ui配置项定义的footer,也就是原来窗口的底部页脚工具栏,工具栏的布局将使用居中对齐方式。
登录按钮预设为禁用的。formBind配置的作用是只有在表单内输入符合要求时才能使用该按钮,这个设计在Ext JS4也是新加入的,很方便,不再需要自己去写代码实现这个了。登录按钮将调用onLogin方法。重置按钮很简单,只是简单的调用onReset方法。
余下要完成的是onLogin和onReset方法。先来完成简单onReset方法,基本功能就是重置表单,并将焦点移动到第一个文本字段,也就是用户名那里,还要刷新验证码,代码如下:
onReset: function () { var me = this; me.form.getForm().reset(); if (me.form.items.items[0]) { me.form.items.items[0].focus(true, 10); } me.onRefrehImage(); }
代码中要注意的是获取表单中第一个文本字段的代码,因为表单在实例化后,items属性指向的是MixedCollection实例,因为要在其items内才能找到文本自动对象。
接着完成的是onLogin方法,难度也不大, 就是先调用isValid方法,验证表单是否符合提交要求,然后调用submit方法提交。其实不调用isValid也行,因为登录按钮只要在isValid为true时才能用,不过笔者习惯如何,多余就多余点吧。代码如下:onLogin: function () { var me = this, f = me.form.getForm(); if (f.isValid()) { f.submit({ waitMsg: "正在登录,请等待……", waitTitle: "正在登录", success: function (form, action) { window.location.reload(); }, failure: function(){ }, scope: me }); } }
在submit方法内,waitMsg和waitTitle会显示一个等待对话框,并用一个遮罩遮住表单,不让用户编辑,其中waitMsg就是要显示的提示信息,waitTitle就是弹出窗口的标题的。不过,在4.1.1版本,这里有bug,笔者已提交至官方论坛,修正方法可参阅http://www.sencha.com/forum/showthread.php?228970-4.1.1-GA-Form-Submit-Problem。这里笔者就不修正了,测试的时候会屏蔽这两句代码。
登录成功(success配置项)后,会刷新一下页面,让页面写入验证信息到Cookie。当然,也可以跳转到另外一页,不过笔者认为不如这样来得简便,这个稍后会说到。
登录失败(failure配置项),只写了一个空函数的目的是因为表单的提交返回的数据格式是一样的,处理方式也一样,因而可使用同一个函数进行处理,但是还没写到,因而先保留一个空函数。
最后,别忘了将表单加入窗口的items里,这个必须放在调用callParent之前,不如不会初始化表单,代码如下:
me.items = [me.form]
至此,登录窗口就暂时写好了,今天的进度也完成了。
嘿嘿,这样的项目进度会郁闷死项目经理的。
懒得上传文件了,以下是完整的Login.js代码:
/// <reference path="../Ext.js" /> Ext.define("Ext.ux.Login", { extend: "Ext.window.Window", singleton: true, title: '简单的CMS后台管理系统后台登录窗口', width: 400, height: 400, modal: true, closable: false, resizable: false, closeAction: 'hide', hideMode: 'offsets', initComponent: function () { var me = this; me.image = Ext.create(Ext.Img, { src: "/VerifyCode", listeners: { click: me.onRefrehImage, element: "el", scope: me } }); me.form = Ext.create(Ext.form.Panel, { border: false, bodyPadding: 5, bodyStyle: "background:#DFE9F6", url: "Account/Login", defaultType: "textfield", fieldDefaults: { labelWidth: 80, labelSeparator: ":", anchor: "0", allowBlank: false }, items: [ { fieldLabel: "用户名", name: "UserName" }, { fieldLabel: "密码", name: "Password", inputType: "password" }, { fieldLabel: "验证码", name: "Vcode", minLength: 6, minLength: 6 }, { xtype: "container", height: 80, anchor: "-5", layout: "fit", items: [me.image] }, { xtype: "container", anchor: "-5", html: "**验证码不区分大小写,如果看不清楚验证码,可单击图片刷新验证码。" } ], dockedItems: [{ xtype: 'toolbar', dock: 'bottom', ui: 'footer', layout: { pack: "center" }, items: [ { text: "登录", width: 80, disabled: true, formBind: true, handler: me.onLogin, scope: me }, { text: "重置", width: 80, handler: me.onReset, scope: me } ] }] }); me.items = [me.form] me.callParent(arguments); }, onRefrehImage: function () { this.image.setSrc("/VerifyCode?_dc=" + (new Date()).getTime()); }, onReset: function () { var me = this; me.form.getForm().reset(); if (me.form.items.items[0]) { me.form.items.items[0].focus(true, 10); } me.onRefrehImage(); }, onLogin: function () { var me = this, f = me.form.getForm(); if (f.isValid()) { f.submit({ waitMsg: "正在登录,请等待……", waitTitle: "正在登录", success: function (form, action) { window.location.reload(); }, failure: function(){ }, scope: me }); } } });