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.Base;
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;
}
///
/// Load the state of this view model from a JSON object.
/// The default implementation is a mirror of .
/// For the following properties on this class, we will try to set from the JSON object:
///
/// - Public
/// - Not read-only
/// - Not marked with [JsonIgnore]
/// - Not a type within the SerializerIgnoredTypes
/// - Not a name within the SerializerIgnoredNames
///
///
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);
}
}
}
///
/// Saves the state of this view model to a JSON object.
/// The default implementation uses reflection to
/// save all properties that are:
///
/// - Public
/// - Not read-only
/// - Not marked with [JsonIgnore]
/// - Not a type within the SerializerIgnoredTypes
/// - Not a name within the SerializerIgnoredNames
///
///
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;
}
///
/// Serialize a model to a JSON object.
///
protected static JsonObject SerializeModel(T model)
{
var node = JsonSerializer.SerializeToNode(model);
return node?.AsObject() ?? throw new
NullReferenceException("Failed to serialize state to JSON object.");
}
///
/// Deserialize a model from a JSON object.
///
protected static T DeserializeModel(JsonObject state)
{
return state.Deserialize() ?? throw new
NullReferenceException("Failed to deserialize state from JSON object.");
}
}