Flex ComboBox中的空值问题

3

如何创建一个ComboBox,使用户可以选择null

如果你只是在dataprovider中添加null来创建ComboBox,那么null的值会出现,但用户无法选择它:

<mx:ComboBox id="cb" dataProvider="{[null, 'foo', 'bar']}" />

有没有办法使其可以选择null?

一个解决方法是在dataProvider中添加一个代表null的非null对象,并且每次访问combobox时将null和该对象映射。但这不是一种优雅的解决方案;你必须始终记住所有访问“可为空”combobox的代码中都要使用这个映射...

编辑:进一步说明为什么我不喜欢这个解决方案: 当然,它可以在子类中完成,但是要么我引入新的属性(如nullableSelectedItem),但是那么就必须小心地始终使用这些属性。要么就覆盖ComboBoxes selectedItem;但我担心这会破坏基类:它可能不喜欢来自内部的关于当前所选项目的想法的变化。即使这个脆弱的hack起作用了,在selectedItemdataProvider之上,还需要特殊处理这个nullItem,以便在渲染器的datalistDatalabelFunction中,以及可能仍然在ComboBox发送的事件中公开它... 它可能有效,但仅仅为了解决用户点击该项时不激活的问题,这是相当麻烦的。(另一种选择是将一个ui组件委托给一个ComboBox,但这样做只是为了避免这个小问题而需要更多的代码)


你可以创建一个 ComboBox 的子类,仅封装这种空值行为。 - Simon
Spark ComboBox 有什么想法吗? - Bruno Medeiros
5个回答

5

似乎唯一正确地管理空项的方法是实际向组合框的数据提供程序添加一个项目。以下子类将自动处理此操作。

为了支持对数据提供程序的更改,例如项目添加/删除以及完全重新分配数据提供程序本身(只需考虑数组集合绑定到远程服务的响应),实现有点棘手。

package {

    import flash.events.Event;
    import flash.events.FocusEvent;
    import mx.collections.ArrayCollection;
    import mx.collections.ICollectionView;
    import mx.collections.IList;
    import mx.containers.FormItem;
    import mx.controls.ComboBox;
    import mx.events.CollectionEvent;
    import mx.events.ListEvent;
    import mx.validators.Validator;

    public class EmptyItemComboBox extends ComboBox {

        protected var _emptyItem:Object = null;
        protected var _originalDataProvider:ICollectionView = null;

        public function EmptyItemComboBox() {
            super();
            addEmptyItem();
            addEventListener(Event.CHANGE, onChange);
        }

        private function onChange(event:Event):void {
            dispatchEvent(new Event("isEmptySelectedChanged"));
        }

        [Bindable]
        public function get emptyItem():Object {
            return _emptyItem;
        }

        public function set emptyItem(value:Object):void {
            if (_emptyItem != value) {
                clearEmptyItem();
                _emptyItem = value;
                addEmptyItem();
            }
        }

        [Bindable(event="isEmptySelectedChanged")]
        public function get isEmptySelected():Boolean {
                return (selectedItem == null || (_emptyItem != null && selectedItem == _emptyItem));
        }

        override public function set selectedItem(value:Object):void {
            if (value == null && emptyItem != null) {
                super.selectedItem = emptyItem;
            } else    if (value != selectedItem) {
                super.selectedItem = value;
            }
            dispatchEvent(new Event("isEmptySelectedChanged"));
        }

        override public function set dataProvider(value:Object):void {
            if (_originalDataProvider != null) {
                _originalDataProvider.removeEventListener(
                        CollectionEvent.COLLECTION_CHANGE,
                        onOriginalCollectionChange);
            }
            super.dataProvider = value;
            _originalDataProvider = (dataProvider as ICollectionView);
            _originalDataProvider.addEventListener(
                    CollectionEvent.COLLECTION_CHANGE,
                    onOriginalCollectionChange)
            addEmptyItem();
        }

        private function clearEmptyItem():void {
            if (emptyItem != null && dataProvider != null 
                    && dataProvider is IList) {
                var dp:IList = dataProvider as IList;
                var idx:int = dp.getItemIndex(_emptyItem);
                if (idx >=0) {
                    dp.removeItemAt(idx);    
                }
            }
            dispatchEvent(new Event("isEmptySelectedChanged"));
        }

        private function addEmptyItem():void {
            if (emptyItem != null) {
                 if (dataProvider != null && dataProvider is IList) {
                    var dp:IList = dataProvider as IList;
                    var idx:int = dp.getItemIndex(_emptyItem);
                    if (idx == -1) {
                        var newDp:ArrayCollection = new ArrayCollection(dp.toArray().concat());
                        newDp.addItemAt(_emptyItem, 0);
                        super.dataProvider = newDp;
                    }
                }
            }
            dispatchEvent(new Event("isEmptySelectedChanged"));
        }

        private function onOriginalCollectionChange(event:CollectionEvent):void {
            if (_emptyItem != null) {
                dataProvider = _originalDataProvider;
                addEmptyItem();
            }
        }
    }
}

关于该类的一些说明:

  • 它会自动将空对象插入到列表中...这是我的场景强烈要求的:数据提供者由远程服务返回,它们不能包含额外的元素来支持Flex UI,我也不能手动监视每个集合以在每个集合刷新时创建空项目。

  • 由于它必须使用集合的内容,它将在内部创建一个原始副本,其中包含相同的项实例和空项,因此原始集合的实例不会被触及,可以在其他上下文中重复使用。

  • 它将侦听原始数据提供程序的更改,允许对其进行操作甚至分配完全新的数据提供程序:空项将自动重新创建。

  • 您可以通过emptyItem属性控制要用作“空项”的实际实例:这允许与集合的其余部分保持一致的数据类型(如果使用了类型化对象),或者定义一个自定义标签。

以下是一些使用它的示例代码...

    <mx:Script>
    <![CDATA[
        import mx.controls.Alert;
    ]]>
    </mx:Script>

    <mx:ArrayCollection id="myDP">
    <mx:source>
        <mx:Array>
            <mx:Object value="1" label="First"/>  
            <mx:Object value="2" label="Second"/>
            <mx:Object value="3" label="Third"/>
        </mx:Array>
    </mx:source>
    </mx:ArrayCollection>

    <local:EmptyItemComboBox id="comboBox" dataProvider="{myDP}" labelField="label">
    <local:emptyItem>
        <mx:Object value="{null}" label="(select an item)"/>  
    </local:emptyItem>
    </local:EmptyItemComboBox>

    <mx:Button label="Show selected item" click="Alert.show(comboBox.selectedItem.value)"/>

    <mx:Button label="Clear DP" click="myDP.removeAll()"/>
    <mx:Button label="Add item to DP" click="myDP.addItem({value: '4', label: 'Fourth'})"/>
    <mx:Button label="Reset DP" click="myDP = new ArrayCollection([{value: '1', label: 'First'}])"/>

    <mx:Label text="{'comboBox.isEmptySelected = ' + comboBox.isEmptySelected}"/>

</mx:Application>


你的自动添加代码以及对原始集合进行更改的监视看起来不错。但正如我在问题中解释的那样,我真的不喜欢有一个空项“代表”null;而使用Hrundik提出的NullList就不再需要这个了。 - Wouter Coekaerts
如果您控制数据提供程序集合的创建(因此您有机会添加空值),则Hrundik建议的解决方案是正确的选择(我在这里非常支持KISS原则)。我的子类不是关于具有实际对象实例与空引用(它可以重写使用null项/ dropDownFactory方法)。更专注于在您的集合可能随时刷新且您无法控制的情况下管理这些内容,这是一个非常常见的情况。希望如果有人遇到相同的问题,这将是有用的。 - Cosma Colanicchia
我喜欢你的解决方案。谢谢。我的之前的想法接近这个,但是实现上有问题。现在我只是稍微简化了一下你的方案:不使用emptyItem,而是使用emptyLabel(字符串)。 - RicardoS
我刚刚重构了这个方法,将任何源IList包装在一个自定义的FixedElementsList(实现IList)中,该列表将保留一组添加到原始源之上的固定元素,请参见我的github上的源代码https://github.com/cosma/Apache-Flex-Personal-Whiteboard/blob/master/FixedElementsList.as。 - Cosma Colanicchia

3
以下解决方案可能是最简单的:

以下解决方案可能是最简单的:

<mx:ComboBox id="cb" dataProvider="{[{label:"", data:null}, {label:'foo', data:'foo'}, {label:'bar', data:'bar'}]}" />

并且使用cb.selectedItem.data访问数据。

然而,正如Wouter所提到的那样,这个简单的解决方案不太适合绑定。

因此,这里有一个更加棘手的解决方案,可以允许选择空对象:

<mx:ComboBox id="cb" dataProvider="{[null, 'foo', 'bar']}" dropdownFactory="{new ClassFactory(NullList)}" />

NullList是以下类:

package test
{
import mx.controls.List;

public class NullList extends List
{
    public function NullList()
    {
        super();
    }

    override public function isItemSelectable(data:Object):Boolean
    {
        if (!selectable)
            return false;
        return true;
    }

}
}

这是一个简化版本的解决方法。但是这甚至不允许你将selectedItem绑定到模型上。换句话说,你不能像这样做:<mx:Combobox ... selectedItem="{model.currentProduct}" />。 - Wouter Coekaerts
是的,你说得对,对于那个解决方案来说绑定会更加困难。(但仍然有可能 - 你可以绑定到selectedIndex并使用一个简单的函数来获取项目索引)。我已经想出了解决你确切问题的解决方案。希望能有所帮助。 - Hrundik
很好!正是我在寻找的。 它有一个小问题:当将鼠标悬停在空项目上时,它不会高亮显示。 - Wouter Coekaerts
是的,我会尽量找时间在几天内修复它。这更加复杂,因为它发生在List(甚至是ListBase)组件的深处。如果您自己找到了解决方法,请随时发布您自己的答案。 - Hrundik

1

不幸的是,这是不可能的。

然而,一个好的解决方案是创建一个从ComboBox继承的类,并具有自己的DataProvider属性,这样你就不必“记住这个映射”了。

这个属性设置器将处理空值并在超级ComboBox类上表示它。


我并不是很喜欢它。但只要没有人提出能够使“真正”的空值可选的东西,我想你是对的:这是不可能的,我们所能做的最好的就是尽可能地将解决方法封装在子类中。 - Wouter Coekaerts

0

设置 requireSelection="false" 将允许空值,而 prompt 属性则允许您输入用于该空值的文本。


0
一个非常简单但也非常有限的解决方案是添加 prompt="" 属性。
这将防止 ComboBox 自动选择数据提供程序中的第一项,但是一旦用户选择了一个项目,空行就不会再显示出来了。

仍然能够选择空行非常重要,这是整个问题的关键所在。 - Wouter Coekaerts
支持这种情况需要一些复杂的编码。请看看我的其他答案。 - Cosma Colanicchia

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接