diff --git a/Framework/PythonInterface/mantid/api/src/Exports/ITableWorkspace.cpp b/Framework/PythonInterface/mantid/api/src/Exports/ITableWorkspace.cpp
index b88671b0c277cec4cea21714f44ee0e850ae431a..aebe266ed2d00f06c1defd73d351bf44e7b33544 100644
--- a/Framework/PythonInterface/mantid/api/src/Exports/ITableWorkspace.cpp
+++ b/Framework/PythonInterface/mantid/api/src/Exports/ITableWorkspace.cpp
@@ -471,12 +471,16 @@ PyObject *cell(ITableWorkspace &self, const object &value, int row_or_col) {
  * column if value is an index
  */
 void setCell(ITableWorkspace &self, const object &col_or_row,
-             const int row_or_col, const object &value) {
+             const int row_or_col, const object &value,
+             const bool &notify_replace) {
   Mantid::API::Column_sptr column;
   int row(-1);
   getCellLoc(self, col_or_row, row_or_col, column, row);
   setValue(column, row, value);
-  self.modified();
+
+  if (notify_replace) {
+    self.modified();
+  }
 }
 } // namespace
 
@@ -670,10 +674,11 @@ void export_ITableWorkspace() {
 
       .def("setCell", &setCell,
            (arg("self"), arg("row_or_column"), arg("column_or_row"),
-            arg("value")),
+            arg("value"), arg("notify_replace") = true),
            "Sets the value of a given cell. If the row_or_column argument is a "
            "number then it is interpreted as a row otherwise it "
-           "is interpreted as a column name.")
+           "is interpreted as a column name. If notify replace is false, then "
+           "the replace workspace event is not triggered.")
 
       .def("toDict", &toDict, (arg("self")),
            "Gets the values of this workspace as a dictionary. The keys of the "
diff --git a/qt/applications/workbench/workbench/plotting/figuremanager.py b/qt/applications/workbench/workbench/plotting/figuremanager.py
index 728a24aa83330b07bfad9918d2601f123c46d792..4d8cfefd502da9b8aacc7fe2c8552f762074cde4 100644
--- a/qt/applications/workbench/workbench/plotting/figuremanager.py
+++ b/qt/applications/workbench/workbench/plotting/figuremanager.py
@@ -92,7 +92,7 @@ class FigureManagerADSObserver(AnalysisDataServiceObserver):
     def replaceHandle(self, _, workspace):
         """
         Called when the ADS has replaced a workspace with one of the same name.
-        If this workspace is attached tho this figure then its data is updated
+        If this workspace is attached to this figure then its data is updated
         :param _: The name of the workspace. Unused
         :param workspace: A reference to the new workspace
         """
diff --git a/qt/applications/workbench/workbench/plugins/workspacewidget.py b/qt/applications/workbench/workbench/plugins/workspacewidget.py
index 3be0d0b1d1de63d12ab2c6ce1e2c3824ee225df3..0f80d5a968f1e908dae1446cfb2c76d711029ad4 100644
--- a/qt/applications/workbench/workbench/plugins/workspacewidget.py
+++ b/qt/applications/workbench/workbench/plugins/workspacewidget.py
@@ -137,7 +137,7 @@ class WorkspaceWidget(PluginWidget):
                     presenter.view.show()
                 except ValueError:
                     logger.error(
-                        "Could not open workspace: {0} with either MatrixWorkspaceDisplay nor TableWorkspaceDisplay.")
+                        "Could not open workspace: {0} with neither MatrixWorkspaceDisplay nor TableWorkspaceDisplay.")
 
     def _action_double_click_workspace(self, name):
         self._do_show_data([name])
diff --git a/qt/python/CMakeLists.txt b/qt/python/CMakeLists.txt
index f6874684176a792eb3629871a7d0e58b266c3454..d69051578fee39b12c72d85e46abe3f57eaa8926 100644
--- a/qt/python/CMakeLists.txt
+++ b/qt/python/CMakeLists.txt
@@ -100,6 +100,10 @@ endif ()
     mantidqt/widgets/samplelogs/test/test_samplelogs_model.py
     mantidqt/widgets/samplelogs/test/test_samplelogs_presenter.py
 
+    mantidqt/widgets/common/test/test_workspacedisplay_ads_observer.py
+    mantidqt/widgets/common/test/test_observing_presenter.py
+    mantidqt/widgets/common/test/test_observing_view.py
+
     mantidqt/widgets/matrixworkspacedisplay/test/test_matrixworkspacedisplay_model.py
     mantidqt/widgets/matrixworkspacedisplay/test/test_matrixworkspacedisplay_presenter.py
     mantidqt/widgets/matrixworkspacedisplay/test/test_matrixworkspacedisplay_table_view_model.py
diff --git a/qt/python/mantidqt/widgets/common/observing_presenter.py b/qt/python/mantidqt/widgets/common/observing_presenter.py
new file mode 100644
index 0000000000000000000000000000000000000000..b71c0e8a3893d5cea023b295647e365a1eca5588
--- /dev/null
+++ b/qt/python/mantidqt/widgets/common/observing_presenter.py
@@ -0,0 +1,49 @@
+# Mantid Repository : https://github.com/mantidproject/mantid
+#
+# Copyright © 2018 ISIS Rutherford Appleton Laboratory UKRI,
+#     NScD Oak Ridge National Laboratory, European Spallation Source
+#     & Institut Laue - Langevin
+# SPDX - License - Identifier: GPL - 3.0 +
+#  This file is part of the mantid workbench.
+
+from __future__ import (absolute_import, division, print_function)
+
+
+class ObservingPresenter(object):
+    """
+    This class provides some common functions for classes that need to be observable.
+    It is not a GUI class, and if used, should be inherited by the presenter.
+    It is designed to be used with a view that inherits `ObservingView`.
+
+    The presenter that is inheriting this must initialise a model and a view,
+    that implement the methods required in the functions below.
+    """
+
+    def clear_observer(self):
+        """
+        If the observer is not cleared here then the C++ object is never freed,
+        and observers keep getting created, and triggering on ADS events.
+
+        This method is called from the view's close_later to ensure that
+        observer are deleted both when the workspace is deleted and when
+        the view window is closed manually (via the X)
+        """
+        self.ads_observer = None
+
+    def close(self, workspace_name):
+        if self.model.workspace_equals(workspace_name):
+            # if the observer is not cleared here then the C++ object is never freed,
+            # and observers keep getting created, and triggering on ADS events
+            self.ads_observer = None
+            self.view.emit_close()
+
+    def force_close(self):
+        self.ads_observer = None
+        self.view.emit_close()
+
+    def replace_workspace(self, workspace_name, workspace):
+        raise NotImplementedError("This method must be overridden.")
+
+    def rename_workspace(self, old_name, new_name):
+        if self.model.workspace_equals(old_name):
+            self.view.emit_rename(new_name)
diff --git a/qt/python/mantidqt/widgets/common/observing_view.py b/qt/python/mantidqt/widgets/common/observing_view.py
new file mode 100644
index 0000000000000000000000000000000000000000..9b549c6a8b41bb9bf5912e9226b5c2bf3c792299
--- /dev/null
+++ b/qt/python/mantidqt/widgets/common/observing_view.py
@@ -0,0 +1,54 @@
+# Mantid Repository : https://github.com/mantidproject/mantid
+#
+# Copyright © 2019 ISIS Rutherford Appleton Laboratory UKRI,
+#     NScD Oak Ridge National Laboratory, European Spallation Source
+#     & Institut Laue - Langevin
+# SPDX - License - Identifier: GPL - 3.0 +
+#  This file is part of the mantid workbench.
+
+from __future__ import (absolute_import, division, print_function)
+
+
+class ObservingView(object):
+    """
+    This class provides some common functions needed across views observing the ADS.
+    It runs the close_signal so that the view is closed from the GUI thread,
+    and ensures that the closeEvent will clear the observer to prevent a memory leak.
+
+    It is designed to be used with a presenter that inherits `ObservingPresenter`.
+
+    If this class is inherited a `close_signal` and a `rename_signal` must be declared.
+
+    It is not possible to do that here, as this is not a QObject, however it was
+    attempted to do the declaration here, but the signals don't seem to work
+    through inheritance.
+
+    This class shouldn't inherit from a QObject/QWidget, otherwise the closeEvent
+    doesn't replace the closeEvent of the view that is inheriting this. This also
+    makes it easily testable, as the GUI library isn't pulled in.
+    """
+
+    TITLE_STRING = "{} - Mantid"
+
+    def emit_close(self):
+        """
+        Emits a close signal to the main GUI thread that triggers the built-in close method.
+
+        This function can be called from outside the main GUI thread. It is currently
+        used to close the window when the relevant workspace is deleted.
+
+        When the signal is emitted, the execution returns to the GUI thread, and thus
+        GUI code can be executed.
+        """
+        self.close_signal.emit()
+
+    def closeEvent(self, event):
+        # This clear prevents a leak when the window is closed from X by the user
+        self.presenter.clear_observer()
+        event.accept()
+
+    def emit_rename(self, new_name):
+        self.rename_signal.emit(new_name)
+
+    def _rename(self, new_name):
+        self.setWindowTitle(self.TITLE_STRING.format(new_name))
diff --git a/qt/python/mantidqt/widgets/common/test/__init__.py b/qt/python/mantidqt/widgets/common/test/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/qt/python/mantidqt/widgets/common/test/test_observing_presenter.py b/qt/python/mantidqt/widgets/common/test/test_observing_presenter.py
new file mode 100644
index 0000000000000000000000000000000000000000..16aaf163512f939ab90158274d523e1201ecf755
--- /dev/null
+++ b/qt/python/mantidqt/widgets/common/test/test_observing_presenter.py
@@ -0,0 +1,63 @@
+# Mantid Repository : https://github.com/mantidproject/mantid
+#
+# Copyright © 2019 ISIS Rutherford Appleton Laboratory UKRI,
+#     NScD Oak Ridge National Laboratory, European Spallation Source
+#     & Institut Laue - Langevin
+# SPDX - License - Identifier: GPL - 3.0 +
+#  This file is part of the mantid workbench.
+from __future__ import (absolute_import, division, print_function)
+
+import unittest
+
+from mantidqt.widgets.common.test_mocks.mock_observing import MockObservingPresenter
+
+
+def with_presenter(workspaces_are_equal=True):
+    def func_wrapper(func):
+        def wrapper(self, *args, **kwargs):
+            presenter = MockObservingPresenter(workspaces_are_equal)
+            return func(self, presenter, *args, **kwargs)
+
+        return wrapper
+
+    return func_wrapper
+
+
+class ObservingPresenterTest(unittest.TestCase):
+    @with_presenter()
+    def test_close(self, presenter):
+        mock_name = "dawn"
+        presenter.close(mock_name)
+        presenter.model.workspace_equals.assert_called_once_with(mock_name)
+        presenter.view.close_signal.emit.assert_called_once_with()
+        self.assertIsNone(presenter.ads_observer)
+
+    @with_presenter(workspaces_are_equal=False)
+    def test_not_closing_with_invalid_name(self, presenter):
+        mock_name = "dawn"
+        presenter.close(mock_name)
+        presenter.model.workspace_equals.assert_called_once_with(mock_name)
+        self.assertEqual(0, presenter.view.close_signal.emit.call_count)
+        self.assertIsNotNone(presenter.ads_observer)
+
+    @with_presenter(workspaces_are_equal=False)
+    def test_force_close(self, presenter):
+        presenter.force_close()
+        presenter.view.close_signal.emit.assert_called_once_with()
+        self.assertIsNone(presenter.ads_observer)
+
+    @with_presenter(workspaces_are_equal=False)
+    def test_replace_workspace_not_implemented(self, presenter):
+        self.assertRaises(NotImplementedError, presenter.replace_workspace, "", "")
+
+    @with_presenter()
+    def test_rename_workspace(self, presenter):
+        new_name = "xax"
+        presenter.rename_workspace("", new_name)
+        presenter.view.rename_signal.emit.assert_called_once_with(new_name)
+
+    @with_presenter(workspaces_are_equal=False)
+    def test_not_renaming_workspace_with_invalid_name(self, presenter):
+        new_name = "xax"
+        presenter.rename_workspace("", new_name)
+        self.assertEqual(0, presenter.view.rename_signal.emit.call_count)
diff --git a/qt/python/mantidqt/widgets/common/test/test_observing_view.py b/qt/python/mantidqt/widgets/common/test/test_observing_view.py
new file mode 100644
index 0000000000000000000000000000000000000000..58efc1447b5517c2fe6b2077907f5a5d08e0beb0
--- /dev/null
+++ b/qt/python/mantidqt/widgets/common/test/test_observing_view.py
@@ -0,0 +1,39 @@
+# Mantid Repository : https://github.com/mantidproject/mantid
+#
+# Copyright © 2019 ISIS Rutherford Appleton Laboratory UKRI,
+#     NScD Oak Ridge National Laboratory, European Spallation Source
+#     & Institut Laue - Langevin
+# SPDX - License - Identifier: GPL - 3.0 +
+#  This file is part of the mantid workbench.
+from __future__ import (absolute_import, division, print_function)
+
+import unittest
+
+from mantidqt.widgets.common.test_mocks.mock_observing import MockObservingView
+from mantidqt.widgets.common.test_mocks.mock_qt import MockQtEvent
+
+
+class ObservingViewTest(unittest.TestCase):
+
+    def setUp(self):
+        self.view = MockObservingView(None)
+
+    def test_emit_close(self):
+        self.view.emit_close()
+        self.view.close_signal.emit.assert_called_once_with()
+
+    def test_closeEvent(self):
+        mock_event = MockQtEvent()
+        self.view.closeEvent(mock_event)
+        self.view.presenter.clear_observer.assert_called_once_with()
+        mock_event.accept.assert_called_once_with()
+
+    def test_rename(self):
+        new_name = "123edsad"
+        self.view.emit_rename(new_name)
+        self.view.rename_signal.emit.assert_called_once_with(new_name)
+
+    def test_rename_action(self):
+        new_name = "123edsad"
+        self.view._rename(new_name)
+        self.view.setWindowTitle.assert_called_once_with(self.view.TITLE_STRING.format(new_name))
diff --git a/qt/python/mantidqt/widgets/common/test/test_workspacedisplay_ads_observer.py b/qt/python/mantidqt/widgets/common/test/test_workspacedisplay_ads_observer.py
new file mode 100644
index 0000000000000000000000000000000000000000..989537a345c445d950df253e683e20660e44d2fa
--- /dev/null
+++ b/qt/python/mantidqt/widgets/common/test/test_workspacedisplay_ads_observer.py
@@ -0,0 +1,45 @@
+# Mantid Repository : https://github.com/mantidproject/mantid
+#
+# Copyright © 2019 ISIS Rutherford Appleton Laboratory UKRI,
+#     NScD Oak Ridge National Laboratory, European Spallation Source
+#     & Institut Laue - Langevin
+# SPDX - License - Identifier: GPL - 3.0 +
+#  This file is part of the mantid workbench.
+from __future__ import (absolute_import, division, print_function)
+
+import unittest
+
+from mock import Mock
+
+from mantidqt.widgets.common.workspacedisplay_ads_observer import WorkspaceDisplayADSObserver
+
+
+class MockWorkspaceDisplay:
+    def __init__(self):
+        self.close = Mock()
+        self.force_close = Mock()
+        self.replace_workspace = Mock()
+
+
+class WorkspaceDisplayADSObserverTest(unittest.TestCase):
+    def test_clearHandle(self):
+        mock_wsd = MockWorkspaceDisplay()
+        observer = WorkspaceDisplayADSObserver(mock_wsd)
+        observer.clearHandle()
+        mock_wsd.force_close.assert_called_once_with()
+
+    def test_deleteHandle(self):
+        mock_wsd = MockWorkspaceDisplay()
+        observer = WorkspaceDisplayADSObserver(mock_wsd)
+        expected_name = "adad"
+        observer.deleteHandle(expected_name, None)
+        mock_wsd.close.assert_called_once_with(expected_name)
+
+    def test_replaceHandle(self):
+        mock_wsd = MockWorkspaceDisplay()
+        observer = WorkspaceDisplayADSObserver(mock_wsd)
+
+        expected_name = "a"
+        expected_parameter = 444555.158
+        observer.replaceHandle(expected_name, expected_parameter)
+        mock_wsd.replace_workspace.assert_called_once_with(expected_name, expected_parameter)
diff --git a/qt/python/mantidqt/widgets/matrixworkspacedisplay/test_helpers/__init__.py b/qt/python/mantidqt/widgets/common/test_mocks/__init__.py
similarity index 79%
rename from qt/python/mantidqt/widgets/matrixworkspacedisplay/test_helpers/__init__.py
rename to qt/python/mantidqt/widgets/common/test_mocks/__init__.py
index 57d5ae5a28a63ed0dd44886f201c25df7cac61ca..2542dc4ea67a9fb17d7f18f017e39f900655e23d 100644
--- a/qt/python/mantidqt/widgets/matrixworkspacedisplay/test_helpers/__init__.py
+++ b/qt/python/mantidqt/widgets/common/test_mocks/__init__.py
@@ -1,9 +1,7 @@
 # Mantid Repository : https://github.com/mantidproject/mantid
 #
-# Copyright © 2018 ISIS Rutherford Appleton Laboratory UKRI,
+# Copyright © 2019 ISIS Rutherford Appleton Laboratory UKRI,
 #     NScD Oak Ridge National Laboratory, European Spallation Source
 #     & Institut Laue - Langevin
 # SPDX - License - Identifier: GPL - 3.0 +
 #  This file is part of the mantid workbench.
-#
-#
diff --git a/qt/python/mantidqt/widgets/matrixworkspacedisplay/test_helpers/matrixworkspacedisplay_common.py b/qt/python/mantidqt/widgets/common/test_mocks/mock_mantid.py
similarity index 66%
rename from qt/python/mantidqt/widgets/matrixworkspacedisplay/test_helpers/matrixworkspacedisplay_common.py
rename to qt/python/mantidqt/widgets/common/test_mocks/mock_mantid.py
index 40621e2032b4da330a29ccedb5b8a6f44670147d..d7ece59a1304594e160156ab7c96146f7e6fed30 100644
--- a/qt/python/mantidqt/widgets/matrixworkspacedisplay/test_helpers/matrixworkspacedisplay_common.py
+++ b/qt/python/mantidqt/widgets/common/test_mocks/mock_mantid.py
@@ -5,44 +5,14 @@
 #     & Institut Laue - Langevin
 # SPDX - License - Identifier: GPL - 3.0 +
 #  This file is part of the mantid workbench.
-#
-#
 from __future__ import (absolute_import, division, print_function)
 
 from mock import Mock
 
-from mantidqt.widgets.matrixworkspacedisplay.table_view_model import MatrixWorkspaceTableViewModel, \
-    MatrixWorkspaceTableViewModelType
-
 AXIS_INDEX_FOR_HORIZONTAL = 0
 AXIS_INDEX_FOR_VERTICAL = 1
 
 
-def setup_common_for_test_data():
-    """
-    Common configuration of variables and mocking for testing
-    MatrixWorkspaceDisplayTableViewModel's data and headerData functions
-    """
-    # Create some mock data for the mock workspace
-    row = 2
-    column = 2
-    # make a workspace with 0s
-    mock_data = [0] * 10
-    # set one of them to be not 0
-    mock_data[column] = 999
-    model_type = MatrixWorkspaceTableViewModelType.x
-    # pass onto the MockWorkspace so that it returns it when read from the TableViewModel
-    ws = MockWorkspace(read_return=mock_data)
-    ws.hasMaskedBins = Mock(return_value=True)
-    ws.maskedBinsIndices = Mock(return_value=[column])
-    model = MatrixWorkspaceTableViewModel(ws, model_type)
-    # The model retrieves the spectrumInfo object, and our MockWorkspace has already given it
-    # the MockSpectrumInfo, so all that needs to be done here is to set up the correct method Mocks
-    model.ws_spectrum_info.hasDetectors = Mock(return_value=True)
-    index = MockQModelIndex(row, column)
-    return ws, model, row, index
-
-
 class MockMantidSymbol:
     TEST_ASCII = "MANTID_ASCII_SYMBOL"
     TEST_UTF8 = "MANTID_UTF8_SYMBOL"
@@ -71,22 +41,6 @@ class MockMantidAxis:
         self.getUnit = Mock(return_value=self.mock_unit)
 
 
-class MockQModelIndexSibling:
-    TEST_SIBLING_DATA = "MANTID_TEST_SIBLING_DATA"
-
-    def __init__(self):
-        self.data = Mock(return_value=self.TEST_SIBLING_DATA)
-
-
-class MockQModelIndex:
-
-    def __init__(self, row, column):
-        self.row = Mock(return_value=row)
-        self.column = Mock(return_value=column)
-        self.mock_sibling = MockQModelIndexSibling()
-        self.sibling = Mock(return_value=self.mock_sibling)
-
-
 class MockSpectrumInfo:
     def __init__(self):
         self.hasDetectors = None
@@ -140,7 +94,7 @@ class MockWorkspace:
 
         self.setCell = Mock()
 
-        self.name = Mock(return_value="MOCK_WORKSPACE_TEST")
+        self.name = Mock(return_value=self.TEST_NAME)
 
         self._column_names = []
         for i in range(self.COLS):
@@ -154,3 +108,5 @@ class MockWorkspace:
         self.rowCount = Mock(return_value=self.row_count)
 
         self.column = Mock(return_value=[1] * self.row_count)
+
+        self.emit_repaint = Mock()
diff --git a/qt/python/mantidqt/widgets/common/test_mocks/mock_matrixworkspacedisplay.py b/qt/python/mantidqt/widgets/common/test_mocks/mock_matrixworkspacedisplay.py
new file mode 100644
index 0000000000000000000000000000000000000000..229b29adb6d66c51a9b50ee91becdb7fd3e26ebc
--- /dev/null
+++ b/qt/python/mantidqt/widgets/common/test_mocks/mock_matrixworkspacedisplay.py
@@ -0,0 +1,32 @@
+# Mantid Repository : https://github.com/mantidproject/mantid
+#
+# Copyright © 2019 ISIS Rutherford Appleton Laboratory UKRI,
+#     NScD Oak Ridge National Laboratory, European Spallation Source
+#     & Institut Laue - Langevin
+# SPDX - License - Identifier: GPL - 3.0 +
+#  This file is part of the mantid workbench.
+from __future__ import (absolute_import, division, print_function)
+
+from mock import Mock
+
+from mantidqt.widgets.common.test_mocks.mock_qt import MockQTab, MockQTableView
+
+
+class MockMatrixWorkspaceDisplayView:
+
+    def __init__(self):
+        self.set_context_menu_actions = Mock()
+        self.table_x = MockQTableView()
+        self.table_y = MockQTableView()
+        self.table_e = MockQTableView()
+        self.set_model = Mock()
+        self.ask_confirmation = None
+        self.emit_close = Mock()
+        self.mock_tab = MockQTab()
+        self.get_active_tab = Mock(return_value=self.mock_tab)
+
+
+class MockMatrixWorkspaceDisplayModel:
+    def __init__(self):
+        self.get_spectrum_plot_label = Mock()
+        self.get_bin_plot_label = Mock()
diff --git a/qt/python/mantidqt/widgets/common/test_mocks/mock_observing.py b/qt/python/mantidqt/widgets/common/test_mocks/mock_observing.py
new file mode 100644
index 0000000000000000000000000000000000000000..f9cbadba3b24e861d21cd8b9d47b26c00ce1ff10
--- /dev/null
+++ b/qt/python/mantidqt/widgets/common/test_mocks/mock_observing.py
@@ -0,0 +1,31 @@
+# Mantid Repository : https://github.com/mantidproject/mantid
+#
+# Copyright © 2019 ISIS Rutherford Appleton Laboratory UKRI,
+#     NScD Oak Ridge National Laboratory, European Spallation Source
+#     & Institut Laue - Langevin
+# SPDX - License - Identifier: GPL - 3.0 +
+#  This file is part of the mantid workbench.
+from __future__ import (absolute_import, division, print_function)
+
+from mock import Mock
+
+from mantidqt.widgets.common.observing_presenter import ObservingPresenter
+from mantidqt.widgets.common.observing_view import ObservingView
+from mantidqt.widgets.common.test_mocks.mock_qt import MockQtSignal
+
+
+class MockObservingView(ObservingView):
+    def __init__(self, _):
+        self.close_signal = MockQtSignal()
+        self.rename_signal = MockQtSignal()
+        self.presenter = Mock()
+        self.presenter.clear_observer = Mock()
+        self.setWindowTitle = Mock()
+
+
+class MockObservingPresenter(ObservingPresenter):
+    def __init__(self, workspaces_are_equal=True):
+        self.ads_observer = Mock()
+        self.view = MockObservingView(None)
+        self.model = Mock()
+        self.model.workspace_equals = Mock(return_value=workspaces_are_equal)
diff --git a/qt/python/mantidqt/widgets/tableworkspacedisplay/test_helpers/mock_plotlib.py b/qt/python/mantidqt/widgets/common/test_mocks/mock_plotlib.py
similarity index 96%
rename from qt/python/mantidqt/widgets/tableworkspacedisplay/test_helpers/mock_plotlib.py
rename to qt/python/mantidqt/widgets/common/test_mocks/mock_plotlib.py
index 762a5a5b73feb4940fc9666f5e2767d9748e1fcd..92f11f5d0735dde705034f23c508225a6153ad18 100644
--- a/qt/python/mantidqt/widgets/tableworkspacedisplay/test_helpers/mock_plotlib.py
+++ b/qt/python/mantidqt/widgets/common/test_mocks/mock_plotlib.py
@@ -5,8 +5,6 @@
 #     & Institut Laue - Langevin
 # SPDX - License - Identifier: GPL - 3.0 +
 #  This file is part of the mantid workbench.
-#
-#
 
 """
 The aim of this module is to provide a swap-in object for
@@ -25,6 +23,7 @@ only the ones that have been necessary so far. If another function
 needs to be mocked it can be freely added in the relevant class below
 and it should not break any existing tests.
 """
+from __future__ import (absolute_import, division, print_function)
 
 
 from mock import Mock
diff --git a/qt/python/mantidqt/widgets/matrixworkspacedisplay/test_helpers/mock_matrixworkspacedisplay.py b/qt/python/mantidqt/widgets/common/test_mocks/mock_qt.py
similarity index 69%
rename from qt/python/mantidqt/widgets/matrixworkspacedisplay/test_helpers/mock_matrixworkspacedisplay.py
rename to qt/python/mantidqt/widgets/common/test_mocks/mock_qt.py
index bbf0884186fee7e3dfb0eeb9609b64c2cc358206..a678044aafde251cde646b7990da69c746a80dd5 100644
--- a/qt/python/mantidqt/widgets/matrixworkspacedisplay/test_helpers/mock_matrixworkspacedisplay.py
+++ b/qt/python/mantidqt/widgets/common/test_mocks/mock_qt.py
@@ -1,12 +1,12 @@
 # Mantid Repository : https://github.com/mantidproject/mantid
 #
-# Copyright © 2018 ISIS Rutherford Appleton Laboratory UKRI,
+# Copyright © 2019 ISIS Rutherford Appleton Laboratory UKRI,
 #     NScD Oak Ridge National Laboratory, European Spallation Source
 #     & Institut Laue - Langevin
 # SPDX - License - Identifier: GPL - 3.0 +
 #  This file is part of the mantid workbench.
-#
-#
+from __future__ import (absolute_import, division, print_function)
+
 from mock import Mock
 
 from mantidqt.widgets.matrixworkspacedisplay.table_view_model import MatrixWorkspaceTableViewModelType
@@ -63,17 +63,38 @@ class MockQTableView:
         self.selectionModel = Mock(return_value=self.mock_selection_model)
 
 
-class MockMatrixWorkspaceDisplayView:
+class MockViewport:
+    def __init__(self):
+        self.update = Mock()
+
+
+class MockQTab:
+    def __init__(self):
+        self.mock_viewport = MockViewport()
+        self.viewport = Mock(return_value=self.mock_viewport)
+
+
+class MockQModelIndexSibling:
+    TEST_SIBLING_DATA = "MANTID_TEST_SIBLING_DATA"
+
+    def __init__(self):
+        self.data = Mock(return_value=self.TEST_SIBLING_DATA)
+
+
+class MockQModelIndex:
+
+    def __init__(self, row, column):
+        self.row = Mock(return_value=row)
+        self.column = Mock(return_value=column)
+        self.mock_sibling = MockQModelIndexSibling()
+        self.sibling = Mock(return_value=self.mock_sibling)
+
+
+class MockQtEvent:
     def __init__(self):
-        self.set_context_menu_actions = Mock()
-        self.table_x = MockQTableView()
-        self.table_y = MockQTableView()
-        self.table_e = MockQTableView()
-        self.set_model = Mock()
-        self.ask_confirmation = None
+        self.accept = Mock()
 
 
-class MockMatrixWorkspaceDisplayModel:
+class MockQtSignal:
     def __init__(self):
-        self.get_spectrum_plot_label = Mock()
-        self.get_bin_plot_label = Mock()
+        self.emit = Mock()
diff --git a/qt/python/mantidqt/widgets/common/workspacedisplay_ads_observer.py b/qt/python/mantidqt/widgets/common/workspacedisplay_ads_observer.py
new file mode 100644
index 0000000000000000000000000000000000000000..2bdc70b294189e2f8e180f22ba8f01453b18ca72
--- /dev/null
+++ b/qt/python/mantidqt/widgets/common/workspacedisplay_ads_observer.py
@@ -0,0 +1,76 @@
+# Mantid Repository : https://github.com/mantidproject/mantid
+#
+# Copyright © 2017 ISIS Rutherford Appleton Laboratory UKRI,
+#     NScD Oak Ridge National Laboratory, European Spallation Source
+#     & Institut Laue - Langevin
+# SPDX - License - Identifier: GPL - 3.0 +
+#  This file is part of the mantid workbench.
+
+from __future__ import (absolute_import, division, print_function)
+
+import sys
+from functools import wraps
+
+from mantid.api import AnalysisDataServiceObserver
+
+
+def _catch_exceptions(func):
+    """
+    Catch all exceptions in method and print a traceback to stderr
+    """
+
+    @wraps(func)
+    def wrapper(*args, **kwargs):
+        try:
+            func(*args, **kwargs)
+        except Exception:
+            sys.stderr.write("Error occurred in handler:\n")
+            import traceback
+            traceback.print_exc()
+
+    return wrapper
+
+
+class WorkspaceDisplayADSObserver(AnalysisDataServiceObserver):
+
+    def __init__(self, workspace_display, observe_replace=True):
+        super(WorkspaceDisplayADSObserver, self).__init__()
+        self.window = workspace_display
+
+        self.observeClear(True)
+        self.observeDelete(True)
+        self.observeReplace(observe_replace)
+        self.observeRename(True)
+
+    @_catch_exceptions
+    def clearHandle(self):
+        """
+        Called when the ADS is deleted all of its workspaces
+        """
+        self.window.force_close()
+
+    @_catch_exceptions
+    def deleteHandle(self, name, _):
+        """
+        Called when the ADS has deleted a workspace. Checks the
+        attached axes for any hold a plot from this workspace. If removing
+        this leaves empty axes then the parent window is triggered for
+        closer
+        :param _: The name of the workspace. Unused
+        :param __: A pointer to the workspace. Unused
+        """
+        self.window.close(name)
+
+    @_catch_exceptions
+    def replaceHandle(self, name, workspace):
+        """
+        Called when the ADS has replaced a workspace with one of the same name.
+        If this workspace is attached to this figure then its data is updated
+        :param _: The name of the workspace. Unused
+        :param workspace: A reference to the new workspace
+        """
+        self.window.replace_workspace(name, workspace)
+
+    @_catch_exceptions
+    def renameHandle(self, old_name, new_name):
+        self.window.rename_workspace(old_name, new_name)
diff --git a/qt/python/mantidqt/widgets/instrumentview/presenter.py b/qt/python/mantidqt/widgets/instrumentview/presenter.py
index 03f44680fbf9ac660f156e610f5be17efb428726..fb96adb88f1f61c90d602bbf4effecd748e51f33 100644
--- a/qt/python/mantidqt/widgets/instrumentview/presenter.py
+++ b/qt/python/mantidqt/widgets/instrumentview/presenter.py
@@ -13,15 +13,53 @@ Contains the presenter for displaying the InstrumentWidget
 from __future__ import (absolute_import, unicode_literals)
 
 # local imports
+from mantidqt.widgets.common.observing_presenter import ObservingPresenter
+from mantidqt.widgets.common.workspacedisplay_ads_observer import WorkspaceDisplayADSObserver
 from .view import InstrumentView
 
 
-class InstrumentViewPresenter(object):
+class InstrumentViewPresenter(ObservingPresenter):
     """
     Presenter holding the view widget for the InstrumentView.
     It has no model as its an old widget written in C++ with out MVP
     """
+
     view = None
 
-    def __init__(self, ws, parent=None):
-        self.view = InstrumentView(self, ws.name(), parent)
+    def __init__(self, ws, parent=None, ads_observer=None):
+        super(InstrumentViewPresenter, self).__init__()
+        self.ws_name = ws.name()
+        self.view = InstrumentView(self, self.ws_name, parent)
+
+        if ads_observer:
+            self.ads_observer = ads_observer
+        else:
+            self.ads_observer = WorkspaceDisplayADSObserver(self, observe_replace=False)
+
+    def close(self, workspace_name):
+        """
+        This closes the external window of the Instrument view.
+
+        The C++ InstrumentWidget handles all events to the workspace itself,
+        if the workspace is deleted then the widget closes itself.
+
+        The InstrumentWidget is also wrapped in a QWidget made from Python,
+        and that needs to be closed from here, otherwise we are left with an empty window,
+        when the InstrumentWidget closes itself on workspace deletion.
+
+        :param workspace_name: Used to check if it is the current workspace of the instrument view.
+                               If it is - then close the instrument view,
+                               but if it isn't - it does nothing
+        """
+        if self.ws_name == workspace_name:
+            self.clear_observer()
+            self.view.emit_close()
+
+    def replace_workspace(self, workspace_name, workspace):
+        # replace is handled by the InstrumentWidget inside C++
+        # this method is also unused, but is added to conform to the interface
+        pass
+
+    def rename_workspace(self, old_name, new_name):
+        # rename is handled by the InstrumentWidget inside C++
+        pass
diff --git a/qt/python/mantidqt/widgets/instrumentview/view.py b/qt/python/mantidqt/widgets/instrumentview/view.py
index 7abfce3a63fce6506d2b3109e731516d4f0606ae..22c457d9782d72ea13c7004352857887185f76b3 100644
--- a/qt/python/mantidqt/widgets/instrumentview/view.py
+++ b/qt/python/mantidqt/widgets/instrumentview/view.py
@@ -13,36 +13,48 @@ Contains the Python wrapper class for the C++ instrument widget
 from __future__ import (absolute_import, unicode_literals)
 
 # 3rdparty imports
-from qtpy.QtCore import Qt
+from qtpy.QtCore import Qt, Signal, Slot
 from qtpy.QtWidgets import QVBoxLayout, QWidget
 
 # local imports
 from mantidqt.utils.qt import import_qt
-
-
 # import widget class from C++ wrappers
+from mantidqt.widgets.common.observing_view import ObservingView
+
 InstrumentWidget = import_qt('._instrumentview', 'mantidqt.widgets.instrumentview',
                              'InstrumentWidget')
 
 
-class InstrumentView(QWidget):
+class InstrumentView(QWidget, ObservingView):
     """
     Defines a Window wrapper for the instrument widget. Sets
     the Qt.Window flag and window title. Holds a reference
     to the presenter and keeps it alive for the duration that
     the window is open
     """
-    _presenter = None
+    presenter = None
     _widget = None
 
+    close_signal = Signal()
+
     def __init__(self, presenter, name, parent=None):
         super(InstrumentView, self).__init__(parent)
-        self._presenter = presenter
+
+        self.widget = InstrumentWidget(name)
+
+        self.presenter = presenter
 
         self.setWindowTitle(name)
         self.setWindowFlags(Qt.Window)
 
         layout = QVBoxLayout()
         layout.setContentsMargins(0, 0, 0, 0)
-        layout.addWidget(InstrumentWidget(name))
+
+        layout.addWidget(self.widget)
         self.setLayout(layout)
+
+        self.close_signal.connect(self._run_close)
+
+    @Slot()
+    def _run_close(self):
+        self.close()
diff --git a/qt/python/mantidqt/widgets/matrixworkspacedisplay/model.py b/qt/python/mantidqt/widgets/matrixworkspacedisplay/model.py
index 8afc6d1b6af9499691bba176ff693985619ff779..b8b8353768d945544ae3f82fb9b7b954b4a6dc9b 100644
--- a/qt/python/mantidqt/widgets/matrixworkspacedisplay/model.py
+++ b/qt/python/mantidqt/widgets/matrixworkspacedisplay/model.py
@@ -48,3 +48,6 @@ class MatrixWorkspaceDisplayModel(object):
         return (MatrixWorkspaceTableViewModel(self._ws, MatrixWorkspaceTableViewModelType.x),
                 MatrixWorkspaceTableViewModel(self._ws, MatrixWorkspaceTableViewModelType.y),
                 MatrixWorkspaceTableViewModel(self._ws, MatrixWorkspaceTableViewModelType.e))
+
+    def workspace_equals(self, workspace_name):
+        return workspace_name == self._ws.name()
diff --git a/qt/python/mantidqt/widgets/matrixworkspacedisplay/presenter.py b/qt/python/mantidqt/widgets/matrixworkspacedisplay/presenter.py
index e01fb1768b4761af792617dc9daa73c9b438ffd5..91ff6860f2d001c8ee0c409271596735d0e18e82 100644
--- a/qt/python/mantidqt/widgets/matrixworkspacedisplay/presenter.py
+++ b/qt/python/mantidqt/widgets/matrixworkspacedisplay/presenter.py
@@ -10,20 +10,22 @@
 from __future__ import absolute_import, division, print_function
 
 from mantid.plots.utility import MantidAxType
+from mantidqt.widgets.common.observing_presenter import ObservingPresenter
 from mantidqt.widgets.common.table_copying import copy_bin_values, copy_cells, copy_spectrum_values, \
     show_no_selection_to_copy_toast
+from mantidqt.widgets.common.workspacedisplay_ads_observer import WorkspaceDisplayADSObserver
 from mantidqt.widgets.matrixworkspacedisplay.table_view_model import MatrixWorkspaceTableViewModelType
 from .model import MatrixWorkspaceDisplayModel
 from .view import MatrixWorkspaceDisplayView
 
 
-class MatrixWorkspaceDisplay(object):
+class MatrixWorkspaceDisplay(ObservingPresenter):
     NO_SELECTION_MESSAGE = "No selection"
     COPY_SUCCESSFUL_MESSAGE = "Copy Successful"
     A_LOT_OF_THINGS_TO_PLOT_MESSAGE = "You selected {} spectra to plot. Are you sure you want to plot that many?"
     NUM_SELECTED_FOR_CONFIRMATION = 10
 
-    def __init__(self, ws, plot=None, parent=None, model=None, view=None):
+    def __init__(self, ws, plot=None, parent=None, model=None, view=None, ads_observer=None):
         """
         Creates a display for the provided workspace.
 
@@ -32,6 +34,8 @@ class MatrixWorkspaceDisplay(object):
         :param parent: Parent of the widget
         :param model: Model to be used by the widget. Passed in as parameter to allow mocking
         :param view: View to be used by the widget. Passed in as parameter to allow mocking
+        :param ads_observer: ADS observer to be used by the presenter. If not provided the default
+                             one is used. Mainly intended for testing.
         """
         # Create model and view, or accept mocked versions
         self.model = model if model else MatrixWorkspaceDisplayModel(ws)
@@ -40,12 +44,20 @@ class MatrixWorkspaceDisplay(object):
                                                                  self.model.get_name())
 
         self.plot = plot
+
+        self.ads_observer = ads_observer if ads_observer else WorkspaceDisplayADSObserver(self)
+
         self.setup_tables()
 
         self.view.set_context_menu_actions(self.view.table_y)
         self.view.set_context_menu_actions(self.view.table_x)
         self.view.set_context_menu_actions(self.view.table_e)
 
+    def replace_workspace(self, workspace_name, workspace):
+        if self.model.workspace_equals(workspace_name):
+            self.model = MatrixWorkspaceDisplayModel(workspace)
+            self.view.get_active_tab().viewport().update()
+
     @classmethod
     def supports(cls, ws):
         """
diff --git a/qt/python/mantidqt/widgets/matrixworkspacedisplay/test/test_matrixworkspacedisplay_model.py b/qt/python/mantidqt/widgets/matrixworkspacedisplay/test/test_matrixworkspacedisplay_model.py
index 20b841487d4476a8742db2a3e85cfe8f14cd55c1..75357d2bc594c6889d9a7d7324b9a4e1dbe54911 100644
--- a/qt/python/mantidqt/widgets/matrixworkspacedisplay/test/test_matrixworkspacedisplay_model.py
+++ b/qt/python/mantidqt/widgets/matrixworkspacedisplay/test/test_matrixworkspacedisplay_model.py
@@ -14,10 +14,9 @@ import unittest
 from mock import Mock
 
 from mantid.simpleapi import CreateSampleWorkspace
+from mantidqt.widgets.common.test_mocks.mock_mantid import MockWorkspace
 from mantidqt.widgets.matrixworkspacedisplay.model import MatrixWorkspaceDisplayModel
 from mantidqt.widgets.matrixworkspacedisplay.table_view_model import MatrixWorkspaceTableViewModelType
-from mantidqt.widgets.matrixworkspacedisplay.test_helpers.matrixworkspacedisplay_common import \
-    MockWorkspace
 
 
 class MatrixWorkspaceDisplayModelTest(unittest.TestCase):
diff --git a/qt/python/mantidqt/widgets/matrixworkspacedisplay/test/test_matrixworkspacedisplay_presenter.py b/qt/python/mantidqt/widgets/matrixworkspacedisplay/test/test_matrixworkspacedisplay_presenter.py
index 378dc5956c3868ef52eb8320a9c55b956d8ff7b2..83fd98f3bd1273b0537c1089244e797531e9980a 100644
--- a/qt/python/mantidqt/widgets/matrixworkspacedisplay/test/test_matrixworkspacedisplay_presenter.py
+++ b/qt/python/mantidqt/widgets/matrixworkspacedisplay/test/test_matrixworkspacedisplay_presenter.py
@@ -13,12 +13,22 @@ import unittest
 
 from mock import Mock, patch
 
+from mantidqt.widgets.common.test_mocks.mock_mantid import MockWorkspace
+from mantidqt.widgets.common.test_mocks.mock_matrixworkspacedisplay import MockMatrixWorkspaceDisplayView
+from mantidqt.widgets.common.test_mocks.mock_qt import MockQModelIndex, MockQTableView
 from mantidqt.widgets.matrixworkspacedisplay.model import MatrixWorkspaceDisplayModel
 from mantidqt.widgets.matrixworkspacedisplay.presenter import MatrixWorkspaceDisplay
-from mantidqt.widgets.matrixworkspacedisplay.test_helpers.matrixworkspacedisplay_common import MockQModelIndex, \
-    MockWorkspace
-from mantidqt.widgets.matrixworkspacedisplay.test_helpers.mock_matrixworkspacedisplay import \
-    MockMatrixWorkspaceDisplayView, MockQTableView
+
+
+def with_mock_presenter(func):
+    def wrapper(self, *args):
+        ws = MockWorkspace()
+        view = MockMatrixWorkspaceDisplayView()
+        mock_observer = Mock()
+        presenter = MatrixWorkspaceDisplay(ws, view=view, ads_observer=mock_observer)
+        return func(self, ws, view, presenter, *args)
+
+    return wrapper
 
 
 class MatrixWorkspaceDisplayPresenterTest(unittest.TestCase):
@@ -39,11 +49,8 @@ class MatrixWorkspaceDisplayPresenterTest(unittest.TestCase):
 
     @patch('mantidqt.widgets.common.table_copying.show_mouse_toast')
     @patch('mantidqt.widgets.common.table_copying.copy_to_clipboard')
-    def test_action_copy_spectrum_values(self, mock_copy, mock_show_mouse_toast):
-        ws = MockWorkspace()
-        view = MockMatrixWorkspaceDisplayView()
-        presenter = MatrixWorkspaceDisplay(ws, view=view)
-
+    @with_mock_presenter
+    def test_action_copy_spectrum_values(self, ws, view, presenter, mock_copy, mock_show_mouse_toast):
         mock_table = MockQTableView()
 
         # two rows are selected in different positions
@@ -63,10 +70,9 @@ class MatrixWorkspaceDisplayPresenterTest(unittest.TestCase):
 
     @patch('mantidqt.widgets.common.table_copying.show_mouse_toast')
     @patch('mantidqt.widgets.common.table_copying.copy_to_clipboard')
-    def test_action_copy_spectrum_values_no_selection(self, mock_copy, mock_show_mouse_toast):
-        ws = MockWorkspace()
-        view = MockMatrixWorkspaceDisplayView()
-        presenter = MatrixWorkspaceDisplay(ws, view=view)
+    @with_mock_presenter
+    def test_action_copy_spectrum_values_no_selection(self, ws, view, presenter, mock_copy,
+                                                      mock_show_mouse_toast):
 
         mock_table = MockQTableView()
         mock_table.mock_selection_model.hasSelection = Mock(return_value=False)
@@ -83,10 +89,8 @@ class MatrixWorkspaceDisplayPresenterTest(unittest.TestCase):
 
     @patch('mantidqt.widgets.common.table_copying.show_mouse_toast')
     @patch('mantidqt.widgets.common.table_copying.copy_to_clipboard')
-    def test_action_copy_bin_values(self, mock_copy, mock_show_mouse_toast):
-        ws = MockWorkspace()
-        view = MockMatrixWorkspaceDisplayView()
-        presenter = MatrixWorkspaceDisplay(ws, view=view)
+    @with_mock_presenter
+    def test_action_copy_bin_values(self, ws, view, presenter, mock_copy, mock_show_mouse_toast):
         mock_table = MockQTableView()
 
         # two columns are selected at different positions
@@ -108,11 +112,8 @@ class MatrixWorkspaceDisplayPresenterTest(unittest.TestCase):
 
     @patch('mantidqt.widgets.common.table_copying.show_mouse_toast')
     @patch('mantidqt.widgets.common.table_copying.copy_to_clipboard')
-    def test_action_copy_bin_values_no_selection(self, mock_copy, mock_show_mouse_toast):
-        ws = MockWorkspace()
-        view = MockMatrixWorkspaceDisplayView()
-        presenter = MatrixWorkspaceDisplay(ws, view=view)
-
+    @with_mock_presenter
+    def test_action_copy_bin_values_no_selection(self, ws, view, presenter, mock_copy, mock_show_mouse_toast):
         mock_table = MockQTableView()
         mock_table.mock_selection_model.hasSelection = Mock(return_value=False)
         mock_table.mock_selection_model.selectedColumns = Mock()
@@ -128,10 +129,8 @@ class MatrixWorkspaceDisplayPresenterTest(unittest.TestCase):
 
     @patch('mantidqt.widgets.common.table_copying.show_mouse_toast')
     @patch('mantidqt.widgets.common.table_copying.copy_to_clipboard')
-    def test_action_copy_cell(self, mock_copy, mock_show_mouse_toast):
-        ws = MockWorkspace()
-        view = MockMatrixWorkspaceDisplayView()
-        presenter = MatrixWorkspaceDisplay(ws, view=view)
+    @with_mock_presenter
+    def test_action_copy_cell(self, ws, view, presenter, mock_copy, mock_show_mouse_toast):
         mock_table = MockQTableView()
 
         # two columns are selected at different positions
@@ -147,10 +146,8 @@ class MatrixWorkspaceDisplayPresenterTest(unittest.TestCase):
 
     @patch('mantidqt.widgets.common.table_copying.show_mouse_toast')
     @patch('mantidqt.widgets.common.table_copying.copy_to_clipboard')
-    def test_action_copy_cell_no_selection(self, mock_copy, mock_show_mouse_toast):
-        ws = MockWorkspace()
-        view = MockMatrixWorkspaceDisplayView()
-        presenter = MatrixWorkspaceDisplay(ws, view=view)
+    @with_mock_presenter
+    def test_action_copy_cell_no_selection(self, ws, view, presenter, mock_copy, mock_show_mouse_toast):
         mock_table = MockQTableView()
         mock_table.mock_selection_model.hasSelection = Mock(return_value=False)
 
@@ -314,6 +311,48 @@ class MatrixWorkspaceDisplayPresenterTest(unittest.TestCase):
         self.assertNotCalled(mock_table.mock_selection_model.selectedColumns)
         self.assertNotCalled(mock_plot)
 
+    @with_mock_presenter
+    def test_close_incorrect_workspace(self, ws, view, presenter):
+        presenter.close(ws.TEST_NAME + "123")
+        self.assertNotCalled(view.emit_close)
+        self.assertIsNotNone(presenter.ads_observer)
+
+    @with_mock_presenter
+    def test_close(self, ws, view, presenter):
+        presenter.close(ws.TEST_NAME)
+        view.emit_close.assert_called_once_with()
+        self.assertIsNone(presenter.ads_observer)
+
+    @with_mock_presenter
+    def test_force_close_even_with_incorrect_name(self, _, view, presenter):
+        # window always closes, regardless of the workspace
+        presenter.force_close()
+        view.emit_close.assert_called_once_with()
+        self.assertIsNone(presenter.ads_observer)
+
+    @with_mock_presenter
+    def test_force_close(self, _, view, presenter):
+        presenter.force_close()
+        view.emit_close.assert_called_once_with()
+        self.assertIsNone(presenter.ads_observer)
+
+    @with_mock_presenter
+    def test_replace_incorrect_workspace(self, ws, view, presenter):
+        presenter.replace_workspace(ws.TEST_NAME + "123", ws)
+        self.assertNotCalled(view.get_active_tab)
+        self.assertNotCalled(view.mock_tab.viewport)
+        self.assertNotCalled(view.mock_tab.mock_viewport.update)
+
+    @with_mock_presenter
+    def test_replace(self, ws, view, presenter):
+        # patch this out after the constructor of the presenter has finished,
+        # so that we reset any calls it might have made
+        presenter.replace_workspace(ws.TEST_NAME, ws)
+
+        view.get_active_tab.assert_called_once_with()
+        view.mock_tab.viewport.assert_called_once_with()
+        view.mock_tab.mock_viewport.update.assert_called_once_with()
+
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/qt/python/mantidqt/widgets/matrixworkspacedisplay/test/test_matrixworkspacedisplay_table_view_model.py b/qt/python/mantidqt/widgets/matrixworkspacedisplay/test/test_matrixworkspacedisplay_table_view_model.py
index 9d7fb191d5cc55dcceb66d3e2ce766e1ef9ff1a2..e374d413f2e880ae34bc3b6f03b0696fc1ff5945 100644
--- a/qt/python/mantidqt/widgets/matrixworkspacedisplay/test/test_matrixworkspacedisplay_table_view_model.py
+++ b/qt/python/mantidqt/widgets/matrixworkspacedisplay/test/test_matrixworkspacedisplay_table_view_model.py
@@ -16,11 +16,36 @@ from mock import Mock, call
 from qtpy import QtCore
 from qtpy.QtCore import Qt
 
+from mantidqt.widgets.common.test_mocks.mock_mantid import AXIS_INDEX_FOR_HORIZONTAL, AXIS_INDEX_FOR_VERTICAL, \
+    MockMantidAxis, MockMantidSymbol, MockMantidUnit, MockSpectrum, MockWorkspace
+from mantidqt.widgets.common.test_mocks.mock_qt import MockQModelIndex
 from mantidqt.widgets.matrixworkspacedisplay.table_view_model import MatrixWorkspaceTableViewModel, \
     MatrixWorkspaceTableViewModelType
-from mantidqt.widgets.matrixworkspacedisplay.test_helpers.matrixworkspacedisplay_common import \
-    MockQModelIndex, MockWorkspace, setup_common_for_test_data, AXIS_INDEX_FOR_VERTICAL, MockMantidAxis, MockSpectrum, \
-    MockMantidSymbol, AXIS_INDEX_FOR_HORIZONTAL, MockMantidUnit
+
+
+def setup_common_for_test_data():
+    """
+    Common configuration of variables and mocking for testing
+    MatrixWorkspaceDisplayTableViewModel's data and headerData functions
+    """
+    # Create some mock data for the mock workspace
+    row = 2
+    column = 2
+    # make a workspace with 0s
+    mock_data = [0] * 10
+    # set one of them to be not 0
+    mock_data[column] = 999
+    model_type = MatrixWorkspaceTableViewModelType.x
+    # pass onto the MockWorkspace so that it returns it when read from the TableViewModel
+    ws = MockWorkspace(read_return=mock_data)
+    ws.hasMaskedBins = Mock(return_value=True)
+    ws.maskedBinsIndices = Mock(return_value=[column])
+    model = MatrixWorkspaceTableViewModel(ws, model_type)
+    # The model retrieves the spectrumInfo object, and our MockWorkspace has already given it
+    # the MockSpectrumInfo, so all that needs to be done here is to set up the correct method Mocks
+    model.ws_spectrum_info.hasDetectors = Mock(return_value=True)
+    index = MockQModelIndex(row, column)
+    return ws, model, row, index
 
 
 class MatrixWorkspaceDisplayTableViewModelTest(unittest.TestCase):
diff --git a/qt/python/mantidqt/widgets/matrixworkspacedisplay/view.py b/qt/python/mantidqt/widgets/matrixworkspacedisplay/view.py
index 0cd52d83a1928045b2939b6f6c8cf275d0c84c07..fc20d674eae6a53fb0004ec889d30f6760beabe2 100644
--- a/qt/python/mantidqt/widgets/matrixworkspacedisplay/view.py
+++ b/qt/python/mantidqt/widgets/matrixworkspacedisplay/view.py
@@ -12,11 +12,12 @@ from __future__ import (absolute_import, division, print_function)
 from functools import partial
 
 from qtpy import QtGui
-from qtpy.QtCore import Qt
+from qtpy.QtCore import Qt, Signal, Slot
 from qtpy.QtGui import QKeySequence
 from qtpy.QtWidgets import (QAbstractItemView, QAction, QHeaderView, QMessageBox, QTabWidget, QTableView)
 
 import mantidqt.icons
+from mantidqt.widgets.common.observing_view import ObservingView
 from mantidqt.widgets.matrixworkspacedisplay.table_view_model import MatrixWorkspaceTableViewModelType
 
 
@@ -39,7 +40,10 @@ class MatrixWorkspaceTableView(QTableView):
         header.resizeSection(section, header.defaultSectionSize())
 
 
-class MatrixWorkspaceDisplayView(QTabWidget):
+class MatrixWorkspaceDisplayView(QTabWidget, ObservingView):
+    close_signal = Signal()
+    rename_signal = Signal(str)
+
     def __init__(self, presenter, parent=None, name=''):
         super(MatrixWorkspaceDisplayView, self).__init__(parent)
 
@@ -66,9 +70,21 @@ class MatrixWorkspaceDisplayView(QTabWidget):
         self.table_x = self.add_table("X values")
         self.table_e = self.add_table("E values")
 
+        self.close_signal.connect(self._run_close)
+        self.rename_signal.connect(self._run_rename)
+
         self.resize(600, 400)
         self.show()
 
+    @Slot()
+    def _run_close(self):
+        self.presenter.clear_observer()
+        self.close()
+
+    @Slot(str)
+    def _run_rename(self, new_name):
+        self._rename(new_name)
+
     def add_table(self, label):
         tab = MatrixWorkspaceTableView(self)
 
@@ -81,6 +97,9 @@ class MatrixWorkspaceDisplayView(QTabWidget):
             self.presenter.action_keypress_copy(self.tabs[self.currentIndex()])
         super(MatrixWorkspaceDisplayView, self).keyPressEvent(event)
 
+    def get_active_tab(self):
+        return self.tabs[self.active_tab_index]
+
     def set_scroll_position_on_new_focused_tab(self, new_tab_index):
         """
         Updates the new focused tab's scroll position to match the old one.
diff --git a/qt/python/mantidqt/widgets/tableworkspacedisplay/model.py b/qt/python/mantidqt/widgets/tableworkspacedisplay/model.py
index 7ac78cac95aab3b05fa999a6d0accfb4a49e090e..d9f0fb00610aac83dd800207080c8e70a71c9e95 100644
--- a/qt/python/mantidqt/widgets/tableworkspacedisplay/model.py
+++ b/qt/python/mantidqt/widgets/tableworkspacedisplay/model.py
@@ -42,7 +42,6 @@ class TableWorkspaceDisplayModel:
         self.ws = ws
         self.ws_num_rows = self.ws.rowCount()
         self.ws_num_cols = self.ws.columnCount()
-        self.ws_column_types = self.ws.columnTypes()
         self.marked_columns = MarkedColumns()
         self._original_column_headers = self.get_column_headers()
 
@@ -86,4 +85,13 @@ class TableWorkspaceDisplayModel:
         # from the string to that it can be properly set
         if is_v3d:
             data = self._get_v3d_from_str(data)
-        self.ws.setCell(row, col, data)
+        # The False stops the replace workspace ADS event from being triggered
+        # The replace event causes the TWD model to be replaced, which in turn
+        # deletes the previous table item objects, however this happens
+        # at the same time as we are trying to locally update the data in the
+        # item object itself, which causes a Qt exception that the object has
+        # already been deleted and a crash
+        self.ws.setCell(row, col, data, notify_replace=False)
+
+    def workspace_equals(self, workspace_name):
+        return self.ws.name() == workspace_name
diff --git a/qt/python/mantidqt/widgets/tableworkspacedisplay/presenter.py b/qt/python/mantidqt/widgets/tableworkspacedisplay/presenter.py
index a8597856db30d938035b740fe6d37f0c07b9c11d..a15634f4fd0701e28ffa7ef6d70a2cdb80705a41 100644
--- a/qt/python/mantidqt/widgets/tableworkspacedisplay/presenter.py
+++ b/qt/python/mantidqt/widgets/tableworkspacedisplay/presenter.py
@@ -12,10 +12,12 @@ from __future__ import absolute_import, division, print_function
 from functools import partial
 
 from qtpy.QtCore import Qt
-from mantid.kernel import logger
 
+from mantid.kernel import logger
 from mantid.simpleapi import DeleteTableRows, StatisticsOfTableWorkspace
+from mantidqt.widgets.common.observing_presenter import ObservingPresenter
 from mantidqt.widgets.common.table_copying import copy_cells, show_no_selection_to_copy_toast
+from mantidqt.widgets.common.workspacedisplay_ads_observer import WorkspaceDisplayADSObserver
 from mantidqt.widgets.tableworkspacedisplay.error_column import ErrorColumn
 from mantidqt.widgets.tableworkspacedisplay.plot_type import PlotType
 from mantidqt.widgets.tableworkspacedisplay.workbench_table_widget_item import WorkbenchTableWidgetItem
@@ -23,7 +25,7 @@ from .model import TableWorkspaceDisplayModel
 from .view import TableWorkspaceDisplayView
 
 
-class TableWorkspaceDisplay(object):
+class TableWorkspaceDisplay(ObservingPresenter):
     A_LOT_OF_THINGS_TO_PLOT_MESSAGE = "You selected {} spectra to plot. Are you sure you want to plot that many?"
     TOO_MANY_SELECTED_FOR_X = "Too many columns are selected to use as X. Please select only 1."
     TOO_MANY_SELECTED_TO_SORT = "Too many columns are selected to sort by. Please select only 1."
@@ -39,7 +41,7 @@ class TableWorkspaceDisplay(object):
     INVALID_DATA_WINDOW_TITLE = "Invalid data - Mantid Workbench"
     COLUMN_DISPLAY_LABEL = 'Column {}'
 
-    def __init__(self, ws, plot=None, parent=None, model=None, view=None, name=None):
+    def __init__(self, ws, plot=None, parent=None, model=None, view=None, name=None, ads_observer=None):
         """
         Creates a display for the provided workspace.
 
@@ -50,8 +52,10 @@ class TableWorkspaceDisplay(object):
         :param model: Model to be used by the widget. Passed in as parameter to allow mocking
         :param view: View to be used by the widget. Passed in as parameter to allow mocking
         :param name: Custom name for the window
+        :param ads_observer: ADS observer to be used by the presenter. If not provided the default
+                             one is used. Mainly intended for testing.
         """
-        # Create model and view, or accept mocked versions
+
         self.model = model if model else TableWorkspaceDisplayModel(ws)
         self.name = self.model.get_name() if name is None else name
         self.view = view if view else TableWorkspaceDisplayView(self, parent, self.name)
@@ -59,6 +63,8 @@ class TableWorkspaceDisplay(object):
         self.plot = plot
         self.view.set_context_menu_actions(self.view)
 
+        self.ads_observer = ads_observer if ads_observer else WorkspaceDisplayADSObserver(self)
+
         self.update_column_headers()
         self.load_data(self.view)
 
@@ -75,11 +81,15 @@ class TableWorkspaceDisplay(object):
         """
         return TableWorkspaceDisplayModel.supports(ws)
 
+    def replace_workspace(self, workspace_name, workspace):
+        if self.model.workspace_equals(workspace_name):
+            self.model = TableWorkspaceDisplayModel(workspace)
+            self.load_data(self.view)
+            self.view.emit_repaint()
+
     def handleItemChanged(self, item):
         """
         :type item: WorkbenchTableWidgetItem
-        :param item:
-        :return:
         """
         try:
             self.model.set_cell_data(item.row(), item.column(), item.data(Qt.DisplayRole), item.is_v3d)
diff --git a/qt/python/mantidqt/widgets/tableworkspacedisplay/test/test_tableworkspacedisplay_model.py b/qt/python/mantidqt/widgets/tableworkspacedisplay/test/test_tableworkspacedisplay_model.py
index f6cb3a2d6d089cb47e3ba366973b9476cb0d8663..e952ba7719e06ac06091d9a4ee9fe539ca784e13 100644
--- a/qt/python/mantidqt/widgets/tableworkspacedisplay/test/test_tableworkspacedisplay_model.py
+++ b/qt/python/mantidqt/widgets/tableworkspacedisplay/test/test_tableworkspacedisplay_model.py
@@ -15,8 +15,7 @@ import unittest
 from mock import Mock
 
 from mantid.kernel import V3D
-from mantidqt.widgets.matrixworkspacedisplay.test_helpers.matrixworkspacedisplay_common import \
-    MockWorkspace
+from mantidqt.widgets.common.test_mocks.mock_mantid import MockWorkspace
 from mantidqt.widgets.tableworkspacedisplay.model import TableWorkspaceDisplayModel
 
 
@@ -72,7 +71,7 @@ class TableWorkspaceDisplayModelTest(unittest.TestCase):
 
         # check that the correct conversion function was retrieved
         # -> the one for the column for which the data is being set
-        model.ws.setCell.assert_called_once_with(expected_row, expected_col, test_data)
+        model.ws.setCell.assert_called_once_with(expected_row, expected_col, test_data, notify_replace=False)
 
     @with_mock_model
     def test_set_cell_data_v3d(self, model):
@@ -88,7 +87,7 @@ class TableWorkspaceDisplayModelTest(unittest.TestCase):
 
         # check that the correct conversion function was retrieved
         # -> the one for the column for which the data is being set
-        model.ws.setCell.assert_called_once_with(expected_row, expected_col, V3D(1, 2, 3))
+        model.ws.setCell.assert_called_once_with(expected_row, expected_col, V3D(1, 2, 3), notify_replace=False)
 
     def test_no_raise_with_supported_workspace(self):
         from mantid.simpleapi import CreateEmptyTableWorkspace
diff --git a/qt/python/mantidqt/widgets/tableworkspacedisplay/test/test_tableworkspacedisplay_presenter.py b/qt/python/mantidqt/widgets/tableworkspacedisplay/test/test_tableworkspacedisplay_presenter.py
index 85a104e04d9716038a739ece1ab3d3e23aaf8354..aa1c894bdabbcfd8a262501cd91758f022f369aa 100644
--- a/qt/python/mantidqt/widgets/tableworkspacedisplay/test/test_tableworkspacedisplay_presenter.py
+++ b/qt/python/mantidqt/widgets/tableworkspacedisplay/test/test_tableworkspacedisplay_presenter.py
@@ -13,14 +13,13 @@ import unittest
 
 from mock import Mock, call, patch
 
-from mantidqt.widgets.matrixworkspacedisplay.test_helpers.matrixworkspacedisplay_common import MockQModelIndex, \
-    MockWorkspace
-from mantidqt.widgets.matrixworkspacedisplay.test_helpers.mock_matrixworkspacedisplay import MockQSelectionModel
+from mantidqt.widgets.common.test_mocks.mock_mantid import MockWorkspace
+from mantidqt.widgets.common.test_mocks.mock_plotlib import MockAx, MockPlotLib
+from mantidqt.widgets.common.test_mocks.mock_qt import MockQModelIndex, MockQSelectionModel
 from mantidqt.widgets.tableworkspacedisplay.error_column import ErrorColumn
 from mantidqt.widgets.tableworkspacedisplay.model import TableWorkspaceDisplayModel
 from mantidqt.widgets.tableworkspacedisplay.plot_type import PlotType
 from mantidqt.widgets.tableworkspacedisplay.presenter import TableWorkspaceDisplay
-from mantidqt.widgets.tableworkspacedisplay.test_helpers.mock_plotlib import MockAx, MockPlotLib
 from mantidqt.widgets.tableworkspacedisplay.view import TableWorkspaceDisplayView
 from mantidqt.widgets.tableworkspacedisplay.workbench_table_widget_item import WorkbenchTableWidgetItem
 
@@ -44,7 +43,8 @@ def with_mock_presenter(add_selection_model=False, add_plot=False):
     And an answer with a little more description of the logic behind it all
     https://stackoverflow.com/a/25827070/2823526
 
-    :param add_selection_model:
+    :param add_selection_model: Adds a mock selection model to the presenter
+    :param add_plot: Adds mock plotting to the presenter
     """
 
     def real_decorator(func, *args, **kwargs):
@@ -97,7 +97,7 @@ class TableWorkspaceDisplayPresenterTest(unittest.TestCase):
 
         item.row.assert_called_once_with()
         item.column.assert_called_once_with()
-        ws.setCell.assert_called_once_with(5, 5, "magic parameter")
+        ws.setCell.assert_called_once_with(5, 5, "magic parameter", notify_replace=False)
         item.update.assert_called_once_with()
         item.reset.assert_called_once_with()
 
@@ -553,6 +553,49 @@ class TableWorkspaceDisplayPresenterTest(unittest.TestCase):
         twd.plot.mock_fig.show.assert_called_once_with()
         twd.plot.mock_ax.legend.assert_called_once_with()
 
+    @with_mock_presenter()
+    def test_close_incorrect_workspace(self, ws, view, presenter):
+        presenter.close(ws.TEST_NAME + "123")
+        self.assertNotCalled(view.emit_close)
+        self.assertIsNotNone(presenter.ads_observer)
+
+    @with_mock_presenter()
+    def test_close(self, ws, view, presenter):
+        presenter.close(ws.TEST_NAME)
+        view.emit_close.assert_called_once_with()
+        self.assertIsNone(presenter.ads_observer)
+
+    @with_mock_presenter()
+    def test_force_close_even_with_incorrect_name(self, _, view, presenter):
+        # window always closes, regardless of the workspace
+        presenter.force_close()
+        view.emit_close.assert_called_once_with()
+        self.assertIsNone(presenter.ads_observer)
+
+    @with_mock_presenter()
+    def test_force_close(self, _, view, presenter):
+        presenter.force_close()
+        view.emit_close.assert_called_once_with()
+        self.assertIsNone(presenter.ads_observer)
+
+    @with_mock_presenter()
+    def test_replace_incorrect_workspace(self, ws, view, presenter):
+        with patch(
+                'mantidqt.widgets.tableworkspacedisplay.presenter.TableWorkspaceDisplay.load_data') as mock_load_data:
+            presenter.replace_workspace(ws.TEST_NAME + "123", ws)
+            self.assertNotCalled(mock_load_data)
+            self.assertNotCalled(view.emit_repaint)
+
+    @with_mock_presenter()
+    def test_replace(self, ws, view, presenter):
+        # patch this out after the constructor of the presenter has finished,
+        # so that we reset any calls it might have made
+        with patch(
+                'mantidqt.widgets.tableworkspacedisplay.presenter.TableWorkspaceDisplay.load_data') as mock_load_data:
+            presenter.replace_workspace(ws.TEST_NAME, ws)
+            mock_load_data.assert_called_once_with(view)
+            view.emit_repaint.assert_called_once_with()
+
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/qt/python/mantidqt/widgets/tableworkspacedisplay/test_helpers/__init__.py b/qt/python/mantidqt/widgets/tableworkspacedisplay/test_helpers/__init__.py
deleted file mode 100644
index 0bd8c24be2b8bafb760d7df108f16bcb30c5ad01..0000000000000000000000000000000000000000
--- a/qt/python/mantidqt/widgets/tableworkspacedisplay/test_helpers/__init__.py
+++ /dev/null
@@ -1,21 +0,0 @@
-from mock import Mock
-
-from mantidqt.widgets.matrixworkspacedisplay.test_helpers.mock_matrixworkspacedisplay import MockQTableView
-
-
-class MockTableWorkspaceDisplayView:
-    def __init__(self):
-        self.set_context_menu_actions = Mock()
-        self.table_x = MockQTableView()
-        self.table_y = MockQTableView()
-        self.table_e = MockQTableView()
-        self.set_model = Mock()
-        self.copy_to_clipboard = Mock()
-        self.show_mouse_toast = Mock()
-        self.ask_confirmation = None
-
-
-class MockTableWorkspaceDisplayModel:
-    def __init__(self):
-        self.get_spectrum_plot_label = Mock()
-        self.get_bin_plot_label = Mock()
diff --git a/qt/python/mantidqt/widgets/tableworkspacedisplay/view.py b/qt/python/mantidqt/widgets/tableworkspacedisplay/view.py
index ac4eb5a950cb33e930f14ea51ed858169a5bd178..b885d3c0f7f684c1dbbf1ff2a655f4062ab53798 100644
--- a/qt/python/mantidqt/widgets/tableworkspacedisplay/view.py
+++ b/qt/python/mantidqt/widgets/tableworkspacedisplay/view.py
@@ -13,16 +13,18 @@ import sys
 from functools import partial
 
 from qtpy import QtGui
-from qtpy.QtCore import QVariant, Qt
+from qtpy.QtCore import QVariant, Qt, Signal, Slot
 from qtpy.QtGui import QKeySequence
 from qtpy.QtWidgets import (QAction, QHeaderView, QItemEditorFactory, QMenu, QMessageBox,
                             QStyledItemDelegate, QTableWidget)
 
 import mantidqt.icons
+from mantidqt.widgets.common.observing_view import ObservingView
 from mantidqt.widgets.tableworkspacedisplay.plot_type import PlotType
 
 
 class PreciseDoubleFactory(QItemEditorFactory):
+
     def __init__(self):
         QItemEditorFactory.__init__(self)
 
@@ -36,7 +38,11 @@ class PreciseDoubleFactory(QItemEditorFactory):
         return widget
 
 
-class TableWorkspaceDisplayView(QTableWidget):
+class TableWorkspaceDisplayView(QTableWidget, ObservingView):
+    close_signal = Signal()
+    rename_signal = Signal(str)
+    repaint_signal = Signal()
+
     def __init__(self, presenter, parent=None, name=''):
         super(TableWorkspaceDisplayView, self).__init__(parent)
 
@@ -54,6 +60,10 @@ class TableWorkspaceDisplayView(QTableWidget):
         self.setWindowTitle("{} - Mantid".format(name))
         self.setWindowFlags(Qt.Window)
 
+        self.close_signal.connect(self._run_close)
+        self.rename_signal.connect(self._run_rename)
+        self.repaint_signal.connect(self._run_repaint)
+
         self.resize(600, 400)
         self.show()
 
@@ -65,6 +75,21 @@ class TableWorkspaceDisplayView(QTableWidget):
         header = self.horizontalHeader()
         header.setSectionResizeMode(QHeaderView.Interactive)
 
+    def emit_repaint(self):
+        self.repaint_signal.emit()
+
+    @Slot()
+    def _run_repaint(self):
+        self.viewport().update()
+
+    @Slot()
+    def _run_close(self):
+        self.close()
+
+    @Slot(str)
+    def _run_rename(self, new_name):
+        self._rename(new_name)
+
     def handle_double_click(self, section):
         header = self.horizontalHeader()
         header.resizeSection(section, header.defaultSectionSize())