-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Issue with undo/redo and modifications to filtered lists #737 #792
Issue with undo/redo and modifications to filtered lists #737 #792
Conversation
v1@yamidark submitted v1 for review.
Checkout this PR version locallygit fetch https://github.com/se-edu/addressbook-level4.git refs/pr/792/1/head:BRANCHNAME where |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[2/3] The rationale for having a requireNonNull(predicate)
is over here: 6075317
I'm thinking of an alternate algorithm as suggested in #737 (comment).
That way, whenever you redo, you will be editing/deleting that particular person, and there's no need to consider both indices & the current predicate / view anymore. What do you think? :) |
Yes, that alternate algorithm seems to be a cleaner way of resolving this bug. Will start working on it. |
@yamidark sorry for the false alarm in #792 (comment)! :( |
@Zhiyuan-Amos Nah, I should have done the check and confirmed the bug myself too. So should I work on using #737 (comment) or continue with the current method? |
Hmm after thinking further about it, I'm not too sure. Iirc, the developer team was thinking of making all commands undoable. I.e. if I execute: In this case, by having But specifically for this PR, I noticed that you will have to call The plus point of using the solution here is that there's no need to call @damithc Prof, what do you think? |
I don't think that's true. A UI framework which re-renders on every trivial change of the model would lead to extremely bad performance. From what I can tell, JavaFX will only re-render once the application thread yields control to the event loop. In other words, if we run a command that gets stuck in an infinite loop, the UI won't update at all. But this prompts me to the fact that performance could also be a factor to consider as well. So, I started to do some simple benchmarks with a large addressbook, only to find an old performance regression instead :-P Issue coming soon. Anyway, I don't mind using another approach. I don't really have a horse in this race, and this is my last post on this matter. |
Wow that's insightful, thanks Paul ^^ @yamidark can you try doing the next iteration just by updating |
1a2c273
to
449969b
Compare
v2@yamidark submitted v2 for review.
(📚 Archive) (📈 Interdiff between v1 and v2) Checkout this PR version locallygit fetch https://github.com/se-edu/addressbook-level4.git refs/pr/792/2/head:BRANCHNAME where |
@yamidark seems like the first iteration looks way neater because there's no need to store the boolean flags. Thanks for your second iteration! :) You can revert back to your first iteration's code, update commit [2/3] as per #792 (review) and submit the 3rd iteration. |
3608440
to
fe40136
Compare
v3@yamidark submitted v3 for review.
(📚 Archive) (📈 Interdiff between v2 and v3) Checkout this PR version locallygit fetch https://github.com/se-edu/addressbook-level4.git refs/pr/792/3/head:BRANCHNAME where |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[2/2]
- Shouldn't Undo be updated as well?
protected final void undo() { requireAllNonNull(model, previousAddressBook); model.resetData(previousAddressBook); model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); }
E.g. if I perform find John then find Mary, undo'ing find Mary will show all people again?
- Take note of the last line in Redo method too
model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS);
- The javadoc for Redo should be updated as well
/**
* Executes the command and updates the filtered person
* list to show all persons.
*/
- Please include, in the commit message, the sequence that you're trying to fix too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[1/2] Commit message: There's no need to back tick all code. We usually back tick only when the code contains multiple words (see c205d6f)
Commit title can be reworded to:
Model: Add method to get filteredPersonList's predicate
Then you can update the 1st and 3rd paragraph accordingly ^^
Also, for the 2nd paragraph, it's more of Other classes have no means to get...
, rather than This prevents other classes from getting...
@@ -148,6 +154,9 @@ public void addPerson(ReadOnlyPerson person) throws DuplicatePersonException { | |||
public ReadOnlyAddressBook getAddressBook() { | |||
return new AddressBook(); | |||
} | |||
|
|||
@Override | |||
public Predicate<ReadOnlyPerson> getFilteredPersonListPredicate() { return PREDICATE_SHOW_ALL_PERSONS; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a reason why you put this method here instead of putting it in ModelStub
, just like for getFilteredPersonListPredicate()
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Zhiyuan-Amos The header comment of ModelStub
mentions that all of its methods should be failing, so I thought it would be more consistent to do the same for getFilteredPersonListPredicate()
to fail in ModelStub
and then return a default value in its subclasses like getAddressBook()
method.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh ok got it! ^^ On first look, it doesn't seem intuitive that both methods getAddressBook()
and getFilteredPersonListPredicate()
are needed here, since the header comments for this class says: A Model stub that always throw a DuplicatePersonException when trying to add a person.
As such, it seems that we only need to override addPerson
.
@eugenepeh should we have some kind of comments to denote that getAddressBook()
and getFilteredPersonListPredicate()
are required for AddCommand
to execute?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm @Zhiyuan-Amos , in such case, I think getFilteredPersonListPredicate()
shouldn't even be called in the first place, since throwing DuplicatePersonException won't make AddCommand execute successfully. Hence @yamidark, you should make getFilteredPersonListPredicate()
fails for ModelStubThrowingDuplicatePersonException
In this few scenarios, the main purpose is to test whether the functionality is working as per expectation. So if its not suppose to execute, we should override the default functionality to make sure it fails. For the ModelStubAcceptingPersonAdded
, however, we can leave it as it is without overriding since
- it doesn't not affect any of the test results
- even if we integrate Undo/Redo into systemtest, the default functionality does not differ from what we're testing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@eugenepeh Actually getFilteredPersonListPredicate()
gets called before the AddCommand
executes and the DuplicatePersonException
gets thrown, since we need to save the "state" before actually executing the command, which is why I needed to override again getFilteredPersonListPredicate()
to pass in ModelStubThrowingDuplicatePersonException
and ModelStubAcceptingPersonAdded
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup, @eugenepeh both getAddressBook()
and getFilteredPersonListPredicate()
are called before addPerson()
is called. So, we can't have getFilteredPersonListPredicate()
failing :P
As such, should we leave comments? Maybe something like
~~~~~~~~~~~~~~~~~These methods below are called prior to calling addPerson(ReadOnlyPerson)~~~~~~
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ahh i c, I thought it was AddCommand execute successfully > get predicate > save state.
In that case, do we even need to override it? It make sense that doing the same for the rest makes it more uniform but at the same time, it doesn't matter (to the test itself) as long as it returns something.
I would prefer retain the default functionality so that our tests cover more area.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm if I'm not wrong, we should override all methods for a Stub
so that no matter what implementations are being changed in ModelManager
's methods, this test will be unaffected. I.e. The Stub
should be independent of ModelManager
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ahh, oh yess... we're extending an empty ModelStub -my turn to get sacked for forgetting the fundamentals >_<-
Yes you're right, I guess the comments are fine then
@@ -165,6 +174,9 @@ public void addPerson(ReadOnlyPerson person) throws DuplicatePersonException { | |||
public ReadOnlyAddressBook getAddressBook() { | |||
return new AddressBook(); | |||
} | |||
|
|||
@Override | |||
public Predicate<ReadOnlyPerson> getFilteredPersonListPredicate() { return PREDICATE_SHOW_ALL_PERSONS; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same as above ^^
@@ -92,6 +92,11 @@ public void updatePerson(ReadOnlyPerson target, ReadOnlyPerson editedPerson) | |||
return FXCollections.unmodifiableObservableList(filteredPersons); | |||
} | |||
|
|||
@Override | |||
public Predicate<ReadOnlyPerson> getFilteredPersonListPredicate() { | |||
return (Predicate<ReadOnlyPerson>) filteredPersons.getPredicate(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this method may return null, we should specify it in the header comments what does null
return means.
@@ -42,6 +54,7 @@ protected final void undo() { | |||
protected final void redo() { | |||
requireNonNull(model); | |||
try { | |||
previousPredicate.ifPresent(model::updateFilteredPersonList); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
btw @Zhiyuan-Amos , in the case where the state is used in both undo and redo, wouldn't it be confusing to have "previous" prefix in variable names?
If I'm reading you correctly, you're saying that Though I'm not sure whether we should be doing that in this PR, since this PR should specifically fix the bug of wrong |
icic. In my point of view, however, the main problem here is that the undo/redo doesn't set the "state" back to the previous one. I believe that by tackling that issue in this PR, we will resolve the existing issue with redo too. Hence, i think it is justifiable that we fix them all in this PR. |
Hmm ok! I think these few choices work:
I think 1. seems good, it seems intuitive as well since by undo-ing, you reset the view to show the user the previous view as well. What do you think? ^^ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[2/2] commit message: Remove back ticks as mentioned in the above comment.
Also, you can be more specific by mentioning that EditCommand
& DeleteCommand
executes on a particular index depending on the current view, which causes the possibility of error. This will then explain why only the tests for EditCommand & DeleteCommand are updated because they are the only commands which executes based on index.
Will review the tests some other time ^^
|
||
/** | ||
* Represents a command which can be undone and redone. | ||
*/ | ||
public abstract class UndoableCommand extends Command { | ||
private ReadOnlyAddressBook previousAddressBook; | ||
private Optional<Predicate<ReadOnlyPerson>> previousPredicate = Optional.empty(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
shouldn't use Optional
as a field in classes :P previousPredicate
can be of type Predicate<ReadOnlyPerson>
.
I'm thinking that for ModelManager#getFilteredPersonListPredicate()
, because FilteredList#getPredicate()
returns null
if there's no Predicate
, so what we can do is (make this code neater, i just typed it out quickly :P):
if (filteredPersons.getPredicate() == null) {
return PREDICATE_SHOW_ALL_PERSONS;
} else {
return (Predicate<ReadOnlyPerson>) filteredPersons.getPredicate();
}
This way, you don't have to deal with a null
return.
That way, in UndoableCommand#redo()
, you can simply:
model.updateFilteredPersonList(previousPredicate);
@@ -24,6 +29,13 @@ private void saveAddressBookSnapshot() { | |||
this.previousAddressBook = new AddressBook(model.getAddressBook()); | |||
} | |||
|
|||
/** | |||
* Stores the current state of {@code model#filteredPersonList#Predicate} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks like a violation of Law of Demeter :P There's no need to be that specific, maybe you can write something like Stores the predicate used by the model to filter the list of persons
.
I also thought of doing 1. initially and removing However, I thought that since I should be focusing only on the current issue in this PR hence I ended up doing 2. instead. As such if fixing the issue of restoring the "state" after |
We don't need to remove
protected final void undo() {
requireAllNonNull(model, previousAddressBook);
model.resetData(previousAddressBook);
model.updateFilteredPersonList(previousPredicate); // the change is only done in this line
} |
@Zhiyuan-Amos Yes, that's right. I just thought that restoring the view for |
@yamidark [3/4] & [4/4] 3rd paragraphs do not match. |
EditCommand stores the target index of the person to edit. As a result, redoing of EditCommand will be executed on the incorrect person due to different indexing in differing views. Let's update EditCommand#preprocessUndoableCommand() to find and store the personToEdit before its first execution. This ensures that future execution of the same command will be on the correct person, independent of the current view. Let's also update EditCommand#equals(Object) method to include checking of personToEdit. This allows other classes to know that EditCommand transits between 2 states, preprocessed and not-preprocessed.
DeleteCommandTest does not have any integration tests for the interaction with UndoCommand and RedoCommand on an unfiltered list. Let's add integration tests for the interaction of DeleteCommand with UndoCommand and RedoCommand on an unfiltered list.
EditCommandTest does not have any integration tests for the interaction with UndoCommand and RedoCommand on an unfiltered list. Let's add integration tests for the interaction of EditCommand with UndoCommand and RedoCommand on an unfiltered list.
2051721
to
4be7dd7
Compare
v18@yamidark submitted v18 for review.
(📚 Archive) (📈 Interdiff between v17 and v18) Checkout this PR version locallygit fetch https://github.com/se-edu/addressbook-level4.git refs/pr/792/18/head:BRANCHNAME where |
Sorry @yamidark , some additional concerns of mine
|
You are right that if the How I see it is that we are recognizing that there are 2 states in which the list can be in: Either filtered or unfiltered. Which is why we choose to test for both states. Not too sure if this argument is strong enough though. |
Hmm, okay. Not a very strong argument but I guess it is sounds legit. Lastly, I would like to point out that if this commit is not going to be squash merged, the commit message for 5/6 and 6/6 needs to be updated.
Integration tests were implemented in 3/6 & 4/6 |
@eugenepeh [5/6] and [6/6] add integration tests on the unfiltered list as stated in the commit message, while [3/6] and [4/6] integration tests are on the filtered list. |
Oh okay, my eye sight are getting worse >_< |
Yup I know it's not a strong argument >< For now it's also for consistency, since for |
Hurray @yamidark! |
…se-edu#792) EditCommand and DeleteCommand stores the target index of the person to edit or delete respectively. As a result, redoing of these index-based commands will be executed on the incorrect person due to different indexing in differing views. Let's update EditCommand and DeleteCommand to find and store the personToEdit or personToDelete respectively before it's first execution. This ensures that future execution of the same command will be on the correct person.
Fixes #737
EditCommand and DeleteCommand stores the target index of the person to edit
or delete respectively.
As a result, redoing of these index-based commands will be executed on the incorrect
person due to different indexing in differing views.
Let's update EditCommand and DeleteCommand to find and store the personToEdit
or personToDelete respectively before it's first execution. This ensures that future
execution of the same command will be on the correct person.