Add plugin state persistence via PersistStateAttribute serialization

This commit is contained in:
Stone_Red
2025-12-25 18:14:01 +01:00
parent 9e9c49214a
commit 16c5c98f1a
5 changed files with 197 additions and 1 deletions
+5
View File
@@ -46,4 +46,9 @@ internal class PluginData(PluginWindow window, PluginSettings pluginSettings) :
{
window.UpdatePluginWindow();
}
public void SaveState()
{
window.SavePluginState();
}
}
+149 -1
View File
@@ -14,6 +14,7 @@ using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;
using System.Text.Json;
using System.Threading;
using System.Timers;
using System.Windows;
@@ -107,6 +108,14 @@ public partial class PluginWindow : Window, IPluginWindow
UpdateTimer_Elapsed(updateTimer, null);
}
public void SavePluginState()
{
if (pluginClassInstance is not null)
{
SaveState(pluginClassInstance);
}
}
public void Exit()
{
IsRunning = false;
@@ -258,6 +267,7 @@ public partial class PluginWindow : Window, IPluginWindow
return;
}
LoadState(pluginClassInstance);
LoadOptions(pluginClassInstance);
BindDefaultSettings(pluginClassInstance);
@@ -401,6 +411,140 @@ public partial class PluginWindow : Window, IPluginWindow
}
}
private void LoadState(object instance)
{
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Loading plugin state", source: "Plugin");
try
{
if (settings.State.Count == 0)
{
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - No state found, using defaults", source: "Plugin");
return;
}
#pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields
FieldInfo[] fields = instance.GetType().GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
PropertyInfo[] properties = instance.GetType().GetProperties(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
#pragma warning restore S3011
// Load fields
foreach (FieldInfo field in fields)
{
PersistStateAttribute? persistAttribute = field.GetCustomAttribute<PersistStateAttribute>();
if (persistAttribute is null)
{
continue;
}
if (settings.State.TryGetValue(field.Name, out JsonElement jsonElement))
{
try
{
object? value = jsonElement.Deserialize(field.FieldType);
field.SetValue(instance, value);
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Restored state field: {field.Name}", source: "Plugin");
}
catch (Exception ex)
{
App.Logger.LogWarn($"\"{PluginMetadata.Name}\" - Failed to restore field {field.Name}: {ex.Message}", source: "Plugin");
}
}
}
// Load properties
foreach (PropertyInfo property in properties)
{
PersistStateAttribute? persistAttribute = property.GetCustomAttribute<PersistStateAttribute>();
if (persistAttribute is null || !property.CanWrite)
{
continue;
}
if (settings.State.TryGetValue(property.Name, out JsonElement jsonElement))
{
try
{
object? value = jsonElement.Deserialize(property.PropertyType);
property.SetValue(instance, value);
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Restored state property: {property.Name}", source: "Plugin");
}
catch (Exception ex)
{
App.Logger.LogWarn($"\"{PluginMetadata.Name}\" - Failed to restore property {property.Name}: {ex.Message}", source: "Plugin");
}
}
}
}
catch (Exception ex)
{
App.Logger.LogError($"\"{PluginMetadata.Name}\" - Error loading state: {ex.Message}", source: "Plugin");
}
}
private void SaveState(object instance)
{
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Saving plugin state", source: "Plugin");
try
{
#pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields
FieldInfo[] fields = instance.GetType().GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
PropertyInfo[] properties = instance.GetType().GetProperties(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
#pragma warning restore S3011
// Save fields
foreach (FieldInfo field in fields)
{
PersistStateAttribute? persistAttribute = field.GetCustomAttribute<PersistStateAttribute>();
if (persistAttribute is null)
{
continue;
}
try
{
object? value = field.GetValue(instance);
JsonElement jsonElement = JsonSerializer.SerializeToElement(value, field.FieldType);
settings.State[field.Name] = jsonElement;
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Saved state field: {field.Name}", source: "Plugin");
}
catch (Exception ex)
{
App.Logger.LogWarn($"\"{PluginMetadata.Name}\" - Failed to save field {field.Name}: {ex.Message}", source: "Plugin");
}
}
// Save properties
foreach (PropertyInfo property in properties)
{
PersistStateAttribute? persistAttribute = property.GetCustomAttribute<PersistStateAttribute>();
if (persistAttribute is null || !property.CanRead)
{
continue;
}
try
{
object? value = property.GetValue(instance);
JsonElement jsonElement = JsonSerializer.SerializeToElement(value, property.PropertyType);
settings.State[property.Name] = jsonElement;
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Saved state property: {property.Name}", source: "Plugin");
}
catch (Exception ex)
{
App.Logger.LogWarn($"\"{PluginMetadata.Name}\" - Failed to save property {property.Name}: {ex.Message}", source: "Plugin");
}
}
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - State saved successfully ({settings.State.Count} items)", source: "Plugin");
}
catch (Exception ex)
{
App.Logger.LogError($"\"{PluginMetadata.Name}\" - Error saving state: {ex.Message}", source: "Plugin");
}
}
private async void UpdateTimer_Elapsed(object? sender, ElapsedEventArgs? e)
{
try
@@ -479,7 +623,11 @@ public partial class PluginWindow : Window, IPluginWindow
try
{
pluginClassInstance?.Stop();
if (pluginClassInstance is not null)
{
SaveState(pluginClassInstance);
pluginClassInstance.Stop();
}
}
catch (Exception ex)
{
@@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Windows;
@@ -16,6 +17,7 @@ public class PluginSettings : INotifyPropertyChanged
private string? currentThemeName;
private List<SettingElement> settings = [];
private Dictionary<string, JsonElement> state = [];
private bool enabled = false;
private Point position = new Point(100, 100);
private Point size = new Point(300, 300);
@@ -61,6 +63,23 @@ public class PluginSettings : INotifyPropertyChanged
}
}
/// <summary>
/// Dictionary storing plugin state. Key is the field/property name, value is the JsonElement.
/// This is persisted per layout in settings.json.
/// </summary>
public Dictionary<string, JsonElement> State
{
get => state;
set
{
if (state != value)
{
state = value;
OnPropertyChanged();
}
}
}
public bool Enabled
{
get => enabled;
+9
View File
@@ -50,4 +50,13 @@ public interface IPluginData
/// <param name="message">The message to display.</param>
/// <param name="title">The title of the message box.</param>
void ShowMessage(string message, string title);
/// <summary>
/// Saves the current state of fields and properties marked with <see cref="PersistStateAttribute"/>
/// </summary>
/// <remarks>
/// State is automatically saved when the plugin stops, but this method can be called
/// to save state at any time, such as after important user interactions.
/// </remarks>
void SaveState();
}
@@ -0,0 +1,15 @@
using System;
namespace DesktopMagic.Api;
/// <summary>
/// Marks a field or property to be automatically persisted between plugin sessions.
/// </summary>
/// <remarks>
/// The field or property must be JSON serializable. Complex types should have parameterless constructors.
/// State is saved when the plugin stops and loaded when it starts.
/// </remarks>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public sealed class PersistStateAttribute : Attribute
{
}