Index: src/sas/qtgui/MainWindow/DataExplorer.py
===================================================================
--- src/sas/qtgui/MainWindow/DataExplorer.py	(revision 345b3b382101552fe1f6febd4afefb152aa1765e)
+++ src/sas/qtgui/MainWindow/DataExplorer.py	(revision 2eeda9335609875dbf439fe36d6823b0f9067e67)
@@ -235,4 +235,18 @@
             self.readProject(filename)
 
+    def loadAnalysis(self):
+        """
+        Called when the "Open Analysis" menu item chosen.
+        """
+        kwargs = {
+            'parent'    : self,
+            'caption'   : 'Open Analysis',
+            'filter'    : 'Project (*.fitv);;All files (*.*)',
+            'options'   : QtWidgets.QFileDialog.DontUseNativeDialog
+        }
+        filename = QtWidgets.QFileDialog.getOpenFileName(**kwargs)[0]
+        if filename:
+            self.readAnalysis(filename)
+
     def saveProject(self):
         """
@@ -256,4 +270,39 @@
         with open(filename, 'w') as outfile:
             self.saveDataToFile(outfile)
+
+    def saveAsAnalysisFile(self, tab_id=1):
+        """
+        Show the save as... dialog and return the chosen filepath
+        """
+        default_name = "FitPage"+str(tab_id)+".fitv"
+
+        wildcard = "fitv files (*.fitv)"
+        kwargs = {
+            'caption'   : 'Save As',
+            'directory' : default_name,
+            'filter'    : wildcard,
+            'parent'    : None,
+        }
+        # Query user for filename.
+        filename_tuple = QtWidgets.QFileDialog.getSaveFileName(**kwargs)
+        filename = filename_tuple[0]
+        return filename
+
+    def saveAnalysis(self, data, tab_id=1):
+        """
+        Called when the "Save Analysis" menu item chosen.
+        """
+        filename = self.saveAsAnalysisFile(tab_id)
+        if not filename:
+            return
+        _, extension = os.path.splitext(filename)
+        if not extension:
+            filename = '.'.join((filename, 'fitv'))
+        self.communicator.statusBarUpdateSignal.emit("Saving analysis... %s\n" % os.path.basename(filename))
+
+        with open(filename, 'w') as outfile:
+            GuiUtils.saveData(outfile, data)
+
+        self.communicator.statusBarUpdateSignal.emit('Analysis saved.')
 
     def allDataForModel(self, model):
@@ -275,7 +324,28 @@
         return all_data
 
-    def saveDataToFile(self, outfile):
-        """
-        Save every dataset to a json file
+    def getDataForID(self, id):
+        # return the dataset with the given ID
+        all_data = []
+        for model in (self.model, self.theory_model):
+            for i in range(model.rowCount()):
+                properties = {}
+                item = model.item(i)
+                data = GuiUtils.dataFromItem(item)
+                if data is None: continue
+                if data.id != id: continue
+                # We found the dataset - save it.
+                filename = data.filename
+                is_checked = item.checkState()
+                properties['checked'] = is_checked
+                other_datas = GuiUtils.plotsFromFilename(filename, model)
+                # skip the main plot
+                other_datas = list(other_datas.values())[1:]
+                all_data = [data, properties, other_datas]
+                break
+        return all_data
+
+    def getAllData(self):
+        """
+        converts all datasets into serializable dictionary
         """
         data = self.allDataForModel(self.model)
@@ -289,19 +359,13 @@
                 raise ValueError("Inconsistent data in Project file.")
             all_data[key] = value
-
-        # perspectives
-        current_perspective = self._perspective()
-        try:
-            perspective_dump = current_perspective.getCurrentStateAsXml()
-        except Exception:
-            ex = "State of " + current_perspective.windowTitle() + \
-                " cannot be saved."
-            logging.error(ex)
-        if perspective_dump is not None:
-            assert(isinstance(perspective_dump, dict))
-            all_data['perspective'] = perspective_dump
+        return all_data
+
+    def saveDataToFile(self, outfile):
+        """
+        Save every dataset to a json file
+        """
+        all_data = self.getAllData()
         # save datas
         GuiUtils.saveData(outfile, all_data)
-        return
 
     def readProject(self, filename):
@@ -311,12 +375,39 @@
         with open(filename, 'r') as infile:
             all_data = GuiUtils.readDataFromFile(infile)
-        # clear the model
-        self.model.clear()
-
+            items = self.updateModelFromData(all_data)
+
+        pass # debugger
+
+    def readAnalysis(self, filename):
+        """
+        Read out a single dataset and fitpage from file
+        """
+        with open(filename, 'r') as infile:
+            all_data = GuiUtils.readDataFromFile(infile)
+            # simulate full project structure
+            all_data_dict = {1:all_data['fit_data']}
+            items = self.updateModelFromData(all_data_dict)
+            # TODO: allow other perspectives
+            # send newly created item to its perspective
+            if len(items) > 0:
+                self.sendItemToPerspective(items[0])
+            # Make the perspective read the rest of the read data
+            self._perspective().updateFromParameters(all_data['fit_params'])
+
+            pass
+
+    def updateModelFromData(self, data):
+        """
+        Given data from analysis/project file,
+        create indices and populate data/theory models
+        """
+        # model items for top level datasets
+        items = []
         #self.model.beginResetModel()
-        for key, value in all_data.items():
+        for key, value in data.items():
             # key - cardinal number of dataset
             # value - main dataset, [dependant filesets]
             # add the main index
+            if not value: continue
             new_data = value[0]
             assert isinstance(new_data, (Data1D, Data2D))
@@ -325,4 +416,5 @@
             new_item = GuiUtils.createModelItemWithPlot(new_data, new_data.filename)
             new_item.setCheckState(is_checked)
+            items.append(new_item)
             model = self.theory_model
             if new_data.is_data:
@@ -337,4 +429,5 @@
                 assert isinstance(plot, (Data1D, Data2D))
                 GuiUtils.updateModelItemWithPlot(new_item, plot, plot.name)
+        return items
 
     def deleteFile(self, event):
@@ -455,4 +548,23 @@
             retval = msgbox.exec_()
 
+    def sendItemToPerspective(self, item):
+        """
+        Send the passed item data to the current perspective and set the relevant notifiers
+        """
+        # Set the signal handlers
+        self.communicator.updateModelFromPerspectiveSignal.connect(self.updateModelFromPerspective)
+        selected_items = [item]
+        # Notify the GuiManager about the send request
+        try:
+            self._perspective().setData(data_item=selected_items, is_batch=False)
+        except Exception as ex:
+            msg = "%s perspective returned the following message: \n%s\n" %(self._perspective().name, str(ex))
+            logging.error(msg)
+            msg = str(ex)
+            msgbox = QtWidgets.QMessageBox()
+            msgbox.setIcon(QtWidgets.QMessageBox.Critical)
+            msgbox.setText(msg)
+            msgbox.setStandardButtons(QtWidgets.QMessageBox.Ok)
+            retval = msgbox.exec_()
 
     def freezeCheckedData(self):
Index: src/sas/qtgui/MainWindow/GuiManager.py
===================================================================
--- src/sas/qtgui/MainWindow/GuiManager.py	(revision 6040cd7f770dace4120940137d2f21ba88262912)
+++ src/sas/qtgui/MainWindow/GuiManager.py	(revision 2eeda9335609875dbf439fe36d6823b0f9067e67)
@@ -244,5 +244,4 @@
         if self._current_perspective:
             self._current_perspective.setClosable()
-            #self._workspace.workspace.removeSubWindow(self._current_perspective)
             self._current_perspective.close()
             self._workspace.workspace.removeSubWindow(self._current_perspective)
@@ -548,5 +547,5 @@
         """
         """
-        print("actionOpen_Analysis TRIGGERED")
+        self.filesWidget.loadAnalysis()
         pass
 
@@ -561,5 +560,19 @@
         Menu File/Save Analysis
         """
-        self.communicate.saveAnalysisSignal.emit()
+        per = self.perspective()
+        if not isinstance(per, FittingWindow):
+            return
+        # get fit page serialization
+        params = per.getSerializedFitpage()
+        data_id = per.currentTabDataId()
+        tab_id = per.currentTab.tab_id
+        data = self.filesWidget.getDataForID(data_id)
+        analysis = {}
+        analysis['fit_data'] = data
+        analysis['fit_params'] = params
+
+        self.filesWidget.saveAnalysis(analysis, tab_id)
+
+        pass
 
     def actionQuit(self):
@@ -1023,4 +1036,10 @@
         """
         self._workspace.actionReport.setEnabled(False)
+        self._workspace.actionOpen_Analysis.setEnabled(False)
+        self._workspace.actionSave_Analysis.setEnabled(False)
+        if hasattr(perspective, 'isSerializable') and perspective.isSerializable():
+            self._workspace.actionOpen_Analysis.setEnabled(True)
+            self._workspace.actionSave_Analysis.setEnabled(True)
+
         if isinstance(perspective, Perspectives.PERSPECTIVES["Fitting"]):
             self.checkAnalysisOption(self._workspace.actionFitting)
Index: src/sas/qtgui/Perspectives/Fitting/FittingPerspective.py
===================================================================
--- src/sas/qtgui/Perspectives/Fitting/FittingPerspective.py	(revision d2007a805f2f66a26d1df36c9ff287c51563446f)
+++ src/sas/qtgui/Perspectives/Fitting/FittingPerspective.py	(revision 2eeda9335609875dbf439fe36d6823b0f9067e67)
@@ -112,4 +112,31 @@
     def onLatexCopy(self):
         self.currentTab.onCopyToClipboard("Latex")
+
+    def getSerializedFitpage(self):
+        # serialize current(active) fitpage
+        fitpage_state = self.currentTab.getFitPage()
+        fitpage_state += self.currentTab.getFitModel()
+        # put the text into dictionary
+        line_dict = {}
+        for line in fitpage_state:
+            #content = line.split(',')
+            if len(line) > 1:
+                line_dict[line[0]] = line[1:]
+        return line_dict
+
+    def currentTabDataId(self):
+        """
+        Returns the data ID of the current tab
+        """
+        tab_id = None
+        if self.currentTab.data:
+            tab_id = self.currentTab.data.id
+        return tab_id
+
+    def updateFromParameters(self, parameters):
+        """
+        Pass the update parameters to the current fit page
+        """
+        self.currentTab.createPageForParameters(parameters)
 
     def closeEvent(self, event):
@@ -258,4 +285,10 @@
         return True
 
+    def isSerializable(self):
+        """
+        Tell the caller that this perspective writes its state
+        """
+        return True
+
     def setData(self, data_item=None, is_batch=False):
         """
Index: src/sas/qtgui/Perspectives/Fitting/FittingWidget.py
===================================================================
--- src/sas/qtgui/Perspectives/Fitting/FittingWidget.py	(revision 9a42ea1df8683fcc0ee9616e8ab82486d4b10fa6)
+++ src/sas/qtgui/Perspectives/Fitting/FittingWidget.py	(revision 2eeda9335609875dbf439fe36d6823b0f9067e67)
@@ -52,4 +52,5 @@
 TAB_POLY = 3
 CATEGORY_DEFAULT = "Choose category..."
+MODEL_DEFAULT = "Choose model..."
 CATEGORY_STRUCTURE = "Structure Factor"
 CATEGORY_CUSTOM = "Plugin Models"
@@ -566,6 +567,4 @@
         # Signals from other widgets
         self.communicate.customModelDirectoryChanged.connect(self.onCustomModelChange)
-        self.communicate.saveAnalysisSignal.connect(self.savePageState)
-        #self.communicate.loadAnalysisSignal.connect(self.loadPageState)
         self.smearing_widget.smearingChangedSignal.connect(self.onSmearingOptionsUpdate)
 
@@ -1019,4 +1018,14 @@
         model = self.cbModel.currentText()
 
+        if model == MODEL_DEFAULT:
+            # if the previous category was not the default, keep it.
+            # Otherwise, just return
+            if self._previous_model_index != 0:
+                # We need to block signals, or else state changes on perceived unchanged conditions
+                self.cbModel.blockSignals(True)
+                self.cbModel.setCurrentIndex(self._previous_model_index)
+                self.cbModel.blockSignals(False)
+            return
+
         # Assure the control is active
         if not self.cbModel.isEnabled():
@@ -1026,5 +1035,7 @@
             return
         self.chkMagnetism.setEnabled(self.canHaveMagnetism())
+        self.chkMagnetism.setEnabled(self.canHaveMagnetism())
         self.tabFitting.setTabEnabled(TAB_MAGNETISM, self.chkMagnetism.isChecked() and self.canHaveMagnetism())
+        self._previous_model_index = self.cbModel.currentIndex()
 
         # Reset parameters to fit
@@ -1200,5 +1211,6 @@
             self._model_model.clear()
             return
-
+        # Wipe out the parameter model
+        self._model_model.clear()
         # Safely clear and enable the model combo
         self.cbModel.blockSignals(True)
@@ -1212,5 +1224,8 @@
         model_list = self.master_category_dict[category]
         # Populate the models combobox
+        self.cbModel.blockSignals(True)
+        self.cbModel.addItem(MODEL_DEFAULT)
         self.cbModel.addItems(sorted([model for (model, _) in model_list]))
+        self.cbModel.blockSignals(False)
 
     def onPolyModelChange(self, top, bottom):
@@ -3366,39 +3381,4 @@
         return report_logic.reportList()
 
-    def savePageState(self):
-        """
-        Create and serialize local PageState
-        """
-        filepath = self.saveAsAnalysisFile()
-        if filepath is None or filepath == "":
-            return
-
-        fitpage_state = self.getFitPage()
-        fitpage_state += self.getFitModel()
-
-        with open(filepath, 'w') as statefile:
-            for line in fitpage_state:
-                statefile.write(str(line))
-
-        self.communicate.statusBarUpdateSignal.emit('Analysis saved.')
-
-    def saveAsAnalysisFile(self):
-        """
-        Show the save as... dialog and return the chosen filepath
-        """
-        default_name = "FitPage"+str(self.tab_id)+".fitv"
-
-        wildcard = "fitv files (*.fitv)"
-        kwargs = {
-            'caption'   : 'Save As',
-            'directory' : default_name,
-            'filter'    : wildcard,
-            'parent'    : None,
-        }
-        # Query user for filename.
-        filename_tuple = QtWidgets.QFileDialog.getSaveFileName(**kwargs)
-        filename = filename_tuple[0]
-        return filename
-
     def loadPageStateCallback(self,state=None, datainfo=None, format=None):
         """
@@ -3519,4 +3499,7 @@
         """
         param_list = []
+        if self.kernel_module is None:
+            return param_list
+
         param_list.append(['model_name', str(self.cbModel.currentText())])
 
@@ -3623,5 +3606,4 @@
         cb_text = cb.text()
 
-        context = {}
         lines = cb_text.split(':')
         if lines[0] != 'sasview_parameter_values':
@@ -3635,8 +3617,29 @@
                 line_dict[content[0]] = content[1:]
 
+        self.updatePageWithParameters(line_dict)
+
+    def createPageForParameters(self, line_dict):
+        """
+        Sets up page with requested model/str factor
+        and fills it up with sent parameters
+        """
+        if 'fitpage_category' in line_dict:
+            self.cbCategory.setCurrentIndex(self.cbCategory.findText(line_dict['fitpage_category'][0]))
+        if 'fitpage_model' in line_dict:
+            self.cbModel.setCurrentIndex(self.cbModel.findText(line_dict['fitpage_model'][0]))
+        if 'fitpage_structure' in line_dict:
+            self.cbStructureFactor.setCurrentIndex(self.cbStructureFactor.findText(line_dict['fitpage_structure'][0]))
+
+        # Now that the page is ready for parameters, fill it up
+        self.updatePageWithParameters(line_dict)
+
+    def updatePageWithParameters(self, line_dict):
+        """
+        Update FitPage with parameters in line_dict
+        """
+        if 'model_name' not in line_dict.keys():
+            return
         model = line_dict['model_name'][0]
-
-        if 'model_name' not in line_dict.keys():
-            return False
+        context = {}
 
         if 'multiplicity' in line_dict.keys():
Index: src/sas/qtgui/Perspectives/Fitting/UnitTesting/ComplexConstraintTest.py
===================================================================
--- src/sas/qtgui/Perspectives/Fitting/UnitTesting/ComplexConstraintTest.py	(revision 725d9c06c2a66018f1352faba2f96cd31d8e9654)
+++ src/sas/qtgui/Perspectives/Fitting/UnitTesting/ComplexConstraintTest.py	(revision 2eeda9335609875dbf439fe36d6823b0f9067e67)
@@ -34,6 +34,11 @@
         category_index = self.tab1.cbCategory.findText("Shape Independent")
         self.tab1.cbCategory.setCurrentIndex(category_index)
+        model_index = self.tab1.cbModel.findText("be_polyelectrolyte")
+        self.tab1.cbModel.setCurrentIndex(model_index)
+
         category_index = self.tab2.cbCategory.findText("Cylinder")
         self.tab2.cbCategory.setCurrentIndex(category_index)
+        model_index = self.tab2.cbModel.findText("barbell")
+        self.tab2.cbModel.setCurrentIndex(model_index)
 
         tabs = [self.tab1, self.tab2]
Index: src/sas/qtgui/Perspectives/Fitting/UnitTesting/FittingWidgetTest.py
===================================================================
--- src/sas/qtgui/Perspectives/Fitting/UnitTesting/FittingWidgetTest.py	(revision d2007a805f2f66a26d1df36c9ff287c51563446f)
+++ src/sas/qtgui/Perspectives/Fitting/UnitTesting/FittingWidgetTest.py	(revision 2eeda9335609875dbf439fe36d6823b0f9067e67)
@@ -175,7 +175,9 @@
         category_index = self.widget.cbCategory.findText("Shape Independent")
         self.widget.cbCategory.setCurrentIndex(category_index)
+        model_index = self.widget.cbModel.findText("be_polyelectrolyte")
+        self.widget.cbModel.setCurrentIndex(model_index)
 
         # test the model combo content
-        self.assertEqual(self.widget.cbModel.count(), 29)
+        self.assertEqual(self.widget.cbModel.count(), 30)
 
         # Try to change back to default
@@ -201,4 +203,6 @@
         category_index = self.widget.cbCategory.findText("Shape Independent")
         self.widget.cbCategory.setCurrentIndex(category_index)
+        model_index = self.widget.cbModel.findText("be_polyelectrolyte")
+        self.widget.cbModel.setCurrentIndex(model_index)
 
         # check the enablement of controls
@@ -215,5 +219,5 @@
         # 
         # Now change the model
-        self.widget.cbModel.setCurrentIndex(3)
+        self.widget.cbModel.setCurrentIndex(4)
         self.assertEqual(self.widget.cbModel.currentText(),'dab')
 
@@ -226,5 +230,5 @@
         self.widget.data_is_loaded = True
         # Reset the sasmodel index
-        self.widget.cbModel.setCurrentIndex(1)
+        self.widget.cbModel.setCurrentIndex(2)
         self.assertEqual(self.widget.cbModel.currentText(),'broad_peak')
 
@@ -377,4 +381,7 @@
         category_index = self.widget.cbCategory.findText("Shape Independent")
         self.widget.cbCategory.setCurrentIndex(category_index)
+        model_index = self.widget.cbModel.findText("be_polyelectrolyte")
+        self.widget.cbModel.setCurrentIndex(model_index)
+
         # Check the poly model
         self.assertEqual(self.widget._poly_model.rowCount(), 0)
@@ -383,4 +390,5 @@
         # Change the category index so we have a model available
         self.widget.cbCategory.setCurrentIndex(2)
+        self.widget.cbModel.setCurrentIndex(1)
 
         # Check the poly model
@@ -556,4 +564,6 @@
         category_index = self.widget.cbCategory.findText("Sphere")
         self.widget.cbCategory.setCurrentIndex(category_index)
+        model_index = self.widget.cbModel.findText("adsorbed_layer")
+        self.widget.cbModel.setCurrentIndex(model_index)
 
         # Check the magnetic model
@@ -634,4 +644,6 @@
         category_index = self.widget.cbCategory.findText("Sphere")
         self.widget.cbCategory.setCurrentIndex(category_index)
+        model_index = self.widget.cbModel.findText("adsorbed_layer")
+        self.widget.cbModel.setCurrentIndex(model_index)
 
         # Check the enablement/text
@@ -973,4 +985,6 @@
         category_index = self.widget.cbCategory.findText("Sphere")
         self.widget.cbCategory.setCurrentIndex(category_index)
+        model_index = self.widget.cbModel.findText("adsorbed_layer")
+        self.widget.cbModel.setCurrentIndex(model_index)
         self.widget.main_params_to_fit = ['scale']
 
@@ -986,5 +1000,5 @@
         self.assertListEqual(fp.main_params_to_fit, ['scale'])
 
-    def testPushFitPage(self):
+    def notestPushFitPage(self):
         """
         Push current state of fitpage onto stack
@@ -997,4 +1011,6 @@
         self.widget.data = item
         category_index = self.widget.cbCategory.findText("Sphere")
+        model_index = self.widget.cbModel.findText("adsorbed_layer")
+        self.widget.cbModel.setCurrentIndex(model_index)
 
         # Asses the initial state of stack
Index: src/sas/qtgui/Utilities/GuiUtils.py
===================================================================
--- src/sas/qtgui/Utilities/GuiUtils.py	(revision 345b3b382101552fe1f6febd4afefb152aa1765e)
+++ src/sas/qtgui/Utilities/GuiUtils.py	(revision 2eeda9335609875dbf439fe36d6823b0f9067e67)
@@ -270,7 +270,4 @@
     sendDataToGridSignal = QtCore.pyqtSignal(list)
 
-    # Action Save Analysis triggered
-    saveAnalysisSignal = QtCore.pyqtSignal()
-
     # Mask Editor requested
     maskEditorSignal = QtCore.pyqtSignal(Data2D)
@@ -577,4 +574,6 @@
     if isinstance(data.process, list) and data.process:
         for process in data.process:
+            if process is None:
+                continue
             process_date = process.date
             process_date_item = QtGui.QStandardItem("Date: " + process_date)
