内容


编写一个定制的 Dojo 应用程序

Comments

简介

我们最近刚刚完成了一个 Web 2.0 Dojo 原型的开发。这个原型十分宽泛,为信息管理提供了一个新的功能。我们还与用户体验团队协作以确保此应用程序可用。屏幕由一名图形 Web 设计人员设计,以使它们看上去更专业一些。

本文记录了我们进行此原型的 Web 2.0 开发的实际体验。由于 Web 2.0 相对来说是个比较新的技术,所以在需要时,开始使用和进行定制可能会较为困难。我们并没有为我们的 Dojo 应用程序使用开箱即用的外观。因为我们需要的是一致的图形设计以符合我们产品线的品牌效应。因此,我们必须使用 Dojo 进行定制。定制是绝大多数开发人员耗费时间最多的一项工作,对于那些不知道如何解决此问题的开发人员,更是如此。

由于本文的重点在于 Dojo 应用程序的定制,所以这里我们不对示例中出现的每个小部件属性做详细的描述。本文假设您对 Dojo 和 CSS 有一定的了解。本文中的示例所基于的是 Dojo 1.1.0(相关链接,参见 参考资料)。

创建一个 Dojo 应用程序

设计方法

有经验的软件工程师在开发时大都习惯使用面向对象(Object Oriented,OO)技术。我们希望我们的 Web 2.0 应用程序开发也能沿袭我们用了很多年的 Java™ 编程原理。我们发现在很大程度上我们可以通过使用 Dojo 小部件和模板模式以及 JavaScript/Dojo 对象来做到这一点。

Dojo 小部件和模板模式的使用

我们编写了一些定制小部件,它们由 JavaScript 类组成,并包括 HTML 模板以便用来进行呈现。这让我们可以把更多的 OO 方法应用于我们的应用程序,而不仅仅是编写一些使用全局 JavaScript 函数的 HTML 文件。通过这些定制的小部件,我们获得了多种特性:可借助良好定义的小部件实现的对象隔离;在需要时可供那些使用 HTML 的小部件更新 DOM 的动态内容;同一个 HTML 模板可有多个实例(通过使用定制小部件生成不同的 HTML ID)。我们还能扩展我们自已编写的这些定制小部件,从而制做出更多的个性化版本。

清单 1 所示的是定制小部件 JavaScript 文件的一部分,该文件在 dijit._Widgetdijit._Templated 基础之上构建。

清单 1. 定制小部件 JavaScript 文件
dojo.provide("mywidgets.MyWidget");
// put any other requires the widget needs here
dojo.declare(
  'mywidgets.MyWidget',
  [dijit._Widget, dijit._Templated],
{
  widgetsInTemplate: true,

templatePath: dojo.moduleUrl( "mywidgets", 
"../templates/mywidgets/templateMyWidget.html");
// put any other variables and methods for this widget here
// can also override methods from the base classes here

});

widgetsInTemplate 属性告知 Dojo 它需要解析这个模板文件,因它含有 Dojo 小部件,而不只是 HTML 标记。templatePath 属性告知 Dojo 这个小部件将使用这个特定的 HTML 模板来进行呈现。当小部件被实例化时,比如使用 new mywidgets.MyWidget() 实例,此模板的 HTML 在此对象在 DOM 的插入处被呈现。

清单 2 所示的是这个小部件的 HTML 模板文件。

清单 2. HTML 模板
<div class="templateMyWidget">
    <!-- Other Widgets and HTML can be included here -->
<button dojoType="dijit.form.Button" id="myButton_${id}" 
label="My Button" dojoAttachPoint="myButton"></button>
</div>

在本例中,为了防止代码通过其 ID 访问此按钮,用变量替换了此 ID。${id} 被这个 ID 属性的值替换,这个属性继承自 dijit._Widget 类。此 ID 是惟一的;因而,将其用作模板内的多个 ID 中的一个就能实现在实例化几个小部件的时候,每个小部件都具有一个惟一的 ID。我们还包括了 dojoAttachPoint 属性。这就在小部件内创建了一个指向此 DOM 节点的属性。所以如果已经访问了这个小部件(比如,myWidgetObj),就可以使用 myWidgetObj.myButton 访问到此 DOM 节点。而无需知道此 ID 并且可以省去该属性,让系统创建该元素的惟一 ID。并且可以通过使用 myWidgetObj.myButton.id 检索这个由系统生成的 ID。

如果您下载了这些 Dojo 源代码,可能会注意到 Dojo 也是如此编写的。每个 Dojo 小部件都有一个 JavaScript 文件和与之相关的一个 HTML 文件(通常二者具有相同的名字)。这些模板可以在与小部件的 JavaScript 文件同级的 templates 的目录内找到。比如,此 Button 小部件的 JavaScript 文件是 <dojo_root>\dijit\form\Button.js,而其模板文件是 <dojo_root>\dijit\form\templates\Button.html,其中 <dojo_root> 是包含所下载的 Dojo 代码的那个目录。

JavaScript 和 Dojo 对象的使用

在我们开始开发此原型时,我们并不具备 JavaScript 的太多经验,但是,它的确可以让习惯于使用 OO 原理的人使用 JavaScript Object 进行开发。Dojo 提供了使用 dojo.declare 结构定义自己的类的更好方法。我们建议您参阅 developerWorks 文章“针对 Java 开发人员的 Dojo 概念”(参见 参考资料 小节)。与任何原型一样,我们的要求不仅限于初始设计,所以代码变得越来越复杂和混乱。所以最好是采用足够多的设计模式。有关设计模式的文章很多,其中包含了 JavaScript 和 Dojo 的示例。我们推荐其中的一个称为 “MVC with JavaScript” 的示例(参见 参考资料)。

错误处理

与 Java 代码类似,JavaScript 也具备异常处理,我们推荐使用它来进行错误处理。

清单 3 中所示的 JavaScript 内的 try/catch 处理与 Java 代码很相似。在这个异常对象内可以看到一个堆栈跟踪。

清单 3. Try/catch 处理
try {
     // your logic
}catch(e){
     // error handling
}

在 JavaScript 中,异常可以由方法抛出,这与 Java 编程相同。

清单 4. 抛出异常
if( <test for error condition> ) throw "meaningful error message";

Dojo 的开发环境

  • 起点 - Dojo 示例/演示和测试代码:Dojo 很快也很容易上手,因为有很多示例,您可以使用任何一种 Web 浏览器运行这些示例,然后再根据自己的需要加以修改。大多数这类示例都位于 Dojo 源代码内的 tests 目录。
  • 集成的开发环境(Integrated development environment,IDE):在我们刚开始开发我们的项目时,对 Ajax/Dojo 支持很少甚至没有。我们的所有开发都是在基于 Eclipse 的 IDE 内进行的,因为那里也是我们进行 Java 开发的地方。我们的确发现了一些有助于开发 JavaScript 和 Dojo 代码的插件。但是,在我们的需要与这些示例代码相去甚远时,我们常常必须要读懂这些实际的 Dojo 类才能了解如何使用它们。我们所面对的最大的调整是 JavaScript 没有编译时检查。这就意味着直到存在 bug 的路径被执行时才能发现这个 bug。
  • 帮助 - Dojo 社区:因为 Dojo 很流行,所以拥有强大的社区支持。各种论坛层出不穷,开发人员在此发布各自遇到的问题并交流如何解决这些问题。我们常常发现所发布的这些示例很有帮助;它们通常都能为我们提供一些参考,帮助我们解决我们一直苦思冥想的问题。
  • 调试 – Firebug 插件:对于我们的原型,我们需要一种 Web 浏览器,因此选择了 Mozilla Firefox。我们还向其添加了 Firebug 插件。Firebug 可从很多地方下载,在 参考资料 小节可以找到链接。Firebug 的功能之一是记录 HTTP 请求/响应以及计时。在 JavaScript 文件内可设置断点以便调试。它还允许查看/编辑 HTML、CSS 和 DOM 代码。Firebug 也非常有助于找出在这些 Dojo 小部件内使用的是哪些样式属性。

    此外,Firebug 还向 Web 应用程序添加了一个称为 “console” 的全局变量。此变量具有很多方法,可用来进行日志记录和跟踪,比如 debuginfowarnerrorlog。这些方法在此控制台内编写消息以及 JavaScript 文件名和所跟踪消息的行数。我们还启用了条件跟踪以便能够使用一个全局属性来启用和禁用控制台跟踪。

这里有一个技巧,我们很想与您共享,我们发现在我们使用 dojo.require 语句(如清单 5 所示)包含我们自己的小部件时,所跟踪的消息文件名以及控制台内的行数将不可用,直到我们再添加一个显式的 script src 语句(如清单 6 所示),情况才能改变。

清单 5. 使用 dojo.require 语句包含小部件
dojo.require("acme.MyWidget");
清单 6. 添加一个显式的 script src 语句
<script type="text/javaacript"src="widget/MyWidget.js"></script>

定制 Dojo 应用程序

我们考察了几个 JavaScript 框架,本来我们需要的是一个级别稍微高于 Dojo 的框架,但是 Dojo 具备所有所需的构建块、定制点和强大的社区支持。

如何定制

所以,我们决定使用 Dojo,但是要加以定制,而这个过程涉及到很多方面。定制 Dojo 小部件观感的一种最简单方式是使用定制样式表(CSS)。也可以覆盖一个 Dojo 小部件方法、针对特定行为子类化一个现有的 Dojo 小部件或创建您自己的小部件。在本节中,我们将介绍此原型所使用的多种定制方法。

注意:此列表仅代表为我们的应用程序实际开发的一个子集。很多其他的定制因尚处于知识产权过程中所以并未包括在本文。

重新样式化 Dojo 小部件

Dojo 具有三个基本的皮肤,可开箱即用,它们是:soria、tundra 和 nihilo。通过在应用程序的整个 HTML 主体标记的类属性内指定,也可以将这些皮肤用作基础。然后,再用您自己的 CSS 覆盖所选择的个别样式。也可以从头编写所有 CSS;不过,如果应用程序中使用了很多小部件,那么这个过程会很耗时。要查看这些预定义的皮肤在 Dojo 小部件中的效果,可以运行 Dijit Theme Tester,它位于 <dojo_home>/dijit/themes/themeTester.html,其中 <dojo_home> 是所下载的 Dojo 源代码所在的目录。

Dojo 小部件的模板包含可用于定义定制样式表的那些类属性,但是我们并没有发现这些样式的名称在任何文档中提到过。为了给这些小部件找到类名,我们发现有两种方式很有用。第一个方法是查看与这些小部件相关联的模板 .html 文件。dijit.form.Button 小部件的模板文件如清单 7 所示。

清单 7. dijit.form.Button 小部件的模板
<div class="dijit dijitReset dijitLeft dijitInline"
    dojoAttachEvent="onclick:_onButtonClick,onmouseenter:_onMouse,onmouseleave:
  _onMouse,onmousedown:_onMouse"
    waiRole="presentation"
    ><button class="dijitReset dijitStretch dijitButtonNode 
dijitButtonContents" dojoAttachPoint="focusNode,titleNode"
        type="${type}" waiRole="button" waiState="labelledby-${id}_label"
        ><span class="dijitReset dijitInline ${iconClass}" dojoAttachPoint="iconNode" 
             ><span class="dijitReset dijitToggleButtonIconChar">✓</span 
        ></span
        ><div class="dijitReset dijitInline"><center class="dijitReset dijitButtonText"
id="${id}_label" dojoAttachPoint="containerNode">${label}</center></div
    ></button
></div>

就样式化而言,应该格外关注模板的类属性值。由 ${} 围起来的类值,比如 ${iconClass},被在 Button 标记内指定的 iconClass 属性的值替代。

在上面的例子中,dijitButtonText 用来样式化在此按钮中显示的文本。所以,如果想要文本是蓝色的,就需要在样式表内包括如下的代码:

清单 8. 将按钮文本变为蓝色所需的代码
.dijitButtonText
{
   color: blue;
}

此外,还可以更细致和更进一步地限定样式。对于该类名与其他小部件的类名重复的情况,就更应如此了。比如,.dijitButtonText 还用在 ComboButton DropdownButton 模板内,所以,对于上面给出的这个 CSS 示例,所有这些控件都将具有蓝色的文本。这些控件的 JavaScript 文件包含一个用在最外层的 baseClass 属性,可用来在 CSS 内区分它们。ButtonbaseClassdijitButton,而另外两个的 baseClass 分别是 dijitComboButtondijitDropDownButton。如果只需要让常规的按钮具有蓝色的文本,那么这个 CSS 就应该类似于清单 9。

清单 9. 只让常规的按钮具有蓝色文本所需的代码
.dijitButton .dijitButtonText
{ 
   color: blue;
}

如果使用一种 Dojo 皮肤作为基础,我们建议向该行的前端添加 .soria,甚至可以添加更多的级别,比如 .dijitButtonNode。这是因为如果在两个不同的文件中使用了同一个 CSS 类,它有可能使用非您预期的那个;因为它只会使用更符合条件的那个。 CSS 规范(参见 参考资料)的 6.4.3 节提供了这方面的详细内容。也可以通过添加 CSS !important 限定语句(在此规范的 6.4.2 节介绍)强制其使用。

在使用多种皮肤和样式时,有时很难知道某个特定的类使用的是哪个样式定义。在这方面,Firebug 可以提供帮助。要在浏览 Web 页面时打开 Firebug 控制台,可以单击浏览器窗口左下脚的那个图标。在 Firebug 控制台的左侧有一个 HTML 选项卡,其中显示了屏幕上正在显示的 HTML 代码,包括类属性。如果单击这个 HTML 标记,能影响此标记的相关 CSS 就会显示在右侧的 Style 选项卡下面。如果相同的类属性在代码内多次定义,它会显示所有的定义以及哪些定义被覆盖了、哪些定义正在使用。如果想要查看屏幕上某个特定区域的样式,可以单击 Firebug 工具栏内的 Inspect,然后在屏幕上单击希望查看的区域/小部件。这会将您带到负责该区域的 HTML 代码并在右侧显示相关的 CSS。

图 1 所示的是在 Dijit Theme Tester 中对 soria 主题的 Create 按钮进行的检查(若想查看放大图,单击 这里)。在 Firebug 中,可以看到在 dijits.css 中指定的填充样式被在 soria.css 中指定的填充样式覆盖,这一点通过 .soria .dijitButtonNode(而不只是 .dijitButtonNode)被进一步限定。

图 1. 我们的应用程序的默认 Dojo 样式
我们的应用程序的默认 Dojo 样式
我们的应用程序的默认 Dojo 样式

我们在使用 Dojo 基础小部件时碰到一些限制。第一个限制是给某些小部件添加圆角的能力。一种添加圆角的方式是使用特定于 Mozilla 的 CSS 样式,但要实现这一点,必须有一个纯色背景,并且它只对 Mozilla Firefox 起作用。因此,除非是一个大小固定的图像,否则您有可能无法对您的控件使用圆形的渐变图作为背景,但这在按钮和标题栏中是不现实的,这是因为它们里面包含的文本是变化的或者它们占据的大小是按屏幕的百分比计算的,而不是使用固定的宽度和高度。

不过,Dojo 的确通过某些小部件提供了这个能力,比如 dijit.form.Button。 Dojo 的模板包含具有类属性 dijitLeftdijitRight<div> 元素,这两个类属性可用来分别显示圆角左图像和圆角右图像。注意在初始版本 1.1.0 内,这个 dijitRight 属性从模板中删除了,所以它没有显示在我们上面显示的这个 Button 的模板示例中;不过,这个问题已经修复。如果您遇到与此类似的情况,即这种修复对您的应用程序十分关键,那么您可以从每夜构建(nightly builds)获得这些修改后的文件(参见 参考资料 内的 Dojo 工具箱链接)。

图 2 展示了使用开箱即用的 Dojo soria 主题的应用程序的最初外观。

图 2. 我们的应用程序的默认 Dojo 样式
我们的应用程序的默认 Dojo 样式
我们的应用程序的默认 Dojo 样式

图 3 展示了经过样式定制的应用程序的最终外观(单击 这里 查看放大的图像)。

图 3. 我们的应用程序的定制 Dojo 样式
我们的应用程序的定制 Dojo 样式
我们的应用程序的定制 Dojo 样式

导航辅助 - breadcrumb trail(浏览路径记录)

breadcrumb trail 用于很多 Web 站点来跟踪导航。Dojo 提供了一个 dijit.layout.StackController 小部件,可与 dijit.layout.StackContainer 小部件联合使用,这就让您能够在 StackContainer 内导航页面。清单 10 给出了一个基本的使用示例。该代码可以下载并可从 Dojo 源代码的 <dojo_home>\dijit\tests\layout\ 目录运行。

清单 10. 联合使用 dijit.layout.StackController 小部件和 dijit.layout.StackContainer 小部件
<div dojoType="dijit.layout.StackController" containerId="myStackContainer"></div>
    
<div id="myStackContainer" dojoType="dijit.layout.StackContainer"
    style="width: 90%; border: 2px solid; height: 100px;">
    <div id="page1" dojoType="dijit.layout.ContentPane" 
  title="page 1">This is page 1.</div>
    <div id="page2" dojoType="dijit.layout.ContentPane" 
  title="page 2">This is page 2.</div>
    <div id="page3" dojoType="dijit.layout.ContentPane" 
  title="page 3">This is page 3.</div>
</div>

使用 Dojo 的 soria 样式表后,此代码应该类似图 4。

图 4. 使用 soria 样式表的 StackContainer 应用程序
使用 soria 样式表的 StackContainer 应用程序
使用 soria 样式表的 StackContainer 应用程序

单击 StackController 的每个按钮将会显示相应的页面。但是,若想要获得典型的 breadcrumb 行为,即额外的页面会随着您向下钻取而添加并且单击页面 1 将会回到第一个页面并会从 breadcrumb trail 删除所有的其他页面(以及此 StackContainer),需要一些定制代码。

以清单 10 相同的代码开始,但没有 StackContainer 内的任何初始静态页面,如清单 11 所示。

清单 11. 没有初始静态页面的代码
<div dojoType="dijit.layout.StackController" containerId="myStackContainer">
</div>
    
<div id="myStackContainer" dojoType="dijit.layout.StackContainer"
    style="width: 90%; border: 2px solid; height: 100px;">
</div>

为了模拟向下钻取到某个页面,需要添加一个按钮来向此 StackContainer 动态添加页面。

清单 12. 添加一个按钮来动态添加页面
<button dojoType="dijit.form.Button"
    showLabel="true"    
    label="Add Page"
    onClick="loadPage">
</button>

loadPage 代码创建 dijit.layout.ContentPane 并将其添加到 myStackContainer,如清单 13 所示。

清单 13. 创建 dijit.layout.Content.Pane
var nPages = 0;
  function loadPage()
  {
    nPages = nPages + 1;
    var container = dijit.byId('myStackContainer');
    if( container )
    {
      var pageid = "page" + nPages;
      add(container,"Page " + nPages, pageid);
      container.selectChild(pageid);
    }
  }
  
  function add(parent,name,id)
  {
    var node = document.createElement("div");
    var child = new dijit.layout.ContentPane
      ( {title: name, id: id },node );
    parent.addChild(child);

    //add content to the new page
    dojo.byId(id).innerHTML = name;    
  }

有了这些补充,初始屏幕应该类似图 5。

图 5. 使用 soria 样式表修改后的应用程序 - 初始屏幕
 使用 soria           样式表修改后的应用程序 - 初始屏幕
使用 soria 样式表修改后的应用程序 - 初始屏幕

两次按下 Add Page 按钮后,屏幕应该类似图 6。

图 6. 使用 soria 样式表修改后的应用程序 - 添加了两个页面后
使用 soria           样式表修改后的应用程序 - 添加了两个页面后
使用 soria 样式表修改后的应用程序 - 添加了两个页面后

即便在 Page 1 上单击,还是会将 Page 2 留在 StackController 内。如按下 Stack Controller 内的按钮且该页面还未显示,它就会触发特定于 StackContainerselectChild 事件。我们需要订阅该事件以便在用户选择返回到 breadcrumb trail 内之前的页面时能立即处理此 Stack Controller 。清单 14 所示的代码能实现此目的,其中 selectedPage 是方法名,我们将实现此方法来处理这个行为。

清单 14. 实现 selectedPage 方法
dojo.subscribe("myStackContainer-selectChild","","selectedPage");

selectedPage 方法会调用一个名为 removeDownstream 的 helper 方法。removeDownstream 获得被单击页面的索引并删除位于该给定索引之后的所有页面,如清单 15 所示。

清单 15. 使用 removeDownstream 方法
function selectedPage(page)
 {
    removeDownstream(page.id) ;
 }

function removeDownstream( pageid )
{
    var widget = dijit.byId('myStackContainer');
    if(widget)
    {
      var children = widget.getChildren();
      var index = dojo.indexOf(children, dijit.byId(pageid)); 
        //get index of page that was selected from the breadcrumb trail
      index = index+1; //start removing from the page to the right
      while( children.length > index )
      {
        widget.removeChild(children[ index ]);
        index = index + 1;
       }
    }
}

包含上述代码后,返回 trail 内任何之前的页面都将会删除 breadcrumb trail 内所有下游的页面以及这个 StackContainer 小部件(这可以通过下载和运行此代码并在 Firebug 内检查 HTML 源代码加以验证)。

要让其看上去更像是一个典型的 breadcrumb trail,CSS 和 DOM 样式处理可用来样式化这些 breadcrumb 节点。这个完整的示例可下载得到,其内包含了样式化的部分,但这里不作详细解释。最后的示例应该如下所示。

通过四次单击 Add Page 按钮添加四个页面后,breadcrumb trail 将会如图 7 所示。

图 7. 用 breadcrumb trail 修改后的应用程序 - 4 个页面
用 breadcrumb           trail 修改后的应用程序 - 4 个页面
用 breadcrumb trail 修改后的应用程序 - 4 个页面

在 Page 2 上单击会出现如图 8 所示的结果。

图 8. 用 breadcrumb trail 修改后的应用程序 – 回至 page 2
用 breadcrumb           trail 修改后的应用程序 – 回至 page 2
用 breadcrumb trail 修改后的应用程序 – 回至 page 2

Tree - 启用/禁用节点

Dojo 的 dijit 库包含一个具备基础树功能的 Tree 小部件。并且还有各种方法让您能扩展和折叠每个节点。不过,我们所需要的树应该还能提供禁用节点的功能。有几种方法可以实现这个目的:从头创建一个新的小部件,我们可以使用 Dojo 的 Tree 小部件作为基础来创建自己的小部件;我们也可以利用 Dojo 提供给我们的其他功能来实现此任务。接下来我们将带您亲历这个过程。

Dojo 树的每一层都包含一个图标和一个标签。在 Dojo 中,可以通过在这个 Tree 的标记内指定回调方法来获取标签和图标的样式类,并将其用于树中的每一项。我们将用它来样式化我们的禁用节点以使它们看上去是禁用的。在本示例中,我们将禁用所有的非叶节点,因为通常非叶节点只用来对希望对其执行任务的那些项进行分类。

首先,定义 CSS 样式,如清单 16 所示。

清单 16. 定义 CSS 样式
.enabled
 {
        color: black;
        cursor: pointer;
 }
 
 .disabled
 {
        color:gray;
        cursor: not-allowed;
       background-color: transparent !important;
 }

当节点被单击,它将获得一个新的类属性 dijitTreeLabelFocused。通过为此禁用样式使用 !important,就确保了它将在由 dijitTreeLabelFocused 设定的背景色上使用透明的背景色。通过这样做,我们使它看上去像节点,却在单击时不能进行选择。

这个用于标签样式的回调方法有两个由 Tree 小部件传递进来的参数,即被单击的项和一个布尔参数指定该树节点是否现在要打开。我们的方法如清单 17 所示。

清单 17. 原型的回调方法
function getLabelClassForMyTree(
            /*dojo.data.Item*/ item, /*Boolean*/ opened)
 {
        if( item && item.children )
                  return "disabled";
      
        return "enabled";
 }

要想使用这种方法,我们需要指定我们的这个 tree 小部件标记的 getLabelClass 属性,如清单 18 所示。

清单 18. 指定 getLabelClass 属性
<div dojoType="dijit.Tree" id="myTree" model="model"
        onClick="onSelectItem"
        getIconClass="getIconClassForMyTree"
        getLabelClass="getLabelClassForMyTree">
        </div>

注意,这个样式只应用于标签本身。所以如果将鼠标悬浮于图标或该标签右侧的空白处,将看不到这个禁用了的光标样式。要用定制图像设置此图标的样式并在鼠标悬浮于其上时将其显示为禁用状态,可以使用如清单 19 所示的回调方法。此函数之后会在这个 tree 小部件标记的 getIconClass 属性内指定,如清单 18 所示。

清单 19. 用定制图像样式化图标
function getIconClassForMyTree(
/*dojo.data.Item*/ item, /*Boolean*/ opened)
     {
          var style = "";
          if( item )
          {
               if( item.children )
               {
                    //add icon image style
          style = opened ? "customFolderOpenedIcon" : 
"customFolderClosedIcon";
                         
                    //add disabled style
                    style = style + " disabled";
                     }
                    else
{   
                          //add icon image styling and enabled styling
                    style = "noteIcon enabled";
                }
          }
          return style;
}

然而,此行的剩余部分还是不能在鼠标悬浮时显示为禁用。由于 Dojo 只为此图标类和此标签提供回调,所以禁用此行的其余部分并不是件容易的事。一种做法是在此树构建后迭代所有节点并向封装此标签和图标的行添加禁用或启用样式。该 DOM 节点可通过使用每个树节点的 rowNode 属性访问。

要启用被禁用了的节点函数,只需覆盖 onClick 以针 对禁用节点返回。清单 20 中所示的方法可用来忽略 onClick 事件,只返回某个被禁用了的节点是否被单击。

清单 20. 覆盖 onClick
function onSelectItem(item) {            
       if(item.children) //disabled node, ignore click event
           return;

       if(item) {    
              // Display basic attribute values
              dojo.byId('uLabel').value = item ? 
                  store.getLabel(item) : "";
       }
}

清单 20 中所示的方法可以进一步修改,以使这些选中的节点能在一个启用了的节点被选中后单击某个被禁用的节点时仍保持其样式,如清单 21 所示。

清单 21. 让选中了的节点保持其样式
function onSelectItem(item,node){
            
        if(item.children)
        {
            if(previouslySelectedNode)
                  //keep currently selected node highlighted
dojo.addClass(previouslySelectedNode,"dijitTreeLabelFocused");                
            return;
        }

        if(item){
            if(previouslySelectedNode)
                //in case we set that ourself, remove it 
dojo.removeClass(previouslySelectedNode,"dijitTreeLabelFocused"); 
                
            //keep track of enabled selected node
            previouslySelectedNode = node.labelNode;
                
            // Display basic attribute values
            dojo.byId('uLabel').value = item ? store.getLabel(item) : "";
        }
        }

要查看该示例的运行效果,可以将 test_Tree_disable_style.html 下载到 <dojo_home>/dijit/tests/ 目录并在浏览器内运行这些文件。第一个文件展示了禁用节点与这些回调的使用,而第二个文件则使用 rowNode 来使这些节点看上去是禁用的。

每行具有不同选项的 Grid 编辑器

当今,大多数 Web 2.0 技术都为称为网格 的一种复杂表提供了各种小部件。这些网格表包括了列值选择功能,类似于 HTML SELECT。不过,这种列选择存在的局限性是它们是静态的,并且整个表只能有一组选项(例如,1st 列的 11 行具有相同的选项)。图 9 所示的是一个简单的 Dojo 网格应用程序,其中 1st 列的所有选项对每行都是相同的(normal、note 和 important)。

图 9. Dojo 网格编辑器选择 - 标准
Dojo 网格编辑器选择 - 标准
Dojo 网格编辑器选择 - 标准

但是我们需要的是动态选项,以便对于网格表的每一行提供不同的选项,因此,我们必须进行一些定制。解决这个问题的方式很多。最基本的一种方式是覆盖此网格 Select 编辑器的格式化程序来返回针对所选行的选项信息。不过完成这一步后,针对网格模型列的选项和值就失去意义了。在将网格的行与网格外任何类型的模型数据相关联的时候,必须要格外小心,因为网格内的行在任何时候都可被重新排序(例如,使用 sort 动作)。如果将所扩展的模型信息保留在实际的网格模型之外,那么在网格模型内修改的行放到网格之外的数据中时很可能不会如预期的那样。为了避免这类不匹配,我们发现最好是将额外的非可视信息保留在网格模型内,比如该行的选项和值。然后,此格式化程序覆盖就能返回所扩展的模型信息。代码片段如清单 22 至清单 26 所示。

总的来说,实现此目的需要三个基本的步骤:

  1. 用每行的额外选项信息扩展网格的数据模型
  2. 创建一个定制的网格编辑器,专注于 format 方法
  3. 将定制选择器的使用与此网格布局联系起来

用每行的额外选项信息扩展网格的数据模型

清单 22 给出了为每行提供不同选项而进行修改前的网格数据。

清单 22. 修改前的网格数据
var data = [ 
    ["normal",   "message-1","Priority-1"],
    ["note",     "message-2","Priority-2"],
    ["important","message-3","Priority-3"]
 ];

清单 23 给出了进行修改后的数据数组的第 4 列的网格数据。

清单 23. 修改后的网格数据
var data = [ 
    ["normal-1",   "message-1","Priority-1",["normal-1","note-1","important-1"]],
     "note-2",     "message-2","Priority-2",["normal-2","note-2","important-2"] ],
    ["important-3","message-3","Priority-3",["normal-3","note-3","important-3"] ]
];

创建一个定制的网格编辑器来返回针对所选行的选项

为此,我们创建了 dojox.grid.editors.Select 的一个子类并专注于 format 方法。我们将此编辑器的选项设为当前行的选项,并让父类来实际进行此信息的格式化。这就使得每行的选项不同。注意,在本例中,选项和值是相同的。我们应该让这二者不同,于是我们在此网格模型数据内添加了另外一个值列。清单 24 内的示例通过创建一个新编辑器 MySelect 展示了这一点。

清单 24. 让选项和值不同
dojo.declare("com.ibm.editor.MySelect", [dojox.grid.editors.Select], {
     format: function(inDatum, inRowIndex){
           var row = this.cell.grid.model.data[inRowIndex]; 
            this.options = this.values = row[OPTION_INDEX];   // could have diff values
            return this.inherited("format",arguments);        // return HTML select stmt
      }, 
});

将定制选择器的使用与此网格布局联系起来

清单 25 内的示例展示了使用标准 dojo 网格编辑器的网格布局。注意选项和值被包含在网格布局之内,这是因为二者对于此表内的所有行都是相同的。

清单 25. 使用标准 dojo 网格编辑器的网格布局
gridLayout = [{
         type: 'dojox.GridRowView', width: '20px'
         },{
        defaultCell: { width: 8, editor: dojox.grid.editors.Input, styles: 'text-
        align: right;' },
        rows: [[
                {name: 'Source Schema: Current',styles: '',    width: '40%',    editor: 
               dojox.grid.editors.Select, options: ["normal", "note", "important"], 
               values: [0, 1, 2], formatter: function(inDatum) { return 
               this.options[inDatum]} },
               {name: 'Mapping',    styles: '',    width: '20%'}, 
               {name: 'Target Schema: ',    styles: '',    width: '40%'}
        ]]
}];

清单 26 内的示例展示了使用定制网格编辑器的网格布局。请注意我们无需为整个表定义选项,因为针对每行的选项都不同。

清单 26. 使用定制网格编辑器的网格布局
gridLayout = [{
      type: 'dojox.GridRowView', width: '20px'
      },{
      defaultCell: { width: 8, editor: dojox.grid.editors.Input, styles: 
                               'text-align: right;' },
      rows: [[
            {name: 'Source Schema: Current',styles: '', width: '40%', 
              editor: com.ibm.editor.MySelect }
            },
            {name: 'Mapping',	styles: '',	width: '20%'}, 
            {name: 'Target Schema: ',	styles: '',	width: '40%'}

      ]]
}];

图 10 所示的是为此网格表选择编辑器而进行的 Dojo 小部件定制的最终结果,其中,每行具有不同的选项。请注意 normal-3、note-3 和 important-3 最后一行的选择与 normal-1 和 note-2 的其他行所选中的选项不同。

图 10 Dojo 网格编辑器选择 - 定制
Dojo 网格编辑器选择 - 定制
Dojo 网格编辑器选择 - 定制

为支持每行不同的选项,Dojox 网格示例 <dojo_home>/dojox/grid/tests/test_edit.html 被进行了修改。要查看这些示例的运行效果,可以将 test_edit_simple.html 和 test_edit_diff_options.html 下载到 <dojo_home>/dojox/grid/tests 目录并在浏览器内运行这些文件。第一个文件展示了正常的网格表操作,在一个非常简单的应用程序内所有行均有相同的选项;而第二个文件则展示了定制网格编辑器的使用。

Tree – 可编辑

我们的应用程序让用户能对一个图形树进行某些微小程度的编辑。用户能删除(丢弃)此树中的某个节点或其子节点、重命名某节点的标签或是撤销/清除在某节点上所进行的任何编辑。编辑某个节点包含如下的两个步骤:

  1. 基于此编辑动作,定义和向所选中的节点应用某种样式(例如,修改颜色、为文本加删除线等)
  2. 针对某些编辑动作,更改节点(比如,更改标签、折叠节点等)

为此,我们如往常一样创建了一个树,并在 HTML 模板内创建了相应的存储、模型(未显示)和树。

清单 27. 创建一个树
<div dojoType="dijit.Tree" 
        id="current_tree_${id}" 
        class="container"
        model="current_tree_model_${id}"
        labelAttr="name"
        childrenAttr="children, items" 
        dojoAttachEvent="onClick:_onClickSelect"
        getLabel="ourCustomLabel"
        getLabelClass="getOurLabelClass"
        getIconClass="getOurIconClass">            
</div>

我们向此树添加了一个具有编辑选项的右键单击上下文菜单。清单 28 内的 connect 将此菜单附加到此树。

清单 28. 附加右键单击上下文菜单
<script type="dojo/connect">
        var menu = dijit.byId("edit_tree_menu_${id}");                
        menu.bindDomNode(this.domNode);
</script>

注意:本例假设用户使用鼠标的左键选择感兴趣的节点。

在 HTML 模板内对该菜单的定义如清单 29 所示。

清单 29. 菜单的定义
<ul dojoType="dijit.Menu" 
id="edit_tree_menu_${id}" 
style="display: none;" 
iconClass="dijitMenuItemIcon">
        
        <li id="edit_clear_menu_${id}"
        dojoType="dijit.MenuItem" 
        dojoAttachEvent="onClick:_onClickClear" 
        iconClass="dijitEditorIcon dijitEditorIconUndo" 
        disabled="true">
        Clear Edit</li>
        
        <li dojoType="dijit.MenuItem" 
        iconClass="dijitEditorIcon dijitEditorIconWikiword" 
        disabled="true" >
        Create Expression</li>
        
        <li id="edit_drop_menu_${id}" 
        dojoType="dijit.MenuItem" 
        dojoAttachEvent="onClick:_onClickDrop" 
        iconClass="dijitEditorIcon dijitEditorIconDelete" 
        disabled="false" >
        Drop</li>                    
        
        <li dojoType="dijit.MenuItem" 
        dojoAttachEvent="onClick:_onClickRename" 
        iconClass="dijitEditorIcon dijitEditorIconCreateLink">
        Rename</li>
        
        <li id="edit_menu_remove_${id}" 
        dojoType="dijit.MenuItem" 
        dojoAttachEvent="onClick:_onClickRemove" 
        iconClass="dijitEditorIcon dijitEditorIconCut">
        Remove</li>                    
    
</ul>

我们还向此树的可编辑节点添加了一个我们在 Dojo 的一个论坛内发现的小技巧。

清单 30. 通过覆盖禁用按键
<script type="dojo/method" event="_onKeyPress">
        //disable keypress via override, so we can use inline editor on nodes
</script>

最后,基于所选择的选项,我们为此编辑动作创建了一个合适的处理程序,它可以应用样式并很可能更改此节点。

remove 节点处理程序的一个代码片段如清单 31 所示(该用户在调用此处理程序之前选择了此节点。节点选择设置 this.item)。其显示效果是应用了一个样式来表明此节点已经删除(红色的删除线),而 undo 菜单项现在对此节点是启用的。

清单 31. 删除节点的代码片段
/**
* User clicked on remove button or right context menu
* @param {Object} e
*/
_onClickRemove: function (/*Event*/ e) {
        . . .
        this.store.setValue(this.item,"edit","editRemove"); //add remove style
        dijit.byId("edit_clear_menu_"+this.id).setDisabled(false);//enable undo
        // process remove, e.g. report to server . . .
},

删除树节点样式的定义如下面的清单 32 所示。

清单 32. 定义删除树节点样式
#edit .editRemove
{
        color: red;
        text-decoration: line-through;
}

rename 节点处理程序的代码片段如清单 33 所示。它让用户能够重命名树节点的标签。

清单 33. 重命名节点处理程序的代码片段
 /**
 * User clicked on rename button or right context menu
 * Bring up editor for tree node name
 * @param {Object} e
 */
    _onClickRename: function (/*Event*/ e) {
        . . .
        this.store.setValue(this.item,"edit","editRename"); //add rename style
        
        var labelNode = tree._itemNodeMap[ this.item.ID ].labelNode;
        var txt = document.createElement('span');
        txt.innerHTML = labelNode.innerHTML;
        labelNode.innerHTML = "";
        labelNode.appendChild(txt);    
        
        var editor = new dijit.InlineEditBox({
            autoSave: true, // false=save/cancel button shown in html
            onChange: function(value) {
                this.setDisabled(true); // done, disable editing
                // process rename, e.g. report to server . . .
            },
            renderAsHtml: true,
            width: "150px"
        }, txt);
        editor.setDisabled(false); //enable editing
        editor._edit();
        
        dijit.byId("edit_clear_menu_"+this.id).setDisabled(false);//enable undo
        . . .
},

此重命名树节点样式的定义如清单 34 所示。

清单 34. 重命名树节点样式
#edit .editRename
{
        color: blue;
}

图 11 所示的是此树节点编辑器在进行节点重命名动作时的外观。

图 11. 编辑树节点 - 重命名
编辑树节点 - 重命名

其最终结果类似图 12。右键单击上下文菜单内现在有几个不同的编辑选项可用。删除或是丢弃的那些树节点用红色的删除线进行样式化,重命名的那些树节点则显示为蓝色。

图 12. 右键单击上下文菜单以及样式化了的被编辑节点
右键单击上下文菜单以及样式化了的被编辑节点

树 - 具有右键单击上下文菜单以及右键单击选择

在之前的小节中,需要左键单击节点来将所选中的节点存储在一个变量内,以便当从右键单击上下文菜单中选中一个选项时使用。之所以这么做是因为右键单击上下文菜单绑定到整个树,而不是一个树节点。要想根据右键单击的具体节点来获得特定的行为,可以连接到此菜单的 _openMyself 方法,如清单 35 所示。

清单 35. 连接到 _openMyself 方法
<div dojoType="dijit.Tree" … >

        <script type="dojo/connect">
            var menu = dijit.byId("tree_menu"); 
            
            dojo.connect(menu, "_openMyself", this, function(e){

                // get the tree node that was the source of this open event
                var tn = dijit.getEnclosingWidget(e.target);

                // if this tree node does not have any children,
        // disable all of the menu items
        // note: these lines are not related to the above section, just 
        // shown to illustrate how menu items would be disabled 
        // depending on which node was clicked.
                menu.getChildren().forEach(
            function(i){ 
                i.setDisabled(!tn.item.children);
            });

                // IMPLEMENT CUSTOM MENU BEHAVIOR HERE
            });
        </script>
</div>

tn 变量可以作为一个全局变量存储,可以在菜单选项被单击后访问。这样一来,用户就不需要首先左键单击将要应用菜单动作的节点。

常见错误

本节给出了我们遇到的几个常见错误,其中包括:

  • 多个全局 JavaScript 方法具有相同名称。不会报告错误,但是无法判断哪个方法才是应该被调用的。
  • 对象隔离不够,因而容易造成对全局函数的过多使用。这就使得代码很难维护并且还可能会导致上面提到的常见错误。
  • 日志记录/跟踪太多或对日志消息的过滤太少,这常会带来超负荷的日志信息。
  • 无效的错误处理让调试十分困难。异常处理应该更多,对错误发生的路径也应多加考虑。
  • 对象属性范围(全局还是本地)的使用不当。
  • 在需要的时候没有为回调使用 dojo.hitch。这有可能会导致对回调是否运行在正确的对象上下文内的检测滞后(例如,对象实例变量的值不正确)。

测试这个 Dojo 应用程序

本地和远程

Web 2.0 应用程序使用 RESTful 服务来获得其信息。REST 是 REpresentational State Transfer 的缩写。它是 World Wide Web 所基于的架构模型。REST 的原理包括:以资源为中心、所有相关资源均使用 URI 寻址、使用 HTTP (GET、POST、PUT、DELETE)统一访问。我们希望能够在非连接的本地模式下进行测试。因此,我们将 RESTful 服务的结果保存到测试文件内,并创建了一个抽象方法来获得给定 REST 调用的 URL。

自动检测和交换的例子

此抽象借助 document.location.protocol 的 JavaScript 值来判断访问是本地的还是远程的,并返回正确的 URL。这可通过将这些本地测试文件放入类似于此服务器 URI 的一个目录结构实现。比如,对于 URI: myui/catalog/types on server http://<server>:<port>,我们只是将此 URI 置于本地的基础测试目录 <local-test-base-dir>/myui/catalog/types。根据访问是本地还是远程,只有此 URI 的基础部分需要更改。我们对配置对象 BASE_URL 属性的设置如下所示。

config.BASE_URL = (document.location.protocol=="file:") ? "data" : "..";

结束语

总的来说,我们发现开始进行 Dojo 开发所需的学习曲线很少,这归功于 Dojo 工具箱以及 Dojo 社区提供的大量可用示例以及 Internet 上丰富的 JavaScript 信息。但是,作为 Java 开发人员,我们还是缺少强大的 IDE 支持、优秀的 API 文档、强类型缺乏、不同 Web 浏览器的不同运行时行为以及 JavaScript 开发的编译时检查。学习如何进行定制有时十分痛苦,并且通常定制的过程十分琐碎且容易出现错误。还好,当我们掌握了这些过程后,我们发现它们是可重复进行的。我们最终得到的原型是一个看起来很专业的应用程序,并且具备启用了 Ajax 的 Web 应用程序所应具备的所有性能优点。


下载资源


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Web development
ArticleID=366505
ArticleTitle=编写一个定制的 Dojo 应用程序
publish-date=01222009