0

I am facing a problem in synchronizing my model and view threads

I have a view which is table. In it, user can select a few rows. I update the view as soon as the user clicks on any row since I don't want the UI to be slow. This updating is done by a logic which runs in the controller thread below.

At the same time, the controller will update the model data too, which takes place in a different thread. i.e., controller puts the query in a queue, which is then executed by the model thread - which is a single-threaded interface.

As soon as the query executes, controller will get a signal.

Now, In order to keep the view and model synchronized, I will update the view again based on the return value of the query (the data returned by model) - even though I updated the view already for that user action.

But, I am facing issues because, its taking a lot of time for the model to return the result, by that time user would have performed multiple clicks. So, as a result of updating the view again based on the information from model, the view sometimes goes back to the state in which the previous clicks were made

(Suppose user clicks thrice on different rows. I update the view as soon as the click happens. Also, I update the view when I get data back from the model - which is supposed to be same as the already updated state of the view. Now, when the user clicks third time, I get data for the first click from model. As a result, view goes back to a state which is generated by the first click)

Is there any way to handle such a synchronization issue?

  • Just something that is worth noting for the sake of precision here: your code is not MVC. In MVC, the state of the view always represents the current state of the model, and is only changed in response to changes in the model. You may want to separate out an application model to reflect the current ui state as it differs from your domain model. See martinfowler.com/eaaDev/uiArchs.html for a useful overview of this and related patterns – Jules Nov 18 '14 at 10:06
1

You describe a situation in which the user updates three rows. The first transaction completes after the third click, and all three rows update themselves with the stale data.

You need to think about the granularity of your model items. It sounds like you want each row on the UI to represent a single aggregate (transactional boundary). So when an aggregate is updated in the database, you should only update that aggregate's row on the UI.

In other words, I think the events to which your view is binding are too coarse-grained. When a model item is updated in the database, you should consider returning only that item's new state from the back end. Since the commands from the UI are being handled atomically, the resulting events should be atomic too.

Pseudo-C#:

class ViewController
{
    ...
    void RowUpdatedEventHandler(RowUpdatedEvent e)
    {
        var row = this.rows[e.AggregateId];
        row.Name = e.NewName;
        row.Price = e.NewPrice;
    }
}
0

You may want to think about using a MVVM approach. You said you want your view to be responsive, hence, you update on every click, but then you also state that it is taking a lot of time for the model to return the result. These two just don't work together.

With a MVVM design you get a separate view-model (the last VM), which should be created in such a way that it can provide the view with data almost instantaneously when you click another row. Also, if the model data itself is changed, the view-model can tell two things: 1) if the changed data is even relevant to the current view, i.e. whether it is currently displayed at all and 2) whether the change is an actual change as opposed to the data it gave the view last time. In both cases, the view need not change.

Finally, you move the synchronization aspect between the threads to another boundary, where updates on the model need to be sent to the view-model, which may be queried at the same time due to clicks in the view. You have to synchronize on these updates of course, however, both the update of the view-model and the queries should be really fast. In particular, as the view-model shouldn't do much more than derive some string values or such for displaying, it is much faster than a user clicking on different rows. Furthermore, the queries are fast enough to allow blocking in your mouse listener, such that you do not even have to care about race conditions.

tl;dr - create a separate model for your view, which supports fast queries, and make the view updates blocking (i.e. not asynchronous).

0

Have the view maintain a reference to the last element that caused a relevant event, so that it can decide whether to display incoming results or not:

var last_request = nil
void row_selected(any) :
    last_request = any.id
    controller.query(any.id)

void query_completed(id, result) :
    if id == last_request then
        display(result)
    else
        log(result)

(Note: your view probably stores this already by holding a reference to what is currently selected.)

This would also handle the case where people click A, then B, and then A again before the first result is in: showing A without popping up with B.


I doubt if this will work.

Here's a quick and dirty SCCE (in Java) that should prove it works.

public class TheView extends JPanel {
  public static void main(String[] args) {
    SwingUtilities.invokeLater( () -> {
      TheView view = new TheView();
      Controller ctrl = new Controller(view);

      JFrame frame = new JFrame();
      frame.getContentPane().add(view);
      frame.pack();
      frame.setVisible(true);
      frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    });
  }

  JTextArea output;
  JTable table;

  public TheView() {
    super(new BorderLayout());

    table = new JTable(new Object[][] {
      {1, "Tootsie"},
      {8, "Elvis"},
      {14, "Michael"}
    }, new Object[] {
      "ID", "Name"
    });
    add(new JScrollPane(table), BorderLayout.NORTH);

    output = new JTextArea(6, 80);
    add(new JScrollPane(output));
  }

  public void addListSelectionListener(ListSelectionListener l) {
    table.getSelectionModel().addListSelectionListener(l);
  }

  public Object getValueAtSelected(int column) {
    int idx = table.getSelectedRow();
    if ( idx < 0 ) return null;
    return table.getValueAt(idx, column);
  }

  public void addOutput(String str) {
    output.append(str + '\n');
  }
}

class Controller {
  ExecutorService executor = Executors.newCachedThreadPool();
  private volatile int lastID;
  TheView view;

  public Controller(TheView view) {
    this.view = view;
    view.addListSelectionListener( (evt) -> {
      Object o = view.getValueAtSelected(0);
      if ( o instanceof Integer && (Integer) o != lastID ) {
        processRequest((Integer) o);
      }
    });
  }

  public void processRequest(int id) {
    lastID = id;
    executor.submit( () -> {
      offerReply(id, new SlowProcessor(id).call());
      return null;
    });
  }

  void offerReply(int id, String result) {
    if ( id == lastID ) {
      SwingUtilities.invokeLater( () -> view.addOutput(result) );
    }
  }
}

class SlowProcessor implements Callable<String> {
  private final int id;

  public SlowProcessor(int id) {
    this.id = id;
  }

  public String call() throws InterruptedException {
    Thread.sleep(2000);
    final String response = "I am the reply to ID " + id;
    return response;
  }
}
  • I doubt if this will work. Since, the controller updates the view based on the query result from model. So, it would not know for which query id the model is returning the result. It can be an old one too. – Hariprasad May 21 '14 at 5:01
  • I don't really see how it wouldn't work, so I added an example to clarify. – JvR May 21 '14 at 12:55
  • The problem is that I cannot pollute the model API by introducing query ids. Its legacy code, and used by a lot of other apps. – Hariprasad May 21 '14 at 17:33

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.