JavaFX糟糕的设计:TableView后面的可观察列表中的行标识? [英] JavaFX bad design: Row identity in observable lists behind the TableView?

查看:178
本文介绍了JavaFX糟糕的设计:TableView后面的可观察列表中的行标识?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

假设我使用 <$ c $显示非常长的表格C>的TableView 。手动说, TableView


旨在可视化无限数量的数据行

is designed to visualize an unlimited number of rows of data

因此,由于数百万行不适合RAM,我会介绍一些缓存。这意味着,允许 ObservableList#get()返回相同行索引的不同实例。

So, since million of rows won't fit the RAM, I would introduce some caching. This means, that ObservableList#get() is allowed to return different instances for the same row index.

是这是真的吗?

对面怎么样?我可以为填充了不同数据的所有行索引返回相同的实例吗?

What about opposite? May I return the same instance for all row indices filled with different data?

我注意到,这意味着行编辑存在一些问题。我应该在什么时候将数据传递到商店?看起来像 TableView 从不调用 ObservableList #set()但只是改变获得的实例。

I noticed, that this implies some problem with row editing. At which moment should I pass data to the store? Looks like TableView never calls ObservableList#set() but just mutates obtained instance.

拦截地点?

更新

想象这个非常大的表在服务器端更新。假设,增加了一百万条记录。

Also imagine this very big table was updated at server side. Suppose, one million of records were added.

报告它的唯一方法是触发可观察列表添加事件,而添加事件也包含对所有添加行的引用。这是废话 - 为什么发送数据,这不是事件显示?

The only way to report about it -- is by firing observable list addition event, while an addition event also contains reference to all added rows. Which is nonsense -- why send data, which is not event displayed?

推荐答案

我认为Javadocs中声明的意图您引用

I think the intention of the statement in the Javadocs that you quote


旨在可视化无限数量的数据行

is designed to visualize an unlimited number of rows of data

意味着暗示 TableView 不对表数据的大小施加任何(附加)约束:换句话说,视图是基本上可以在恒定的内存消耗下扩展这通过虚拟化来实现:表格视图仅为可见数据创建单元格,并且例如在用户滚动时将它们重新用于支持列表中的不同项目。由于在典型的应用程序中,单元格(图形化)消耗的内存远远多于数据,这代表了大的性能节省,并且允许表格中的行数尽可能地由用户处理。

is meant to imply that the TableView imposes no (additional) constraints on the size of the table data: in other words that the view is essentially scalable at a constant memory consumption. This is achieved by the "virtualization": the table view creates cells only for the visible data, and reuses them for different items in the backing list as, e.g., the user scrolls. Since in a typical application the cells (which are graphical) consume far more memory than the data, this represents a big performance saving and allows for as many rows in the table as could feasibly be handled by the user.

当然,表视图仍然存在对表数据大小的其他限制。模型(即可观察列表)需要存储数据,因此存储器约束(在默认实现中)将对表中的行数施加约束。如果需要,您可以实现缓存列表(请参见下文)以减少内存占用。正如@fabian在问题下面的评论中指出的那样,用户体验可能会在您达到该点之前很久就施加约束(我建议使用分页或某种过滤)。

There are still, of course, other constraints on the table data size that are not imposed by the table view. The model (i.e. observable list) needs to store the data and consequently memory constraints will (in the default implementation) impose a constraint on the number of rows in the table. You could implement a caching list (see below) to reduce the memory footprint, if needed. And as @fabian points out in the comments below the question, user experience is likely to impose constraints long before you reach that point (I'd recommend using pagination or some kind of filtering).

关于从列表中检索的元素的身份的问题在缓存实现中是相关的:它基本上归结为列表实现是否有义务保证 list.get(i)== list .get(i),或仅仅是为了保证 list.get(i).equals(list.get(i)) 。据我所知, TableView 只需要后者,所以 ObservableList 的实现缓存一个相对较小的数字元素并根据需要重新创建它们。

Your question about identity of elements retrieved from the list is pertinent in a caching implementation: it basically boils down to whether a list implementation is obliged to guarantee list.get(i) == list.get(i), or whether it is enough merely to guarantee list.get(i).equals(list.get(i)). To the best of my knowledge, TableView only expects the latter, so an implementation of ObservableList that caches a relatively small number of elements and recreates them as needed should work.

为了证明概念,这里是一个不可修改的缓存可观察列表的实现:

For proof of concept, here is an implementation of an unmodifiable caching observable list:

import java.util.LinkedList;
import java.util.function.IntFunction;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import javafx.collections.ObservableListBase;

public class CachedObservableList<T> extends ObservableListBase<T> {

    private final int maxCacheSize ;
    private int cacheStartIndex ;
    private int actualSize ;
    private final IntFunction<T> generator ;

    private final LinkedList<T> cache ;

    public CachedObservableList(int maxCacheSize, int size, IntFunction<T> generator) {
        this.maxCacheSize = maxCacheSize ;
        this.generator = generator ;

        this.cache = new LinkedList<T>();

        this.actualSize = size ;
    }

    @Override
    public T get(int index) {

        int debugCacheStart = cacheStartIndex ;
        int debugCacheSize = cache.size(); 

        if (index < cacheStartIndex) {
            // evict from end of cache:
            int numToEvict = cacheStartIndex + cache.size() - (index + maxCacheSize);
            if (numToEvict < 0) {
                numToEvict = 0 ;
            }
            if (numToEvict > cache.size()) {
                numToEvict = cache.size();
            }
            cache.subList(cache.size() - numToEvict, cache.size()).clear();

            // create new elements:
            int numElementsToCreate = cacheStartIndex - index ;
            if (numElementsToCreate > maxCacheSize) {
                numElementsToCreate = maxCacheSize ;
            }
            cache.addAll(0, 
                    IntStream.range(index, index + numElementsToCreate)
                    .mapToObj(generator)
                    .collect(Collectors.toList()));

            cacheStartIndex = index ;

        } else if (index >= cacheStartIndex + cache.size()) {
            // evict from beginning of cache:
            int numToEvict = index - cacheStartIndex - maxCacheSize + 1 ;
            if (numToEvict < 0) {
                numToEvict = 0 ;
            }
            if (numToEvict >= cache.size()) {
                numToEvict = cache.size();
            }

            cache.subList(0, numToEvict).clear();

            // create new elements:

            int numElementsToCreate = index - cacheStartIndex - numToEvict - cache.size() + 1; 
            if (numElementsToCreate > maxCacheSize) {
                numElementsToCreate = maxCacheSize ;
            }

            cache.addAll(
                    IntStream.range(index - numElementsToCreate + 1, index + 1)
                    .mapToObj(generator)
                    .collect(Collectors.toList()));

            cacheStartIndex = index - cache.size() + 1 ;
        }

        try {
            T t = cache.get(index - cacheStartIndex);
            assert(generator.apply(index).equals(t));
            return t ;
        } catch (Throwable exc) {
            System.err.println("Exception retrieving index "+index+": cache start was "+debugCacheStart+", cache size was "+debugCacheSize);
            throw exc ;
        }

    }

    @Override
    public int size() {
        return actualSize ;
    }

}

以下是使用它的快速示例,表中有100,000,000行。从用户体验的角度来看,这显然无法使用,但它似乎运行得很好(即使您将缓存大小更改为小于显示的单元格数)。

And here's a quick example using it, that has 100,000,000 rows in the table. Obviously this is unusable from a user experience perspective, but it seems to work perfectly well (even if you change the cache size to be smaller than the number of displayed cells).

import java.util.Objects;

import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.stage.Stage;

public class CachedTableView extends Application {

    @Override
    public void start(Stage primaryStage) {
        CachedObservableList<Item> data = new CachedObservableList<>(100, 100_000_000, i -> new Item(String.format("Item %,d",i)));

        TableView<Item> table = new TableView<>();
        table.setItems(data);

        TableColumn<Item, String> itemCol = new TableColumn<>("Item");
        itemCol.setCellValueFactory(cellData -> cellData.getValue().nameProperty());
        itemCol.setMinWidth(300);
        table.getColumns().add(itemCol);

        Scene scene = new Scene(table, 600, 600);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    public static class Item {
        private final StringProperty name = new SimpleStringProperty();


        public Item(String name) {
            setName(name) ;
        }

        public final StringProperty nameProperty() {
            return this.name;
        }


        public final String getName() {
            return this.nameProperty().get();
        }


        public final void setName(final String name) {
            this.nameProperty().set(name);
        }

        @Override
        public boolean equals(Object o) {
            if (o.getClass() != Item.class) {
                return false ;
            }
            return Objects.equals(getName(), ((Item)o).getName());
        }
    }


    public static void main(String[] args) {
        launch(args);
    }
}

如果你想要的话,显然还有很多工作要做实施清单,使其可以修改;首先考虑一下 set(index,element)所需的确切行为,如果 index 不在缓存中...然后子类 ModifiableObservableListBase

There's obviously quite a lot more to do if you want to implement the list so that it is modifiable; start by thinking about exactly what behavior you would need for set(index, element) if index is not in the cache... and then subclass ModifiableObservableListBase.

编辑:


我注意到,这意味着行存在一些问题编辑。我应该在什么时候将数据传递到商店?看起来TableView从不调用ObservableList #set()但只是改变获得的实例。

I noticed, that this implies some problem with row editing. At which moment should I pass data to the store? Looks like TableView never calls ObservableList#set() but just mutates obtained instance.

你有三个选项我可以看到:

You have three options that I can see:

如果域对象使用JavaFX属性,则默认行为是在提交编辑时更新属性。您可以使用属性注册侦听器,并在更改后更新后备存储。

If your domain objects use JavaFX properties, then the default behavior is to update the property when editing is committed. You can register listeners with the properties and update the backing store if they change.

或者,您可以使用 TableColumn onEditCommit 处理程序C>;这将在表中提交编辑时得到通知,因此您可以从此更新商店。请注意,这将替换默认的编辑提交行为,因此您还需要更新该属性。如果由于某种原因对商店的更新失败,这使您有机会否决对缓存属性的更新,并且可能是您想要的选项。

Alternatively, you can register an onEditCommit handler with the TableColumn; this will get notified when an edit is committed in the table, and so you could update the store from this. Note that this will replace the default edit commit behavior, so you will also need to update the property. This gives you the opportunity to veto the update to the cached property if the update to the store fails for some reason, and is probably the option you want.

第三,如果您自己实现编辑单元,而不是使用默认实现,如 TextFieldTableCell ,您可以直接从单元格中的控件调用模型上的方法。这可能是不可取的,因为它违反了标准设计模式并避免了表视图中内置的常用编辑通知,但在某些情况下它可能是一个有用的选项。

Thirdly, if you implement the editing cells yourself, instead of using default implementations such as TextFieldTableCell, you could invoke methods on the model directly from the controls in the cell. This is probably not desirable, as it violates the standard design patterns and avoids the usual editing notifications built into the table view, but it may be a useful option in some cases.


还想象这个非常大的表在服务器端更新。假设,添加了一百万条记录。

Also imagine this very big table was updated at server side. Suppose, one million of records were added.

报告它的唯一方法是通过触发可观察列表添加事件,而添加事件也包含对所有记录的引用添加行。

The only way to report about it -- is by firing observable list addition event, while an addition event also contains reference to all added rows.

据我所知,这不是真的。 ListChangeListener.Change 有一个 getAddedSublist()方法,但是这个状态的API文档会返回

That's not true, as far as I can tell. ListChangeListener.Change has a getAddedSublist() method, but the API docs for this state it returns


列表的子列表视图,仅包含添加的元素

a subList view of the list that contains only the elements added

所以它应该只返回 getItems()。sublist(change.getFrom(),change.getTo())。当然,这只是返回缓存列表实现的子列表视图,因此除非您明确请求它们,否则不会创建对象。 (注意 getRemoved()可能会导致更多问题,但也应该有办法解决这个问题。)

so it should simply return getItems().sublist(change.getFrom(), change.getTo()). Of course, this simply returns a sublist view of the cached list implementation, so doesn't create the objects unless you explicitly request them. (Note that getRemoved() might potentially cause more problems, but there should be some way to work around that too.)

最后,为了实现这个完整的循环,而可观察列表实现在这里工作缓存元素并使模型在其可支持的行数中无限制(最多整数。 MAX_VALUE ),如果表视图没有实现虚拟化,则无法在表视图中使用它。表视图的非虚拟化实现将为列表中的每个项创建单元格(即,它将为 0< =调用 get(i) i< items.size(),为每个创建一个单元格,将单元格放在滚动窗格实现中,即使列表中有缓存,内存消耗也会爆炸。因此,Javadocs中的无限制确实意味着任何限制都被推迟到模型的实现。

Finally, to bring this full circle, while the observable list implementation is doing work here of caching the elements and making the model "unlimited" in the number of rows it can support (up to Integer.MAX_VALUE), it wouldn't be possible to use this in the table view if the table view didn't implement "virtualization". A non-virtualized implementation of table view would create cells for each item in the list (i.e. it would call get(i) for 0 <= i < items.size(), creating a cell for each), place the cells in a scroll pane implementation, and the memory consumption would blow up even with the caching in the list. So "unlimited" in the Javadocs really does mean that any limit is deferred to implementation of the model.

这篇关于JavaFX糟糕的设计:TableView后面的可观察列表中的行标识?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆