You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
210 lines
8.0 KiB
210 lines
8.0 KiB
using System; |
|
using System.Linq; |
|
using System.Reflection; |
|
using System.Text.Json; |
|
using System.Text.Json.Nodes; |
|
using System.Text.Json.Serialization; |
|
using System.Windows.Input; |
|
using CommunityToolkit.Mvvm.Input; |
|
using NLog; |
|
using StabilityMatrix.Avalonia.Models; |
|
|
|
namespace StabilityMatrix.Avalonia.ViewModels; |
|
|
|
public abstract class LoadableViewModelBase : ViewModelBase, IJsonLoadableState |
|
{ |
|
private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); |
|
|
|
// ReSharper disable once MemberCanBePrivate.Global |
|
protected static readonly Type[] SerializerIgnoredTypes = |
|
{ |
|
typeof(ICommand), |
|
typeof(IRelayCommand) |
|
}; |
|
|
|
// ReSharper disable once MemberCanBePrivate.Global |
|
protected static readonly string[] SerializerIgnoredNames = |
|
{ |
|
nameof(HasErrors), |
|
}; |
|
|
|
protected static readonly JsonSerializerOptions SerializerOptions = new() |
|
{ |
|
IgnoreReadOnlyProperties = true, |
|
}; |
|
|
|
private static bool ShouldIgnoreProperty(PropertyInfo property) |
|
{ |
|
// Skip if read-only and not IJsonLoadableState |
|
if (property.SetMethod is null && !typeof(IJsonLoadableState).IsAssignableFrom(property.PropertyType)) |
|
{ |
|
Logger.Trace("Skipping {Property} - read-only", property.Name); |
|
return true; |
|
} |
|
// Check not JsonIgnore |
|
if (property.GetCustomAttributes(typeof(JsonIgnoreAttribute), true).Length > 0) |
|
{ |
|
Logger.Trace("Skipping {Property} - has [JsonIgnore]", property.Name); |
|
return true; |
|
} |
|
// Check not excluded type |
|
if (SerializerIgnoredTypes.Contains(property.PropertyType)) |
|
{ |
|
Logger.Trace("Skipping {Property} - serializer ignored type {Type}", property.Name, property.PropertyType); |
|
return true; |
|
} |
|
// Check not ignored name |
|
if (SerializerIgnoredNames.Contains(property.Name, StringComparer.Ordinal)) |
|
{ |
|
Logger.Trace("Skipping {Property} - serializer ignored name", property.Name); |
|
return true; |
|
} |
|
|
|
return false; |
|
} |
|
|
|
/// <summary> |
|
/// Load the state of this view model from a JSON object. |
|
/// The default implementation is a mirror of <see cref="SaveStateToJsonObject"/>. |
|
/// For the following properties on this class, we will try to set from the JSON object: |
|
/// <list type="bullet"> |
|
/// <item>Public</item> |
|
/// <item>Not read-only</item> |
|
/// <item>Not marked with [JsonIgnore]</item> |
|
/// <item>Not a type within the SerializerIgnoredTypes</item> |
|
/// <item>Not a name within the SerializerIgnoredNames</item> |
|
/// </list> |
|
/// </summary> |
|
public virtual void LoadStateFromJsonObject(JsonObject state) |
|
{ |
|
// Get all of our properties using reflection |
|
var properties = GetType().GetProperties(); |
|
Logger.Trace("Serializing {Type} with {Count} properties", GetType(), properties.Length); |
|
|
|
foreach (var property in properties) |
|
{ |
|
// Check if property is in the JSON object |
|
if (!state.TryGetPropertyValue(property.Name, out var value)) |
|
{ |
|
Logger.Trace("Skipping {Property} - not in JSON object", property.Name); |
|
continue; |
|
} |
|
|
|
// Check if we should ignore this property |
|
if (ShouldIgnoreProperty(property)) |
|
{ |
|
continue; |
|
} |
|
|
|
// For types that also implement IJsonLoadableState, defer to their load implementation |
|
if (typeof(IJsonLoadableState).IsAssignableFrom(property.PropertyType)) |
|
{ |
|
Logger.Trace("Loading {Property} ({Type}) with IJsonLoadableState", property.Name, property.PropertyType); |
|
|
|
// Value must be non-null |
|
if (value is null) |
|
{ |
|
throw new InvalidOperationException($"Property {property.Name} is IJsonLoadableState but value to be loaded is null"); |
|
} |
|
|
|
// Check if the current object at this property is null |
|
if (property.GetValue(this) is not IJsonLoadableState propertyValue) |
|
{ |
|
// If null, it must have a default constructor |
|
if (property.PropertyType.GetConstructor(Type.EmptyTypes) is not { } constructorInfo) |
|
{ |
|
throw new InvalidOperationException($"Property {property.Name} is IJsonLoadableState but current object is null and has no default constructor"); |
|
} |
|
|
|
// Create a new instance and set it |
|
propertyValue = (IJsonLoadableState) constructorInfo.Invoke(null); |
|
property.SetValue(this, propertyValue); |
|
} |
|
|
|
// Load the state from the JSON object |
|
propertyValue.LoadStateFromJsonObject(value.AsObject()); |
|
} |
|
else |
|
{ |
|
Logger.Trace("Loading {Property} ({Type})", property.Name, property.PropertyType); |
|
|
|
var propertyValue = value.Deserialize(property.PropertyType, SerializerOptions); |
|
property.SetValue(this, propertyValue); |
|
} |
|
} |
|
} |
|
|
|
/// <summary> |
|
/// Saves the state of this view model to a JSON object. |
|
/// The default implementation uses reflection to |
|
/// save all properties that are: |
|
/// <list type="bullet"> |
|
/// <item>Public</item> |
|
/// <item>Not read-only</item> |
|
/// <item>Not marked with [JsonIgnore]</item> |
|
/// <item>Not a type within the SerializerIgnoredTypes</item> |
|
/// <item>Not a name within the SerializerIgnoredNames</item> |
|
/// </list> |
|
/// </summary> |
|
public virtual JsonObject SaveStateToJsonObject() |
|
{ |
|
// Get all of our properties using reflection. |
|
var properties = GetType().GetProperties(); |
|
Logger.Trace("Serializing {Type} with {Count} properties", GetType(), properties.Length); |
|
|
|
// Create a JSON object to store the state. |
|
var state = new JsonObject(); |
|
|
|
// Serialize each property marked with JsonIncludeAttribute. |
|
foreach (var property in properties) |
|
{ |
|
if (ShouldIgnoreProperty(property)) |
|
{ |
|
continue; |
|
} |
|
|
|
// For types that also implement IJsonLoadableState, defer to their implementation. |
|
if (typeof(IJsonLoadableState).IsAssignableFrom(property.PropertyType)) |
|
{ |
|
Logger.Trace("Serializing {Property} ({Type}) with IJsonLoadableState", property.Name, property.PropertyType); |
|
var value = property.GetValue(this); |
|
if (value is not null) |
|
{ |
|
var model = (IJsonLoadableState) value; |
|
var modelState = model.SaveStateToJsonObject(); |
|
state.Add(property.Name, modelState); |
|
} |
|
} |
|
else |
|
{ |
|
Logger.Trace("Serializing {Property} ({Type})", property.Name, property.PropertyType); |
|
var value = property.GetValue(this); |
|
if (value is not null) |
|
{ |
|
state.Add(property.Name, JsonSerializer.SerializeToNode(value, SerializerOptions)); |
|
} |
|
} |
|
} |
|
|
|
return state; |
|
} |
|
|
|
/// <summary> |
|
/// Serialize a model to a JSON object. |
|
/// </summary> |
|
protected static JsonObject SerializeModel<T>(T model) |
|
{ |
|
var node = JsonSerializer.SerializeToNode(model); |
|
return node?.AsObject() ?? throw new |
|
NullReferenceException("Failed to serialize state to JSON object."); |
|
} |
|
|
|
/// <summary> |
|
/// Deserialize a model from a JSON object. |
|
/// </summary> |
|
protected static T DeserializeModel<T>(JsonObject state) |
|
{ |
|
return state.Deserialize<T>() ?? throw new |
|
NullReferenceException("Failed to deserialize state from JSON object."); |
|
} |
|
}
|
|
|