Skip to content
109 changes: 99 additions & 10 deletions Sources/Data/Microsoft.Psi.Data/Dataset.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,22 +26,67 @@ public class Dataset
/// </summary>
public const string DefaultName = "Untitled Dataset";

private string name;

/// <summary>
/// Initializes a new instance of the <see cref="Dataset"/> class.
/// </summary>
/// <param name="name">The name of the new dataset. Default is <see cref="DefaultName"/>.</param>
/// <param name="filename">An optional filename that indicates the location to save the dataset.<see cref="DefaultName"/>.</param>
/// <param name="autoSave">Whether the dataset automatically autosave changes if a path is given (optional, default is false).</param>
/// <param name="useRelativePaths">Indicates whether to use full or relative store paths (optional, default is true).</param>
[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<Session>();
if (this.AutoSave && filename == string.Empty)
{
throw new ArgumentException("filename needed to be provided for autosave dataset.");
}
}

/// <summary>
/// Event raise when the dataset's structure changed.
/// </summary>
public event EventHandler DatasetChanged;

/// <summary>
/// Gets or sets the name of this dataset.
/// </summary>
[DataMember]
public string Name { get; set; }
public string Name
{
get => this.name;
set
{
this.name = value;
this.OnDatasetChanged();
}
}

/// <summary>
/// Gets or sets the current save path of this dataset.
/// </summary>
public string Filename { get; set; }

/// <summary>
/// Gets or sets a value indicating whether autosave is enabled.
/// </summary>
public bool AutoSave { get; set; }

/// <summary>
/// Gets or sets a value indicating whether to use full or relative store paths.
/// </summary>
public bool UseRelativePaths { get; set; }

/// <summary>
/// Gets a value indicating whether changes to this dataset have been saved.
/// </summary>
public bool HasUnsavedChanges { get; private set; } = false;

/// <summary>
/// Gets the originating time interval (earliest to latest) of the messages in this dataset.
Expand Down Expand Up @@ -76,8 +121,9 @@ public Dataset(string name = Dataset.DefaultName)
/// Loads a dataset from the specified file.
/// </summary>
/// <param name="filename">The name of the file that contains the dataset to be loaded.</param>
/// <param name="autoSave">A value to indicate whether to enable autosave (optional, default is false).</param>
/// <returns>The newly loaded dataset.</returns>
public static Dataset Load(string filename)
public static Dataset Load(string filename, bool autoSave = false)
{
var serializer = JsonSerializer.Create(
new JsonSerializerSettings()
Expand All @@ -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<Dataset>(jsonReader);
var dataset = serializer.Deserialize<Dataset>(jsonReader);
dataset.AutoSave = autoSave;
dataset.Filename = filename;
return dataset;
}

/// <summary>
Expand All @@ -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;
}

Expand All @@ -128,6 +178,7 @@ public Session CreateSession(string sessionName = Session.DefaultName)
public void RemoveSession(Session session)
{
this.InternalSessions.Remove(session);
this.OnDatasetChanged();
}

/// <summary>
Expand All @@ -145,30 +196,48 @@ public void Append(Dataset inputDataset)
newSession.AddStorePartition(StreamReader.Create(p.StoreName, p.StorePath, p.StreamReaderTypeName), p.Name);
}
}

this.OnDatasetChanged();
}

/// <summary>
/// Saves this dataset to the specified file.
/// Saves this dataset.
/// </summary>
/// <param name="filename">The name of the file to save this dataset into.</param>
/// <param name="useRelativePaths">Indicates whether to use full or relative store paths.</param>
public void Save(string filename, bool useRelativePaths = true)
/// <param name="filename">The filename that indicates the location to save the dataset.</param>
/// <param name="useRelativePaths">Indicates whether to use full or relative store paths (optional, default is true).</param>
public void SaveAs(string filename, bool useRelativePaths = true)
{
this.Filename = filename;
this.UseRelativePaths = useRelativePaths;
this.Save();
}

/// <summary>
/// Saves this dataset.
/// </summary>
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,
TypeNameHandling = TypeNameHandling.Auto,
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;
}

/// <summary>
Expand Down Expand Up @@ -414,6 +483,25 @@ public void AddSessionsFromPsiStores(string path, string partitionName = null)
}
}

/// <summary>
/// Method called when structure of the dataset changed.
/// </summary>
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);
}

/// <summary>
/// Adds a session to this dataset and updates its originating time interval.
/// </summary>
Expand All @@ -427,6 +515,7 @@ private void AddSession(Session session)
}

this.InternalSessions.Add(session);
this.OnDatasetChanged();
}

[OnDeserialized]
Expand Down
18 changes: 18 additions & 0 deletions Sources/Data/Microsoft.Psi.Data/Session.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ private Session()
{
}

/// <summary>
/// Event invoked when the structure of the session changed.
/// </summary>
public event EventHandler SessionChanged;

/// <summary>
/// Gets the dataset that this session belongs to.
/// </summary>
Expand All @@ -64,6 +69,7 @@ public string Name
}

this.name = value;
this.OnSessionChanged();
}
}

Expand Down Expand Up @@ -293,6 +299,17 @@ await Task.Run(
public void RemovePartition(IPartition partition)
{
this.InternalPartitions.Remove(partition);
this.OnSessionChanged();
}

/// <summary>
/// Method called when structure of the session changed.
/// </summary>
protected virtual void OnSessionChanged()
{
this.Dataset?.OnDatasetChanged();
EventHandler handler = this.SessionChanged;
handler?.Invoke(this, EventArgs.Empty);
}

/// <summary>
Expand All @@ -308,6 +325,7 @@ private void AddPartition(IPartition partition)
}

this.InternalPartitions.Add(partition);
this.OnSessionChanged();
}

[OnDeserialized]
Expand Down
116 changes: 114 additions & 2 deletions Sources/Data/Test.Psi.Data/DatasetTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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<int>("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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@
<Image Source="{Binding ., Converter={StaticResource IconUriConverter}, ConverterParameter=dataset-open.png}" Height="16" Margin="4,0,0,0"/>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="_Save Dataset" Command="{Binding SaveDatasetCommand}" Height="25">
<MenuItem Header="_Save Dataset As" Command="{Binding SaveDatasetAsCommand}" Height="25">
<MenuItem.Icon>
<Image Source="{Binding ., Converter={StaticResource IconUriConverter}, ConverterParameter=dataset-save.png}" Height="16" Margin="4,0,0,0"/>
</MenuItem.Icon>
Expand All @@ -83,7 +83,7 @@
<Button Command="{Binding OpenDatasetCommand}" Margin="1" ToolTip="Open Dataset" AutomationProperties.Name="OpenDatasetButton">
<Image Source="{Binding ., Converter={StaticResource IconUriConverter}, ConverterParameter=dataset-open.png}"/>
</Button>
<Button Command="{Binding SaveDatasetCommand}" Margin="1" ToolTip="Save Dataset" AutomationProperties.Name="SaveDatasetButton">
<Button Command="{Binding SaveDatasetAsCommand}" Margin="1" ToolTip="Save Dataset As" AutomationProperties.Name="SaveDatasetAsButton">
<Image Source="{Binding ., Converter={StaticResource IconUriConverter}, ConverterParameter=dataset-save.png}"/>
</Button>
<Separator Margin="4,2,4,2" Background="{StaticResource SeparatorColorBrush}" AutomationProperties.IsOffscreenBehavior="Offscreen"/>
Expand Down
Loading