Simplifying Non-Trivial User Workflows with Conversations

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

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.

12 Comments

  1. Nice articles, keep them coming! I like short and clear code.

    Do you not want to end the conversation though (e.g. when leaving the team page)? No need to keep things in memory if not needed anymore, than waiting for the conversation to timeout I think. Close early I’d say. :)

    • Hi Martijn,

      You are right, in most situations it is a good idea to end() the conversations after the Save or Cancel button is pressed. UserAction can be modified to have the end() method added that calls conversation.end() and the code in the Save button would then look like this: action.apply().end();

      I havent done this in this article because we are still working out how to allow pages to recover from conversation loss in the wicket-cdi project. Right now we throw a ConversationExpiredException when we detect this. The user can listen to this exception in a IRequestCycleListener and act on it.

      For example, if the user goes through a wizard, clicks Finish, and then uses the back button to get to page 2 of 10 and starts changing stuff again we need to show them an error and tell them to start the wizard over. With the conversation not-ended, chances are the UI will still work correctly.

      • Yeah of course, when being busy in a wizard or tabs all related to the same thing – sure keep it open until done. Just meant it as when really leaving the team page (e.g. by the cancel link in your example of the Team).

        • I think you and I were in agreement. Suppose the user goes through a ten step wizard and presses the save button. We end the conversation and redirect to another page. Now the user uses the back button to go to page three of the wizard and presses Next. What should happen at this point? If the conversation is no longer there the application needs to somehow react to this fact…

          • Yeah we are it seems. :)

            And the ConversationExpiredException is fine then, after all the user finished his wizard (ordering a product or anything) and went doing something else. You don’t want to suddenly restart in the middle then or resend (post) the same order. Perhaps you could restart the wizard from the start when you detect this or the user should be educated to just use the menu in this case. :) Guess it all depends on the case you’re dealing with.

  2. Igor,

    With these last two posts you have convinced me that CDI is flat-out important tech. Previously it was something on my mental periphery that I was going to look into “later”.

    So thank you.

    • Hi Scott, Thanks!

      It is important to note that CDI containers are not the only ones with conversational scopes. I think Spring and the like have them as well, but they are missing the non-trivial Wicket integration provided by the wicket-cdi project.

  3. Hey Igor,

    I’m trying to figure out what is the proper pattern for creating new entities in a page (vs editing existing ones). The use case is to create a brand new entity (not yet persisted), and to use this as the model for the various fields on the page, and then save the entity on submit.

    I didn’t see an example of that in blog-cdidemo, but I assumed I could do something like this in the page constructor:

    action.begin();
    IModel model = new ConversationModel(new MyEntity());

    and then during save/submit:

    myRepository.saveMyEntity(model.getObject());
    action.apply();

    But when I try this I see that the MyEntity constructor gets called twice (once at begin(), and once again for when I call “new MyEntity()”). And during apply(), Hibernate complains with “org.hibernate.HibernateException: Don’t change the reference to a collection with cascade=”all-delete-orphan”.

    It’s as if the entity is somehow being constructed twice, and Hibernate sees this as changing one of the entities fields.

    I created a branch from your code and added a simple example “SportsPage”. It mounts at localhost:8080/sports; enter a name and click save and you’ll see what I mean.

    https://github.com/armhold/blog-cdidemo/tree/sports

    Thanks

  4. The constructor being called twice is not strange. The call to action.begin() causes the first access to EntityManager which in turn causes Hibernate to initialize. When Hibernate initializes it test-instantiates all entities to make sure that it can. This is the cause for the first call to entity’s constructor. Set a breakpoint in your entity’s constructor and look at the stacktrace to confirm this.

  5. Yes, the breakpoint does indeed show the ctor call coming from hibernate init, thanks for clearing that up.

    I did some more digging, and the error is happening because deep inside hibernate there is a check when flushing a collection to see that the “key” of the collection is consistent with the database state. I won’t pretend to understand what it’s really doing, but orphanDeleteAndRoleChanged is being set to true at line 232 of org.hibernate.engine.Collections. Some googling shows that this tends to happen when the underlying Session has changed.

    I can persist this object just fine outside of the page (I verified by doing so in ModelInitializer); it’s only when CDI is involved that it throws this error. Not yet ruling out some stupidity on my part, but it seems like something is not quite right with the injection.

    • Seems to me like a bug in Hibernate that has to do with the handling of delete-orphan collections. The sequence:

      1. em.persit(new Sport());
      2. em.getTransaction().begin();
      3. em.flush();
      4. em.getTransaction().commit();

      breaks, while the sequence:

      1. em.getTransaction().begin();
      2. em.persit(new Sport());
      3. em.flush();
      4. em.getTransaction().commit();

      works fine, which doesnt make much sense.

      Another interesting fact is that this happens on transaction commit. Flush works fine and all the sql is written out into jdbc.

      Please file a bug in their JIRA and keep us informed.

Submit a Comment