Index: trunk/src/org/openstreetmap/josm/actions/ValidateAction.java
===================================================================
--- trunk/src/org/openstreetmap/josm/actions/ValidateAction.java	(revision 10879)
+++ trunk/src/org/openstreetmap/josm/actions/ValidateAction.java	(revision 10880)
@@ -169,5 +169,5 @@
             }
             tests = null;
-            if (Main.pref.getBoolean(ValidatorPreference.PREF_USE_IGNORE, true)) {
+            if (ValidatorPreference.PREF_USE_IGNORE.get()) {
                 getProgressMonitor().subTask(tr("Updating ignored errors ..."));
                 for (TestError error : errors) {
Index: trunk/src/org/openstreetmap/josm/actions/upload/ValidateUploadHook.java
===================================================================
--- trunk/src/org/openstreetmap/josm/actions/upload/ValidateUploadHook.java	(revision 10879)
+++ trunk/src/org/openstreetmap/josm/actions/upload/ValidateUploadHook.java	(revision 10880)
@@ -61,6 +61,5 @@
             test.visit(selection);
             test.endTest();
-            if (ValidatorPreference.PREF_OTHER.get() &&
-                Main.pref.getBoolean(ValidatorPreference.PREF_OTHER_UPLOAD, false)) {
+            if (ValidatorPreference.PREF_OTHER.get() && ValidatorPreference.PREF_OTHER_UPLOAD.get()) {
                 errors.addAll(test.getErrors());
             } else {
@@ -83,5 +82,5 @@
             return true;
 
-        if (Main.pref.getBoolean(ValidatorPreference.PREF_USE_IGNORE, true)) {
+        if (ValidatorPreference.PREF_USE_IGNORE.get()) {
             int nume = 0;
             for (TestError error : errors) {
Index: trunk/src/org/openstreetmap/josm/data/validation/OsmValidator.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/validation/OsmValidator.java	(revision 10879)
+++ trunk/src/org/openstreetmap/josm/data/validation/OsmValidator.java	(revision 10880)
@@ -183,5 +183,5 @@
     private static void loadIgnoredErrors() {
         ignoredErrors.clear();
-        if (Main.pref.getBoolean(ValidatorPreference.PREF_USE_IGNORE, true)) {
+        if (ValidatorPreference.PREF_USE_IGNORE.get()) {
             Path path = Paths.get(getValidatorDir()).resolve("ignorederrors");
             if (Files.exists(path)) {
@@ -217,5 +217,5 @@
 
     public static synchronized void initializeErrorLayer() {
-        if (!Main.pref.getBoolean(ValidatorPreference.PREF_LAYER, true))
+        if (!ValidatorPreference.PREF_LAYER.get())
             return;
         if (errorLayer == null) {
Index: trunk/src/org/openstreetmap/josm/data/validation/TestError.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/validation/TestError.java	(revision 10879)
+++ trunk/src/org/openstreetmap/josm/data/validation/TestError.java	(revision 10880)
@@ -269,4 +269,8 @@
     }
 
+    /**
+     * Set the tester that raised the error.
+     * @param tester te tester
+     */
     public void setTester(Test tester) {
         this.tester = tester;
Index: trunk/src/org/openstreetmap/josm/gui/dialogs/ValidatorDialog.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/dialogs/ValidatorDialog.java	(revision 10879)
+++ trunk/src/org/openstreetmap/josm/gui/dialogs/ValidatorDialog.java	(revision 10880)
@@ -151,5 +151,5 @@
         buttons.add(fixButton);
 
-        if (Main.pref.getBoolean(ValidatorPreference.PREF_USE_IGNORE, true)) {
+        if (ValidatorPreference.PREF_USE_IGNORE.get()) {
             ignoreButton = new SideButton(new AbstractAction() {
                 {
@@ -193,5 +193,4 @@
         }
         super.setVisible(v);
-        Main.map.repaint();
     }
 
Index: trunk/src/org/openstreetmap/josm/gui/dialogs/validator/ValidatorTreePanel.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/dialogs/validator/ValidatorTreePanel.java	(revision 10879)
+++ trunk/src/org/openstreetmap/josm/gui/dialogs/validator/ValidatorTreePanel.java	(revision 10880)
@@ -11,5 +11,4 @@
 import java.util.EnumMap;
 import java.util.Enumeration;
-import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
@@ -18,4 +17,5 @@
 import java.util.Set;
 import java.util.function.Predicate;
+import java.util.stream.Collectors;
 
 import javax.swing.JTree;
@@ -36,5 +36,5 @@
 import org.openstreetmap.josm.gui.util.GuiHelper;
 import org.openstreetmap.josm.tools.Destroyable;
-import org.openstreetmap.josm.tools.MultiMap;
+import org.openstreetmap.josm.tools.ListenerList;
 
 /**
@@ -73,6 +73,5 @@
     private transient Set<? extends OsmPrimitive> filter;
 
-    /** a counter to check if tree has been rebuild */
-    private int updateCount;
+    private final ListenerList<Runnable> invalidationListeners = ListenerList.create();
 
     /**
@@ -135,4 +134,5 @@
         }
         super.setVisible(v);
+        invalidationListeners.fireEvent(Runnable::run);
     }
 
@@ -141,5 +141,4 @@
      */
     public void buildTree() {
-        updateCount++;
         final DefaultMutableTreeNode rootNode = new DefaultMutableTreeNode();
 
@@ -172,54 +171,19 @@
         }
 
-        Map<Severity, MultiMap<String, TestError>> errorTree = new EnumMap<>(Severity.class);
-        Map<Severity, HashMap<String, MultiMap<String, TestError>>> errorTreeDeep = new EnumMap<>(Severity.class);
-        for (Severity s : Severity.values()) {
-            errorTree.put(s, new MultiMap<String, TestError>(20));
-            errorTreeDeep.put(s, new HashMap<String, MultiMap<String, TestError>>());
-        }
-
-        final Boolean other = ValidatorPreference.PREF_OTHER.get();
-        for (TestError e : errors) {
-            if (e.isIgnored()) {
-                continue;
-            }
-            Severity s = e.getSeverity();
-            if (!other && s == Severity.OTHER) {
-                continue;
-            }
-            String d = e.getDescription();
-            String m = e.getMessage();
-            if (filter != null) {
-                boolean found = false;
-                for (OsmPrimitive p : e.getPrimitives()) {
-                    if (filter.contains(p)) {
-                        found = true;
-                        break;
-                    }
-                }
-                if (!found) {
-                    continue;
-                }
-            }
-            if (d != null) {
-                MultiMap<String, TestError> b = errorTreeDeep.get(s).get(m);
-                if (b == null) {
-                    b = new MultiMap<>(20);
-                    errorTreeDeep.get(s).put(m, b);
-                }
-                b.put(d, e);
-            } else {
-                errorTree.get(s).put(m, e);
-            }
-        }
+        Predicate<TestError> filterToUse = e -> !e.isIgnored();
+        if (!ValidatorPreference.PREF_OTHER.get()) {
+            filterToUse = filterToUse.and(e -> e.getSeverity() != Severity.OTHER);
+        }
+        if (filter != null) {
+            filterToUse = filterToUse.and(e -> e.getPrimitives().stream().anyMatch(filter::contains));
+        }
+        Map<Severity, Map<String, Map<String, List<TestError>>>> errorTreeDeep
+            = errors.stream().filter(filterToUse).collect(
+                    Collectors.groupingBy(e -> e.getSeverity(), () -> new EnumMap<>(Severity.class),
+                            Collectors.groupingBy(e -> e.getDescription() == null ? "" : e.getDescription(),
+                                    Collectors.groupingBy(e -> e.getMessage()))));
 
         List<TreePath> expandedPaths = new ArrayList<>();
-        for (Severity s : Severity.values()) {
-            MultiMap<String, TestError> severityErrors = errorTree.get(s);
-            Map<String, MultiMap<String, TestError>> severityErrorsDeep = errorTreeDeep.get(s);
-            if (severityErrors.isEmpty() && severityErrorsDeep.isEmpty()) {
-                continue;
-            }
-
+        errorTreeDeep.forEach((s, severityErrorsDeep) -> {
             // Severity node
             DefaultMutableTreeNode severityNode = new GroupTreeNode(s);
@@ -230,41 +194,44 @@
             }
 
-            for (Entry<String, Set<TestError>> msgErrors : severityErrors.entrySet()) {
-                // Message node
-                Set<TestError> errs = msgErrors.getValue();
-                String msg = tr("{0} ({1})", msgErrors.getKey(), errs.size());
-                DefaultMutableTreeNode messageNode = new DefaultMutableTreeNode(msg);
-                severityNode.add(messageNode);
-
-                if (oldSelectedRows.contains(msgErrors.getKey())) {
-                    expandedPaths.add(new TreePath(new Object[] {rootNode, severityNode, messageNode}));
-                }
-
-                for (TestError error : errs) {
-                    // Error node
-                    DefaultMutableTreeNode errorNode = new DefaultMutableTreeNode(error);
-                    messageNode.add(errorNode);
-                }
-            }
-            for (Entry<String, MultiMap<String, TestError>> bag : severityErrorsDeep.entrySet()) {
+            Map<String, List<TestError>> severityErrors = severityErrorsDeep.get("");
+            if (severityErrors != null) {
+                for (Entry<String, List<TestError>> msgErrors : severityErrors.entrySet()) {
+                    // Message node
+                    List<TestError> errs = msgErrors.getValue();
+                    String msg = tr("{0} ({1})", msgErrors.getKey(), errs.size());
+                    DefaultMutableTreeNode messageNode = new DefaultMutableTreeNode(msg);
+                    severityNode.add(messageNode);
+
+                    if (oldSelectedRows.contains(msgErrors.getKey())) {
+                        expandedPaths.add(new TreePath(new Object[] {rootNode, severityNode, messageNode}));
+                    }
+
+                    errs.stream().map(DefaultMutableTreeNode::new).forEach(messageNode::add);
+                }
+            }
+
+            severityErrorsDeep.forEach((description, errorlist) -> {
+                if (description.isEmpty()) {
+                    return;
+                }
                 // Group node
-                MultiMap<String, TestError> errorlist = bag.getValue();
-                DefaultMutableTreeNode groupNode = null;
+                DefaultMutableTreeNode groupNode;
                 if (errorlist.size() > 1) {
-                    groupNode = new GroupTreeNode(bag.getKey());
+                    groupNode = new GroupTreeNode(description);
                     severityNode.add(groupNode);
-                    if (oldSelectedRows.contains(bag.getKey())) {
+                    if (oldSelectedRows.contains(description)) {
                         expandedPaths.add(new TreePath(new Object[] {rootNode, severityNode, groupNode}));
                     }
-                }
-
-                for (Entry<String, Set<TestError>> msgErrors : errorlist.entrySet()) {
+                } else {
+                    groupNode = null;
+                }
+
+                errorlist.forEach((message, errs) -> {
                     // Message node
-                    Set<TestError> errs = msgErrors.getValue();
                     String msg;
                     if (groupNode != null) {
-                        msg = tr("{0} ({1})", msgErrors.getKey(), errs.size());
+                        msg = tr("{0} ({1})", message, errs.size());
                     } else {
-                        msg = tr("{0} - {1} ({2})", msgErrors.getKey(), bag.getKey(), errs.size());
+                        msg = tr("{0} - {1} ({2})", message, description, errs.size());
                     }
                     DefaultMutableTreeNode messageNode = new DefaultMutableTreeNode(msg);
@@ -275,5 +242,5 @@
                     }
 
-                    if (oldSelectedRows.contains(msgErrors.getKey())) {
+                    if (oldSelectedRows.contains(message)) {
                         if (groupNode != null) {
                             expandedPaths.add(new TreePath(new Object[] {rootNode, severityNode, groupNode, messageNode}));
@@ -283,12 +250,8 @@
                     }
 
-                    for (TestError error : errs) {
-                        // Error node
-                        DefaultMutableTreeNode errorNode = new DefaultMutableTreeNode(error);
-                        messageNode.add(errorNode);
-                    }
-                }
-            }
-        }
+                    errs.stream().map(DefaultMutableTreeNode::new).forEach(messageNode::add);
+                });
+            });
+        });
 
         valTreeModel.setRoot(rootNode);
@@ -296,4 +259,23 @@
             this.expandPath(path);
         }
+
+        invalidationListeners.fireEvent(Runnable::run);
+    }
+
+    /**
+     * Add a new invalidation listener
+     * @param listener The listener
+     */
+    public void addInvalidationListener(Runnable listener) {
+        invalidationListeners.addListener(listener);
+    }
+
+    /**
+     * Remove an invalidation listener
+     * @param listener The listener
+     * @since 10880
+     */
+    public void removeInvalidationListener(Runnable listener) {
+        invalidationListeners.removeListener(listener);
     }
 
@@ -426,12 +408,4 @@
     }
 
-    /**
-     * Returns a value to check if tree has been rebuild
-     * @return the current counter
-     */
-    public int getUpdateCount() {
-        return updateCount;
-    }
-
     private void clearErrors() {
         if (errors != null) {
Index: trunk/src/org/openstreetmap/josm/gui/layer/ValidatorLayer.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/layer/ValidatorLayer.java	(revision 10879)
+++ trunk/src/org/openstreetmap/josm/gui/layer/ValidatorLayer.java	(revision 10880)
@@ -41,6 +41,5 @@
  */
 public class ValidatorLayer extends Layer implements LayerChangeListener {
-
-    private int updateCount = -1;
+    private final Runnable invalidator = this::invalidate;
 
     /**
@@ -50,4 +49,5 @@
         super(tr("Validation errors"));
         Main.getLayerManager().addLayerChangeListener(this);
+        Main.map.validatorDialog.tree.addInvalidationListener(invalidator);
     }
 
@@ -68,5 +68,4 @@
     @Override
     public void paint(final Graphics2D g, final MapView mv, Bounds bounds) {
-        updateCount = Main.map.validatorDialog.tree.getUpdateCount();
         DefaultMutableTreeNode root = Main.map.validatorDialog.tree.getRoot();
         if (root == null || root.getChildCount() == 0)
@@ -124,9 +123,4 @@
 
     @Override
-    public boolean isChanged() {
-        return updateCount != Main.map.validatorDialog.tree.getUpdateCount();
-    }
-
-    @Override
     public void visitBoundingBox(BoundingXYVisitor v) {
         // Do nothing
@@ -168,5 +162,4 @@
             e.scheduleRemoval(Collections.singleton(this));
         } else if (e.getRemovedLayer() == this) {
-            Main.getLayerManager().removeLayerChangeListener(this);
             OsmValidator.errorLayer = null;
         }
@@ -177,3 +170,10 @@
         return LayerPositionStrategy.IN_FRONT;
     }
+
+    @Override
+    public void destroy() {
+        Main.map.validatorDialog.tree.removeInvalidationListener(invalidator);
+        Main.getLayerManager().removeLayerChangeListener(this);
+        super.destroy();
+    }
 }
Index: trunk/src/org/openstreetmap/josm/gui/preferences/validator/ValidatorPreference.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/preferences/validator/ValidatorPreference.java	(revision 10879)
+++ trunk/src/org/openstreetmap/josm/gui/preferences/validator/ValidatorPreference.java	(revision 10880)
@@ -40,5 +40,5 @@
 
     /** The preferences key for error layer */
-    public static final String PREF_LAYER = PREFIX + ".layer";
+    public static final BooleanProperty PREF_LAYER = new BooleanProperty(PREFIX + ".layer", true);
 
     /** The preferences key for enabled tests */
@@ -46,5 +46,5 @@
 
     /** The preferences key for enabled tests */
-    public static final String PREF_USE_IGNORE = PREFIX + ".ignore";
+    public static final BooleanProperty PREF_USE_IGNORE = new BooleanProperty(PREFIX + ".ignore", true);
 
     /** The preferences key for enabled tests before upload*/
@@ -52,5 +52,5 @@
 
     /** The preferences key for ignored severity other on upload */
-    public static final String PREF_OTHER_UPLOAD = PREFIX + ".otherUpload";
+    public static final BooleanProperty PREF_OTHER_UPLOAD = new BooleanProperty(PREFIX + ".otherUpload", false);
 
     /** The preferences for ignored severity other */
Index: trunk/src/org/openstreetmap/josm/gui/preferences/validator/ValidatorTestsPreference.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/preferences/validator/ValidatorTestsPreference.java	(revision 10879)
+++ trunk/src/org/openstreetmap/josm/gui/preferences/validator/ValidatorTestsPreference.java	(revision 10880)
@@ -58,9 +58,9 @@
         testPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
 
-        prefUseIgnore = new JCheckBox(tr("Use ignore list."), Main.pref.getBoolean(ValidatorPreference.PREF_USE_IGNORE, true));
+        prefUseIgnore = new JCheckBox(tr("Use ignore list."), ValidatorPreference.PREF_USE_IGNORE.get());
         prefUseIgnore.setToolTipText(tr("Use the ignore list to suppress warnings."));
         testPanel.add(prefUseIgnore, GBC.eol());
 
-        prefUseLayer = new JCheckBox(tr("Use error layer."), Main.pref.getBoolean(ValidatorPreference.PREF_LAYER, true));
+        prefUseLayer = new JCheckBox(tr("Use error layer."), ValidatorPreference.PREF_LAYER.get());
         prefUseLayer.setToolTipText(tr("Use the error layer to display problematic elements."));
         testPanel.add(prefUseLayer, GBC.eol());
@@ -71,5 +71,5 @@
 
         prefOtherUpload = new JCheckBox(tr("Show informational level on upload."),
-                Main.pref.getBoolean(ValidatorPreference.PREF_OTHER_UPLOAD, false));
+                ValidatorPreference.PREF_OTHER_UPLOAD.get());
         prefOtherUpload.setToolTipText(tr("Show the informational tests in the upload check windows."));
         testPanel.add(prefOtherUpload, GBC.eol());
@@ -117,8 +117,8 @@
         Main.pref.putCollection(ValidatorPreference.PREF_SKIP_TESTS, tests);
         Main.pref.putCollection(ValidatorPreference.PREF_SKIP_TESTS_BEFORE_UPLOAD, testsBeforeUpload);
-        Main.pref.put(ValidatorPreference.PREF_USE_IGNORE, prefUseIgnore.isSelected());
+        ValidatorPreference.PREF_USE_IGNORE.put(prefUseIgnore.isSelected());
         ValidatorPreference.PREF_OTHER.put(prefOther.isSelected());
-        Main.pref.put(ValidatorPreference.PREF_OTHER_UPLOAD, prefOtherUpload.isSelected());
-        Main.pref.put(ValidatorPreference.PREF_LAYER, prefUseLayer.isSelected());
+        ValidatorPreference.PREF_OTHER_UPLOAD.put(prefOtherUpload.isSelected());
+        ValidatorPreference.PREF_LAYER.put(prefUseLayer.isSelected());
         return false;
     }
