jface databinding(数据挷定)中的数据转换(IConverter)和数据验证(IValidator )

时间:2021-03-04 22:22:17

前几天在做对话框界面过程中,对行文本框中的输入数值需要进行验证,于是对Text组件做了扩展,做了一个可以验证输入的字符串是否为数值的NumText组件,参见 《java SWT:限制数值输入的Text文本框通用组件》,但是在使用过程中发现,这种方式有缺陷,就是如果用户输入非法的字符,结果就是输不进去,界面上不会有报错也不会有任何提示,用户体验不好。
之前对databinding有过简单的了解,知道它可以实现UI组件和用户数据之前的同步更新以及数据类型转换和验证,但是觉着它太复杂,我的应用似乎用不上,所以一直没有进一步深入了解。发现自己设计的NumText组件用户体验存在问题后,才下决心对jface databinding做深入的了解。

WindowBuilder下数据绑定操作

如果你已经知道怎么进行数据绑定可以跳过本节

之前看过一些关于jface databinding方面的文章,文章都很长,代码好多,感觉好复杂的样子,一下子把我吓住了,尼玛,这jface databinding本是要简化代码设计的,要是需要写更多更复杂代码,还不累死人呀。
后来发现,在WindowBuilder下数据绑定操作已经可以像UI设计一样在UI界面下点点鼠标就能生成代码了,大大降低了学习门槛,简化了代码编写工作量。
使用WindowBuilder对进行数据绑定(databinding)比较直观方便,可以帮助我们自动生成一些必要的代码。
我们以一个Text文本框为例,来说明如果将一个文本框的内容与一个POJO对象中的属性进行绑定。
如下图,一个简单对话框中有一个Text文本框,
jface databinding(数据挷定)中的数据转换(IConverter)和数据验证(IValidator )
鼠标右键点击文本框,选择绑定功能(Bindings),然后选择Text的text属性,也就是保存Text文本框文本内容的属性。
jface databinding(数据挷定)中的数据转换(IConverter)和数据验证(IValidator )
然后会出现这样的界面,让我们选择要绑定的数据对象,在本例中数据对象的类型是TestBinding.Configurtion,变量名是editorConfig,要绑定的属性是globalAspectRatio(看这个变量名,你应该能猜到这是个浮点型数据,这就引出了后面的数据类型转换)
jface databinding(数据挷定)中的数据转换(IConverter)和数据验证(IValidator )
然后会显示绑定的细节属性,这里我们都使用默认值,所以点击确定就好了
jface databinding(数据挷定)中的数据转换(IConverter)和数据验证(IValidator )

这样一个简单的数据绑定就完成了。

数据转换和数据验证

如果数据挷定的两个对象属性的类型是一样,那上面的工作就算完成了。
但如果类型不同,就需要涉及到数据类型转换和数据验证的技术了。
jface databinding提供了两个基本的接口用于数据类型转换和数据验证
分别是IConverter和IValidator。
对于基本的数据类型转换,jface已经提供了IConverter接口的实现:

StringToNumberConverter类用于将String转换成数值(Float,Double,Integer,Long,BigDecimal…)
NumberToStringConverter类用于将数值转换成String

而IValidator则需要根据实际需求自己来写。
下面的代码实现了Float类型的属性与Text组件之间的数据绑定,这其中用到了StringToNumberConverter进行数据类型转换。同时用IValidator实现数据的合法性验证。
TestBinding.java

package testwb;

import org.eclipse.jface.dialogs.Dialog;
import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.SWT;
import org.eclipse.swt.widgets.Text;

import org.eclipse.core.databinding.Binding;
import org.eclipse.core.databinding.DataBindingContext;
import org.eclipse.core.databinding.UpdateValueStrategy;
import org.eclipse.core.databinding.observable.value.IObservableValue;
import org.eclipse.core.databinding.validation.IValidator;
import org.eclipse.core.databinding.validation.ValidationStatus;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.jface.databinding.swt.WidgetProperties;
import org.eclipse.core.databinding.beans.PojoProperties;
import org.eclipse.core.databinding.conversion.StringToNumberConverter;
import org.eclipse.swt.widgets.Display;
import org.eclipse.core.databinding.observable.Realm;
import org.eclipse.jface.databinding.fieldassist.ControlDecorationSupport;
import org.eclipse.jface.databinding.swt.DisplayRealm;

public class TestBinding extends Dialog {
    /** * 数据对象定义 * @author guyadong * */
    public class Configuration {
        private Float globalAspectRatio;

        public Float getGlobalAspectRatio() {
            return globalAspectRatio;
        }

        public void setGlobalAspectRatio(Float globalAspectRatio) {
            this.globalAspectRatio = globalAspectRatio;
            System.out.printf("updated %f\n",globalAspectRatio);
        }

        public Configuration(Float globalAspectRatio) {
            super();
            this.globalAspectRatio = globalAspectRatio;
        }
    }
    private DataBindingContext m_bindingContext;
    /** * 成员变量:数据对象 */
    protected Configuration editorConfig=new Configuration(0.5f);
    /** * Text组件 */
    private Text globalAspectRatioValue;

    /** * Create the dialog. * @param parentShell */
    public TestBinding(Shell parentShell) {
        super(parentShell);
    }

    /** * Create contents of the dialog. * @param parent */
    @Override
    protected Control createDialogArea(Composite parent) {
        Composite container = (Composite) super.createDialogArea(parent);
        container.setLayout(null);
        globalAspectRatioValue = new Text(container, SWT.BORDER);
        globalAspectRatioValue.setBounds(38, 28, 73, 23);

        return container;
    }

    /** * Create contents of the button bar. * @param parent */
    @Override
    protected void createButtonsForButtonBar(Composite parent) {
        createButton(parent, IDialogConstants.OK_ID, IDialogConstants.OK_LABEL, true);
        createButton(parent, IDialogConstants.CANCEL_ID, IDialogConstants.CANCEL_LABEL, false);
        m_bindingContext = initDataBindings();
    }

    /** * Return the initial size of the dialog. */
    @Override
    protected Point getInitialSize() {
        return new Point(362, 298);
    }
    @Override
    protected void configureShell(Shell newShell) {
        newShell.setText("设置");  
        super.configureShell(newShell);
    }
    public static void main(String[] args) {
        Display display = Display.getDefault();
        Realm.runWithDefault(DisplayRealm.getRealm(display), new Runnable() {
            public void run() {
                try {
                    TestBinding setting = new TestBinding(null);
                    setting.open();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
    }

    protected DataBindingContext initDataBindings() {
        DataBindingContext bindingContext = new DataBindingContext();
        IObservableValue observeTextGlobalAspectRatioValueObserveWidget = WidgetProperties.text(SWT.Modify).observe(globalAspectRatioValue);
        IObservableValue globalAspectRatioEditorConfigObserveValue = PojoProperties.value("globalAspectRatio").observe(editorConfig);
        // 创建String 到 Float的转换器
        StringToNumberConverter converter = StringToNumberConverter.toFloat(false);
        // 更新策略对象(Text内容改变时更新)
        UpdateValueStrategy updateStrategy = new UpdateValueStrategy(UpdateValueStrategy.POLICY_UPDATE).setConverter(converter);
        // 设置Set方法验证器(Set方法修改Text文本内容之前验证)
        // Lambda表达式实现,验证失败返回错误信息
        updateStrategy.setBeforeSetValidator((value) -> {
                // 这里value的类型是数据对象的属性类型 (Float)
                if ((Float)value<100) {
                    return ValidationStatus.ok();
                }
                return ValidationStatus.error("globalAspectRatio must <100");
        });
        // 设置Get方法验证器(Get方法获取Text文本内容之后验证)
        // 传统匿名类实现,验证失败返回错误信息
        updateStrategy.setAfterGetValidator(new IValidator() {
            @Override
            public IStatus validate(Object value) {         
                // 这里value的类型是Text对象的text属性类型(String) 
                boolean ok=false;
                try{
                    Float s = (Float) converter.convert(value);
                    if (s<100) {
                        ok=true;
                        return ValidationStatus.ok();
                    }
                    return ValidationStatus.error("globalAspectRatio must <100");
                }catch(Exception e){
                    return ValidationStatus.error("无效数字");
                }finally{
                    // 数据无效时 disable OK按钮
                    getButton(IDialogConstants.OK_ID).setEnabled(ok);
                }}
        });
        // 调用bindValue完成Text.text到数据对象的globalAspectRatio属性的绑定
        Binding bindValue = bindingContext.bindValue(observeTextGlobalAspectRatioValueObserveWidget, globalAspectRatioEditorConfigObserveValue, 
                updateStrategy,
                null);
        // 创建错误提示组件,当验证失败时显示提示信息
        ControlDecorationSupport.create(bindValue, SWT.TOP | SWT.LEFT);
        return bindingContext;
    }
}

如下图运行程序,当输入无效字符时,Text左上角会显示出错的红X,鼠标移动到X上会显示我们的验证器(IValidator)返回的错误信息。
这是由这行代码ControlDecorationSupport.create(bindValue, SWT.TOP | SWT.LEFT);创建的ControlDecorationSupport对象实现的

jface databinding(数据挷定)中的数据转换(IConverter)和数据验证(IValidator )
jface databinding(数据挷定)中的数据转换(IConverter)和数据验证(IValidator )

当验证失败,数据对象的绑定属性不会被更新。

说明:
本例中只是实现了Text组件向Configuration类的Float类型属性的单向数据同步。事实上jface databinding可以实现双向数据同步。

参考资料:

《AJFace Data Binding - Tutorial》
《JFace Data Binding》