本文是“使用phoneGap和Sencha Touch 2开发Android应用程序”系列教程的第2章, 在这一章,我们会继续开发一个能够在用户手机上运行的用于保存笔记的小应用程序。
到目前为止,我们正在开发用于显示用户所创建的笔记列表的视图。
为了创建列表视图,我们在app/view文件夹下创建了NotesListContainer.js文件,这个js文件就是用来定义NotesListContainer视图类的。现在我们对这个视图类进行一些改造,让它能有拥有更好的封装性和可维护性。
在上一章,NotesListContainer视图的定义如下:
Ext.define("NotesApp.view.NotesListContainer", {
extend: "Ext.Container",
config: {
items: [{
xtype: "toolbar",
docked: "top",
title: "My Notes",
items: [{
xtype: "spacer"
}, {
xtype: "button",
text: "New",
ui: "action",
id:"new-note-btn"
}]
}]
}
});
现在我们用视图的initialize()方法来定义视图所包含的组件,方法定义如下:
Ext.define("NotesApp.view.NotesListContainer", {
extend: "Ext.Container",
alias: "widget.noteslistcontainer",
initialize: function () {
this.callParent(arguments);
}
});
在这里我们使用了alias配置项,它的意义相当于我们给新建的这个视图类声明了xtype,这样,我们就能够通过形如xtype=“noteslistcontainer”的方式来引用该类。在本章的后面部分,我们会通过这种方式来实例化该类。
在Sencha Touch2中,每一个类都拥有一个initialize()方法,该方法通常在类被实例化的时候被用来执行一些逻辑,这个方法被用于替代Sench Touch 1中的initComponent()方法,下面我们使用该方法来给顶部工具栏添加一个新建按钮:
Ext.define("NotesApp.view.NotesListContainer", {
extend: "Ext.Container",
alias: "widget.noteslistcontainer",
initialize: function () {
this.callParent(arguments);
var newButton = {
xtype: "button",
text: 'New',
ui: 'action',
handler: this.onNewButtonTap,
scope: this
};
var topToolbar = {
xtype: "toolbar",
title: 'My Notes',
docked: "top",
items: [
{ xtype: 'spacer' },
newButton
]
};
this.add([topToolbar]);
},
onNewButtonTap: function () {
console.log("newNoteCommand");
this.fireEvent("newNoteCommand", this);
},
config: {
layout: {
type: 'fit'
}
}
});
在这里的initialize()方法中,我们首先调用callParent()方法,然后定义了一个工具栏和一个新建按钮,然后把间隔符和新建按钮添加到工具栏中。
在initialize()方法的最后,我们使用add()方法将该工具栏添加到视图中去。
在回来看看新建按钮的定义,我们使用handler配置项给按钮添加了一个用来处理单击事件的方法onNewButtonTap:
var newButton = {
xtype: "button",
text: 'New',
ui: 'action',
handler: this.onNewButtonTap,
scope: this
};
onNewButtonTap方法将捕获按钮上的单击事件(动作),然后将它转换成应用程序业务逻辑所能识别的某一具体事件(这里是newNoteCommand),该方法代码如下:
onNewButtonTap: function () {
console.log("newNoteCommand");
this.fireEvent("newNoteCommand", this);
}
这是本章对NotesListContainer所做的一个重要的改变,在上一章,按钮和对按钮的事件处理我们是通过在控制器中定义ref和对应control来实现的。
Ext.define("NotesApp.controller.Notes", {
extend: "Ext.app.Controller",
config: {
refs: {
newNoteBtn: "#new-note-btn"
},
control: {
newNoteBtn: {
tap: "onNewNote"
}
}
},
onNewNote: function () {
console.log("onNewNote");
}
// Rest of the controller's code omitted for brevity.
});
而现在,当捕获到视图中发生的事件时,会把经过转换的新的事件newNoteCommand传播出去,这个新的事件newNoteCommand将被控制器捕获,然后由控制器进行相应的处理。(注:这样做实际上就是把MVC中的V和C分开,V中只负责捕获事件,并将事件传播出去,对事件的定义和处理逻辑统统交给C来处理,这样做提高了代码的可维护性):
onNewButtonTap: function () {
console.log("newNoteCommand");
this.fireEvent("newNoteCommand", this);
}
尽管两种方法都是有效的,但是第二种做法会带来更重要的好处:
- 首先,视图类会看起来更加简洁,现在视图所要做的仅仅只是传播事件,这样做更加符合业务逻辑(视图类不负责其他处理逻辑)。
- 其次,视图类更容易修改及维护,而控制器并不需要熟悉View的内部运作。
只要View的公共事件保持不变,控制器就能够与视图协同运行。例如,我们可以在“视图”中改变用来建立新的笔记的元素(注:比如在新建一个其他按钮,用来新建笔记),而不会影响控制器对这一事件的处理。控制器只需要监听从视图中触发的newNoteCommand事件即可。
接下来我们将对app.js进行重构,我们将修改application()方法,使用类的别名来创建我们的NoteListContainer实例对象:
Ext.application({
name: "NotesApp",
controllers: ["Notes"],
views: ["NotesListContainer"],
launch: function () {
var notesListContainer = {
xtype: "noteslistcontainer"
};
Ext.Viewport.add(notesListContainer);
}
});
最后,我们回到到控制器的代码中,修改refs部分,这样我们可以根据xtype 查找ref的引用对象,而不再是根据id 查找:
Ext.define("NotesApp.controller.Notes",{
extend : "Ext.app.Controller", //
config : {
refs:{
// 将检索xtype为noteslistcontainer的视图
notesListContainer:"noteslistcontainer"
},
control : {
notesListContainer :{
// 这两个command将被notes list container触发
newNoteCommand : "onNewNoteCommand",
editNoteCommand : "onEditNoteCommand"
}
}
},
// 新增及修改的command
onNewNoteCommand : function(){
// 当 New按钮点击时调用
alert("onNewNoteCommand");
},
onEditNoteCommand : function(list,record){
// 当 Edit按钮点击时调用
alert("onEditNoteCommand");
},
// Notes类的init和launch方法
launch : function(){
this.callParent(arguments);
//alert("controller Notes 已启动");
},
init : function(){
this.callParent(arguments);
//alert("controller Notes 初始化");
}
});
注意,我们还在控制器中添加了onEditNoteCommand事件和editNoteCommand事件处理方法。本系列教程的下一节中,当我们创建笔记列表视图的时候,将同时定义onEditNoteCommand 。
做完上述的改动后,我们可以在连接设备或者模拟器上运行本应用程序(注:原文是在WebKit浏览器中打开index.html页面),确认运行效果是否和我们预期的一样。点击“新建”按钮,我们的onNewButtonTap方法会弹出提示框:
创建Notes列表视图
笔记列表视图是用来将本地缓存的所有笔记显示出来的组件,对应的文件是app/view/NotesList.js 。为了创建这个组件,我们将继承Ext.dataview.List类
Ext.define("NotesApp.view.NotesList",{
extend:"Ext.dataview.List",
alias:"widget.noteslist",
config:{
loadText:"正在加载笔记....",
emptyText:'<pre><div class="notes-list-empty-text">没有找到相关笔记。</div></pre>',
onItemDisclosure:true,
itemTpl:'<pre><div class="list-item-title">{title}</div><div class="list-item-narrative">{narrative}</div></pre>'
}
});
在上面的代码中,我们将onItemDisclosure的配置项设置为true,这项配置可以使列表中显示的每条笔记记录旁边右边加上“显示“按钮:
当单击“显示“按钮时会进入笔记编辑功能。我们稍后会创建该“显示”事件的处理方法。
在NotesList类中,我们对itemTpl和emptyText两个配置项使用了不同的CSS类 ,这样将让我们能够很好地显示列表项的样式,以及当列表中没有记录时,显示“没有找到相关笔记”提示消息。
我们在resources/css目录下,创建app.css文件,并把下面的样式代码加进去:
下面是我们所需要的css样式:
/* Increase height of list item so title and narrative lines fit */
.x-list .x-list-item .x-list-item-label
{
min-height: 3.5em!important;
}
/* Move up the disclosure button to account for the list item height increase */
.x-list .x-list-disclosure {
position: absolute;
bottom: 0.85em;
right: 0.44em;
}
.list-item-title
{
float:left;
width:100%;
font-size:90%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding-right:25px;
line-height:150%;
}
.list-item-narrative
{
float:left;
width:95%;
color:#666666;
font-size:80%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding-right:25px;
}
.x-item-selected .list-item-title
{
color:#ffffff;
}
.x-item-selected .list-item-narrative
{
color:#ffffff;
}
.notes-list-empty-text
{
padding:10px;
}
为了显示NotesList的实例,我们首先需要到应用程序的views配置项中添加该视图类:
views: ["NotesList", "NotesListContainer"]
然后,我们需要将它添加到NotesListContainer类。在NotesListContainer.js文件的initialize()方法中添加notesList变量:
Ext.define("NotesApp.view.NotesListContainer",{
extend : "Ext.Container",
alias : "widget.noteslistcontainer",
initialize : function(){
this.callParent(arguments);
var newButton ={
xtype:"button",
text:"New",
ui:"action",
handler:this.onNewButtonTap,
scope:this
};
var topToolbar = {
xtype:"toolbar",
title:"My Notes",
docked:"top",
items:[
{
xtype:"spacer"
},
newButton
]
};
var notesList = {
xtype:"noteslist",
store:Ext.getStore("Notes"),
listeners:{
disclose:{
fn:this.onNotesListDisclose,
scope:this
}
}
}
this.add([topToolbar,notesList]);
},
onNewButtonTap:function(){
//alert("new note");
this.fireEvent("newNoteCommand",this);
},
onNotesListDisclose:function(list,record,target,index,evt,options){
// alert("editNoteCommand");
this.fireEvent("editNoteCommand",this,record);
},
config:{
layout:{
type:"fit"
}
}
});
同样的,我们对列表记录的“显示”事件设置一个监听器:
var notesList = {
xtype: "noteslist",
listeners: {
disclose: { fn: this.onNotesListDisclose, scope: this }
}
};
然后定义onNotesListDisclose()方法:
onNotesListDisclose: function (list, record, target, index, evt, options) {
console.log("editNoteCommand");
this.fireEvent('editNoteCommand', this, record);
}
在这里,我们采取的方法,我们遵循和之前“新建”按钮类似的第二种做法,不再采用控制器监听笔记列表的“显示”事件,而是创建editNoteCommand事件,并将其暴露给控制器。这种做法使应用更灵活,更易于维护。
创建Note(笔记)对应的Sencha Touch 数据模型
笔记列表视图需要一个数据存储(Store)对象,这个store对象用于描述列表项(本例是笔记对象)的相关信息。为了创建这个store对象,我们首先需要定义一个数据模型,该模型代表笔记类(注:包含了类的属性字段,以及字段验证等等,这里的模型即MVC中的Model,store对象熟悉ExtJs的应该不会陌生,主要是定义对哪个model,采用那种代理处理,按照哪个属性对model排序等等,它是model和view的关联)。
下面我们来定义Note类。我们将会把这个类定义成Note.js文件将保存在model目录下:
笔记model包含4个属性字段: id, date created(创建日期), title(笔记标题)和 narrative(笔记内容):
Ext.define("NotesApp.model.Note", {
extend: "Ext.data.Model",
config: {
idProperty: 'id',
fields: [
{ name: 'id', type: 'int' },
{ name: 'dateCreated', type: 'date', dateFormat: 'c' },
{ name: 'title', type: 'string' },
{ name: 'narrative', type: 'string' }
]
}
});
我们将使用idProperty的配置项来明确声明,id字段是可以用来识别笔记对象的唯一标识。在我们的例子中,这似乎是微不足道的,因为我们对数据模型的名称有完全的控制权(即这里我们能够自行定义id为主键)。但是,在开发中可能遇到各种情况,例如,数据模型的字段是严格绑定到一个已有的数据库表的列名的,而用于唯一地标识一个记录的列名并不是是“id” ,这时候idProperty的配置就是相当必要的。
在 Sencha Touch中设置模型验证器
id, dateCreated and title 这三个属性字段是笔记模型的必填项。我们需要用validations配置项来对每个字段进行声明验证:
Ext.define("NotesApp.model.Note",{
extend:"Ext.data.Model",
config:{
idProperty:"id",
fields:[
{name:"id",type:"int"},
{name:"dateCreated",type:"date",dateFormat:"c"},
{name:"title",type:"string"},
{name:"narrative",type:"string"}
],
validations:[
{field:"id",type:"presence"},
{field:"dateCreated",type:"presence"},
{field:"title",type:"presence",message:"请输入笔记的标题"}
]
}
});
对于标题字段,我们使用了message配置项来定义当用户没有输入笔记标题而视图保存时的提示信息。
创建数据存储之前,我们需要将模型添加到应用程序的models配置项上:
Ext.application({
name : 'NotesApp',
models:["Note"],
//下面的代码省略..
});
创建Sencha Touch 数据存储
现在,我们可以开始创建数据存储,该数据存储将被填充到列表视图中去。我们将在app/store目录下创建Notes.js,这个文件就是笔记模型的数据存储类:
我们先暂时在store中以硬编码的形式添加一些笔记记录:
Ext.define("NotesApp.store.Notes",{
extend:"Ext.data.Store",
config:{
model:"NotesApp.model.Note",
data:[
{title:"Note 1", narrative:"这是Note 1 的笔记内容"},
{title:"Note 2", narrative:"这是Note 2 的笔记内容"},
{title:"Note 3", narrative:"这是Note 3 的笔记内容"},
{title:"Note 4", narrative:"这是Note 4 的笔记内容"},
{title:"Note 5", narrative:"这是Note 5 的笔记内容"},
{title:"Note 6", narrative:"这是Note 6 的笔记内容"},
]
}
});
要特别注意的是,我们需要通过model配置项来指明是对哪个模型对象进行存储。
如果我们希望对所有笔记按照创建日期来排序的话,可以使用sorter配置项来指定排序字段以及升序还是降序:
Ext.define("NotesApp.store.Notes",{
extend:"Ext.data.Store",
requires:"Ext.data.proxy.LocalStorage",
config:{
model:"NotesApp.model.Note",
data:[
{title:"Note 1", narrative:"这是Note 1 的笔记内容"},
{title:"Note 2", narrative:"这是Note 2 的笔记内容"},
{title:"Note 3", narrative:"这是Note 3 的笔记内容"},
{title:"Note 4", narrative:"这是Note 4 的笔记内容"},
{title:"Note 5", narrative:"这是Note 5 的笔记内容"},
{title:"Note 6", narrative:"这是Note 6 的笔记内容"},
],
sorters:[{property:'dateCreated',direction:'DESC'}]
}
});
现在回到NotesListContainer.js,在noteList视图对象的声明中添加刚刚新建的store对象:
var notesList = {
xtype: "noteslist",
store: Ext.getStore("Notes"),
listeners: {
disclose: { fn: this.onNotesListDisclose, scope: this }
}
};
让我们回到控制器的定义文件controller/Notes.js中,在launch()方法中,调用store对象的load()方法:
launch : function(){
this.callParent(arguments);
Ext.getStore("Notes").load();
alert("controller Notes 已启动");
}
在这里,其实我们并不需要这个调用load()方法 - 因为模型数据是硬编码的 - 但我们在本教程的下一章,我们将停止使用硬编码的数据,并开始使用存储在浏览器缓存中的数据。
和models,views和controllers类似,我们需要在app.js的配置中添加stores:
Ext.application({
name : 'NotesApp',
models:["Note"],
controllers : ["Notes"],
views : ["NotesListContainer","NotesList"],
stores :["Notes"],
launch : function() {
// alert("App launch");
// var notesListContainer = Ext.create("NotesApp.view.NotesListContainer");
var notesListContainer = {
xtype:"noteslistcontainer"
}
Ext.Viewport.add(notesListContainer);
}
});
启动模拟器,就会看到我们刚刚以硬编码形式保存的所有笔记记录都显示在屏幕上了:
总结
在这一章,我们对应用程序的主视图进行了一些修改。首先,在NotesListContainer类中增加了一个initialize()方法,并使用这个方法实例化了工具栏和笔记列表视图。
第二,更重要的变化是引入了两个新的的事件, newNoteCommand和editNoteCommand ,当我们的用户需要创建或编辑笔记的时候这两个事件将被触发并传播。这种修改方式提高了应用的可维护性和可靠性。
第三,修改NotesListContainer类之后,我们创建了NotesList类,继承自Ext.dataview.List类,这是我们用于缓存中笔记的组件。这个类需要一个数据存储对象来保存所有已有的笔记,因此,我们还创建了Notes store。
在本教程的下一章,我们将开始编写用户用来编辑和删除笔记的视图类。同时,我们还将学习Sencha Touch的表单,模型验证器,以及使用本地存储代理对象来在客户端进行缓存。
未完待续!
下载
源代码已发布到迅雷快传:http://kuai.xunlei.com/d/KINUTYSNDOIO
原文出处:http://miamicoder.com/2012/how-to-create-a-sencha-touch-2-app-part-2/
本教程快速链接
- 使用phoneGap和Sencha Touch 2开发Android应用程序(一)
- 使用phoneGap和Sencha Touch 2开发Android应用程序(二)
- 使用phoneGap和Sencha Touch 2开发Android应用程序(三)
- 使用phoneGap和Sencha Touch 2开发Android应用程序(四)
- 使用phoneGap和Sencha Touch 2开发Android应用程序(五)