org.jboss.errai.ui.client.widget.ListWidget.java Source code

Java tutorial

Introduction

Here is the source code for org.jboss.errai.ui.client.widget.ListWidget.java

Source

/*
 * Copyright 2011 JBoss, by Red Hat, Inc
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.jboss.errai.ui.client.widget;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

import org.jboss.errai.common.client.api.Assert;
import org.jboss.errai.databinding.client.BindableListChangeHandler;
import org.jboss.errai.databinding.client.BindableListWrapper;
import org.jboss.errai.ioc.client.container.IOC;
import org.jboss.errai.ioc.client.container.SyncToAsyncBeanManagerAdpater;
import org.jboss.errai.ioc.client.container.async.AsyncBeanDef;
import org.jboss.errai.ioc.client.container.async.AsyncBeanManager;
import org.jboss.errai.ioc.client.container.async.CreationalCallback;

import com.google.gwt.event.dom.client.ChangeEvent;
import com.google.gwt.event.dom.client.ChangeHandler;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.user.client.ui.ComplexPanel;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.HasValue;
import com.google.gwt.user.client.ui.IsWidget;
import com.google.gwt.user.client.ui.VerticalPanel;
import com.google.gwt.user.client.ui.Widget;

/**
 * A type of widget that displays and manages a child widget for each item in a list of model
 * objects. The widget instances are managed by Errai's IOC container and are arranged in a
 * {@link ComplexPanel}. By default, a {@link VerticalPanel} is used, but an alternative can be
 * specified using {@link #ListWidget(ComplexPanel)}.
 * 
 * @param <M>
 *          the model type
 * @param <W>
 *          the item widget type, needs to implement {@link HasModel} for associating the widget
 *          instance with the corresponding model instance.
 * 
 * @author Christian Sadilek <csadilek@redhat.com>
 */
public abstract class ListWidget<M, W extends HasModel<M> & IsWidget> extends Composite
        implements HasValue<List<M>>, BindableListChangeHandler<M> {

    private final ComplexPanel panel;
    private BindableListWrapper<M> items;

    private final List<WidgetCreationalCallback> callbacks = new LinkedList<WidgetCreationalCallback>();
    private int pendingCallbacks;

    private boolean valueChangeHandlerInitialized;

    protected ListWidget() {
        this(new VerticalPanel());
    }

    protected ListWidget(ComplexPanel panel) {
        this.panel = Assert.notNull(panel);
        initWidget(panel);
    }

    /**
     * Returns the class object for the item widget type <W> to look up new instances of the widget
     * using the client-side bean manager.
     * 
     * @return the item widget type.
     */
    protected abstract Class<W> getItemWidgetType();

    /**
     * Called after all item widgets have been rendered. By default, this is a NOOP, but subclasses
     * can add behaviour if needed.
     * <p>
     * Using the standard synchronous bean manager this method is invoked before
     * {@link #setItems(List)} returns. However, when using the asynchronous bean manager and
     * declaring @LoadAsync on the item widget, this method might be called after
     * {@link #setItems(List)} returns and after the corresponding JavaScript code has been
     * downloaded.
     * 
     * @param items
     *          the rendered item list. Every change to this list will update the corresponding
     *          rendered item widgets.
     */
    protected void onItemsRendered(List<M> items) {
    };

    /**
     * Returns the panel that contains all item widgets.
     * 
     * @return the item widget panel, never null.
     */
    protected ComplexPanel getPanel() {
        return panel;
    }

    /**
     * Sets the list of model objects. A widget instance of type <W> will be added to the panel for
     * each object in the list. The list will be wrapped in an {@link BindableListWrapper} to make
     * direct changes to the list observable.
     * <p>
     * If the standard synchronous bean manager is used it is guaranteed that all widgets have been
     * added to the panel when this method returns. In case the asynchronous bean manager is used this
     * method might return before the widgets have been added to the panel. See
     * {@link #onItemsRendered()}.
     * 
     * @param items
     *          The list of model objects. If null or empty all existing child widgets will be
     *          removed.
     */
    public void setItems(List<M> items) {
        boolean changed = this.items != items;

        if (items instanceof BindableListWrapper) {
            this.items = (BindableListWrapper<M>) items;
        } else {
            this.items = new BindableListWrapper<M>(items);
        }

        if (changed) {
            this.items.addChangeHandler(this);
        }
        init();
    }

    private void init() {
        // The AsyncBeanManager API works in both synchronous and asynchronous IOC mode
        AsyncBeanManager bm = IOC.getAsyncBeanManager();

        // In the case that this method is executed before the first call has successfully processed all
        // of its callbacks, we must cancel those uncompleted callbacks in flight to prevent duplicate
        // data in the ListWidget.
        for (WidgetCreationalCallback callback : callbacks) {
            callback.discard();
        }
        callbacks.clear();
        pendingCallbacks = 0;

        // clean up the old widgets before we add new ones (this will eventually become a feature of the
        // framework: ERRAI-375)
        Iterator<Widget> it = panel.iterator();
        while (it.hasNext()) {
            bm.destroyBean(it.next());
            it.remove();
        }

        if (items == null)
            return;

        pendingCallbacks = items.size();
        AsyncBeanDef<W> itemBeanDef = bm.lookupBean(getItemWidgetType());
        for (final M item : items) {
            final WidgetCreationalCallback callback = new WidgetCreationalCallback(item);
            callbacks.add(callback);
            itemBeanDef.newInstance(callback);
        }
    }

    /**
     * Returns the widget at the specified index.
     * 
     * @param index
     *          the index to be retrieved
     * 
     * @return the widget at the specified index
     * 
     * @throws IndexOutOfBoundsException
     *           if the index is out of range
     */
    @SuppressWarnings("unchecked")
    public W getWidget(int index) {
        return (W) panel.getWidget(index);
    }

    /**
     * Returns the widget currently displaying the provided model.
     * 
     * @param model
     *          the model displayed by the widget
     * 
     * @return the widget displaying the provided model instance, null if no widget was found for the model.
     */
    public W getWidget(M model) {
        int index = items.indexOf(model);
        return getWidget(index);
    }

    @Override
    public HandlerRegistration addValueChangeHandler(ValueChangeHandler<List<M>> handler) {
        if (!valueChangeHandlerInitialized) {
            valueChangeHandlerInitialized = true;
            addDomHandler(new ChangeHandler() {
                @Override
                public void onChange(ChangeEvent event) {
                    ValueChangeEvent.fire(ListWidget.this, getValue());
                }
            }, ChangeEvent.getType());
        }
        return addHandler(handler, ValueChangeEvent.getType());
    }

    @Override
    public List<M> getValue() {
        if (items == null) {
            items = new BindableListWrapper<M>(new ArrayList<M>());
            items.addChangeHandler(this);
        }
        return items;
    }

    @Override
    public void setValue(List<M> value) {
        setValue(value, false);
    }

    @Override
    public void setValue(List<M> value, boolean fireEvents) {
        List<M> oldValue = getValue();
        setItems(value);
        if (fireEvents) {
            ValueChangeEvent.fireIfNotEqual(this, oldValue, value);
        }
    }

    /**
     * A callback invoked by the {@link AsyncBeanManager} or {@link SyncToAsyncBeanManagerAdpater}
     * when the widget instance was created. It will associate the corresponding model instance with
     * the widget and add the widget to the panel.
     */
    private class WidgetCreationalCallback implements CreationalCallback<W> {
        private boolean discard;
        private final M item;

        private WidgetCreationalCallback(M item) {
            this.item = item;
        }

        @Override
        public void callback(W widget) {
            if (!discard) {
                widget.setModel(item);
                panel.add(widget);

                if (--pendingCallbacks == 0) {
                    onItemsRendered(items);
                }
            }
        }

        public void discard() {
            this.discard = true;
        }
    }

    @Override
    public void onItemAdded(List<M> oldList, M item) {
        addWidget(item);
    }

    @Override
    public void onItemAddedAt(List<M> oldList, int index, M item) {
        for (int i = index; i < items.size(); i++) {
            addAndReplaceWidget(index, i);
        }
    }

    @Override
    public void onItemsAdded(List<M> oldList, Collection<? extends M> items) {
        for (M m : items) {
            addWidget(m);
        }
    }

    @Override
    public void onItemsAddedAt(List<M> oldList, int index, Collection<? extends M> item) {
        for (int i = index; i < items.size(); i++) {
            addAndReplaceWidget(index, i);
        }
    }

    @Override
    public void onItemsCleared(List<M> oldList) {
        panel.clear();
    }

    @Override
    public void onItemRemovedAt(List<M> oldList, int index) {
        panel.remove(index);
    }

    @Override
    public void onItemsRemovedAt(List<M> oldList, List<Integer> indexes) {
        for (Integer index : indexes) {
            panel.remove(index);
        }
    }

    @Override
    public void onItemChanged(List<M> oldList, int index, M item) {
        for (int i = index; i < items.size(); i++) {
            addAndReplaceWidget(index, i);
        }
    }

    private void addAndReplaceWidget(final int startIndex, final int index) {
        if (index < panel.getWidgetCount()) {
            panel.remove(startIndex);
        }
        addWidget(items.get(index));
    }

    private void addWidget(final M m) {
        // This call is always synchronous, since the list can only be manipulated after
        // onItemsRendered was called. At that point the code of a potential split point must have
        // already been downloaded.
        AsyncBeanDef<W> itemBeanDef = IOC.getAsyncBeanManager().lookupBean(getItemWidgetType());
        itemBeanDef.getInstance(new CreationalCallback<W>() {
            @Override
            public void callback(W widget) {
                widget.setModel(m);
                panel.add(widget);
            }
        });
    }

}