Browse Source

Fix serialization support for JsonPropertyName

pull/165/head
Ionite 1 year ago
parent
commit
352163422b
No known key found for this signature in database
  1. 160
      StabilityMatrix.Avalonia/ViewModels/Base/LoadableViewModelBase.cs

160
StabilityMatrix.Avalonia/ViewModels/Base/LoadableViewModelBase.cs

@ -21,20 +21,18 @@ public abstract class LoadableViewModelBase : ViewModelBase, IJsonLoadableState
typeof(IRelayCommand)
};
private static readonly string[] SerializerIgnoredNames =
{
nameof(HasErrors),
};
private static readonly string[] SerializerIgnoredNames = { nameof(HasErrors), };
private static readonly JsonSerializerOptions SerializerOptions = new()
{
IgnoreReadOnlyProperties = true,
};
private 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))
if (
property.SetMethod is null
&& !typeof(IJsonLoadableState).IsAssignableFrom(property.PropertyType)
)
{
Logger.Trace("Skipping {Property} - read-only", property.Name);
return true;
@ -48,7 +46,11 @@ public abstract class LoadableViewModelBase : ViewModelBase, IJsonLoadableState
// Check not excluded type
if (SerializerIgnoredTypes.Contains(property.PropertyType))
{
Logger.Trace("Skipping {Property} - serializer ignored type {Type}", property.Name, property.PropertyType);
Logger.Trace(
"Skipping {Property} - serializer ignored type {Type}",
property.Name,
property.PropertyType
);
return true;
}
// Check not ignored name
@ -60,7 +62,22 @@ public abstract class LoadableViewModelBase : ViewModelBase, IJsonLoadableState
return false;
}
/// <summary>
/// True if we should include property without checking exclusions
/// </summary>
private static bool ShouldIncludeProperty(PropertyInfo property)
{
// Has JsonIncludeAttribute
if (property.GetCustomAttributes(typeof(JsonIncludeAttribute), true).Length > 0)
{
Logger.Trace("Including {Property} - has [JsonInclude]", 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"/>.
@ -81,51 +98,84 @@ public abstract class LoadableViewModelBase : ViewModelBase, IJsonLoadableState
foreach (var property in properties)
{
var name = property.Name;
// If JsonPropertyName provided, use that as the key
if (
property
.GetCustomAttributes(typeof(JsonPropertyNameAttribute), true)
.FirstOrDefault()
is JsonPropertyNameAttribute jsonPropertyName
)
{
Logger.Trace(
"Deserializing {Property} ({Type}) with JsonPropertyName {JsonPropertyName}",
property.Name,
property.PropertyType,
jsonPropertyName.Name
);
if (property.GetValue(this) is string jsonPropertyNameValue)
{
name = jsonPropertyNameValue;
}
}
// Check if property is in the JSON object
if (!state.TryGetPropertyValue(property.Name, out var value))
if (!state.TryGetPropertyValue(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))
if (!ShouldIncludeProperty(property) && 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);
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");
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)
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");
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);
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);
}
@ -149,60 +199,90 @@ public abstract class LoadableViewModelBase : ViewModelBase, IJsonLoadableState
// 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))
if (!ShouldIncludeProperty(property) && ShouldIgnoreProperty(property))
{
continue;
}
var name = property.Name;
// If JsonPropertyName provided, use that as the key.
if (
property
.GetCustomAttributes(typeof(JsonPropertyNameAttribute), true)
.FirstOrDefault()
is JsonPropertyNameAttribute jsonPropertyName
)
{
Logger.Trace(
"Serializing {Property} ({Type}) with JsonPropertyName {JsonPropertyName}",
property.Name,
property.PropertyType,
jsonPropertyName.Name
);
if (property.GetValue(this) is string value)
{
name = value;
}
}
// 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);
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 model = (IJsonLoadableState)value;
var modelState = model.SaveStateToJsonObject();
state.Add(property.Name, modelState);
state.Add(name, modelState);
}
}
else
{
Logger.Trace("Serializing {Property} ({Type})", property.Name, property.PropertyType);
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));
state.Add(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.");
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.");
return state.Deserialize<T>()
?? throw new NullReferenceException("Failed to deserialize state from JSON object.");
}
}

Loading…
Cancel
Save