001: /*
002: * $Id: ListView.java 5343 2006-04-12 00:58:25 -0700 (Wed, 12 Apr 2006)
003: * gwynevans $ $Revision: 530670 $ $Date: 2006-04-12 00:58:25 -0700 (Wed, 12 Apr
004: * 2006) $
005: *
006: * ==============================================================================
007: * Licensed under the Apache License, Version 2.0 (the "License"); you may not
008: * use this file except in compliance with the License. You may obtain a copy of
009: * the License at
010: *
011: * http://www.apache.org/licenses/LICENSE-2.0
012: *
013: * Unless required by applicable law or agreed to in writing, software
014: * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
015: * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
016: * License for the specific language governing permissions and limitations under
017: * the License.
018: */
019: package wicket.markup.html.list;
020:
021: import java.io.Serializable;
022: import java.util.Collections;
023: import java.util.Iterator;
024: import java.util.List;
025:
026: import wicket.Component;
027: import wicket.markup.MarkupStream;
028: import wicket.markup.html.WebMarkupContainer;
029: import wicket.markup.html.link.Link;
030: import wicket.model.IModel;
031: import wicket.model.Model;
032: import wicket.version.undo.Change;
033:
034: /**
035: * A ListView holds ListItem children. Items can be re-ordered and deleted,
036: * either one at a time or many at a time.
037: * <p>
038: * Example:
039: *
040: * <pre>
041: * <tbody>
042: * <tr wicket:id="rows" class="even">
043: * <td><span wicket:id="id">Test ID</span></td>
044: * ...
045: * </pre>
046: *
047: * <p>
048: * Though this example is about a HTML table, ListView is not at all limited to
049: * HTML tables. Any kind of list can be rendered using ListView.
050: * <p>
051: * The related Java code:
052: *
053: * <pre>
054: * add(new ListView("rows", listData)
055: * {
056: * public void populateItem(final ListItem item)
057: * {
058: * final UserDetails user = (UserDetails)item.getModelObject();
059: * item.add(new Label("id", user.getId()));
060: * }
061: * });
062: * </pre>
063: *
064: * <p>
065: * WARNING: though you can nest ListViews within Forms, you HAVE to set the
066: * optimizeItemRemoval property to true in order to have validation work
067: * properly. By default, optimizeItemRemoval is false, which has the effect that
068: * ListView replaces all child components by new instances. The idea behind this
069: * is that you always render the fresh data, and as people usually use ListViews
070: * for displaying read-only lists (at least, that's what we think), this is good
071: * default behavior. <br />
072: * However, as the components are replaced before the rendering starts, the
073: * search for specific messages for these components fails as they are replaced
074: * with other instances. Another problem is that 'wrong' user input is kept as
075: * (temporary) instance data of the components. As these components are replaced
076: * by new ones, your user will never see the wrong data when optimizeItemRemoval
077: * is false.
078: * </p>
079: *
080: * @author Jonathan Locke
081: * @author Juergen Donnerstag
082: * @author Johan Compagner
083: * @author Eelco Hillenius
084: */
085: public abstract class ListView extends WebMarkupContainer {
086: /** Index of the first item to show */
087: private int firstIndex = 0;
088:
089: /**
090: * If true, re-rendering the list view is more efficient if the window
091: * doesn't get changed at all or if it gets scrolled (compared to paging).
092: * But if you modify the listView model object, than you must manually call
093: * listView.removeAll() in order to rebuild the ListItems. If you nest a
094: * ListView in a Form, ALWAYS set this property to true, as otherwise
095: * validation will not work properly.
096: */
097: private boolean reuseItems = false;
098:
099: /** Max number (not index) of items to show */
100: private int viewSize = Integer.MAX_VALUE;
101:
102: /**
103: * @see wicket.Component#Component(String)
104: */
105: public ListView(final String id) {
106: super (id);
107: }
108:
109: /**
110: * @see wicket.Component#Component(String, IModel)
111: */
112: public ListView(final String id, final IModel model) {
113: super (id, model);
114:
115: if (model == null) {
116: throw new IllegalArgumentException(
117: "Null models are not allowed. If you have no model, you may prefer a Loop instead");
118: }
119:
120: // A reasonable default for viewSize can not be determined right now,
121: // because list items might be added or removed until ListView
122: // gets rendered.
123: }
124:
125: /**
126: * @param id
127: * See Component
128: * @param list
129: * List to cast to Serializable
130: * @see wicket.Component#Component(String, IModel)
131: */
132: public ListView(final String id, final List list) {
133: this (id, new Model((Serializable) list));
134: }
135:
136: /**
137: * Gets the list of items in the listView. This method is final because it
138: * is not designed to be overridden. If it were allowed to be overridden,
139: * the values returned by getModelObject() and getList() might not coincide.
140: *
141: * @return The list of items in this list view.
142: */
143: public final List getList() {
144: final List list = (List) getModelObject();
145: if (list == null) {
146: return Collections.EMPTY_LIST;
147: }
148: return list;
149: }
150:
151: /**
152: * If true re-rendering the list view is more efficient if the windows
153: * doesn't get changed at all or if it gets scrolled (compared to paging).
154: * But if you modify the listView model object, than you must manually call
155: * listView.removeAll() in order to rebuild the ListItems. If you nest a
156: * ListView in a Form, ALLWAYS set this property to true, as otherwise
157: * validation will not work properly.
158: *
159: * @return Whether to reuse items
160: * @deprecated Use {@link #getReuseItems()} instead
161: */
162: // TODO Post 1.2: Remove
163: public boolean getOptimizeItemRemoval() {
164: return getReuseItems();
165: }
166:
167: /**
168: * If true re-rendering the list view is more efficient if the windows
169: * doesn't get changed at all or if it gets scrolled (compared to paging).
170: * But if you modify the listView model object, than you must manually call
171: * listView.removeAll() in order to rebuild the ListItems. If you nest a
172: * ListView in a Form, ALLWAYS set this property to true, as otherwise
173: * validation will not work properly.
174: *
175: * @return Whether to reuse items
176: */
177: public boolean getReuseItems() {
178: return reuseItems;
179: }
180:
181: /**
182: * Get index of first cell in page. Default is: 0.
183: *
184: * @return Index of first cell in page. Default is: 0
185: */
186: public final int getStartIndex() {
187: return this .firstIndex;
188: }
189:
190: /**
191: * Based on the model object's list size, firstIndex and view size,
192: * determine what the view size really will be. E.g. default for viewSize is
193: * Integer.MAX_VALUE, if not set via setViewSize(). If the underlying list
194: * has 10 elements, the value returned by getViewSize() will be 10 if
195: * startIndex = 0.
196: *
197: * @return The number of items to be populated and rendered.
198: */
199: public int getViewSize() {
200: int size = this .viewSize;
201:
202: final Object modelObject = getModelObject();
203: if (modelObject == null) {
204: return size == Integer.MAX_VALUE ? 0 : size;
205: }
206:
207: // Adjust view size to model object's list size
208: final int modelSize = getList().size();
209: if (firstIndex > modelSize) {
210: return 0;
211: }
212:
213: if ((size == Integer.MAX_VALUE)
214: || ((firstIndex + size) > modelSize)) {
215: size = modelSize - firstIndex;
216: }
217:
218: // firstIndex + size must be smaller than Integer.MAX_VALUE
219: if ((Integer.MAX_VALUE - size) < firstIndex) {
220: throw new IllegalStateException(
221: "firstIndex + size must be smaller than Integer.MAX_VALUE");
222: }
223:
224: return size;
225: }
226:
227: /**
228: * Returns a link that will move the given item "down" (towards the end) in
229: * the listView.
230: *
231: * @param id
232: * Name of move-down link component to create
233: * @param item
234: * @return The link component
235: */
236: public final Link moveDownLink(final String id, final ListItem item) {
237: return new Link(id) {
238: private static final long serialVersionUID = 1L;
239:
240: /**
241: * @see wicket.Component#onAttach()
242: */
243: protected void onAttach() {
244: setAutoEnable(false);
245: if (getList().indexOf(item.getModelObject()) == (getList()
246: .size() - 1)) {
247: setEnabled(false);
248: }
249: }
250:
251: /**
252: * @see wicket.markup.html.link.Link#onClick()
253: */
254: public void onClick() {
255: final int index = getList().indexOf(
256: item.getModelObject());
257: if (index != -1) {
258: addStateChange(new Change() {
259: private static final long serialVersionUID = 1L;
260:
261: final int oldIndex = index;
262:
263: public void undo() {
264: Collections.swap(getList(), oldIndex + 1,
265: oldIndex);
266: }
267:
268: });
269:
270: // Swap list items and invalidate listView
271: Collections.swap(getList(), index, index + 1);
272: }
273: }
274: };
275: }
276:
277: /**
278: * Returns a link that will move the given item "up" (towards the beginning)
279: * in the listView.
280: *
281: * @param id
282: * Name of move-up link component to create
283: * @param item
284: * @return The link component
285: */
286: public final Link moveUpLink(final String id, final ListItem item) {
287: return new Link(id) {
288: private static final long serialVersionUID = 1L;
289:
290: /**
291: * @see wicket.Component#onAttach()
292: */
293: protected void onAttach() {
294: setAutoEnable(false);
295: if (getList().indexOf(item.getModelObject()) == 0) {
296: setEnabled(false);
297: }
298: }
299:
300: /**
301: * @see wicket.markup.html.link.Link#onClick()
302: */
303: public void onClick() {
304: final int index = getList().indexOf(
305: item.getModelObject());
306: if (index != -1) {
307:
308: addStateChange(new Change() {
309: private static final long serialVersionUID = 1L;
310:
311: final int oldIndex = index;
312:
313: public void undo() {
314: Collections.swap(getList(), oldIndex - 1,
315: oldIndex);
316: }
317:
318: });
319:
320: // Swap items and invalidate listView
321: Collections.swap(getList(), index, index - 1);
322: }
323: }
324: };
325: }
326:
327: /**
328: * Returns a link that will remove this ListItem from the ListView that
329: * holds it.
330: *
331: * @param id
332: * Name of remove link component to create
333: * @param item
334: * @return The link component
335: */
336: public final Link removeLink(final String id, final ListItem item) {
337: return new Link(id) {
338: private static final long serialVersionUID = 1L;
339:
340: /**
341: * @see wicket.markup.html.link.Link#onClick()
342: */
343: public void onClick() {
344: addStateChange(new Change() {
345: private static final long serialVersionUID = 1L;
346:
347: final Object removedObject = item.getModelObject();
348: final int oldIndex = getList().indexOf(
349: item.getModelObject());
350:
351: public void undo() {
352: getList().add(oldIndex, removedObject);
353: }
354:
355: });
356:
357: item.modelChanging();
358:
359: // Remove item and invalidate listView
360: getList().remove(item.getModelObject());
361:
362: ListView.this .modelChanged();
363: }
364: };
365: }
366:
367: /**
368: * If true re-rendering the list view is more efficient if the windows
369: * doesn't get changed at all or if it gets scrolled (compared to paging).
370: * But if you modify the listView model object, than you must manually call
371: * listView.removeAll() in order to rebuild the ListItems. If you nest a
372: * ListView in a Form, ALLWAYS set this property to true, as otherwise
373: * validation will not work properly.
374: *
375: * @param reuseItems
376: * Whether to reuse items
377: * @return this
378: * @deprecated Use {@link #setReuseItems(boolean)} instead
379: */
380: // TODO Post 1.2: Remove
381: public ListView setOptimizeItemRemoval(boolean reuseItems) {
382: return setReuseItems(reuseItems);
383: }
384:
385: /**
386: * If true re-rendering the list view is more efficient if the windows
387: * doesn't get changed at all or if it gets scrolled (compared to paging).
388: * But if you modify the listView model object, than you must manually call
389: * listView.removeAll() in order to rebuild the ListItems. If you nest a
390: * ListView in a Form, ALLWAYS set this property to true, as otherwise
391: * validation will not work properly.
392: *
393: * @param reuseItems
394: * Whether to reuse the child items.
395: * @return this
396: */
397: public ListView setReuseItems(boolean reuseItems) {
398: this .reuseItems = reuseItems;
399: return this ;
400: }
401:
402: /**
403: * Set the index of the first item to render
404: *
405: * @param startIndex
406: * First index of model object's list to display
407: * @return This
408: */
409: public ListView setStartIndex(final int startIndex) {
410: this .firstIndex = startIndex;
411:
412: if (firstIndex < 0) {
413: firstIndex = 0;
414: } else if (firstIndex > getList().size()) {
415: firstIndex = 0;
416: }
417:
418: return this ;
419: }
420:
421: /**
422: * Define the maximum number of items to render. Default: render all.
423: *
424: * @param size
425: * Number of items to display
426: * @return This
427: */
428: public ListView setViewSize(final int size) {
429: this .viewSize = size;
430:
431: if (viewSize < 0) {
432: viewSize = Integer.MAX_VALUE;
433: }
434:
435: return this ;
436: }
437:
438: /**
439: * Sets the model as the provided list and removes all children, so that the
440: * next render will be using the contents of the model.
441: *
442: * @param list
443: * The list for the new model. The list must implement
444: * {@link Serializable}.
445: * @return This for chaining
446: */
447: public Component setList(List list) {
448: return setModel(new Model((Serializable) list));
449: }
450:
451: /**
452: * Sets the model and removes all current children, so that the next render
453: * will be using the contents of the model.
454: *
455: * @param model
456: * The new model
457: * @return This for chaining
458: *
459: * @see wicket.MarkupContainer#setModel(wicket.model.IModel)
460: */
461: public Component setModel(IModel model) {
462: return super .setModel(model);
463: }
464:
465: /**
466: * Subclasses may provide their own ListItemModel with extended
467: * functionality. The default ListItemModel works fine with mostly static
468: * lists where index remains valid. In cases where the underlying list
469: * changes a lot (many users using the application), it may not longer be
470: * appropriate. In that case your own ListItemModel implementation should
471: * use an id (e.g. the database' record id) to identify and load the list
472: * item model object.
473: *
474: * @param listViewModel
475: * The ListView's model
476: * @param index
477: * The list item index
478: * @return The ListItemModel created
479: */
480: protected IModel getListItemModel(final IModel listViewModel,
481: final int index) {
482: return new ListItemModel(this , index);
483: }
484:
485: /**
486: * @see wicket.MarkupContainer#internalOnAttach()
487: */
488: protected void internalOnAttach() {
489: if (isVisibleInHierarchy()) {
490: // Get number of items to be displayed
491: final int size = getViewSize();
492: if (size > 0) {
493: if (getReuseItems()) {
494: // Remove all ListItems no longer required
495: final int maxIndex = firstIndex + size;
496: for (final Iterator iterator = iterator(); iterator
497: .hasNext();) {
498: // Get next child component
499: final ListItem child = (ListItem) iterator
500: .next();
501: if (child != null) {
502: final int index = child.getIndex();
503: if (index < firstIndex || index >= maxIndex) {
504: iterator.remove();
505: }
506: }
507: }
508: } else {
509: // Automatically rebuild all ListItems before rendering the
510: // list view
511: removeAll();
512: }
513:
514: // Loop through the markup in this container for each item
515: for (int i = 0; i < size; i++) {
516: // Get index
517: final int index = firstIndex + i;
518:
519: // If this component does not already exist, populate it
520: ListItem item = (ListItem) get(Integer
521: .toString(index));
522: if (item == null) {
523: // Create item for index
524: item = newItem(index);
525:
526: // Add list item
527: add(item);
528:
529: // Populate the list item
530: onBeginPopulateItem(item);
531: populateItem(item);
532: }
533: }
534: } else {
535: removeAll();
536: }
537: }
538: }
539:
540: /**
541: * Create a new ListItem for list item at index.
542: *
543: * @param index
544: * @return ListItem
545: */
546: protected ListItem newItem(final int index) {
547: return new ListItem(index, getListItemModel(getModel(), index));
548: }
549:
550: /**
551: * Comes handy for ready made ListView based components which must implement
552: * populateItem() but you don't want to lose compile time error checking
553: * reminding the user to implement abstract populateItem().
554: *
555: * @param item
556: */
557: protected void onBeginPopulateItem(final ListItem item) {
558: }
559:
560: /**
561: *
562: * @see wicket.Component#onRender(wicket.markup.MarkupStream)
563: */
564: protected void onRender(final MarkupStream markupStream) {
565: // Save position in markup stream
566: final int markupStart = markupStream.getCurrentIndex();
567:
568: // Get number of items to be displayed
569: final int size = getViewSize();
570: if (size > 0) {
571: // Loop through the markup in this container for each item
572: for (int i = 0; i < size; i++) {
573: // Get index
574: final int index = firstIndex + i;
575:
576: // Get list item for index
577: ListItem item = (ListItem) get(Integer.toString(index));
578:
579: // Rewind to start of markup for kids
580: markupStream.setCurrentIndex(markupStart);
581:
582: // Render
583: renderItem(item);
584: }
585: } else {
586: markupStream.skipComponent();
587: }
588: }
589:
590: /**
591: * Populate a given item.
592: * <p>
593: * <b>be carefull</b> to add any components to the list item. So, don't do:
594: *
595: * <pre>
596: * add(new Label("foo", "bar"));
597: * </pre>
598: *
599: * but:
600: *
601: * <pre>
602: * item.add(new Label("foo", "bar"));
603: * </pre>
604: *
605: * </p>
606: *
607: * @param item
608: * The item to populate
609: */
610: protected abstract void populateItem(final ListItem item);
611:
612: /**
613: * Render a single item.
614: *
615: * @param item
616: * The item to be rendered
617: */
618: protected void renderItem(final ListItem item) {
619: item.render(getMarkupStream());
620: }
621: }
|