Index: src/sas/qtgui/MainWindow/DataExplorer.py
===================================================================
--- src/sas/qtgui/MainWindow/DataExplorer.py	(revision 722b7d678b0f7efacad11e34c2bf633853825838)
+++ src/sas/qtgui/MainWindow/DataExplorer.py	(revision 722b7d678b0f7efacad11e34c2bf633853825838)
@@ -225,51 +225,46 @@
         Called when the "Open Project" menu item chosen.
         """
+        # check if any items loaded and warn about data deletion
+        if self.model.rowCount() > 0:
+            msg = "This operation will set remove all data, plots and analyses from"
+            msg += " SasView before loading the project. Do you wish to continue?"
+            msgbox = QtWidgets.QMessageBox(self)
+            msgbox.setIcon(QtWidgets.QMessageBox.Warning)
+            msgbox.setText(msg)
+            msgbox.setWindowTitle("Project Load")
+            # custom buttons
+            button_yes = QtWidgets.QPushButton("Yes")
+            msgbox.addButton(button_yes, QtWidgets.QMessageBox.YesRole)
+            button_no = QtWidgets.QPushButton("No")
+            msgbox.addButton(button_no, QtWidgets.QMessageBox.RejectRole)
+            retval = msgbox.exec_()
+            if retval == QtWidgets.QMessageBox.RejectRole:
+                # cancel fit
+                return
+
         kwargs = {
             'parent'    : self,
             'caption'   : 'Open Project',
-            'filter'    : 'Project (*.json);;All files (*.*)',
+            'filter'    : 'Project Files (*.json);;Old Project Files (*.svs);;All files (*.*)',
             'options'   : QtWidgets.QFileDialog.DontUseNativeDialog
         }
         filename = QtWidgets.QFileDialog.getOpenFileName(**kwargs)[0]
         if filename:
-            load_thread = threads.deferToThread(self.readProject, filename)
-            load_thread.addCallback(self.readProjectComplete)
-            load_thread.addErrback(self.readProjectFailed)
-
-    def loadFailed(self, reason):
-        """
-        """
-        print("file load FAILED: ", reason)
-        pass
-
-    def readProjectFailed(self, reason):
-        """
-        """
-        print("readProjectFailed FAILED: ", reason)
-        pass
-
-    def readProject(self, filename):
-        self.communicator.statusBarUpdateSignal.emit("Loading Project... %s" % os.path.basename(filename))
-        try:
-            manager = DataManager()
-            with open(filename, 'r') as infile:
-                manager.load_from_readable(infile)
-
-            self.communicator.statusBarUpdateSignal.emit("Loaded Project: %s" % os.path.basename(filename))
-            return manager
-
-        except:
-            self.communicator.statusBarUpdateSignal.emit("Failed: %s" % os.path.basename(filename))
-            raise
-
-    def readProjectComplete(self, manager):
-        self.model.clear()
-
-        self.manager.assign(manager)
-        self.model.beginResetModel()
-        for id, item in self.manager.get_all_data().items():
-            self.updateModel(item.data, item.path)
-
-        self.model.endResetModel()
+            self.deleteAllItems()
+            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):
@@ -285,11 +280,197 @@
         name_tuple = QtWidgets.QFileDialog.getSaveFileName(**kwargs)
         filename = name_tuple[0]
-        if filename:
-            _, extension = os.path.splitext(filename)
-            if not extension:
-                filename = '.'.join((filename, 'json'))
-            self.communicator.statusBarUpdateSignal.emit("Saving Project... %s\n" % os.path.basename(filename))
-            with open(filename, 'w') as outfile:
-                self.manager.save_to_writable(outfile)
+        if not filename:
+            return
+        _, extension = os.path.splitext(filename)
+        if not extension:
+            filename = '.'.join((filename, 'json'))
+        self.communicator.statusBarUpdateSignal.emit("Saving Project... %s\n" % os.path.basename(filename))
+
+        return filename
+
+    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):
+        # data model
+        all_data = {}
+        for i in range(model.rowCount()):
+            properties = {}
+            item = model.item(i)
+            data = GuiUtils.dataFromItem(item)
+            if data is None: continue
+            # Now, all plots under this item
+            filename = data.filename
+            is_checked = item.checkState()
+            properties['checked'] = is_checked
+            other_datas = []
+            # no need to save other_datas - things will be refit on read
+            #other_datas = GuiUtils.plotsFromFilename(filename, model)
+            # skip the main plot
+            #other_datas = list(other_datas.values())[1:]
+            all_data[data.id] = [data, properties, other_datas]
+        return all_data
+
+    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)
+        theory = self.allDataForModel(self.theory_model)
+
+        all_data = {}
+        for key, value in data.items():
+            all_data[key] = value
+        for key, value in theory.items():
+            if key in all_data:
+                raise ValueError("Inconsistent data in Project file.")
+            all_data[key] = value
+        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)
+
+    def readProject(self, filename):
+        """
+        Read out datasets and fitpages from file
+        """
+        # Find out the filetype based on extension
+        ext = os.path.splitext(filename)[1]
+        all_data = {}
+        if 'svs' in ext.lower():
+            # backward compatibility mode.
+            datasets = GuiUtils.readProjectFromSVS(filename)
+            #[[item_1, state_1], [item_2, state_2],...]
+
+            # Convert fitpage properties and update the dict
+            all_data = GuiUtils.convertFromSVS(datasets)
+        else:
+            with open(filename, 'r') as infile:
+                all_data = GuiUtils.readDataFromFile(infile)
+
+        for key, value in all_data.items():
+            data_dict = {key:value['fit_data']}
+            items = self.updateModelFromData(data_dict)
+            # send newly created item to its perspective
+            if 'fit_params' in value:
+                self.sendItemToPerspective(items[0])
+                # Make the perspective read the rest of the read data
+                self._perspective().updateFromParameters(value['fit_params'])
+
+        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 data.items():
+            # key - cardinal number of dataset
+            # value - main dataset, [dependant filesets]
+            # add the main index
+            if not value: continue
+            new_data = value[0]
+            from sas.sascalc.dataloader.data_info import Data1D as old_data1d
+            from sas.sascalc.dataloader.data_info import Data2D as old_data2d
+            if isinstance(new_data, (old_data1d, old_data2d)):
+                new_data = self.manager.create_gui_data(value[0], new_data.filename)
+            assert isinstance(new_data, (Data1D, Data2D))
+            properties = value[1]
+            is_checked = properties['checked']
+            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:
+                model = self.model
+            model.appendRow(new_item)
+            self.manager.add_data(data_list={new_data.id:new_data})
+
+            # Add the underlying data
+            if not value[2]:
+                continue
+            for plot in value[2]:
+                assert isinstance(plot, (Data1D, Data2D))
+                GuiUtils.updateModelItemWithPlot(new_item, plot, plot.name)
+        return items
 
     def deleteFile(self, event):
@@ -410,4 +591,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):
@@ -1036,5 +1236,5 @@
         self.actionQuick3DPlot.triggered.connect(self.quickData3DPlot)
         self.actionEditMask.triggered.connect(self.showEditDataMask)
-        self.actionDelete.triggered.connect(self.deleteItem)
+        self.actionDelete.triggered.connect(self.deleteSelectedItem)
         self.actionFreezeResults.triggered.connect(self.freezeSelectedItems)
 
@@ -1248,5 +1448,20 @@
                 self.freezeItem(item_to_copy)
 
-    def deleteItem(self):
+    def deleteAllItems(self):
+        """
+        Deletes all datasets from both model and theory_model
+        """
+        deleted_items = [self.model.item(row) for row in range(self.model.rowCount())
+                         if self.model.item(row).isCheckable()]
+        deleted_names = [item.text() for item in deleted_items]
+        # Let others know we deleted data
+        self.communicator.dataDeletedSignal.emit(deleted_items)
+        # update stored_data
+        self.manager.update_stored_data(deleted_names)
+
+        # Clear the model
+        self.model.clear()
+
+    def deleteSelectedItem(self):
         """
         Delete the current item
@@ -1265,20 +1480,27 @@
             return
 
+        indices = self.current_view.selectedIndexes()
+        self.deleteIndices(indices)
+
+    def deleteIndices(self, indices):
+        """
+        Delete model idices from the current view
+        """
+        proxy = self.current_view.model()
+        model = proxy.sourceModel()
+
+        deleted_items = []
+        deleted_names = []
+
         # Every time a row is removed, the indices change, so we'll just remove
         # rows and keep calling selectedIndexes until it returns an empty list.
-        indices = self.current_view.selectedIndexes()
-
-        proxy = self.current_view.model()
-        model = proxy.sourceModel()
-
-        deleted_items = []
-        deleted_names = []
-
         while len(indices) > 0:
             index = indices[0]
-            row_index = proxy.mapToSource(index)
-            item_to_delete = model.itemFromIndex(row_index)
+            #row_index = proxy.mapToSource(index)
+            #item_to_delete = model.itemFromIndex(row_index)
+            item_to_delete = model.itemFromIndex(index)
             if item_to_delete and item_to_delete.isCheckable():
-                row = row_index.row()
+                #row = row_index.row()
+                row = index.row()
 
                 # store the deleted item details so we can pass them on later
@@ -1407,5 +1629,6 @@
         checkbox_item.setCheckable(True)
         checkbox_item.setCheckState(QtCore.Qt.Checked)
-        checkbox_item.setText(os.path.basename(p_file))
+        if p_file is not None:
+            checkbox_item.setText(os.path.basename(p_file))
 
         # Add the actual Data1D/Data2D object
Index: src/sas/qtgui/MainWindow/DataManager.py
===================================================================
--- src/sas/qtgui/MainWindow/DataManager.py	(revision e2e5f3d84e3cde792395c3cd184a5bba1eeb5873)
+++ src/sas/qtgui/MainWindow/DataManager.py	(revision 345b3b382101552fe1f6febd4afefb152aa1765e)
@@ -29,5 +29,4 @@
 from sas.qtgui.Plotting.PlotterData import Data1D
 from sas.qtgui.Plotting.PlotterData import Data2D
-from sas.qtgui.Plotting.Plottables import Plottable
 from sas.qtgui.Plotting.Plottables import PlottableTheory1D
 from sas.qtgui.Plotting.Plottables import PlottableFit1D
Index: src/sas/qtgui/MainWindow/GuiManager.py
===================================================================
--- src/sas/qtgui/MainWindow/GuiManager.py	(revision 0c8330329431af321b3152be7c6603fba3dc80ad)
+++ src/sas/qtgui/MainWindow/GuiManager.py	(revision 0c8330329431af321b3152be7c6603fba3dc80ad)
@@ -254,5 +254,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)
@@ -468,5 +467,5 @@
         self._workspace.actionOpen_Project.triggered.connect(self.actionOpen_Project)
         self._workspace.actionOpen_Analysis.triggered.connect(self.actionOpen_Analysis)
-        self._workspace.actionSave.triggered.connect(self.actionSave)
+        self._workspace.actionSave.triggered.connect(self.actionSave_Project)
         self._workspace.actionSave_Analysis.triggered.connect(self.actionSave_Analysis)
         self._workspace.actionQuit.triggered.connect(self.actionQuit)
@@ -558,12 +557,34 @@
         """
         """
-        print("actionOpen_Analysis TRIGGERED")
-        pass
-
-    def actionSave(self):
+        self.filesWidget.loadAnalysis()
+        pass
+
+    def actionSave_Project(self):
         """
         Menu Save Project
         """
-        self.filesWidget.saveProject()
+        filename = self.filesWidget.saveProject()
+
+        # datasets
+        all_data = self.filesWidget.getAllData()
+
+        # fit tabs
+        params = self.perspective().serializeAllFitpage()
+
+        # project dictionary structure:
+        # analysis[data.id] = [{"fit_data":[data, checkbox, child data],
+        #                       "fit_params":[fitpage_state]}
+        # "fit_params" not present if dataset not sent to fitting
+        analysis = {}
+
+        for id, data in all_data.items():
+            data_content = {"fit_data":data}
+            if id in params.keys():
+                # this dataset is represented also by the fit tab. Add to it.
+                data_content["fit_params"] = params[id]
+            analysis[id] = data_content
+
+        with open(filename, 'w') as outfile:
+            GuiUtils.saveData(outfile, analysis)
 
     def actionSave_Analysis(self):
@@ -571,5 +592,19 @@
         Menu File/Save Analysis
         """
-        self.communicate.saveAnalysisSignal.emit()
+        per = self.perspective()
+        if not isinstance(per, FittingWindow):
+            return
+        # get fit page serialization
+        params = per.serializeCurrentFitpage()
+        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):
@@ -1047,4 +1082,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/MainWindow/UnitTesting/DataExplorerTest.py
===================================================================
--- src/sas/qtgui/MainWindow/UnitTesting/DataExplorerTest.py	(revision a24eacf3b3a9ca51fe5e34059e28454573779b5f)
+++ src/sas/qtgui/MainWindow/UnitTesting/DataExplorerTest.py	(revision b1b71ad1ed5ead42519d7e4b99f61af65d85f3aa)
@@ -854,5 +854,5 @@
 
         # Attempt at deleting
-        self.form.deleteItem()
+        self.form.deleteSelectedItem()
 
         # Test the warning dialog called once
@@ -868,5 +868,5 @@
         self.form.current_view.selectionModel().select(select_index, QtCore.QItemSelectionModel.Rows)
         # delete it. now for good
-        self.form.deleteItem()
+        self.form.deleteSelectedItem()
 
         # Test the warning dialog called once
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 a3c59503022182d68f4de9ae4a2c61fa28ddade7)
@@ -112,4 +112,48 @@
     def onLatexCopy(self):
         self.currentTab.onCopyToClipboard("Latex")
+
+    def serializeAllFitpage(self):
+        # serialize all active fitpages and return
+        # a dictionary: {data_id: fitpage_state}
+        params = {}
+        for i, tab in enumerate(self.tabs):
+            tab_data = self.getSerializedFitpage(tab)
+            if tab.tab_id is None: continue
+            id = tab_data['data_id'][0]
+            params[id] = tab_data
+        return params
+
+    def serializeCurrentFitpage(self):
+        # serialize current(active) fitpage
+        return self.getSerializedFitpage(self.currentTab)
+
+    def getSerializedFitpage(self, tab):
+        """
+        get serialize requested fit tab
+        """
+        fitpage_state = tab.getFitPage()
+        fitpage_state += tab.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 +302,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):
         """
@@ -337,4 +387,13 @@
         pass
 
+    def getCurrentStateAsXml(self):
+        """
+        Returns an XML version of the current state
+        """
+        state = {}
+        for tab in self.tabs:
+            pass
+        return state
+
     @property
     def currentTab(self):
Index: src/sas/qtgui/Perspectives/Fitting/FittingWidget.py
===================================================================
--- src/sas/qtgui/Perspectives/Fitting/FittingWidget.py	(revision 0c8330329431af321b3152be7c6603fba3dc80ad)
+++ src/sas/qtgui/Perspectives/Fitting/FittingWidget.py	(revision 0c8330329431af321b3152be7c6603fba3dc80ad)
@@ -52,4 +52,5 @@
 TAB_POLY = 3
 CATEGORY_DEFAULT = "Choose category..."
+MODEL_DEFAULT = "Choose model..."
 CATEGORY_STRUCTURE = "Structure Factor"
 CATEGORY_CUSTOM = "Plugin Models"
@@ -574,6 +575,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)
 
@@ -1029,4 +1028,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():
@@ -1035,4 +1044,9 @@
         if not model:
             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
@@ -1213,5 +1227,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)
@@ -1225,5 +1240,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):
@@ -3410,39 +3428,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):
         """
@@ -3563,4 +3546,7 @@
         """
         param_list = []
+        if self.kernel_module is None:
+            return param_list
+
         param_list.append(['model_name', str(self.cbModel.currentText())])
 
@@ -3667,5 +3653,4 @@
         cb_text = cb.text()
 
-        context = {}
         lines = cb_text.split(':')
         if lines[0] != 'sasview_parameter_values':
@@ -3679,8 +3664,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 ec4a14367fbe5c81c560b98221b989122cfc2d44)
+++ src/sas/qtgui/Perspectives/Fitting/UnitTesting/FittingWidgetTest.py	(revision ec4a14367fbe5c81c560b98221b989122cfc2d44)
@@ -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(), 28)
+        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,9 @@
         # 
         # Now change the model
+<<<<<<< HEAD
+        self.widget.cbModel.setCurrentIndex(4)
+=======
         self.widget.cbModel.setCurrentIndex(2)
+>>>>>>> ESS_GUI
         self.assertEqual(self.widget.cbModel.currentText(),'dab')
 
@@ -226,5 +234,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 +385,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 +394,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 +568,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 +648,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 +989,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 +1004,5 @@
         self.assertListEqual(fp.main_params_to_fit, ['scale'])
 
-    def testPushFitPage(self):
+    def notestPushFitPage(self):
         """
         Push current state of fitpage onto stack
@@ -997,4 +1015,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 aed159f2c55235b1eb207e55b5601081f66c913c)
+++ src/sas/qtgui/Utilities/GuiUtils.py	(revision b1b71ad1ed5ead42519d7e4b99f61af65d85f3aa)
@@ -11,4 +11,6 @@
 import webbrowser
 import urllib.parse
+import json
+from io import BytesIO
 
 import numpy as np
@@ -26,4 +28,13 @@
 from sas.qtgui.Plotting.PlotterData import Data1D
 from sas.qtgui.Plotting.PlotterData import Data2D
+from sas.qtgui.Plotting.Plottables import Plottable
+from sas.sascalc.dataloader.data_info import Sample, Source, Vector
+from sas.qtgui.Plotting.Plottables import View
+from sas.qtgui.Plotting.Plottables import PlottableTheory1D
+from sas.qtgui.Plotting.Plottables import PlottableFit1D
+from sas.qtgui.Plotting.Plottables import Text
+from sas.qtgui.Plotting.Plottables import Chisq
+from sas.qtgui.MainWindow.DataState import DataState
+
 from sas.sascalc.dataloader.loader import Loader
 from sas.qtgui.Utilities import CustomDir
@@ -257,7 +268,4 @@
     sendDataToGridSignal = QtCore.pyqtSignal(list)
 
-    # Action Save Analysis triggered
-    saveAnalysisSignal = QtCore.pyqtSignal()
-
     # Mask Editor requested
     maskEditorSignal = QtCore.pyqtSignal(Data2D)
@@ -287,5 +295,5 @@
     resultPlotUpdateSignal = QtCore.pyqtSignal(list)
 
-def updateModelItemWithPlot(item, update_data, name=""):
+def updateModelItemWithPlot(item, update_data, name="", checkbox_state=None):
     """
     Adds a checkboxed row named "name" to QStandardItem
@@ -312,8 +320,9 @@
             # Force redisplay
             return
-
     # Create the new item
     checkbox_item = createModelItemWithPlot(update_data, name)
 
+    if checkbox_state is not None:
+        checkbox_item.setCheckState(checkbox_state)
     # Append the new row to the main item
     item.appendRow(checkbox_item)
@@ -566,4 +575,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)
@@ -1140,4 +1151,241 @@
     return result
 
+def saveData(fp, data):
+    """
+    save content of data to fp (a .write()-supporting file-like object)
+    """
+
+    def add_type(dict, type):
+        dict['__type__'] = type.__name__
+        return dict
+
+    def jdefault(o):
+        """
+        objects that can't otherwise be serialized need to be converted
+        """
+        # tuples and sets (TODO: default JSONEncoder converts tuples to lists, create custom Encoder that preserves tuples)
+        if isinstance(o, (tuple, set)):
+            content = { 'data': list(o) }
+            return add_type(content, type(o))
+
+        # "simple" types
+        if isinstance(o, (Sample, Source, Vector)):
+            return add_type(o.__dict__, type(o))
+        if isinstance(o, (Plottable, View)):
+            return add_type(o.__dict__, type(o))
+
+        # DataState
+        if isinstance(o, (Data1D, Data2D)):
+            # don't store parent
+            content = o.__dict__.copy()
+            #content.pop('parent')
+            return add_type(content, type(o))
+
+        # ndarray
+        if isinstance(o, np.ndarray):
+            buffer = BytesIO()
+            np.save(buffer, o)
+            buffer.seek(0)
+            content = { 'data': buffer.read().decode('latin-1') }
+            return add_type(content, type(o))
+
+        # not supported
+        logging.info("data cannot be serialized to json: %s" % type(o))
+        return None
+
+    json.dump(data, fp, indent=2, sort_keys=True, default=jdefault)
+
+def readDataFromFile(fp):
+    '''
+    '''
+    supported = [
+        tuple, set,
+        Sample, Source, Vector,
+        Plottable, Data1D, Data2D, PlottableTheory1D, PlottableFit1D, Text, Chisq, View,
+        DataState, np.ndarray]
+
+    lookup = dict((cls.__name__, cls) for cls in supported)
+
+    class TooComplexException(Exception):
+        pass
+
+    def simple_type(cls, data, level):
+        class Empty(object):
+            def __init__(self):
+                for key, value in data.items():
+                    setattr(self, key, generate(value, level))
+
+        # create target object
+        o = Empty()
+        o.__class__ = cls
+
+        return o
+
+    def construct(type, data, level):
+        try:
+            cls = lookup[type]
+        except KeyError:
+            logging.info('unknown type: %s' % type)
+            return None
+
+        # tuples and sets
+        if cls in (tuple, set):
+            # convert list to tuple/set
+            return cls(generate(data['data'], level))
+
+        # "simple" types
+        if cls in (Sample, Source, Vector):
+            return simple_type(cls, data, level)
+        if issubclass(cls, Plottable) or (cls == View):
+            return simple_type(cls, data, level)
+
+        # DataState
+        if cls == DataState:
+            o = simple_type(cls, data, level)
+            o.parent = None # TODO: set to ???
+            return o
+
+        # ndarray
+        if cls == np.ndarray:
+            buffer = BytesIO()
+            buffer.write(data['data'].encode('latin-1'))
+            buffer.seek(0)
+            return np.load(buffer)
+
+        logging.info('not implemented: %s, %s' % (type, cls))
+        return None
+
+    def generate(data, level):
+        if level > 16: # recursion limit (arbitrary number)
+            raise TooComplexException()
+        else:
+            level += 1
+
+        if isinstance(data, dict):
+            try:
+                type = data['__type__']
+            except KeyError:
+                # if dictionary doesn't have __type__ then it is assumed to be just an ordinary dictionary
+                o = {}
+                for key, value in data.items():
+                    o[key] = generate(value, level)
+                return o
+
+            return construct(type, data, level)
+
+        if isinstance(data, list):
+            return [generate(item, level) for item in data]
+
+        return data
+
+    new_stored_data = {}
+    for id, data in json.load(fp).items():
+        try:
+            new_stored_data[id] = generate(data, 0)
+        except TooComplexException:
+            logging.info('unable to load %s' % id)
+
+    return new_stored_data
+
+def readProjectFromSVS(filepath):
+    """
+    Read old SVS file and convert to the project dictionary
+    """
+    from sas.sascalc.dataloader.readers.cansas_reader import Reader as CansasReader
+    from sas.sascalc.fit.pagestate import Reader
+
+    loader = Loader()
+    loader.associate_file_reader('.svs', Reader)
+    temp = loader.load(filepath)
+    state_reader = Reader()
+    data_svs, state_svs = state_reader.read(filepath)
+
+    output = []
+    if isinstance(temp, list) and isinstance(state_svs, list):
+        for item, state in zip(temp, state_svs):
+            output.append([item, state])
+    else:
+        output[temp, state_svs]
+    return output
+
+def convertFromSVS(datasets):
+    """
+    Read in properties from SVS and convert into a simple dict
+    """
+    content = {}
+    for dataset in datasets:
+        # we already have data - interested only in properties
+        #[[item_1, state_1], [item_2, state_2],...]
+        data = dataset[0]
+        params = dataset[1]
+        content[params.data_id] = {}
+        content[params.data_id]['fit_data'] = [data, {'checked': 2}, []]
+        param_dict = {}
+        param_dict['fitpage_category'] = [params.categorycombobox]
+        param_dict['fitpage_model'] = [params.formfactorcombobox]
+        param_dict['fitpage_structure'] = [params.structurecombobox]
+        param_dict['2D_params'] = [str(params.is_2D)]
+        param_dict['chainfit_params'] = ["False"]
+        param_dict['data_id'] = [params.data_id]
+        param_dict['data_name'] = [params.data_name]
+        param_dict['is_data'] = [str(params.is_data)]
+        param_dict['magnetic_params'] = [str(params.magnetic_on)]
+        param_dict['model_name'] = [params.formfactorcombobox]
+        param_dict['polydisperse_params'] = [str(params.enable_disp)]
+        param_dict['q_range_max'] = [str(params.qmax)]
+        param_dict['q_range_min'] = [str(params.qmin)]
+        # Smearing is a bit trickier. 4.x has multiple keywords,
+        # one for each combobox option
+        if params.enable_smearer:
+            if params.slit_smearer:
+                w = 1
+            elif params.pinhole_smearer:
+                w = 2
+            else:
+                w = 0
+            param_dict['smearing'] = [str(w)]
+        # weighting is a bit trickier. 4.x has multiple keywords,
+        # one for each radio box.
+        if params.dI_noweight:
+            w = 2
+        elif params.dI_didata:
+            w = 3
+        elif params.dI_sqrdata:
+            w = 4
+        elif params.dI_idata:
+            w = 5
+        else:
+            w = 2
+        param_dict['weighting'] = [str(w)]
+
+        # 4.x multi_factor is really the multiplicity
+        if params.multi_factor is not None:
+            param_dict['multiplicity'] = [str(int(params.multi_factor))]
+
+        # playing with titles
+        data.filename = params.file
+        data.title = params.data_name
+        data.name = params.data_name
+
+        # main parameters
+        for p in params.parameters:
+            p_name = p[1]
+            param_dict[p_name] = [str(p[0]), str(p[2]), None, str(p[5][1]), str(p[6][1])]
+        # orientation parameters
+        if params.is_2D:
+            for p in params.orientation_params:
+                p_name = p[1]
+                param_dict[p_name] = [str(p[0]), str(p[2]), None, str(p[5][1]), str(p[6][1])]
+
+        # disperse parameters
+        if params.enable_disp:
+            for p in params.fittable_param:
+                p_name = p[1]
+                param_dict[p_name] = [str(p[0]), str(p[2]), None, str(35), str(3)]
+
+        # magnetic parameters
+
+        content[params.data_id]['fit_params'] = param_dict
+    return content
 
 def enum(*sequential, **named):
Index: src/sas/sascalc/dataloader/readers/cansas_reader.py
===================================================================
--- src/sas/sascalc/dataloader/readers/cansas_reader.py	(revision b8080e1affadb04559f0597dfddc5506d2044e9d)
+++ src/sas/sascalc/dataloader/readers/cansas_reader.py	(revision b1b71ad1ed5ead42519d7e4b99f61af65d85f3aa)
@@ -184,5 +184,5 @@
             if CANSAS_NS.get(self.cansas_version).get("ns") == value.rsplit(" ")[0]:
                 return True
-        if ext == "svs":
+        if ext == ".svs":
             return True # Why is this required?
         # If we get to this point then file isn't valid CanSAS
Index: src/sas/sascalc/fit/pagestate.py
===================================================================
--- src/sas/sascalc/fit/pagestate.py	(revision b8080e1affadb04559f0597dfddc5506d2044e9d)
+++ src/sas/sascalc/fit/pagestate.py	(revision b1b71ad1ed5ead42519d7e4b99f61af65d85f3aa)
@@ -1249,12 +1249,13 @@
 
             else:
-                self.call_back(format=ext)
+                #self.call_back(format=ext)
                 raise RuntimeError("%s is not a file" % path)
 
             # Return output consistent with the loader's api
             if len(output) == 0:
-                self.call_back(state=None, datainfo=None, format=ext)
+                #self.call_back(state=None, datainfo=None, format=ext)
                 return None
             else:
+                states=[]
                 for data in output:
                     # Call back to post the new state
@@ -1281,5 +1282,5 @@
                         if isinstance(data.run_name, dict):
                             # Note: key order in dict is not guaranteed, so sort
-                            name = data.run_name.keys()[0]
+                            name = list(data.run_name.keys())[0]
                         else:
                             name = data.run_name
@@ -1289,13 +1290,14 @@
                     state.version = fitstate.version
                     # store state in fitting
-                    self.call_back(state=state, datainfo=data, format=ext)
+                    #self.call_back(state=state, datainfo=data, format=ext)
                     self.state = state
+                    states.append(state)
                 simfitstate = self._parse_simfit_state(entry)
                 if simfitstate is not None:
-                    self.call_back(state=simfitstate)
-
-                return output
+                    #self.call_back(state=simfitstate)
+                    states.append(simfitstate)
+                return (output, states)
         except:
-            self.call_back(format=ext)
+            #self.call_back(format=ext)
             raise
 
