Cairngorm In One File

最近有机会研究了一下传说中的 Cairngorm 框架,整体感觉还不错,其中的模块化概念可以使项目在变大变复杂之后依然可控。

具体优点就不说了,说说最大的缺点,就是 繁琐 。由于要分离用户触发事件以及对应的 Command 操作,导致几乎每个明显的用户操作都要写一个 Event 类(还真是累阿);另外由于 Cairngorm 本身是一个 design patten 的实践,不是 library,所以造成实际使用的时候需要死版的 implement 一些不着调的空 interface。

简单来说 Cairngorm 是一个婆婆妈妈的 framework,这从技术上来说也不是什么问题,但是重点是会影响写代码人的心情。比如我,一想到要每一个 action 都要写一个没什么特别之处的 Event class 就觉得头疼,一头疼就不想干,一不想干就完了。。。

所以,我就想能不能保留 Cairngorm 的思想,但是用一种更简洁的方式来实现?最后不小心想出了一招,经过一段时间的实验,感觉效果还不错,和大家分享一蛤。

global.as

我把 Cairngorm 的思想融入到一个名叫 global 的类中,使用的时候只需要这一个文件就包含了全部 Cairngorm 相关内容。我打算用一个通过 webservices 查询当前货币兑换比例的例子来具体的说一下 Cairngorm 各方面的特性,以及如何用 global 来实现这些特性。

全局的数据管理

Cairngorm 通过一个 singleton 的 ModelLocator 来连接数据与用户界面,基本思路是全部 ui 相关的数据都不是直接设置到组件上,而是通过一个全局的对象借由 binding 来实现。这样做分离了组件、数据以及命令之间的依靠。当有某个命令修改了某一个数据源之后,不用主动的再去更新组件显示,所有已经设置对应 binding 的组件将会自动更新。

global.as 将自身作为 ModelLocator 来使用,相关代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
// bindable properties, access from global.data model locator.
public var fromCurrency:String=“USD”;
public var toCurrency:String=“CNY”;
public var currencyResult:String=“”;
public var state:String=STATE_LOAD;
public var enabled:Boolean=true;
/**
 * The global singleton model locator.
 */

public static function get data():global
{
    return _data;
}

然后在对应需要响应数据更新的组件上通过绑定 global.data 中的对应属性来实现更新。比如实例中用来显示货币兑换比例结果的 Label 组件绑定了 global.data.currencyResult 属性。

1
<mx:Label text="{global.data.currencyResult}" color="0x00ff00"></mx:label>

全局的事件播报以及响应

在 Cairngorm 中,程序通过全局的 EventDispatcher 来播报全部程序相关的事件,然后对应处理事件的 Command 则通过 FrontController 在程序初始阶段配置好与对应事件的监听关系。副作用是:随着程序复杂程度的增加,会出现一堆只是名字不同的 Event class,以及只为了 execute 一下下却不得不实现的 ICommand 类。

global.as 通过静态方法 fire/listen/unlisten 实现了全局的事件播报以及监听功能,其中为了避免过多 Event class 的出现,fire 方法会播报出 DynamicEvent,并允许设置任意参数到 event 对象上,在实例中当用户点击了 Calculate Conversion Rate 按钮后会这样触发事件

1
click="global.fire(global.EVENT_CONVERSION_RATE,{fromCurrency:fromField.text,toCurrency:toField.text})"

另一方面 command 响应事件,也做了相应的简化:直接用 function 作为 command 来进行实际的业务操作:

1
2
3
4
5
6
7
public function conversionRate(e:DynamicEvent):void
{
    //..
    var fromCurrency:String=String(e.fromCurrency);
    var toCurrency:String=String(e.toCurrency);
    //..
}

最后在程序初始化的时候将 command 和对应的事件建立联系(类似 Cairngorm 中 FrontController 的功能)

1
2
3
4
5
6
function registerDefaultCommands():void
{
    //register default commands here.
    global.listen(global.EVENT_LOAD_WSDL,loadWsdl);
    global.listen(global.EVENT_CONVERSION_RATE,conversionRate);
}

全局的网络服务资源

最后关于远程 service 的部分,Cairngorm 提供了另外一个 singleton 的 ServiceLocator 供程序使用,并通过 Responder 来响应服务返回的结果。

在 global.as 中通过一个 rpc 方法实现了注册以及使用 service 的功能。比如实例中 webservices 的注册如下:

1
2
3
4
5
6
7
function registerDefaultServices():void
{
    //register default services here.
    var s:WebService=new WebService();
    //..
    global.rpc(global.CONVERSION_DESTINATION,s);
}

然后代码通过以下方法访问上面注册的 service:

1
2
3
4
public static function get currencyConvertor():*
{
    return rpc(CONVERSION_DESTINATION);
}

实际上 rpc 方法返回的是一个 service 的 proxy 对象。和直接使用 service 不同的是,通过 proxy 简化了服务方法调用后结果响应部分的代码。比如实例中获取货币兑换比率的方法是这样写的:

1
2
3
4
5
6
7
8
9
10
11
12
13
global.currencyConvertor.ConversionRate(fromCurrency,toCurrency)(
    function(data:Object):void
    {
        global.data.currencyResult=data.result.toString();
    },
    function(info:Object):void
    {
        Alert.show(info.fault.message,info.fault.faultString);
    },
    function(o:*):void
    {
        global.data.enabled=true;
    });

其中第一个函数处理成功,第二个函数处理失败,第三个函数无论成功失败都会先执行。

至此 Cairngorm 的全部功能都被 global.as 实现了。如果有盆友对我瞎掰的这些感兴趣,可以 下载 sample source 然后把你的想法告诉我。

Over :P

Spring MVC 中的中文编码问题小说一蛤

原来以为 Java 中文支持的会很好,没想到一用 Spring 就出乱码啦,看来人家默认情况下都不吊你第三世界国家的。。。

anyway 切入正题,我股沟了一下相关的内容,发现大多数解决方法都是加个 servlet filter 来转换编码。觉得这方法不好,好像虽然有用但是比较糊弄事,不彻底明白始终不爽。所以我就在源代码里游啊游,整理出大概的思路,如下:

页面显示中文

这部分以 jsp 为例, Spring MVC 在 resolve 页面显示的时候是这么一个流程。

InternalResourceViewResolver 利用 InternalResourceView 来 build view。 InternalResourceView 则会将最终页面生成的请求转交给对应的 jsp 来通过 container 实现。

如果页面显示有乱码,问题貌似会出在 InternalResourceView 上,实际上 InternalResourceViewResolver 的父类中有一个叫 UrlBasedViewResolver 的东东,里面有个属性叫做 contentType,并且会在 buid view 的时候最终把这个属性赋值给 InternalResourceView 。貌似找到了问题的原因。我试着这样设置并窃喜:

1
2
3
4
5
<bean id="jspViewResolver"
       class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <property name="contentType" value="utf-8"></property>
    ...
</bean>

页面刷新后乱码依旧。。。回到源文件中,游啊游,发现其实 InternalResourceViewResolver 虽然有给 InternalResourceView 赋值 contentType,但是 InternalResourceView 忽视了这个属性。。。然后直接转交给对应的 jsp 处理。看来问题实际上出在 jsp 上。到对应的 jsp 页面,加上下面的代码:

1
<%@ page pageEncoding="UTF-8" contentType="text/html;charset=UTF-8"%>

然后在编辑器里确保源文件也存成 utf-8,然后刷新页面,好了。

h3. 提交中文参数

原以为这样就 OK 了,后来发现还没完。。。在表单提交的时候,中文依然乱码,sun。再游! 发现要改的地方有两点,第一个是针对表单的 POST,因为默认的 request body 处理的编码不是 utf-8 所以在 decode body 的时候会解出乱码来。这需要写个Interceptor 来设置 request 的 encoding ,比如:

1
2
3
4
5
6
7
8
9
10
11
public class RequestEncodingInterceptor implements HandlerInterceptor
{
    @Override
    public boolean preHandle(HttpServletRequest request,
            HttpServletResponse response, Object handler) throws Exception
    {
        request.setCharacterEncoding("utf-8");
        return true;
    }
    ...
}

然后挂在对应的 handlerMapping 上面,比如:

1
2
3
4
5
6
7
8
9
<bean id="handlerMapping">
        class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping">
    <property name="interceptors">
        <list>
            <bean class="RequestEncodingInterceptor">
        </list>
    </property>
    ...
</bean>

另一方面,如果 url get 中有 encode 之后的中文参数,还需要确保 url decode 也用 utf-8 来解。由于 BeanNameUrlHandlerMapping 通过 UrlPathHelper 来处理 url,所以需要这样配置:

1
2
3
4
5
6
7
8
9
10
<bean id="handlerMapping"
    class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping">
    ....
    <property name="urlPathHelper">
        <bean class="org.springframework.web.util.UrlPathHelper">
            <property name="urlDecode" value="true"></property>
            <property name="defaultEncoding" value="utf-8"></property>
        </bean>
    </property>
</bean>

国际化 i18n

国际化的配置文件如果用 java 的 properties 文件是不支持 utf-8 滴,要换成 Spring 的 ReloadableResourceBundleMessageSource ,酱紫配置:

1
2
3
4
5
<bean id="messageSource">
    class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
    <property name="basename" value="WEB-INF/i18n/messages"></property>
    <property name="defaultEncoding" value="utf-8"></property>
</bean>

搞定!(貌似?)

E4X 的另类应用

一直以来我们在 actionscript 中内嵌大段的多行文本时必须在每一号加上引号,然后再在结尾写加号。问题是一天三遍的写,太麻烦了。。。一直也没见过什么解决办法(除非写个外部文件载入,不过那个不爽,请忽略)。直到有一天。。。也就是昨天晚上,我一不小心想到了一个解决方案,大家请看:

1
2
3
4
5
6
7
8
9
10
11
12
13
var t:String=plain()(<!--
The Flash Player API classes are in the flash.* packages.
The Flash Player API refers to all packages, classes, functions,
properties, constants, events, and errors that are in the flash package.
They are unique to Flash Player, as opposed to the top-level classes,
such as Date, Math, and XML, or the language elements, which are based
on ECMAScript. The Flash Player API contains features that you expect
to find in object-oriented programming languages, such as the
flash.geom package for geometry classes, as well as features specific
to the needs of rich Internet applications, such as the flash.filters
package for expressiveness and the flash.net package for handling data
transmission to and from a server.
-->);

这个 plain 方法把代码中的一整段文本复给 t 变量,而字符串本身不用加引号!关键点就在 标识符,挖哈哈,这个 e4x 的应用还不错吧。

另外一个想到的应用是不用 Flex 的 embed 标签也可以内嵌图片等二进制元素到源码中,因为 Flex 的 embed 语法是不兼容 flash9 的,而且需要莫名其妙的引入 mx 包下的一些类。所以这个应用也是有一定用处地:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var b:String=plain(/W*/g)(<!--
89504e470d0a1a0a0000000d49484452
0000000e000000100803000000114abe
080000000467414d410000afc837058a
e90000001974455874536f6674776172
650041646f626520496d616765526561
647971c9653c0000000f504c5445ffff
ffff0000cccccc9999990000003952e0
8a000000314944415478da626046010c
cc0c08c0c4423a9791118261b28c8c20
1e4231988743165d2fb9ce6042022c0c
2c280020c0006dc501097dfe85550000
000049454e44ae426082
-->);

至于这一大堆乱码是什么意思,大家意会吧。。。

plain 方法的具体实现就不说了,很简单。有兴趣的朋友可以 在这里下载源码和实例 研究一蛤。

另外通过这个方法,我还发现了 as3 的编译器,以及 flex builder 2 的 as3 编辑器在词法分析上还是存在问题的。比如有时候我在 之间加入一整段类/包定义,编译器居然报错了。。。而且编辑器也很勤劳的加上语法高亮什么的。。。

如何去掉 Flash Player 8 中那个烦人的安全设置窗口

Flash Player 8 最让人郁闷的就是那个新的安全机制。所有访问网络资源的程序如果通过在桌面上双击swf来运行,都会无一幸免的弹出这个对话框。

实际上新的安全机制没有错,错就错在那个安全设置对话框太招人烦。如果你不想看繁琐的文档,又想让整个世界清静,以下是几种解决办法(任意一种既可)。

  • 通过 Settings Manager 设置。这个就是方法就是通过点击弹出的安全对话框中的设置按钮来添加Local-trusted位置。这个方法我个人感觉严重影响用户体验,而且只能在本机使用。

  • 如果你有Flash 8程序,可以在输出设置里把local playback选项设置为access network only。

  • 如果你没有Flash 8程序,可以下载 Flash Local Content Updater ,来防止弹出安全对话框。(Danger在此基础上作了个允许在Windows下直接右键修改的版本,有兴趣可以去看看。”SWF 文件安全策略修改器”:http://www.dengjie.com/weblog/comments.asp?post_id=960 )

原理

当通过本地打开swf文件时,Flash Player8执行三种不同的安全机制:

  • Local-with-filesystem 只允许访问本地文件。
  • Local-with-networking 只允许访问网络。
  • Local-trusted 允许指定的位置进行本地和网络访问。

第三种的实现是通过网上的Settings Manager来设置来自macromedia的本地shared object来实现。第一二种则是通过向swf文件中写入一个flash player8所能识别的tag标签来设置是否允许本地访问或网络访问。这个新tag的具体格式如下(个人推断,仅供参考):

  • tagCode : 69
  • tagLength: 4 (不包含tagCodeAndLength的大小,只表示tagContent的大小)
  • tagContent: 0×00000000 (表示Local-with-filesystem) , 0×00000001(表示Local-with-networking)

trace 之谜

昨天我在 swftools 上发现了一个名叫 Flash 7 Trace viewer 的小软件,它可以显示所有swf文件trace出来的信息,简直酷的不得了。

现在问题出来了,你知道它是根据什么原理来显示所有trace信息的么?Socket、LocalConnection、ActiveX,还是……?

提示:用这个软件必须先装上debug版的Flash Player

Continue reading trace 之谜

FlexSpy

昨天写了个小东西,FlexSpy(灵感来自于 SWT Spy )。它可以用来侦测任意鼠标下的MovieClip和TextField。这个工具可以帮助你更深入的了解V2Component的内部构成。

用法很简单:

1
pawaca.debug.FlexSpy.monitoring=true;

这里 下载FlexSpy

ContextMenuManager

In Flex, only top-level components in the application can have context menus. For example, if a DataGrid control is a child of a TabNavigator or VBox container, the DataGrid control cannot have its own context menu.

这是引自Flex文档里的一段话,而且Flash里也是这样,嵌套的MovieClip没法设置menu。我觉得这实在是没道理,在实际应用中,我们很容易碰到需要给嵌套MovieClip设置menu的需求。比如在一个Tree组件中,我们有时会需要根据上下文为每一个TreeNode指定不同的ContextMenu。

不过这种需求在现在MM给的环境中无法直接实现。

于是我开始动手自己写了个ContextMenuManager,目的很明确,就是可以给任意一个MovieClip或者TextField添加相关的ContextMenu。

完整的源码和示例文件可以 从这里下载

ContextMenuManager常规的用法包含以下几步:

  • 从ContextMenuManager中获取一个新的ContextMenu;
1
var cm:ContextMenu = ContextMenuManager.newMenuTemplate();
  • 向ContextMenu中添加ContextMenuItem;
1
ContextMenuManager.appendCustomMenuItem(cm,"pawa!");
  • 把ContextMenu添加到指定的movieclip或者textfield上。
1
ContextMenuManager.setMenu(yourMovieclipOrTextField,cm);

就这样!Enjoy!

I am a firestarter, twisted firestarter

I’m the self inflicted, mind detonator.Yeah, I’m the one infected, twisted animator.