Index: src/sas/qtgui/DataExplorer.py
===================================================================
--- src/sas/qtgui/DataExplorer.py	(revision 1af348e374c1979b8fa98dcf40e27d60c8e4e9c0)
+++ src/sas/qtgui/DataExplorer.py	(revision 630155bd6fe781772e87d6b5b374bff847bca873)
@@ -30,5 +30,5 @@
     # for the gui and providing an interface to the data model.
 
-    def __init__(self, parent=None, guimanager=None):
+    def __init__(self, parent=None, guimanager=None, manager=None):
         super(DataExplorerWindow, self).__init__(parent, guimanager)
 
@@ -43,5 +43,5 @@
         self.parent = guimanager
         self.loader = Loader()
-        self.manager = DataManager()
+        self.manager = manager if manager is not None else DataManager()
         self.txt_widget = QtGui.QTextEdit(None)
         # self.txt_widget = GuiUtils.DisplayWindow()
@@ -164,4 +164,58 @@
 
         self.loadFromURL(path_str)
+
+    def loadProject(self):
+        """
+        Called when the "Open Project" menu item chosen.
+        """
+        kwargs = {
+            'parent'    : self,
+            'caption'   : 'Open Project',
+            'filter'    : 'Project (*.json);;All files (*.*)',
+            'options'   : QtGui.QFileDialog.DontUseNativeDialog
+        }
+        filename = str(QtGui.QFileDialog.getOpenFileName(**kwargs))
+        if filename:
+            load_thread = threads.deferToThread(self.readProject, filename)
+            load_thread.addCallback(self.readProjectComplete)
+
+    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)
+        for id, item in self.manager.get_all_data().iteritems():
+            self.updateModel(item.data, item.path)
+
+        self.model.reset()
+
+    def saveProject(self):
+        """
+        Called when the "Save Project" menu item chosen.
+        """
+        kwargs = {
+            'parent'    : self,
+            'caption'   : 'Save Project',
+            'filter'    : 'Project (*.json)',
+            'options'   : QtGui.QFileDialog.DontUseNativeDialog
+        }
+        filename = str(QtGui.QFileDialog.getSaveFileName(**kwargs))
+        if filename:
+            self.communicator.statusBarUpdateSignal.emit("Saving Project... %s\n" % os.path.basename(filename))
+            with open(filename, 'w') as outfile:
+                self.manager.save_to_writable(outfile)
 
     def deleteFile(self, event):
Index: src/sas/qtgui/GuiManager.py
===================================================================
--- src/sas/qtgui/GuiManager.py	(revision f51ed677cd8461e82d4cf08d0c78ef678c8f9433)
+++ src/sas/qtgui/GuiManager.py	(revision 630155bd6fe781772e87d6b5b374bff847bca873)
@@ -111,5 +111,5 @@
         """
         # Add FileDialog widget as docked
-        self.filesWidget = DataExplorerWindow(self._parent, self)
+        self.filesWidget = DataExplorerWindow(self._parent, self, manager=self._data_manager)
 
         self.dockedFilesWidget = QtGui.QDockWidget("Data Explorer", self._workspace)
@@ -391,7 +391,7 @@
     def actionOpen_Project(self):
         """
-        """
-        print("actionOpen_Project TRIGGERED")
-        pass
+        Menu Open Project
+        """
+        self.filesWidget.loadProject()
 
     def actionOpen_Analysis(self):
@@ -403,7 +403,7 @@
     def actionSave(self):
         """
-        """
-        print("actionSave TRIGGERED")
-        pass
+        Menu Save Project
+        """
+        self.filesWidget.saveProject()
 
     def actionSave_Analysis(self):
Index: src/sas/sasgui/guiframe/data_manager.py
===================================================================
--- src/sas/sasgui/guiframe/data_manager.py	(revision d85c194ea2f78cb2b9b3de9c969e1b8ebea6b6d2)
+++ src/sas/sasgui/guiframe/data_manager.py	(revision 630155bd6fe781772e87d6b5b374bff847bca873)
@@ -2,5 +2,5 @@
 #This software was developed by the University of Tennessee as part of the
 #Distributed Data Analysis of Neutron Scattering Experiments (DANSE)
-#project funded by the US National Science Foundation. 
+#project funded by the US National Science Foundation.
 #
 #See the license text in license.txt
@@ -9,13 +9,15 @@
 ################################################################################
 """
-This module manages all data loaded into the application. Data_manager makes 
-available all data loaded  for the current perspective. 
-
-All modules "creating Data" posts their data to data_manager . 
+This module manages all data loaded into the application. Data_manager makes
+available all data loaded  for the current perspective.
+
+All modules "creating Data" posts their data to data_manager .
 Data_manager  make these new data available for all other perspectives.
 """
+import os
+import copy
 import logging
-import os
-import copy 
+import json
+from StringIO import StringIO
 
 from sas.sasgui.guiframe.data_state import DataState
@@ -26,4 +28,9 @@
 import time
 
+# used for import/export
+import numpy as np
+from sas.sascalc.dataloader.data_info import Sample, Source, Vector
+from sas.sasgui.plottools.plottables import Plottable, Theory1D, Fit1D, Text, Chisq, View
+
 class DataManager(object):
     """
@@ -33,6 +40,6 @@
         """
         Store opened path and data object created at the loading time
-        :param auto_plot: if True the datamanager sends data to plotting 
-                            plugin. 
+        :param auto_plot: if True the datamanager sends data to plotting
+                            plugin.
         :param auto_set_data: if True the datamanager sends to the current
         perspective
@@ -44,5 +51,5 @@
         self.list_of_id = []
         self.time_stamp = time.time()
-      
+
     def __str__(self):
         _str  = ""
@@ -50,19 +57,19 @@
         n_count = 0
         for  value in self.stored_data.values():
-            n_count += 1 
+            n_count += 1
             _str += "State No %s \n"  % str(n_count)
             _str += str(value) + "\n"
         return _str
-        
+
     def create_gui_data(self, data, path=None):
         """
         Receive data from loader and create a data to use for guiframe
         """
-        
+
         if issubclass(Data2D, data.__class__):
-            new_plot = Data2D(image=None, err_image=None) 
-        else: 
+            new_plot = Data2D(image=None, err_image=None)
+        else:
             new_plot = Data1D(x=[], y=[], dx=None, dy=None)
-           
+
         new_plot.copy_from_datainfo(data)
         data.clone_without_data(clone=new_plot)
@@ -82,8 +89,8 @@
         if title.strip() == "":
             title = file_name
-        
+
         if new_plot.filename.strip() == "":
             new_plot.filename = file_name
-        
+
         new_plot.name = name
         new_plot.title = title
@@ -101,5 +108,5 @@
         # plot data
         return new_plot
- 
+
     def rename(self, name):
         """
@@ -108,10 +115,10 @@
         ## name of the data allow to differentiate data when plotted
         name = parse_name(name=name, expression="_")
-        
+
         max_char = name.find("[")
         if max_char < 0:
             max_char = len(name)
         name = name[0:max_char]
-        
+
         if name not in self.data_name_dict:
             self.data_name_dict[name] = 0
@@ -120,9 +127,9 @@
             name = name + " [" + str(self.data_name_dict[name]) + "]"
         return name
-    
-  
+
+
     def add_data(self, data_list):
         """
-        receive a list of 
+        receive a list of
         """
         for id, data in data_list.iteritems():
@@ -138,5 +145,5 @@
                 data_state.path = data.path
                 self.stored_data[id] = data_state
-    
+
     def update_data(self, prev_data, new_data):
         """
@@ -144,11 +151,11 @@
         if prev_data.id not in self.stored_data.keys():
             return None, {}
-        data_state = self.stored_data[prev_data.id] 
+        data_state = self.stored_data[prev_data.id]
         self.stored_data[new_data.id]  = data_state.clone()
         self.stored_data[new_data.id].data = new_data
         if prev_data.id in self.stored_data.keys():
-            del self.stored_data[prev_data.id] 
+            del self.stored_data[prev_data.id]
         return prev_data.id, {new_data.id: self.stored_data[new_data.id]}
-    
+
     def update_theory(self, theory, data_id=None, state=None):
         """
@@ -158,5 +165,5 @@
             uid = theory.id
         if uid in self.stored_data.keys():
-             data_state = self.stored_data[uid] 
+             data_state = self.stored_data[uid]
         else:
             data_state = DataState()
@@ -165,6 +172,6 @@
         self.stored_data[uid] = data_state
         return {uid: self.stored_data[uid]}
-       
-    
+
+
     def get_message(self):
         """
@@ -172,5 +179,5 @@
         """
         return self.message
-    
+
     def get_by_id(self, id_list=None):
         """
@@ -189,13 +196,13 @@
                 if search_id in theory_list.keys():
                      _selected_theory_list[search_id] = theory_list[search_id]
-                   
+
         return _selected_data, _selected_theory_list
-   
-           
+
+
     def freeze(self, theory_id):
         """
         """
         return self.freeze_theory(self.stored_data.keys(), theory_id)
-        
+
     def freeze_theory(self, data_id, theory_id):
         """
@@ -221,6 +228,6 @@
 
         return selected_theory
-                    
-            
+
+
     def delete_data(self, data_id, theory_id=None, delete_all=False):
         """
@@ -232,10 +239,10 @@
                     del self.data_name_dict[data_state.data.name]
                 del self.stored_data[d_id]
-        
+
         self.delete_theory(data_id, theory_id)
         if delete_all:
             self.stored_data = {}
             self.data_name_dict = {}
-            
+
     def delete_theory(self, data_id, theory_id):
         """
@@ -249,5 +256,5 @@
         #del pure theory
         self.delete_by_id(theory_id)
-            
+
     def delete_by_id(self, id_list=None):
         """
@@ -257,6 +264,6 @@
             if id in self.stored_data:
                 del self.stored_data[id]
-         
-    
+
+
     def get_by_name(self, name_list=None):
         """
@@ -269,5 +276,5 @@
                     _selected_data[id] = data_state.data
         return _selected_data
-    
+
     def delete_by_name(self, name_list=None):
         """
@@ -288,5 +295,5 @@
                 _selected_data_state[id] = self.stored_data[id]
         return _selected_data_state
-    
+
     def get_all_data(self):
         """
@@ -294,5 +301,150 @@
         """
         return self.stored_data
-    
-
-        
+
+    def assign(self, other):
+        self.stored_data = other.stored_data
+        self.message = other.message
+        self.data_name_dict = other.data_name_dict
+        self.count = other.count
+        self.list_of_id = other.list_of_id
+        self.time_stamp = other.time_stamp
+
+    def save_to_writable(self, fp):
+        """
+        save content of stored_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, DataState):
+                # don't store parent
+                content = o.__dict__.copy()
+                content.pop('parent')
+                return add_type(content, type(o))
+
+            # ndarray
+            if isinstance(o, np.ndarray):
+                buffer = StringIO()
+                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(self.stored_data, fp, indent=2, sort_keys=True, default=jdefault)
+
+
+    def load_from_readable(self, fp):
+        """
+        load content from tp to stored_data (a .read()-supporting file-like object)
+        """
+
+        supported = [
+            tuple, set,
+            Sample, Source, Vector,
+            Plottable, Data1D, Data2D, Theory1D, Fit1D, 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.iteritems():
+                        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 = StringIO()
+                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.iteritems():
+                        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).iteritems():
+            try:
+                new_stored_data[id] = generate(data, 0)
+            except TooComplexException:
+                logging.info('unable to load %s' % id)
+
+        self.stored_data = new_stored_data
