diff --git a/Sources/Data/Microsoft.Psi.Data/Dataset.cs b/Sources/Data/Microsoft.Psi.Data/Dataset.cs
index d458d009a..a01e11162 100644
--- a/Sources/Data/Microsoft.Psi.Data/Dataset.cs
+++ b/Sources/Data/Microsoft.Psi.Data/Dataset.cs
@@ -26,22 +26,67 @@ public class Dataset
///
public const string DefaultName = "Untitled Dataset";
+ private string name;
+
///
/// Initializes a new instance of the class.
///
/// The name of the new dataset. Default is .
+ /// An optional filename that indicates the location to save the dataset..
+ /// Whether the dataset automatically autosave changes if a path is given (optional, default is false).
+ /// Indicates whether to use full or relative store paths (optional, default is true).
[JsonConstructor]
- public Dataset(string name = Dataset.DefaultName)
+ public Dataset(string name = Dataset.DefaultName, string filename = "", bool autoSave = false, bool useRelativePaths = true)
{
this.Name = name;
+ this.Filename = filename;
+ this.AutoSave = autoSave;
+ this.UseRelativePaths = useRelativePaths;
this.InternalSessions = new List();
+ if (this.AutoSave && filename == string.Empty)
+ {
+ throw new ArgumentException("filename needed to be provided for autosave dataset.");
+ }
}
+ ///
+ /// Event raise when the dataset's structure changed.
+ ///
+ public event EventHandler DatasetChanged;
+
///
/// Gets or sets the name of this dataset.
///
[DataMember]
- public string Name { get; set; }
+ public string Name
+ {
+ get => this.name;
+ set
+ {
+ this.name = value;
+ this.OnDatasetChanged();
+ }
+ }
+
+ ///
+ /// Gets or sets the current save path of this dataset.
+ ///
+ public string Filename { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether autosave is enabled.
+ ///
+ public bool AutoSave { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether to use full or relative store paths.
+ ///
+ public bool UseRelativePaths { get; set; }
+
+ ///
+ /// Gets a value indicating whether changes to this dataset have been saved.
+ ///
+ public bool HasUnsavedChanges { get; private set; } = false;
///
/// Gets the originating time interval (earliest to latest) of the messages in this dataset.
@@ -76,8 +121,9 @@ public Dataset(string name = Dataset.DefaultName)
/// Loads a dataset from the specified file.
///
/// The name of the file that contains the dataset to be loaded.
+ /// A value to indicate whether to enable autosave (optional, default is false).
/// The newly loaded dataset.
- public static Dataset Load(string filename)
+ public static Dataset Load(string filename, bool autoSave = false)
{
var serializer = JsonSerializer.Create(
new JsonSerializerSettings()
@@ -92,7 +138,10 @@ public static Dataset Load(string filename)
});
using var jsonFile = File.OpenText(filename);
using var jsonReader = new JsonTextReader(jsonFile);
- return serializer.Deserialize(jsonReader);
+ var dataset = serializer.Deserialize(jsonReader);
+ dataset.AutoSave = autoSave;
+ dataset.Filename = filename;
+ return dataset;
}
///
@@ -118,6 +167,7 @@ public Session CreateSession(string sessionName = Session.DefaultName)
{
var session = new Session(this, sessionName);
this.InternalSessions.Add(session);
+ this.OnDatasetChanged();
return session;
}
@@ -128,6 +178,7 @@ public Session CreateSession(string sessionName = Session.DefaultName)
public void RemoveSession(Session session)
{
this.InternalSessions.Remove(session);
+ this.OnDatasetChanged();
}
///
@@ -145,20 +196,37 @@ public void Append(Dataset inputDataset)
newSession.AddStorePartition(StreamReader.Create(p.StoreName, p.StorePath, p.StreamReaderTypeName), p.Name);
}
}
+
+ this.OnDatasetChanged();
}
///
- /// Saves this dataset to the specified file.
+ /// Saves this dataset.
///
- /// The name of the file to save this dataset into.
- /// Indicates whether to use full or relative store paths.
- public void Save(string filename, bool useRelativePaths = true)
+ /// The filename that indicates the location to save the dataset.
+ /// Indicates whether to use full or relative store paths (optional, default is true).
+ public void SaveAs(string filename, bool useRelativePaths = true)
{
+ this.Filename = filename;
+ this.UseRelativePaths = useRelativePaths;
+ this.Save();
+ }
+
+ ///
+ /// Saves this dataset.
+ ///
+ public void Save()
+ {
+ if (this.Filename == string.Empty)
+ {
+ throw new ArgumentException("filename to save the dataset must be set before save operation.");
+ }
+
var serializer = JsonSerializer.Create(
new JsonSerializerSettings()
{
// pass the dataset filename in the context to allow relative store paths to be computed using the RelativePathConverter
- Context = useRelativePaths ? new StreamingContext(StreamingContextStates.File, filename) : default,
+ Context = this.UseRelativePaths ? new StreamingContext(StreamingContextStates.File, this.Filename) : default,
Formatting = Formatting.Indented,
NullValueHandling = NullValueHandling.Ignore,
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
@@ -166,9 +234,10 @@ public void Save(string filename, bool useRelativePaths = true)
TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Simple,
SerializationBinder = new SafeSerializationBinder(),
});
- using var jsonFile = File.CreateText(filename);
+ using var jsonFile = File.CreateText(this.Filename);
using var jsonWriter = new JsonTextWriter(jsonFile);
serializer.Serialize(jsonWriter, this);
+ this.HasUnsavedChanges = false;
}
///
@@ -414,6 +483,25 @@ public void AddSessionsFromPsiStores(string path, string partitionName = null)
}
}
+ ///
+ /// Method called when structure of the dataset changed.
+ ///
+ public virtual void OnDatasetChanged()
+ {
+ if (this.AutoSave)
+ {
+ this.Save();
+ }
+ else
+ {
+ this.HasUnsavedChanges = true;
+ }
+
+ // raise the event.
+ EventHandler handler = this.DatasetChanged;
+ handler?.Invoke(this, EventArgs.Empty);
+ }
+
///
/// Adds a session to this dataset and updates its originating time interval.
///
@@ -427,6 +515,7 @@ private void AddSession(Session session)
}
this.InternalSessions.Add(session);
+ this.OnDatasetChanged();
}
[OnDeserialized]
diff --git a/Sources/Data/Microsoft.Psi.Data/Session.cs b/Sources/Data/Microsoft.Psi.Data/Session.cs
index e64b6216b..115dbe7ad 100644
--- a/Sources/Data/Microsoft.Psi.Data/Session.cs
+++ b/Sources/Data/Microsoft.Psi.Data/Session.cs
@@ -42,6 +42,11 @@ private Session()
{
}
+ ///
+ /// Event invoked when the structure of the session changed.
+ ///
+ public event EventHandler SessionChanged;
+
///
/// Gets the dataset that this session belongs to.
///
@@ -64,6 +69,7 @@ public string Name
}
this.name = value;
+ this.OnSessionChanged();
}
}
@@ -293,6 +299,17 @@ await Task.Run(
public void RemovePartition(IPartition partition)
{
this.InternalPartitions.Remove(partition);
+ this.OnSessionChanged();
+ }
+
+ ///
+ /// Method called when structure of the session changed.
+ ///
+ protected virtual void OnSessionChanged()
+ {
+ this.Dataset?.OnDatasetChanged();
+ EventHandler handler = this.SessionChanged;
+ handler?.Invoke(this, EventArgs.Empty);
}
///
@@ -308,6 +325,7 @@ private void AddPartition(IPartition partition)
}
this.InternalPartitions.Add(partition);
+ this.OnSessionChanged();
}
[OnDeserialized]
diff --git a/Sources/Data/Test.Psi.Data/DatasetTests.cs b/Sources/Data/Test.Psi.Data/DatasetTests.cs
index a930f5006..4af740841 100644
--- a/Sources/Data/Test.Psi.Data/DatasetTests.cs
+++ b/Sources/Data/Test.Psi.Data/DatasetTests.cs
@@ -227,7 +227,7 @@ public void DatasetAbsolutePaths()
// save dataset with absolute store paths
string datasetFile = Path.Combine(StorePath, "dataset.pds");
- dataset.Save(datasetFile, false);
+ dataset.SaveAs(datasetFile, false);
// create Temp sub-folder
var tempFolder = Path.Combine(StorePath, "Temp");
@@ -257,7 +257,7 @@ public void DatasetRelativePaths()
// save dataset with relative store paths
string datasetFile = Path.Combine(StorePath, "dataset.pds");
- dataset.Save(datasetFile, true);
+ dataset.SaveAs(datasetFile, true);
// create Temp sub-folder
var tempFolder = Path.Combine(StorePath, "Temp");
@@ -423,6 +423,118 @@ await session.CreateDerivedPsiPartitionAsync(
}
}
+ [TestMethod]
+ [Timeout(60000)]
+ public void DatasetAutoSave()
+ {
+ var datasetPath = Path.Join(StorePath, "autosave.pds");
+
+ var dataset = new Dataset("autosave", datasetPath, autoSave: true);
+ dataset.Name = "autosave-saved";
+ GenerateTestStore("store1", StorePath);
+
+ var session1 = dataset.CreateSession("test-session1");
+ var session2 = dataset.AddSessionFromPsiStore("store1", StorePath);
+ session1.Name = "no-longer-test-session1";
+
+ // open the dataset file as a different dataset and validate information
+ var sameDataset = Dataset.Load(datasetPath);
+ Assert.AreEqual("autosave-saved", sameDataset.Name);
+ Assert.AreEqual(2, sameDataset.Sessions.Count);
+ Assert.AreEqual("no-longer-test-session1", sameDataset.Sessions[0].Name);
+ Assert.AreEqual(session2.Name, sameDataset.Sessions[1].Name);
+
+ // remove a session and verify changes are saved.
+ dataset.RemoveSession(session1);
+ sameDataset = Dataset.Load(datasetPath);
+ Assert.AreEqual(1, sameDataset.Sessions.Count);
+ Assert.AreEqual(session2.Name, sameDataset.Sessions[0].Name);
+ Assert.AreEqual(1, sameDataset.Sessions[0].Partitions.Count);
+ Assert.AreEqual(session2.OriginatingTimeInterval.Left, sameDataset.Sessions[0].OriginatingTimeInterval.Left);
+ Assert.AreEqual(session2.OriginatingTimeInterval.Right, sameDataset.Sessions[0].OriginatingTimeInterval.Right);
+
+ // now we edit the session and we want to make sure the changes stick!
+ GenerateTestStore("store3", StorePath);
+ session2.AddPsiStorePartition("store3", StorePath);
+ sameDataset = Dataset.Load(datasetPath);
+ Assert.AreEqual(session2.Name, sameDataset.Sessions[0].Name);
+ Assert.AreEqual(2, sameDataset.Sessions[0].Partitions.Count);
+ Assert.AreEqual("store3", sameDataset.Sessions[0].Partitions[1].Name);
+ }
+
+ [TestMethod]
+ [Timeout(60000)]
+ public void DatasetAutoSaveAsync()
+ {
+ var datasetPath = Path.Join(StorePath, "autosave.pds");
+ GenerateTestStore("base1", StorePath);
+ GenerateTestStore("base2", StorePath);
+ var dataset = new Dataset("autosave", datasetPath, autoSave: true);
+ dataset.AddSessionFromPsiStore("base1", StorePath, "s1");
+ dataset.AddSessionFromPsiStore("base2", StorePath, "s2");
+ Assert.AreEqual(1, dataset.Sessions[0].Partitions.Count());
+ Assert.AreEqual(1, dataset.Sessions[1].Partitions.Count());
+ Task.Run(async () =>
+ {
+ await dataset.CreateDerivedPartitionAsync(
+ (_, importer, exporter) =>
+ {
+ importer.OpenStream("Root").Select(x => x * x).Write("RootSquared", exporter);
+ },
+ "derived",
+ true,
+ "derived-store");
+ }).Wait(); // wait for the async function to finish
+
+ // open the dataset file as a different dataset and validate information
+ var sameDataset = Dataset.Load(datasetPath);
+ Assert.AreEqual(2, sameDataset.Sessions.Count);
+ Assert.AreEqual(2, sameDataset.Sessions[0].Partitions.Count());
+ Assert.AreEqual(2, sameDataset.Sessions[1].Partitions.Count());
+ Assert.AreEqual("derived", sameDataset.Sessions[1].Partitions[1].Name);
+ Assert.AreEqual("derived-store", sameDataset.Sessions[1].Partitions[1].StoreName);
+ }
+
+ [TestMethod]
+ [Timeout(60000)]
+ public void DatasetUnsavedChanges()
+ {
+ var dataset = new Dataset("autosave");
+ dataset.CreateSession("test-session1");
+ Assert.IsTrue(dataset.HasUnsavedChanges);
+ dataset.SaveAs("unsave.pds");
+ Assert.IsTrue(!dataset.HasUnsavedChanges);
+ }
+
+ [TestMethod]
+ [Timeout(60000)]
+ public void DatasetChangeEvent()
+ {
+ var sessionEventCalled = false;
+ var datasetEventCalled = false;
+ var dataset = new Dataset("autosave");
+
+ GenerateTestStore("base1", StorePath);
+ var session1 = dataset.AddSessionFromPsiStore("base1", StorePath, "session1");
+ dataset.DatasetChanged += (s, e) =>
+ {
+ Assert.AreEqual(e, EventArgs.Empty);
+ datasetEventCalled = true;
+ };
+ session1.SessionChanged += (s, e) =>
+ {
+ Assert.AreEqual(e, EventArgs.Empty);
+ sessionEventCalled = true;
+ };
+
+ // the following change should cause both events to be called.
+ session1.Name = "new name";
+
+ // validate if the events were called.
+ Assert.IsTrue(datasetEventCalled);
+ Assert.IsTrue(sessionEventCalled);
+ }
+
private static void GenerateTestStore(string storeName, string storePath)
{
using var p = Pipeline.Create();
diff --git a/Sources/Tools/PsiStudio/Microsoft.Psi.PsiStudio/MainWindow.xaml b/Sources/Tools/PsiStudio/Microsoft.Psi.PsiStudio/MainWindow.xaml
index 55f88c453..3b3318130 100644
--- a/Sources/Tools/PsiStudio/Microsoft.Psi.PsiStudio/MainWindow.xaml
+++ b/Sources/Tools/PsiStudio/Microsoft.Psi.PsiStudio/MainWindow.xaml
@@ -65,7 +65,7 @@
-
[Browsable(false)]
[IgnoreDataMember]
- public RelayCommand SaveDatasetCommand
+ public RelayCommand SaveDatasetAsCommand
{
get
{
- if (this.saveDatasetCommand == null)
+ if (this.saveDatasetAsCommand == null)
{
- this.saveDatasetCommand = new RelayCommand(
+ this.saveDatasetAsCommand = new RelayCommand(
async () =>
{
SaveFileDialog dlg = new SaveFileDialog
@@ -409,12 +409,12 @@ public RelayCommand SaveDatasetCommand
string filename = dlg.FileName;
// this should be a relatively quick operation so no need to show progress
- await VisualizationContext.Instance.DatasetViewModel.SaveAsync(filename);
+ await VisualizationContext.Instance.DatasetViewModel.SaveAsAsync(filename);
}
});
}
- return this.saveDatasetCommand;
+ return this.saveDatasetAsCommand;
}
}
diff --git a/Sources/Tools/PsiStudio/Microsoft.Psi.PsiStudio/PsiStudioSettings.cs b/Sources/Tools/PsiStudio/Microsoft.Psi.PsiStudio/PsiStudioSettings.cs
index 2a4fd4061..d2b9fe4b4 100644
--- a/Sources/Tools/PsiStudio/Microsoft.Psi.PsiStudio/PsiStudioSettings.cs
+++ b/Sources/Tools/PsiStudio/Microsoft.Psi.PsiStudio/PsiStudioSettings.cs
@@ -38,6 +38,7 @@ public PsiStudioSettings()
this.ShowTimingRelativeToSessionStart = false;
this.ShowTimingRelativeToSelectionStart = false;
this.CurrentLayoutName = null;
+ this.AutoSaveDatasets = false;
this.AdditionalAssemblies = null;
}
@@ -101,6 +102,11 @@ public PsiStudioSettings()
///
public bool ShowTimingRelativeToSelectionStart { get; set; }
+ ///
+ /// Gets or sets a value indicating whether to set any open dataset object into autosave mode.
+ ///
+ public bool AutoSaveDatasets { get; set; }
+
///
/// Gets or sets the list of add-in assemblies.
///
diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/ViewModels/DatasetViewModel.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/ViewModels/DatasetViewModel.cs
index 1a796bbc3..0bd3b02c8 100644
--- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/ViewModels/DatasetViewModel.cs
+++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/ViewModels/DatasetViewModel.cs
@@ -428,9 +428,10 @@ public AuxiliaryStreamInfo ShowAuxiliaryStreamInfo
/// Loads a dataset from the specified file.
///
/// The name of the file that contains the dataset to be loaded.
+ /// A value to indicate whether to enable the autosave feature.
/// The newly loaded dataset view model.
- public static DatasetViewModel Load(string filename) =>
- new DatasetViewModel(Dataset.Load(filename))
+ public static DatasetViewModel Load(string filename, bool autoSave = false) =>
+ new DatasetViewModel(Dataset.Load(filename, autoSave))
{
FileName = filename,
};
@@ -439,12 +440,13 @@ public static DatasetViewModel Load(string filename) =>
/// Asynchronously loads a dataset from the specified file.
///
/// The name of the file that contains the dataset to be loaded.
+ /// A value to indicate whether to enable the dataset's autosave feature.
///
/// A task that represents the asynchronous operation. The value of the TResult parameter
/// contains the newly loaded dataset view model.
///
- public static Task LoadAsync(string filename) =>
- Task.Run(() => Load(filename));
+ public static Task LoadAsync(string filename, bool autoSave = false) =>
+ Task.Run(() => Load(filename, autoSave));
///
/// Creates a new dataset from an existing data store.
@@ -599,9 +601,9 @@ public void CreateSessionFromStore(string sessionName, IStreamReader streamReade
/// Saves this dataset to the specified file.
///
/// The name of the file to save this dataset into.
- public void Save(string filename)
+ public void SaveAs(string filename)
{
- this.dataset.Save(filename);
+ this.dataset.SaveAs(filename);
this.FileName = filename;
}
@@ -610,8 +612,8 @@ public void Save(string filename)
///
/// The name of the file to save this dataset into.
/// A task that represents the asynchronous operation.
- public Task SaveAsync(string filename) =>
- Task.Run(() => this.Save(filename));
+ public Task SaveAsAsync(string filename) =>
+ Task.Run(() => this.SaveAs(filename));
///
/// Removes the specified session from the underlying dataset.
diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationContext.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationContext.cs
index c694af1f1..9d3167e4d 100644
--- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationContext.cs
+++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationContext.cs
@@ -203,7 +203,7 @@ public async Task RunDatasetBatchProcessingTaskAsync(DatasetViewModel datasetVie
// if the dataset has a known associated file, save it.
if (datasetViewModel.FileName != null)
{
- await datasetViewModel.SaveAsync(datasetViewModel.FileName);
+ await datasetViewModel.SaveAsAsync(datasetViewModel.FileName);
}
}
catch (InvalidOperationException)
@@ -263,7 +263,7 @@ public async Task RunSessionBatchProcessingTask(SessionViewModel sessionViewMode
// if the dataset has a known associated file, save it.
if (sessionViewModel.DatasetViewModel.FileName != null)
{
- await sessionViewModel.DatasetViewModel.SaveAsync(sessionViewModel.DatasetViewModel.FileName);
+ await sessionViewModel.DatasetViewModel.SaveAsAsync(sessionViewModel.DatasetViewModel.FileName);
}
}
catch (InvalidOperationException)
@@ -352,8 +352,9 @@ public void VisualizeStream(StreamTreeNode streamTreeNode, VisualizerMetadata vi
///
/// Fully qualified path to dataset file.
/// Indicates whether to show the status window.
+ /// Indicates whether to enable autosave.
/// A task that represents the asynchronous operation.
- public async Task OpenDatasetAsync(string filename, bool showStatusWindow = true)
+ public async Task OpenDatasetAsync(string filename, bool showStatusWindow = true, bool autoSave = false)
{
var loadDatasetTask = default(Task);
if (showStatusWindow)
@@ -374,7 +375,7 @@ public async Task OpenDatasetAsync(string filename, bool showStatusWindow = true
});
// start the load dataset task
- loadDatasetTask = this.LoadDatasetOrStoreAsync(filename, progress);
+ loadDatasetTask = this.LoadDatasetOrStoreAsync(filename, progress, autoSave);
try
{
@@ -389,7 +390,7 @@ public async Task OpenDatasetAsync(string filename, bool showStatusWindow = true
}
else
{
- loadDatasetTask = this.LoadDatasetOrStoreAsync(filename);
+ loadDatasetTask = this.LoadDatasetOrStoreAsync(filename, autoSave: autoSave);
}
try
@@ -473,7 +474,7 @@ public void DisplayObjectProperties(object requestingObject)
this.RequestDisplayObjectProperties?.Invoke(this, new RequestDisplayObjectPropertiesEventArgs(requestingObject));
}
- private async Task LoadDatasetOrStoreAsync(string filename, IProgress<(string, double)> progress = null)
+ private async Task LoadDatasetOrStoreAsync(string filename, IProgress<(string, double)> progress = null, bool autoSave = false)
{
try
{
@@ -481,7 +482,7 @@ private async Task LoadDatasetOrStoreAsync(string filename, IProgress<(string, d
if (fileInfo.Extension == ".pds")
{
progress?.Report(("Loading dataset...", 0.5));
- this.DatasetViewModel = await DatasetViewModel.LoadAsync(filename);
+ this.DatasetViewModel = await DatasetViewModel.LoadAsync(filename, autoSave);
}
else
{