Simplifying Non-Trivial User Workflows with Conversations

Posted by on Dec 1, 2011 in Full Stack Implementation, Software Engineering

Previously we have seen how to use conversations to make dealing with JPA/Hibernate entities easier. In this article we are going to take it up a notch and see how we can build a non-trivial user workflow easily and cleanly. It is a common usecase in applications to have the user perform several actions and only have their effects applied to the system once the save button is pressed. A well-known example of this is a wizard. Another example is an edit screen where the user is allowed to change multiple properties atomically. Usually a typical way to build something like this is to:

  1. Create a DTO (a.k.a Transfer Object) graph that represents the required entity graph in a serialization-friendly manner
  2. Initialize the DTO graph from the entity graph
  3. Pass the DTO graph across all user actions, this is where the user mutates the data
  4. Once the save button is pressed load the entity graph and apply the DTO graph to it

Steps 1,2,4 require a ton of plumbing code that is boring to write, boring to debug, and really boring to maintain. Luckily, using conversations we can get rid of all of it by allowing the developer to interact directly with entities without worrying about having them become detached, etc.

The Setup

In the interest of keeping things concise we are going to use a contrived data model and usecase. The data model looks like this:

@Entity
public class Employee implements Serializable {
  @GeneratedValue @Id private Long id;
  private String firstName, lastName, email;
  private Date hireDate;
}
 
@Entity
public class Position {
  @GeneratedValue @Id private Long id;
  private String name;
}
 
@Entity
public class Member {
  @GeneratedValue @Id private Long id;
  @ManyToOne(optional = false) private Employee employee;
  @ManyToOne(optional = false) private Position position;
  @Temporal(TemporalType.DATE) private Date effectiveDate;
}
 
@Entity
public class Team implements Serializable {
  @GeneratedValue @Id private Long id;
  private String name;
 
  @ManyToMany(cascade = CascadeType.PERSIST)
  @OrderColumn
  private List<Member> members = new ArrayList<Member>();
}

The model basically consists of Employees that can be assigned into Teams under various Positions. To keep things at least somewhat interesting we make the collection of Employees in a Team ordered, modelling it as a List instead of a mere Set.

The usecase is simple: allow the user to edit a team. The user can change employee’s positions, their effective date, etc. The catch is that the edits happen in a separate modal window and the changes are persisted only when the user presses the save button on the page.

The edit screen will look like this:

And when the edit link is clicked a modal like this will popup:

The Implementation

Lets begin building this by constructing the page and the code to list the team members. Since our team members are in a list we will use ListView to construct the table:

public class TeamEditPage extends BasePage {
 
  @Inject Conversation conversation;
  @Inject EntityManager em;//1
 
  public TeamEditPage(PageParameters params) {
    conversation.begin(); //2
 
    Long teamId = params.get("id").toLong(); //3
    IModel<Team> team = new EntityModel<Team>(Team.class, teamId);
    setDefaultModel(team); //4
 
    Form form = new Form("form");
    add(form);
 
    form.add(new ListView<Member>("members", new PropertyModel(team, "members")) { //5
      protected void populateItem(final ListItem<Member> item) {
        IModel<Member> member = item.getModel();
        item.add(new Label("employee", new PropertyModel(member, "employee.fullName")));
        item.add(new Label("effectiveDate", new PropertyModel(member, "effectiveDate")));
        item.add(new Label("position", new PropertyModel(member, "position.name")));
        item.add(moveUpLink("up", item));
        item.add(moveDownLink("down", item));
      }
    }.setReuseItems(true));
  }
}
  1. The current EntityManager, this will be useful when we implement the Save and Cancel buttons later…
  2. Start the conversation so the EntityManager is persisted across requests, this was explained in the previous article
  3. Retrieve the id of the Team we are going to edit from the url and create a model for the corresponding Team entity
  4. Assign the model to the page so we are guaranteed to have at least one component responsible for detaching it
  5. The ListView for the Member list

It is interesting to note the use of models here. Namely, the fact that there is only one root model: the team model. The rest of components on the page chain off that model instead of keeping references to any entities directly. For example, when the employee Label is going to figure out what it should render it is going to ask its model, which in turn will ask the Item‘s model, which in turn ask the team model. This minimizes the footprint of the page, as well as abstracts and centralizes how and where the Team that is being edited is stored. This is one of the keys to maintainable Wicket code. (Also note that I’ve omitted a lot of generics in the code to keep it shorter.)

Next, lets build the Panel that will be the body of the modal and allow the user to edit a Member:

public abstract class MemberEditPanel extends GenericPanel<Member> {
 
  public MemberEditPanel(String id, IModel<Member> member) { //1
    super(id, member);
 
    final FeedbackPanel feedback = new FeedbackPanel("feedback");
    feedback.setOutputMarkupId(true);
    add(feedback);
 
    Form form = new Form("form");
    add(form);
 
    form.add(new EmployeePicker("employee", new PropertyModel(member, "employee")));
    form.add(DateTextField.forDatePattern("effectiveDate", new PropertyModel(member, "effectiveDate")...);
    form.add(new DropDownChoice("position", new PropertyModel(member, "position")...);
 
    form.add(new AjaxButton("apply") { //2
      protected void onSubmit(AjaxRequestTarget target, Form<?> form) {
        MemberEditPanel.this.onApply(target);
      }
      protected void onError(AjaxRequestTarget target, Form<?> form) {
        target.add(feedback);
      }
    });
    form.add(new AjaxLink("cancel") {
      public void onClick(AjaxRequestTarget target) {
        MemberEditPanel.this.onCancel(target);
      }
    });
  }
 
  protected abstract void onApply(AjaxRequestTarget target);//3
  protected abstract void onCancel(AjaxRequestTarget target);
}
  1. The edit panel gets the Member it will edit as a model
  2. The Apply button in the model delegates to an abstract method
  3. This is the abstract method that users of the panel can implement to define Apply button’s behavior

There is not too much interesting stuff happening here, its basically a trivial edit Panel. One small thing to note, however, is the use of abstract methods that allow users of this panel to define its behavior. This is a great pattern that allows for decoupled code. When the Apply button in the panel is clicked we want to close the modal window that contains the panel. Having the button forward to an abstract method allows us to keep all the modal related code out of the panel, which in turn will allow us to use this panel in or out of a modal.

Lets see how this panel is wired into the page:

final ModalWindow modal = new ModalWindow("modal"); //1
form.add(modal);
 
form.add(new ListView<Member>("members", new PropertyModel(team, "members")) {
  protected void populateItem(final ListItem<Member> item) {
	item.setOutputMarkupId(true); //2
	IModel<Member> member = item.getModel();
	item.add(new AjaxLink<Member>("edit", member) {
	  public void onClick(AjaxRequestTarget target) {
		modal.setContent(new MemberEditPanel(ModalWindow.CONTENT_ID, getModel()) { //3
		  protected void onApply(AjaxRequestTarget target) {
			modal.close(target); //4
			target.add(item);
		  }
		  protected void onCancel(AjaxRequestTarget target) {
			modal.close(target);
		  }
		});
		modal.show(target); //5
	  }
	});
  1. Create the modal window instance we will use to display the edit panel
  2. Tell the Item to output its markup id so we can repaint it via ajax
  3. When the Edit link is pressed we are going to create an instance of MemberEditPanel and put it inside the modal
  4. When the Apply button of the MemberEditPanel is pressed we are going to repaint the table row and close the modal
  5. Show the modal window when the edit link is clicked

The code above is not the prettiest, but my main concern was to keep it short. Again, note the use of models. The Item‘s model that contains the member is passed to the edit link, which in turn passes it to the MemberEditPanel – this is how changes from the submitted form eventually end up in the correct Member instance inside the correct Team.

Now, unto the interesting bits: the Save and the Cancel buttons:

form.add(new Link("cancel") {
  public void onClick() {
	em.clear(); //1
	setResponsePage(TeamsListPage.class);
  }
});
form.add(new Button("save") {
  @Override
  public void onSubmit() {
	em.getTransaction().begin(); //2
	em.flush();
	em.getTransaction().commit();
	setResponsePage(TeamsListPage.class);
  }
});
  1. When the Cancel link is pressed we want to undo any changes we’ve done so far, we do this by simply clearing the EntityManager which ensures none of the changed entities are ever flushed to the database
  2. When the Save button is pressed we flush the changed entities inside a transaction

This is the trick that makes all this work.

However, there is still one bit missing. By default, the EntityManager will flush any pending changes before any query. Since our code mixes queries and modifications of entities and we do not want to flush our modifications until we are ready we need to reconfigure the EntityManager to only flush when we tell it to:

conversation.begin();
em.setFlushMode(FlushModeType.COMMIT); //1
  1. Change the flush mode of the entity manager to flush only on transaction commit

With this last change the interface now works as expected.

A Bit of Cleanup

Messing around with conversations and flush modes is fun, but its probably not something we wish to think of every time we need to build a page. Lets put away all the low-level bits into a utility class:

@ConversationScoped //1
public class UserAction implements Serializable {
 
  @Inject Conversation conversation;
 
  @Inject EntityManager em;
 
  public UserAction begin() { //2
    if (conversation.isTransient()) {
      conversation.begin(); //3
    }
    if (em.getTransaction().isActive()) {
      em.getTransaction().commit(); //4
    }
    em.setFlushMode(FlushModeType.COMMIT); //5
    return this;
  }
 
  public UserAction apply() { //6
    EntityTransaction txn = em.getTransaction();
    txn.begin();
    try {
      em.flush();
    } catch (RuntimeException e) {
      txn.rollback();
      throw e;
    }
    txn.commit();
    return this;
  }
 
  public UserAction undo() { //7
    em.clear();
    return this;
  }
}
  1. Keep the UserAction conversational to keep it aligned with EntityManager. This is somewhat arbitrary since the action is currently stateless, but if we wanted it to keep state the correct scope would be the same as the EntityManager
  2. This method is used to signal the start of the action
  3. If the conversation has not already been made persistent make it so
  4. Get rid of any existing transaction because we want one allocated just for our action
  5. Set the EntityManager‘s flush mode as previously discussed
  6. Applies user’s changes by flushing the EntityManager in a transaction
  7. Clears out user’s changes by clearing the EntityManager thereby removing any modified entities

We can now cleanup our page:

public class TeamEditPage extends BasePage {
 
  @Inject UserAction action;
 
  public TeamEditPage(PageParameters params) {
    action.begin(); //1
 
    form.add(new Button("save") {
      public void onSubmit() {
        action.apply(); //2
        setResponsePage(TeamsListPage.class);
      }
    });
    form.add(new Link("cancel") {
      public void onClick() {
        action.undo(); //3
        setResponsePage(TeamsListPage.class);
      }
    });
  }
}
  1. Signal the beginning of the action
  2. Persist user’s changes
  3. Cancel user’s changes

Potential Problems

Since we have disabled automatic flushing of changes, the queries we run will not see those changes. Sometimes this can lead to inconsistencies between query results and what we expect.

For example, we can delete an entity with EntityManager.remove(), but when we run a query the deleted entity may still be returned as part of the result.

Personally, I have only been bitten by this a few times and in both times it was relatively easy to fix by letting the user interface code filter the query and remove anything it knows has been deleted.

Conclusion

Conversations can make our lives as web developers much easier by enabling illusions such as database transactions that span requests. There are more interesting things we can do with the UserAction abstraction. For example, were we to hook up some CDI events to the apply() and undo() methods we could allow other code to participate in the action’s transaction, etc. Some food for thought.

The code for this article is available here.