2011年8月25日星期四

ADF_107:Carousel组件使用指南之二:使用CarouselSpinListener

本文最后一次修改日期:2012-6-21。
运行环境:JDeveloper 11.1.2.2.0 + Oracle Database 10g Express Edition 10.2.0.1。

上一个实验的场景是这样的:点击Table的某行,然后局部刷新Carousel组件显示相应的图片。
本实验的场景将倒过来:旋转Carousel组件中的图片,然后局部刷新Table定位到相应的行。

重点步骤说明:

1. 使用Java类作为Model层实现,并为StoreProducts.java生成Data Control
这一步与上一个实验完全相同。

2. 创建页面
(1)拖放products,生成Carousel。
(2)拖放image组件到Carousel中心区域,并设置相关属性。
最终的Carousel页面代码如下:
<af:carousel currentItemKey="#{bindings.products.treeModel.rootCurrencyRowKey}"
             value="#{bindings.products.treeModel}" var="item" id="c1"
             carouselSpinListener="#{myBackingBean.carouselSpinListener}">
    <f:facet name="nodeStamp">
        <af:carouselItem id="ci1">
             <af:image source="#{item.image}" id="i1"/>
        </af:carouselItem>
    </f:facet>
</af:carousel>

(3)拖放products,生成Read-Only Table,设置Table的PPR属性指向Carousel组件id。

3. Managed Bean中的方法:carouselSpinListener
    public void carouselSpinListener(CarouselSpinEvent carouselSpinEvent) {
        List currentSelectedKey = (List)carouselSpinEvent.getNewItemKey();
        RichCarousel carousel = (RichCarousel)carouselSpinEvent.getSource();
        CollectionModel componentModel = (CollectionModel)carousel.getValue();
        JUCtrlHierBinding carouselTreeBinding = (JUCtrlHierBinding)componentModel.getWrappedData();
        JUCtrlHierNodeBinding selectedCarouselItemNode = carouselTreeBinding.findNodeByKeyPath(currentSelectedKey);
        Key currentCarouselItemKey = selectedCarouselItemNode.getRowKey();
        DCIteratorBinding dcIterBinding = carouselTreeBinding.getIteratorBinding();
        dcIterBinding.setCurrentRowWithKey(currentCarouselItemKey.toStringFormat(true));
    }

说明:旋转图片时,并没有定位到Carousel组件对应Binding的对应节点,因此需要在程序中做这部分工作。
这一点,Carousel组件与Table/Tree组件是不同的,后者是自动定位到对应Binding的对应节点。

4. 运行
点击后面的图片,会发现Table被局部刷新并定位到相应行。


Project 下载:ADF_Carousel(2).7z

参考文献:
1. http://www.oracle.com/technetwork/developer-tools/adf/learnmore/009-carousel-master-detail-169128.pdf

2011年8月24日星期三

ADF_106:Carousel组件使用指南之一:使用程序局部刷新

本文最后一次修改日期:2012-6-20。
运行环境:JDeveloper 11.1.2.2.0 + Oracle Database 10g Express Edition 10.2.0.1。

本实验基础资料(代码、图片)取自于《开发用户界面(基于AJAX)》。

重点步骤说明:

1. 使用Java类作为Model层实现,并为StoreProducts.java生成Data Control


2. 创建页面
(1)拖放products,生成Read-Only Table。
(2)拖放products,生成Carousel。
(3)拖放image组件到Carousel中心区域,并设置相关属性。
最终的Carousel页面代码如下:
<af:carousel currentItemKey="#{bindings.products1.treeModel.rootCurrencyRowKey}"
             value="#{bindings.products1.treeModel}" var="item" id="c8"
             binding="#{myBackingBean.carousel}">
    <f:facet name="nodeStamp">
        <af:carouselItem id="ci1">
            <af:image source="#{item.image}" id="i1" partialTriggers="::pc1:t1"/>
        </af:carouselItem>
    </f:facet>
</af:carousel>


3. 运行

点击Table的不同行,发现Carousel并没有翻到对应的图片,这是为什么呢?
即使在页面代码中,将Carousel组件的PPR指向了Table组件,也不行。
最终发现这是一个BUG,于是只好使用程序刷新。

4. 定制Table的SelectionListener
(1)把Carousel的Binding属性指向到Managed Bean中一个属性。
(2)定制Table的SelectionListener,完整的代码如下:
package view;

import oracle.adf.model.binding.DCIteratorBinding;
import oracle.adf.view.rich.component.rich.data.RichCarousel;
import oracle.adf.view.rich.component.rich.data.RichTable;

import oracle.adf.view.rich.context.AdfFacesContext;

import oracle.jbo.Key;
import oracle.jbo.Row;
import oracle.jbo.uicli.binding.JUCtrlHierBinding;

import oracle.jbo.uicli.binding.JUCtrlHierNodeBinding;

import org.apache.myfaces.trinidad.event.SelectionEvent;
import org.apache.myfaces.trinidad.model.CollectionModel;

public class MyBackingBean {
    private RichCarousel carousel;

    public MyBackingBean() {
    }

    public void tableSelectionListener(SelectionEvent selectionEvent) {
        makeCurrentRow(selectionEvent);
      
        AdfFacesContext adfFacesContext = AdfFacesContext.getCurrentInstance();
        adfFacesContext.addPartialTarget(this.carousel);
    }

    private static void makeCurrentRow(SelectionEvent selectionEvent) {
        RichTable rt = (RichTable)selectionEvent.getSource();
        CollectionModel cm = (CollectionModel)rt.getValue();
        JUCtrlHierBinding tableBinding = (JUCtrlHierBinding)cm.getWrappedData();
        DCIteratorBinding iter = tableBinding.getDCIteratorBinding();

        JUCtrlHierNodeBinding selectedRowData = (JUCtrlHierNodeBinding)rt.getSelectedRowData();
        Key rowKey = selectedRowData.getRowKey();
        iter.setCurrentRowWithKey(rowKey.toStringFormat(true));
        Row row = selectedRowData.getRow();
        System.out.println("%%%%%%%%%%%%%%% The Selected Row Data: " + row.getAttribute("name") + " " +
                           row.getAttribute("category"));
    }

    public void setCarousel(RichCarousel carousel) {
        this.carousel = carousel;
    }

    public RichCarousel getCarousel() {
        return carousel;
    }
}

原理请参考《定制SelectionListener》,这里不再赘述。

5. 运行


6. 如果你不喜欢Carousel下面的bar,可以通过定制CSS,将其去掉
新建一个皮肤文件:mySkin.css,内容如下:
/**ADFFaces_Skin_File / DO NOT REMOVE**/
@namespace af "http://xmlns.oracle.com/adf/faces/rich";

@namespace dvt "http://xmlns.oracle.com/dss/adf/faces";

af|carousel::spin-bar {
visibility: hidden;
}

af|carousel::spin-h-previous-icon-style {
visibility: hidden;
}

af|carousel::spin-h-next-icon-style {
visibility: hidden;
}

af|carousel::spin-info {
visibility: hidden;
}

7. 运行


Project 下载:ADF_Carousel.7z

参考文献:
1. http://technology.amis.nl/2009/11/23/jdeveloper-11112-carousel-component-as-master-and-detail/
2. http://www.oracle.com/technetwork/developer-tools/adf/learnmore/oct2010-otn-harvest-183714.pdf

2011年8月23日星期二

ADF_105:ProgressIndicator组件使用指南之二:彩色进度条

开发环境:JDeveloper 11.1.2.0.0。

1. 页面代码:my_progress_indicator2.jspx
<af:form id="f1">
<af:panelGroupLayout id="pgl1" layout="horizontal">
<af:commandButton text="Start Poll" id="cb1" partialSubmit="true" actionListener="#{viewScope.myBackingBean2.startButton_actionListener}"/>
<af:separator id="s1"/>
<af:commandButton text="Stop Poll" id="cb2" partialSubmit="true" actionListener="#{viewScope.myBackingBean2.stopButton_actionListener}"/>
</af:panelGroupLayout>
<af:poll id="pol1" interval="1000" partialTriggers="cb1 cb2" rendered="true" binding="#{viewScope.myBackingBean2.pollComponent}"/>
<af:progressIndicator id="pi1" partialTriggers="pol1" styleClass="#{viewScope.myBackingBean2.myProgressRangeModel2.styleClass}"
actionListener="#{viewScope.myBackingBean2.progressIndicator_actionListener}" value="#{viewScope.myBackingBean2.myProgressRangeModel2}"/>
</af:form>

2. 为了显示颜色,使用了CSS,创建sample.css。

.red af|progressIndicator::determinate-filled-icon-style {
background-image: url("../../images/redbar.png");
}

.yellow af|progressIndicator::determinate-filled-icon-style {
background-image: url("../../images/yellowbar.png");
}

.green af|progressIndicator::determinate-filled-icon-style {
background-image: url("../../images/greenbar.png");
}

3. 创建trinidad-skins.xml,指向sample.css。
<?xml version="1.0" encoding="UTF-8" ?>
<skins xmlns="http://myfaces.apache.org/trinidad/skin">
<skin>
<id>sample.desktop</id>
<family>sample</family>
<render-kit-id>org.apache.myfaces.trinidad.desktop</render-kit-id>
<extends>fusion.desktop</extends>
<style-sheet-name>skins/sample/sample.css</style-sheet-name>
</skin>
</skins>

4. Managed Bean代码
public void startButton_actionListener(ActionEvent actionEvent) {
startPoll(actionEvent);
}

public void stopButton_actionListener(ActionEvent actionEvent) {
stopPoll(actionEvent);
}

public void progressIndicator_actionListener(ActionEvent actionEvent) {
stopPoll(actionEvent);
}

public void startPoll(ActionEvent actionEvent) {
// pollComponent.setRendered(true);
pollComponent.setInterval(1000);
//AdfFacesContext.getCurrentInstance().addPartialTarget(pollComponent);
myProgressRangeModel2.start(actionEvent);
}

public void stopPoll(ActionEvent actionEvent) {
//pollComponent.setRendered(false);
pollComponent.setInterval(-1);
//AdfFacesContext.getCurrentInstance().addPartialTarget(pollComponent);
myProgressRangeModel2.stop(actionEvent);
}

5. MyProgressRangeModel2.java代码
其最核心的是内部类:ProgressSimulator,该类模拟一个比较耗时业务服务操作。
该类支持多线程,这样就可以运行在另一个线程中,不用等待Managed Bean的方法返回。
这是一个很好的设计:让耗时的业务服务单独启用一个线程,并且使用进度条对用户友好提示。

//Simulate a business service progress
class ProgressSimulator implements Runnable {
public void run() {
try {
//stop fag is true if it is set to true or if value is equals or greater than maximum
stopFlag = stopFlag == true ? true : (getValue() < getMaximum() ? false : true);
//run in loop until stop condition is met. Make sure system doesn't
//fail if values are initially set to the same value
while (!stopFlag && getValue() != getMaximum()) {
Thread.sleep(1000);
setValue(getValue() + stepPace);
//set the color boundaries
if (getValue() >= greenBoundary) {
styleClass = GREEN;
} else if (getValue() >= yellowBoundary) {
styleClass = YELLOW;
} else {
styleClass = RED;
}

if (getValue() == getMaximum()) {
stopFlag = true;
}
}
//stop thread
newProgress.interrupt();
newProgress = null;
} catch (Exception exc) {
exc.printStackTrace();
}
}
}

6. 运行页面






Project下载:MyProgressIndicator2.7z

参考文献:
1. http://www.oracle.com/technetwork/developer-tools/adf/learnmore/42-progressbarcolor-169184.pdf
2. http://forums.oracle.com/forums/thread.jspa?threadID=999826

ADF_104:ProgressIndicator组件使用指南之一:与Poll组件配合使用

开发环境:JDeveloper 11.1.2.0.0。
说明:本文改自本人旧作,使用了目前最新的JDeveloper 11.1.2.0.0重新开发验证。(2011-8-4)

1. 页面代码
原理说明:
(1)Poll组件每隔interval(单位毫秒,-1表示停止)指定时间,调用pollListener方法一次。
(2)ProgressIndicator组件的partialTriggers指向Poll组件,这样pollListener每执行一次,ProgressIndicator组件就刷新一次。从视觉上看,就好像进度条在前进一样。
(3)Poll进度条组件必须使用viewScope的Managed Bean。
(4)注意ProgressIndicator组件的visible属性使用了EL动态设置。

<af:form id="f1">
<af:panelGroupLayout id="pgl1" layout="horizontal">
<af:commandButton text="Start Poll" id="cb1" partialSubmit="true"
actionListener="#{viewScope.myBackingBean.startButton_actionListener}"/>
<af:separator id="s1"/>
<af:commandButton text="Stop Poll" id="cb2" partialSubmit="true"
actionListener="#{viewScope.myBackingBean.stopButton_actionListener}"/>
</af:panelGroupLayout>
<af:poll id="pol1" interval="-1" pollListener="#{viewScope.myBackingBean.poll_listener}"
partialTriggers="cb1 cb2" binding="#{viewScope.myBackingBean.pollComponent}"/>
<af:progressIndicator id="pi1" partialTriggers="pol1"
value="#{viewScope.myBackingBean.myProgressRangeModel}" rendered="true"
visible="#{viewScope.myBackingBean.myProgressRangeModel.value > 0 and viewScope.myBackingBean.myProgressRangeModel.value <viewScope.myBackingBean.myProgressRangeModel.maximum }"/>
</af:form>

2. Managed Bean代码
public void poll_listener(PollEvent pollEvent) {
long value = myProgressRangeModel.getValue();
long count = value + stepPace;
if (count <= maxValue) {
myProgressRangeModel.setValue(count);
} else {
stopPoll();
}
}

public void startButton_actionListener(ActionEvent actionEvent) {
startPoll();
}

public void stopButton_actionListener(ActionEvent actionEvent) {
stopPoll();
}

public void startPoll() {
pollComponent.setInterval(1000);
AdfFacesContext.getCurrentInstance().addPartialTarget(pollComponent);
}

public void stopPoll() {
pollComponent.setInterval(-1);
AdfFacesContext.getCurrentInstance().addPartialTarget(pollComponent);
}

3. 运行页面


Project下载:MyProgressIndicator.7z

参考文献:
1. http://groundside.com/blog/DuncanMills.php?title=the_progress_indicator&more=1&c=1&tb=1&pb=1
2. http://gergerconsulting.blogspot.com/2007/04/adf-faces-progressindicator-example-for.html
3. http://www.gebs.ro/blog/oracle/adf-progress-indicator-in-fusion-middleware-11g/
4. http://jobinesh.blogspot.com/2010/08/example.html
5. http://hi.baidu.com/ganyu21/blog/item/a8baf31f07ac09f5e0fe0bbd.html

2011年8月16日星期二

Tips_011:使用Camtasia录制PPT时的分辨率设置问题

Camtasia录制PPT的操作非常简单,请参考《使用Camtasia为视频配置中文解说:Import media方式》。
这里主要说明一下分辨率设置问题。

我建议首先把机器的分辨率设置成1024*768,然后在此分辨率下录制。
录制时,注意选择全屏,否则录制后旁边会有一个滚动条,不好看。
机器设置成1024*768的好处是:
(1)能够满足绝大多数视频播放的需要,无论是放大或缩小,画面清晰度较好。
(2)生成的视频占据整个播放器的画面,是满屏,不需要缩放画面。
(3)生成的视频文件大小比较小,一个30分钟的PPT,大概60M。

我们可以比较一下,如果在1440*900的分辨率(我的笔记本的默认分辨率)下全屏录制30秒钟PPT:
默认 Camtasia 分辨率:1440*900,FLV 文件大小 2,315KB,播放时,不能占满整个屏幕,需要裁剪;
修改 Camtasia 分辨率:1024*768,FLV 文件大小 1,443KB,播放时,不能占满整个屏幕,需要裁剪。
而机器分辨率1024*768下全屏录制30秒钟PPT,默认 Camtasia 分辨率:1024*768,FLV 文件大小 1,849KB。
虽然比修改 Camtasia 分辨率方式略大,但是不需要裁剪画面,而且清晰度要高。

Tips_010:使用Camtasia为视频配置中文解说:直接录屏方式

看了上一篇文章,我猜有人一定会问:为何不使用Camtasia的Record Screen功能直接录屏,同时配上中文解说?
从原理上完全说得通,用静音方式播放英语视频,然后直接录屏,同时配上解说,岂不一次就解决问题。
哪里需要前面其它软件的配合。
唉,这其中是有苦衷的,我也是实验了很长时间,其实我一开始就是这样做的。
但这样生成出来的文件超大,即使我降低每秒记录的帧数到5帧,一个原本30分钟、60M的英文视频,按照此法生成出来的中文视频有600M————足足有10倍啊!

一开始,我觉得是我使用的问题,在咨询了Camtasia的客服人员后,发现我的使用是对的。
当然,中间做了无数的实验,每一次都是熬人的等待。
难道是Camtasia这个软件的问题,在没找到上面的解决方案时,我很绝望的搜遍了Camtasia指南的各个角落。
后来,我想明白了,还真不赖人家Camtasia,因为录屏时,屏幕上每一次微小的变化,都会被记录,也就是被连续拍照。
而如果使用真实人工操作界面,Camtasia的拍照次数会明显少很多,一是人的动作毕竟慢,二是为了让人看清楚,动作会比平时更慢。
我做了一个实验,在JDeveloper上操作并录制,和拷屏录制JDeveloper操作视频,二者文件大小近3倍。

下面我比较了一下两种方式生成文件的大小:
条件:设置机器分辨率:1024*768,设置Camtasia分辨率:1024*768
录制30秒钟的PPT + 26,204KB的AVI文件
最终合成产生的FLV文件大小:
(1)Import media 方式:121,607KB < 原始视频的5倍。
(2)直接录屏 方式: 136,300KB > 原始视频的5倍。
二者的视频清晰度差不多,所以,还是使用Import media 方式比较好。

2011年8月14日星期日

Tips_009:使用Camtasia为视频配置中文解说:Import media方式

最近在为虚拟开发者活动录制视频讲座,内容既有PPT介绍,也有演示环节。
演示环节如果去搭建环境将会非常费时间,而且我当时的情况是无法拿到的源代码。
即使能够拿到源代码,由于每个人操作习惯不同、软件版本的微小差异,这些都会导致演示内容无法和其他国家的保持一致。
因此,只有一个办法:使用已经录制好的英语视频,去掉英语音频,配上中文音频。
我使用的Camtasia6.0.2,本来以为很简单的事,没想到前后足足花了我近5天时间,呵呵。
没办法,中间走了好多弯路,做了好多实验,而每一次视频文件的生成,都比较耗时。
好,废话少说,以下就是我的重要操作步骤,都是吐血得来的经验值啊!

1. 录制PPT内容
Camtasia6.0.2录制纯PPT内容还是非常简单和方便的,安装好后,打开PowerPoint,发现多了一个菜单项:加载项。
点击录制按钮,右键选择全屏,就可以开始录了。

为了让最终生成的视频文件尽可能的小,请遵从以下建议。
(1)如果不需要鼠标操作,请去掉记录鼠标。(我录制前忘了去掉,呵呵。)

(2)如果需要鼠标操作,请不要快速移动鼠标,每次操作至少有明显停顿,1秒左右。
这样做除了让大家看清楚你的动作外,更重要的原因是,如果鼠标移动太快的话,Camtasia会认为画面有改变,会连续拍照记录。
快照多了,自然视频文件就大了。所谓视频就是一连串的静态图片快速播放,这个道理都懂吧。
(3)最重要的一点,请把屏幕分辨率调整到视频最佳播放需要的分辨率,比如:视频在1024*768的分辨率下观看最佳,那就把你机器的分辨率调整到此值,
因为笔记本的分辨率通常高于此值,这样生成出来的视频屏幕没有最大化,需要调整,而且最终的文件也会很大。
我一开始就是吃了这个亏,机器转了半天,生成了一个2G的视频文件,我晕!

PPT录制完成之后,会生成一个Camtasia会为每个录制视频生成一个.camrec文件,请保存好该文件,后面会用到。
多个.camrec文件,可以加入到一个项目中,Camtasia会为每个项目生成一个.camproj文件,最终合并这些视频资料,生成最终的视频文件。
提示:
(1)如果没有生成.camproj项目文件,你可以双击.camrec文件,Camtasia打开后,再关闭,它会提示你保存为.camproj项目文件。
(2)双击打开.camproj项目文件后,你就可以继续录制新的视频,或者添加已有的视频文件,最终合并这些视频资料,生成最终的视频文件。
(3)Camtasia6.0.2支持添加AVI文件,但不支持FLV文件。

2. 把FLV格式视频转换成AVI格式视频
我这里拿到的是FLV格式的视频,因为FLV比AVI相对来说轻量级一些,适合在网上播放。
但因为Camtasia6.0.2不支持FLV文件,所以要先用转换软件一下,我这里使用的是格式工厂软件:http://www.pcfreetime.com/CN_download.html。
该软件使用非常简单,我这里就不作说明了。


3. 把AVI视频(不含音频)剪切成需要的长度
因为老外录制的视频是包括前面PPT部分的,因此要剪切掉。
我使用的是中文PPT,并且已经在第1步录制好了。
我这里使用的软件是AVI MPEG RM WMV Splitter,一款非常好用和小巧的切割视频软件:http://www.boilsoft.com/。
注意,第2步转换在这里也是非常必要的,因为该软件不支持切割FLV文件。
该软件使用非常简单,我这里就不作说明了。


4. 把AVI视频的音频和视频(不含音频)两部分
转换后的AVI格式视频是英语解说的,因此要把它剥离出去。
我这里使用的软件是Machete,一款非常好用和小巧的音视频剥离软件:http://www.machetesoft.com/。
该软件使用非常简单,我这里就不作说明了。


5. 把切割好的AVI视频(不含音频)导入到Camtasia项目中
现在我们可以导入第4步完成的视频到Camtasia项目中了。

6. 录制演示部分的中文音频
这里有两个方法:
(1)在Camtasia中新建一个项目,录制演示视频,但Produce导出的时候,选择只生成音频文件。

(2)如果已经有了一个录制好的视频文件(中文解说的),可以使用第3步的方法剥离出音频文件。

7. 把中文解说的音频导入到Camtasia项目中,编辑调整视频和音频的Timeline
好了,至此所有的资料都准备好了,下面是最重要的细活:调整Timeline,把各个资料严格放在它们应该出现的位置。
见过钢琴调律师吗,感觉差不多,操纵鼠标的手要毫米级的挪动,相当费神的活儿,呵呵。
不知道为何Camtasia不设置手动输入Timeline的位置,鼠标稍一拖动就是2秒啊,只有输入才是最精确的啊。
下面就是Camtasia项目中最终的合成图。


8. 选择“Produce Video as...",合成生成最终的视频。
经过实验,我发现每秒5帧就可以支持比较好的演示效果。
每个视频可能要求不一样,动画比较多的,要求每秒的帧数也高。


以下是我使用Import media方式的一些经验总结:
问题1:如何选择生成视频的分辨率?
生成视频的分辨率可以在这里修改:

但是改成多少合适呢,主要是跟Import进来的AVI视频分辨率保持一致,比如我这里就是1024*768。

所以,如果有PPT解说的部分,也要按照这个分辨率录制。
也就是说,机器的分辨率要调到1024*768下全屏录制PPT,这样画面会满屏,不用再调整缩放。
同时,Camtasia的分辨率也要设置到1024*768,然后生成最终的合成视频。
这样所有录制的部分都是在一个分辨率下的。

问题2:如何剪切音频?
有时,我们需要把一段音频剪成我们需要的长度,配合该长度的视频。
一开始,我想找一些MP3 Cutter的工具,后来发现其实不用。
我们可以组合一下上面的工具就可以了:
先用AVI Cutter把视频剪切成需要的长度,再用音视频分离软件剥离出音频就可以了。

2011年8月7日星期日

ADF_103:何时应该Enable Commit按钮?如何Enable?

Commit按钮的disabled默认属性值为"#{!bindings.Commit.enabled}",注意这里有个叹号,表示非bindings.Commit.enabled。
即如果bindings.Commit.enabled=true,表示Commit按钮可以使用,这时disable=false;反之,bindings.Commit.enabled=false,表示Commit按钮不可以使用,这时disable=true。
disabled属性可以更加精准地控制Commit按钮的行为:只有当需要使用Commit按钮才使用。因为Commit操作往往需要保存数据到数据库,操作相对费时,因此最好不要让用户随便点击。

需要Enable时才Enable,其判断的依据就是表单数据是否已经更改。如果用户打开一个可编辑表单只是看看,不做任何修改,这时就不应该允许用户点击 Commit按钮。

实际应用中,在做其它操作时,我们也需要判断数据是否已经更改,比如:用户修改了某个表单,没有提交,然后直接转到其它页,这时我们应该提醒它数据已经修改,是否保存?
也就是说,我们需要在Manage Bean中判断bindings.Commit.enabled的值,方法如下:

public boolean isCommitEnabled() {
Boolean commitState = (Boolean)JSFUtils.getManagedBeanValue("bindings.Commit.enabled");
boolean commitEnabled = commitState != null ? commitState.booleanValue() : false;
return commitEnabled;
}

在看过一些老外的例子后,发现也有这样判断断数据是否已经更改的,感觉这个判断更为底层,因为是在AM上判断的,是否只要AM上的VO发生了改变,都可以用这个方法监测到?
我目前也太清楚,但做了个简单的实验后,发现这两种判断结果是一致的。
public boolean isDirty()
ApplicationModule am = ADFUtils.getDCBindingContainer().getDataControl().getApplicationModule();
return am.getTransaction().isDirty();
}

在使用ADF Commit按钮时,还有一个常见问题:当用户修改某一个表单项后,即使焦点转移后,Commit按钮也不会被Enable,这是为啥呢?
经过实验,需要增加两个参数:
(1)增加autoSubmit="true" 。
(2)刷新Commit按钮。
方法一:通过增加valueChangeListener="#backingBeanScope.backing_main.onValueChange}"
public void onValueChange(ValueChangeEvent valueChangeEvent) {
if (this.getCb1().isDisabled()) {
AdfFacesContext.getCurrentInstance().addPartialTarget(this.getCb1());
}
}
方法二:设定Commit按钮的PPR,指向该表单项,这样当该表单项变动后,会局部刷新Commit按钮。

一个矛盾而有意思的现象是:虽然这时Commit按钮被Enable了,但bindings.Commit.enabled却等于fasle。按照这个值,disable="#{!bindings.Commit.enabled}"应该等于true。
留待以后查证吧。

参考文献:
1. http://andrejusb.blogspot.com/2010/01/auto-commit-use-case-in-oracle-adf-11g.html
2. http://blogs.oracle.com/smuenchadf/examples/138
3. http://forums.oracle.com/forums/thread.jspa?threadID=833597
4. http://forums.oracle.com/forums/thread.jspa?threadID=597254

ADF_102:AutoSubmit与PartialSubmit区别

Web2.0应用的两大特点就是异步请求响应和局部页面刷新。
比如:一个天气预报页面内容包括:国家下拉列表、省下拉列表、城市下拉列表,以及选择城市的最近三天天气情况。
当我们选择国家后,只刷新省的下拉列表;选择省后,只刷新城市的下拉列表;选择城市时,刷新最近三天天气情况。

总之,我们希望页面能够局部、动态地刷新,请求发送后不必等待响应就能马上做其它事情,而响应回来后,页面相关组件会得到通知,然后自动更新。
AutoSubmit、PartialSubmit、PartialTriggers这三个参数的主要作用就是完成异步请求响应和局部页面刷新。
一般人对PartialTriggers的作用比较清楚,但AutoSubmit和PartialSubmit的区别就不太清楚了。

1. autoSubmit 只在输入组件上才有的一个属性。
例如:< af:inputText id="productpriceIT" label="Price" autoSubmit="true" value="#{advertisement.price}" >

当设置为true后,改变该组件的值后(焦点离开后),该组件的值(注意,不是整个form)将被提交到服务器,即进入【转换、验证、更新模型、重新呈现】的各个生命周期阶段(注意先后次序)。
表单中的其它组件不受任何影响,表单也并未提交,只是该组件被异步地提交了。这意味着,即使表单中还有其它必填项,也不会提示“该项不能为空”,因为没有触发到这些必填项。
如果在Manage Bean中更新了该组件的值,必须要刷新该组件才能显示新值,即进入【重新呈现】阶段。刷新该组件有两种方法:
(1)在页面中设置autoSubmit + partialTriggers 刷新(这里指向自己)
< af:inputText id="productpriceIT" label="Price" autoSubmit="true" value="#{advertisement.price}" partialTriggers="productpriceIT">
(2)在Manage Bean 中用代码刷新
AdfFacesContext.getCurrentInstance().addPartialTarget(this.productpriceIT);

2. partialSubmit 只在命令组件上才有的一个属性。
例如:< af:commandLink id="showImageCL" text="#{areaAndCategoryProvider.showAreaImage?'Hide image':'Show image'}"
actionListener="#{areaAndCategoryProvider.toggleImage}" partialSubmit="true" partialTriggers="showImageCL" />

默认情况下,单击命令链接或按钮将导致表单提交或页面导航,整个页面将被刷新和重载,其页面效果是页面好像抖动了一下,用户感受稍差一些。
这时,我们可以使用partialSubmit + partialTriggers,来刷新某个组件,其页面效果是只会刷新组件,而不会刷新整个页面。
当然,表单还是会被提交,如果表单中有必填项,将会验证出错,这一点和没有设置partialSubmit=true的按钮或链接一样,二者区别是:设置了partialSubmit=true的按钮或链接界面效果不会抖动,同时点击后会启动 ppr 请求,如果设置了partialTriggers 的话。

与autoSubmit的区别是,后者仅处理更改的组件本身以及在其 partialTriggers 属性中包含引用的所有组件,不会触发表单中的必填项的验证,除非这些必填项的partialTriggers 指向设置了autoSubmit=true的组件。

值得注意的是,CommandToolBarButton 默认partialSubmit =true,所以如果你需要刷新某个组件,必须设置partialTriggers,否则页面不会被刷新。
而CommandButton 默认partialSubmit =false,默认会刷新整个页面。
我曾经在编写一个页面时,点击CommandToolBarButton,怎么也不刷新某个table(我又不想使用partialTriggers来实现),整整花了我2个小时才找到原因,呵呵。

参考文献:
1. http://technology.amis.nl/blog/4931/how-to-declaratively-instruct-adf-faces-rc-to-partially-refresh-other-components-upon-an-event
2. http://vtkrishn.wordpress.com/2010/05/06/difference-between-commandtoolbarbutton-and-commandbutton/
3. http://forums.oracle.com/forums/thread.jspa?threadID=984095

2011年8月6日星期六

ADF_101:如何在一个事务中插入一对多关系的多个EO对象?

开发环境:JDeveloper 11.1.2.2.0 + Oracle XE Database 10gR2。

考虑这种情况:数据库中Departments表和Employees表是一对多的关系,在Employee表中有DepartmentId字段,并且作为外键约束。
也就是说,插入一个Employee对象时,必须要先知道DepartmentId。
如果Department对象不存在,那么就先要插入Department对象,得到DepartmentId后,再插入Employee对象。
使用ADF-BC如何调整这两次插入操作先后顺序呢?如何保证两次插入操作在一个事务之中呢?

首先假定Departments表和Employees表的主键都是通过Sequence获取的。
如何在程序中获取Sequence的下一个值,请参考《ADF-BC中EO常用操作代码之三:增加EO(1)

重要步骤说明:

1. 在AM中增加方法:

public void newEmployeeForNewDepartment(String firstName, String departmentName) {
Date date = new Date(Date.getCurrentDate());
Timestamp today = new Timestamp(date);

// 1. Create a new employee
EmployeesImpl newEmployee = createNewEmployee();
// 2. Create a new department
DepartmentsImpl newDepartment = createNewDepartment();
newDepartment.setDepartmentName(departmentName);
// 3. Set the department id to which the employee pertains
newEmployee.setDepartmentId(newDepartment.getDepartmentId());
newEmployee.setFirstName(firstName);
newEmployee.setLastName("Test");
newEmployee.setEmail(today.toString());
newEmployee.setHireDate(today);
newEmployee.setJobId("AD_VP");

// 4. Commit the transaction
getDBTransaction().commit();
// 5. Construct a bean to hold new supplier id and product id

System.out.println(newEmployee.getEmployeeId());
System.out.println(newDepartment.getDepartmentId());
}

private EmployeesImpl createNewEmployee() {
EntityDefImpl productDef = EmployeesImpl.getDefinitionObject();
return (EmployeesImpl)productDef.createInstance2(getDBTransaction(), null);
}

private DepartmentsImpl createNewDepartment() {
EntityDefImpl supplierDef = DepartmentsImpl.getDefinitionObject();
return (DepartmentsImpl)supplierDef.createInstance2(getDBTransaction(), null);
}


2. 定制Employees EO(即一对多关系中“多”的一方),重写方法:postChanges

public void postChanges(TransactionEvent e) {
// If current entity is new or modified
if (getPostState() == STATUS_NEW || getPostState() == STATUS_MODIFIED) {
// Get the associated department for the employee
DepartmentsImpl deptEO = (DepartmentsImpl)getDepartments1();
// If there is an associated department
if (deptEO != null) {
// And if it's post-status is NEW
if (deptEO.getPostState() == STATUS_NEW) {
// Post the department before posting this entity
deptEO.postChanges(e);
}
}
}
super.postChanges(e);
}

说明:重写后的逻辑保证当插入或修改Employees EO时,首先插入与其关联的Department EO。
也就是说,虽然在newEmployeeForNewDepartment方法中先插入的是Employee,但实际上经过重postChanges后,先插入的是Department(即一对多关系中“一”的一方)。
这样就可以保证在插入Employee时,可以得到其关联的DepartmentId。

3. 运行AM测试


Project 下载:ADF_BC_EO_MasterDetail.7z

参考文献:
1. 《Fusion Developer's Guide for ADF》 之 Controlling Entity Posting Order to Avoid Constraint Violations

2011年8月5日星期五

ADF_100:我常去的一些ADF网站介绍

本文最后一次修改日期:2011-8-5。
  1. http://groups.google.com/group/adf-methodology?hl=en&pli=1
    ADF Enterprise Methodology Group
  2. http://www.oracle.com/technology/products/jdev/index.html
    Oracle JDeveloper官方网站,这里有所有有关JDeveloper &ADF的资料,包括Demo软件下载快速上手教程最佳实践论坛
  3. http://www.oracle.com/technetwork/developer-tools/adf/overview/index.html
    Oracle ADF 官方网站。
  4. http://www.oracle.com/technology/documentation/middleware.html
    Oracle middleware 文档官方网站,包括所有Oracle 中间件文档:安装手册、用户手册、开发指导、API Doc。
  5. http://www.oracle.com/technology/pub/articles/index.html
    http://www.oracle.com/technology/global/cn/pub/articles/index.html
    OTN上的技术文章(中英文对照)
  6. http://www.oracle.com/technology/products/jdev/tips/index.html
    http://www.oracle.com/technology/global/cn/products/jdev/tips/index.html
    JDeveloper "How to"系列(中英文对照)
  7. http://www.oracle.com/technology/products/adf/patterns/index.html
    ADF 设计模式
  8. http://www.oracle.com/technology/pub/articles/adf-from-design-to-reality/index.html
    http://www.oracle.com/technology/global/cn/pub/articles/adf-from-design-to-reality/index.html
    ADF 从设计到实施 (中英文对照)
  9. http://www.oracle.com/technology/products/jdev/tips/fnimphius/index.html
    ADF “代码角”
  10. http://www.oracle.com/technology/pub/articles/adf-development-essentials/index.html
    http://www.oracle.com/technology/global/cn/pub/articles/adf-development-essentials/index.html
    ADF 开发精要(中英文对照)
  11. https://www.samplecode.oracle.com/
    Oracle 代码例子,需要账号。
  12. http://blogs.oracle.com/smuenchadf/examples/
    Steve Muench 写的大量ADF例子。
  13. Frank Nimphius 写的大量ADF例子,没有找到总入口,以下为其精华帖:
    http://www.oracle.com/technology/products/jdev/tips/fnimphius/suggest/autosuggest.html 如何实现AutoSuggest功能
    http://www.oracle.com/technology/products/jdev/tips/fnimphius/clientListener/client-server-listener.html 如何使用client listener和server listener把前端参数传到后端
    http://www.oracle.com/technology/products/jdev/tips/fnimphius/valuestopopup/ValuesToPopup.html 提取页面内容,并显示在popup 窗口中
    http://www.oracle.com/technology/products/jdev/tips/fnimphius/js_disclose_tree_path.html 使用Javascript 展开和收缩树节点
    http://www.oracle.com/technology/products/jdev/tips/fnimphius/getTreeTableSelection/getTreeTableSelection.html 编写树节点select listener 事件
    http://www.oracle.com/technology/products/jdev/tips/fnimphius/increase_css_and_javascript_performance.html 提高Java script性能http://www.oracle.com/technology/products/jdev/tips/fnimphius/onevotreetable/index.html
    http://www.oracle.com/technology/products/jdev/tips/fnimphius/pojopagination/index.html
  14. http://www.avromroyfaderman.com/
    相当有深度的技术高手。
  15. http://technology.amis.nl/blog/tag/adf
    据说是新西兰的Oracle Partner写的有关ADF的博客,知识更新较快。
  16. http://andrejusb.blogspot.com/
    Andrejus Baranovskis的博客,主要是有关Oracle技术的。以下为其精华帖:
    http://andrejusb.blogspot.com/2010/03/applying-view-criteria-from-application.html
    http://andrejusb.blogspot.com/2008/11/adf-query-component-and-view-criteria.html
    http://andrejusb.blogspot.com/2009/11/tree-table-component-in-oracle-adf.html
    http://andrejusb.blogspot.com/2009/11/defining-lov-on-reference-attribute-in.html
    http://andrejusb.blogspot.com/2009/11/crud-operations-in-jdeveloperadf-11g-r1.html
  17. http://biemond.blogspot.com/
    Edwin Biemond的博客,内容质量不错。以下为其精华帖:
    http://biemond.blogspot.com/2009/12/adf-data-push-with-active-data-service.html 如何使用ADS
  18. http://one-size-doesnt-fit-all.blogspot.com/
  19. http://jobinesh.blogspot.com/
    Jobinesh,一个印度哥们写的ADF博客。
  20. http://oracleseeker.com/
    汉得的开发人员写的Oracle ADF开发技巧汇总,国内比较有名。
  21. http://eleven-china.blogspot.com/search/label/ADF
    汉得的开发人员徐翔轩的博客,虽然已经停用,但还是值得参考。
  22. http://geekerdever.blogspot.com/
    汉得的开发人员Derek.Jaa 写的博客。
  23. http://snipkingderek.spaces.live.com/blog/
    一个勤奋的技术写手,值得一看。
  24. http://www.oracle.com/technology/global/cn/pub/articles/jellema-muir-adf.htm
    l
    http://www.oracle.com/technology/global/cn/pub/articles/jellema-muir-adf-part2.html
    用ADF写的购物篮的例子(中英文对照)。
  25. CSDN上的一些博客
    http://blog.csdn.net/wkyb608/archive/2009/06/30/4308695.aspx
    http://blog.csdn.net/iammerryz/archive/2010/01/07/5142113.aspx
    http://blog.csdn.net/chuan122345/archive/2007/04/16/1566590.aspx
  26. http://maping930883.blogspot.com
    最后一个,顶一下自己吧,呵呵。



2011年8月1日星期一

ADF_097:如何处理BLOB类型数据之三:使用Servlet在页面上显示BLOB中的图片

实验环境:JDeveloper 11.1.2.0.0。
接上一个实验《如何处理BLOB类型数据之二:下载BLOB内容并保存到文件中》。

我的设计思想:
这其实是一个动态显示图片的问题,类似于很多网站上登录时要求输入的图片认证码。
一开始,根据前面的思路,我打算继续使用ADF BC的来显示BLOB中的图片内容,后来发现这个功能相对来说比较独立,于是就想写一个通用的,这样没有ADF也可以使用。
于是,我参考了《一个读取BLOB中图片的Servlet代码》,写了一个Servlet。

1. 新建页面:get_image_from_blob.jspx
<af:image source="/blobimageservlet?id=75" id="i5"/>
我这里只是测试Servlet是否工作正常,因此参数id给的是常量75,因为我上传的id=75的文件是一个.jpg。
你可以根据你的需要,把参数动态设置。

2. 创建Servlet:BlobImageServlet.java
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.OutputStream;

import java.io.PrintWriter;

import java.sql.Blob;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;

import java.sql.SQLException;

import javax.naming.Context;
import javax.naming.InitialContext;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import javax.sql.DataSource;

public class BlobImageServlet extends HttpServlet {
private static final String CONTENT_TYPE = "image/jpg; charset=utf-8";

public void init(ServletConfig config) throws ServletException {
super.init(config);
}

public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.setContentType(CONTENT_TYPE);
String idStr = request.getParameter("id");
int id = -1;
if (idStr != null) {
id = Integer.parseInt(idStr);
}

OutputStream os = response.getOutputStream();
Connection conn = null;

try {
Context ctx = new InitialContext();
DataSource ds = (DataSource)ctx.lookup("java:comp/env/jdbc/uploadFileToBlobConnDS");
conn = ds.getConnection();
PreparedStatement statement =
conn.prepareStatement("SELECT UploadedFiles.ID," +
" UploadedFiles.FILENAME, " +
" UploadedFiles.CONTENT " +
"FROM UPLOADED_FILES UploadedFiles " +
"WHERE UploadedFiles.ID = ?");

statement.setInt(1, new Integer(id));
ResultSet rs = statement.executeQuery();
if (rs.next()) {
Blob blob = rs.getBlob("Content");
BufferedInputStream in = new BufferedInputStream(blob.getBinaryStream());
int b;
byte[] buffer = new byte[10240];
while ((b = in.read(buffer, 0, 10240)) != -1) {
os.write(buffer, 0, b);
}
os.close();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (conn != null) {
conn.close();
}
} catch (SQLException sqle) {
System.out.println("SQLException error");
}
}
}
}

3. 运行,访问get_image_from_blob.jspx,显示BLOB中的图片内容
唉,斯人已逝,永远怀念的邓丽君......


4. 深入分析
使用Servlet来显示BLOB中的图片内容,可能会引起性能问题。因为每一个BLOB图片显示,都要占用一个数据库连接。
可以考虑修改Serverlt的逻辑如下:
如果在某个路径下没有某个图片,那么,再去访问数据库,获取图片,并生成在该路径下。
否则,直接访问已经生成的图片文件。
其中,为了保证图片名称的唯一性,图片名称包含时间戳。

5. 问题:如果使用ADF BC该如何做?
我认为在某些情况下,使用ADF BC还是比较方便的,比如一个天气预报表格中,某一列用小图片来表示不同的天气情况。
因为省去了连接数据库的代码,不会有性能问题。
而且可以非常方便的获取BlobDomain对象:row.Content。如果支持如下写法就更好了:
<af:image source="#{row.Content}" id="i5"/>
可惜这样写,是无法显示出图片的。
可以在Managed Bean中写一个方法来处理BlobDomain,放到image的source属性中。
这个以后有空在写吧。

Project 下载:UploadFileToBlob_DownloadBlobToFile_getBlobImage.7z

参考文献:
1. http://kr.forums.oracle.com/forums/thread.jspa?threadID=614954。

ADF_096:如何处理BLOB类型数据之二:下载BLOB内容并保存到文件中

实验环境:JDeveloper 11.1.2.0.0。
接上一个实验《如何处理BLOB类型数据之一:上传文件并保存到BLOB中》。

1. 修改页面代码,把Table中Filename字段改为CommandLink类型,这样点击文件名称即可下载该文件:
<af:commandLink text="#{row.Filename}" id="cl1" actionListener="#{myBackingBean.downloadLink_actionListener}"/>

2. 对应的Managed Bean代码
public void downloadLink_actionListener(ActionEvent actionEvent) {
FacesCtrlHierNodeBinding f = (FacesCtrlHierNodeBinding)this.richTable.getSelectedRowData();
Row row = f.getRow();
BlobDomain fileContent = (BlobDomain)row.getAttribute("Content");
String fileName = row.getAttribute("Filename").toString();
String fileType = "aplication/octet-stream";
if (fileName.endsWith(".pdf")) {
fileType = "application/PDF";
} else if (fileName.endsWith(".doc")) {
fileType = "aplication/msword";
} else if (fileName.endsWith(".txt")) {
fileType = "text/plain";
} else if (fileName.endsWith(".ppt")) {
fileType = "application/vnd.ms-powerpoint";
} else if (fileName.endsWith(".rar")) {
fileType = "aplication/octet-stream";
} else if (fileName.endsWith(".zip")) {
fileType = "aplication/zip";
} else if (fileName.endsWith(".jpg")) {
fileType = "aplication/jpg";
} else {
fileType = "aplication/octet-stream";
}
FacesContext facesContext = FacesContext.getCurrentInstance();
ExternalContext extContext = facesContext.getExternalContext();
Long length = fileContent.getLength();

HttpServletResponse response = (HttpServletResponse)extContext.getResponse();
response.setHeader("Content-Disposition", "attachment;filename=\"" + fileName + "\"");
response.setContentLength((int)length.intValue());

response.setBufferSize(1024);
response.setContentType(fileType);

try {
writeBlobDomainToOutputStream(fileContent,response.getOutputStream());
} catch (Exception e) {
e.printStackTrace();
}

facesContext.responseComplete();
}

3. 运行,点击文件名,出现下载提示。


Project 下载:UploadFileToBlob_DownloadBlobToFile.7z

问题1:重复下载同一文件时,会报出异常:java.net.ProtocolException: Didn't meet stated Content-Length, wrote: '0' bytes instead of stated: '364' bytes。
经过跟踪调试,发现是while ((bytesRead = in.read(buffer, 0, 8192)) != -1) 循环并没有进入。
感觉像是inputStream没有关闭,读指针还停留在上一次结束的位置,即文件尾。
因此,再次读取同一文件时,in.read() == -1,直接返回了。
解决办法: 增加blobDomain.closeInputStream();。
由于BlobDomain对象比较特殊,in.close();并没有关闭BlobDomain对象输入流,必须使用BlobDomain自身的方法closeInputStream()来关闭。
Project已被更新,下载文件地址和名称不变。