Working with BlackBerry list fields – Tutorial

Posted October 13th, 2009 by

Introduction

This tutorial will show you how to create non-trivial lists using ListField. I will create a sample program that allows you to create, remove, update, delete the contents of a list (that’s backed by a Vector). The list field contains rows of selectable list items. It allows you to display a list of items, and load this list of objects from an array or vector.  When using a ListField you have to provide an implementation of the ListFieldCallback interface to perform drawing tasks. This callback constitutes the view and model (using MVC terminology). The controller is the ListField class.

Source code example

Controller

I’m going to create a new ListField subclass (called MyListField), which is the controller. It’s just a subclass of ListField that makes it easy to bind to menu items that perform actions on the model + view (MyListModel). Then, I will add MyListField and MyListModel to a layout manager so it can be displayed to the screen.

Model and View

Then I’m going to create a model that implements ListFieldCallback (called MyListModel), which will provide methods (actions and menuitems) that allow the list to be mutated. MyListModel really does all the work in this example. It holds all the objects in the list (inside of a backing store Vector). It also exposes actions that can be wired directly into the UI using menuitems or buttons. The one strange thing about the model interface is that there is no method to get the number of rows contained in it. So if you want to pre-load items in a list, and not have to create them from scratch (as in this example), then you will have to set this size explicitly on the view. By setting the size, the view will then query the model to figure out how to render all the list items. The method you have to call on the ListField is setSize(int). This behavior of the ListField is very clunky!

Main Class

Here’s the main driver for this example, ListApp, which has a main() method.

public class ListApp extends UiApplication {
// main method
public static void main(String[] args) {

  ListApp theApp = new ListApp();
  UiApplication.getUiApplication().pushScreen(new MyScreen());
  theApp.enterEventDispatcher();

}

}//end ListApp


// MainScreen
public class MyScreen extends MainScreen {

  public MyScreen(){

    // add the model to the list
    final MyListField myListView = new MyListField();
    myListView.setEmptyString("List is empty", DrawStyle.HCENTER);
    final MyListModel myListModel = new MyListModel(myListView);

    // create a context menu on the list...
    myListView.addToContextMenu(myListModel.getAddMenuItem(0, 0));
    myListView.addToContextMenu(myListModel.getRemoveMenuItem(0, 0));
    myListView.addToContextMenu(myListModel.getModifyMenuItem(0, 0));
    myListView.addToContextMenu(myListModel.getEraseMenuItem(0, 0));

    // layout components on the screen...
    {
      // get the MainScreen's VerticalFieldManager...
      Manager vfm = getMainManager();

      // add components to the vfm
      vfm.add(myListView);

      // add a separator
      vfm.add(new SeparatorField(SeparatorField.LINE_HORIZONTAL));

      // add something to the title
      setTitle(new LabelField("ListDemo title"));
    }
    
  }

}

Here’s what’s going on in this code:

  1. The ListApp simply creates a MyScreen screen object and displays it to the screen.
  2. The MyScreen class contains a MyListField (controller) object, and a MyListModel (model and view) object. The menu items from MyListModel are added to the screen, so that a user can interact with the contents of the model in ListApp.
  3. The view(MyListField) is bound to the model + controller (MyListModel) here. The menu items are added to the screen (MyScreen), and are bound to the model + controller (MyListModel).

MyListField – controller

Here’s the listing for MyListField, which is the controller.

/** custom listfield that you can attach context menu items to */
class MyListField extends ListField {

  private Menu _contextMenu = new Menu();

  public void addToContextMenu(MenuItem menuitem) {
    if (menuitem != null) _contextMenu.add(menuitem);
  }

  public ContextMenu getContextMenu() {
    // DO NOT call ContextMenu.getInstance()... this will produce null in many cases!!!

    ContextMenu cMenu = super.getContextMenu();
    cMenu.clear();

    int size = _contextMenu.getSize();

    for (int i = 0; i < size; i++) {
      cMenu.addItem(_contextMenu.getItem(i));
    }

    return cMenu;
  }

}//end class MyListField

Here’s what’s going on in this code:

  1. The methods in this class just make it easier to add menu items to the screen.

MyListModel – model and view

Here’s the listing for MyListModel, which is the model and view.

/** actionListener interface */
public interface ActionListenerIF {

void actionPerformed(Object source);

}//end interface ActionListenerIF

/** custom listmodel that sync's and auto-binds with the listfield */
class MyListModel implements ListFieldCallback {

  private Vector _data = new Vector();
  private ListField _view;
  private int _defaultRowHeight = 32;
  private int _defaultRowWidth = _defaultRowHeight;
  private int _textImagePadding = 5;
  private Bitmap _bitmap;

  /** constructor that saves a ref to the model's view - {@link ListField}, and binds this model to the view */
  public MyListModel(ListField list) {
    // save a ref to the list view
    _view = list;

    // bind this model to the given view
    list.setCallback(this);

    // set the default row height
    _view.setRowHeight(_defaultRowHeight);

    // load the bitmap to use in the cell rendering
    _bitmap = // load some bitmap of your choice here
  }

  //XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
  // implement ListFieldCallback interface
  //XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

  /** list row renderer */
  public void drawListRow(ListField list, Graphics g, int index, int y, int w) {

    String text = (String) _data.elementAt(index);

    // draw the text /w ellipsis if it's too long...
    g.drawText(text,
               _defaultRowWidth + _textImagePadding, y,
               DrawStyle.LEADING | DrawStyle.ELLIPSIS,
               w - _defaultRowWidth - _textImagePadding);

    // draw the to the left of the text...
    g.drawBitmap(0, y, _bitmap.getWidth(), _bitmap.getHeight(), _bitmap, 0, 0);

  }

  /** list row data accessor */
  public Object get(ListField list, int index) {
    return _data.elementAt(index);
  }

  /** used for filtering list elements */
  public int indexOfList(ListField list, String p, int s) {
    return _data.indexOf(p, s);
  }

  /** used for rendering list... provide the width of the list in pixels */
  public int getPreferredWidth(ListField list) {
    return Display.getWidth();
  }

  //XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
  // data manipulation methods...  not part of the interface
  //XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

  /** mutator, which syncs model and view */
  public void insert(String toInsert, int index) {
    // update the model
    _data.addElement(toInsert);

    // update the view
    _view.insert(index);
  }
  /** mutator, which syncs model and view */
  public void delete(int index) {
    // update the model
    _data.removeElementAt(index);

    // update the view
    _view.delete(index);
  }
  /** mutator, which syncs model and view */
  public void erase() {
    int size = _data.size();

    // update the view
    for (int i = 0; i < size; i++) {
      delete(size - i - 1);
    }
  }
  public void modify(String newValue, int index) {
    // update the model
    _data.removeElementAt(index);
    _data.insertElementAt(newValue, index);

    // update the view
    _view.invalidate(index);
  }
  public int size() {
    return _data.size();
  }

  //XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
  // get sample actions
  //XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

  public ActionListenerIF getAddAction() {
    return new ActionListenerIF() {
      public void actionPerformed(Object source) {
        // add a row to the bottom
        int size = size();
        insert("row data" + size,
               size);
      }
    };
  }

  public ActionListenerIF getRemoveAction() {
    return new ActionListenerIF() {
      public void actionPerformed(Object source) {
        // remove from the top
        if (size() > 0) {
          delete(0);
        }
      }
    };
  }

  public ActionListenerIF getEraseAction() {
    return new ActionListenerIF() {
      public void actionPerformed(Object source) {
        // remove all
        erase();
      }
    };
  }

  public ActionListenerIF getModifyAction() {
    return new ActionListenerIF() {
      public void actionPerformed(Object source) {
        // remove all
        modify("new value", size() - 1);
      }
    };
  }

  //XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
  // get sample menuitems
  //XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

  public MenuItem getAddMenuItem(int ordinal, int priority) {
    return new MenuItem("Add", ordinal, priority) {
      public void run() {
        getAddAction().actionPerformed(_view);
      }
    };
  }

  public MenuItem getRemoveMenuItem(int ordinal, int priority) {
    return new MenuItem("Remove", ordinal, priority) {
      public void run() {
        getRemoveAction().actionPerformed(_view);
      }
    };
  }
  public MenuItem getModifyMenuItem(int oridinal, int priority) {
    return new MenuItem("Modify", oridinal, priority) {
      public void run() {
        getModifyAction().actionPerformed(_view);
      }
    };
  }
  public MenuItem getEraseMenuItem(int ordinal, int priority) {
    return new MenuItem("Erase", ordinal, priority) {
      public void run() {
        getEraseAction().actionPerformed(_view);
      }
    };
  }
}// end class MyListModel
  1. The ActionListenerIF interface is defined just to make it easy to invoke CRUD operations on the model (MyListModel). This make it easy to bind the CRUD operations to menu items as well as buttons (which is not done in this example).
  2. MyListModel stores all the items in the list in a Vector. You can access this underlying backing store at any time, to retrieve the contents of this model.
  3. The constructor of MyListModel binds the model (and view) to the ListField (controller). This makes it a little bit simpler to work with.
  4. A bitmap is provided that’s painted in the drawListRow() method. The drawListRow() method is responsible for painting each row in the list. It draws the bitmap on the left, and the list-item-toString on the right.
  5. Accessors to CRUD operations are provided that make it easy to bind them to buttons (getXXXAction) and menuitems (getXXXMenuItem).
  6. The getXXXAction() methods actually provide the implementation of the CRUD operations on the model and are responsible for syncing the Vector backing store with the ListField.

Other things to keep in mind

  1. In your code, when you need to get access to the currently selected item, be sure to use the following method: myListField.getElementAt(myListField.getSelectedIndex()), where myListField is a reference to the ListField.
  2. in the drawListRow() method if you tried to set the background color on the currently selected row, and it wasn’t working, you have to do this – after setting the background color (by calling Graphics.setBackgroundColor(..), call clear() on the ListField, just before painting the text.

Further learning

There is lots more that you can do with lists, such as providing complex/rich visualizations of the objects in the list model, by providing more advanced overriding of the drawListRow() method. Also, you can use keyword filtered list, which allows you to perform type-ahead-searching into the contents of your list. You can also load large datasets in the background (not EDT), and perform lots of background processing that update the list. One thing to keep in mind when using keyword filter list field is that you have to pass an Object[] to loadFrom(Object) method (and not some other type of object) to pre-load the list model. We provide BlackBerry Bootcamp training programs that will teach you how to do this and much more.

Feedback and comments

To share your thoughts on this tutorial, click here.


Comments are closed.