From bcd323cbb7b7c0d64d851cd15e9e0da25c5a1272 Mon Sep 17 00:00:00 2001 From: natekford Date: Wed, 1 Jan 2025 17:09:43 -0800 Subject: [PATCH] Switched from Newtonsoft.Json to System.Text.Json (some very hacky changes were required). Updated dependencies. --- src/SongProcessor.UI/App.axaml.cs | 2 +- src/SongProcessor.UI/JsonSuspensionDriver.cs | 116 ++++++++++++++++++ .../NewtonsoftJsonSkipThis.cs | 16 --- .../NewtonsoftJsonSuspensionDriver.cs | 116 ------------------ src/SongProcessor.UI/SongProcessor.UI.csproj | 13 +- .../ViewModels/AddViewModel.cs | 2 + .../ViewModels/EditViewModel.cs | 8 +- .../ViewModels/MainViewModel.cs | 15 ++- .../ViewModels/SongViewModel.cs | 2 + .../SongProcessor.Tests.csproj | 1 - 10 files changed, 142 insertions(+), 149 deletions(-) create mode 100644 src/SongProcessor.UI/JsonSuspensionDriver.cs delete mode 100644 src/SongProcessor.UI/NewtonsoftJsonSkipThis.cs delete mode 100644 src/SongProcessor.UI/NewtonsoftJsonSuspensionDriver.cs diff --git a/src/SongProcessor.UI/App.axaml.cs b/src/SongProcessor.UI/App.axaml.cs index 2d758c5..4e1ac4e 100644 --- a/src/SongProcessor.UI/App.axaml.cs +++ b/src/SongProcessor.UI/App.axaml.cs @@ -51,7 +51,7 @@ public override void OnFrameworkInitializationCompleted() // Set up suspension to save view model information var suspension = new AutoSuspendHelper(ApplicationLifetime!); - var driver = new NewtonsoftJsonSuspensionDriver("appstate.json") + var driver = new JsonSuspensionDriver("appstate.json") { #if DEBUG DeleteOnInvalidState = false, diff --git a/src/SongProcessor.UI/JsonSuspensionDriver.cs b/src/SongProcessor.UI/JsonSuspensionDriver.cs new file mode 100644 index 0000000..d1e0a95 --- /dev/null +++ b/src/SongProcessor.UI/JsonSuspensionDriver.cs @@ -0,0 +1,116 @@ +using ReactiveUI; + +using SongProcessor.UI.ViewModels; + +using System.Reactive; +using System.Reactive.Linq; +using System.Reflection; +using System.Runtime.Serialization; +using System.Text.Json; +using System.Text.Json.Serialization.Metadata; + +namespace SongProcessor.UI; + +public class JsonSuspensionDriver(string Path) : ISuspensionDriver +{ + private static readonly JsonSerializerOptions _Options = new() + { + IgnoreReadOnlyFields = true, + IgnoreReadOnlyProperties = true, + WriteIndented = true, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + UseDataContract, + UseTypeNamesForViewModels, + IgnoreCertainViewModels, + } + } + }; + public bool DeleteOnInvalidState { get; set; } + + public IObservable InvalidateState() + { + if (DeleteOnInvalidState && File.Exists(Path)) + { + File.Delete(Path); + } + return Observable.Return(Unit.Default); + } + + public IObservable LoadState() + { + // ReactiveUI relies on this method throwing an exception + // to determine if CreateNewAppState should be called + using var fs = File.OpenRead(Path); + var state = JsonSerializer.Deserialize(fs, _Options); + return Observable.Return(state)!; + } + + public IObservable SaveState(object state) + { + using var fs = File.Create(Path); + JsonSerializer.Serialize(fs, state, _Options); + return Observable.Return(Unit.Default); + } + + private static void IgnoreCertainViewModels(JsonTypeInfo typeInfo) + { + if (typeInfo.Type != typeof(RoutingStateWorkaround)) + { + return; + } + + // This will be an issue if settings are serialized at any time other than + // application shutdown + typeInfo.OnSerializing = static obj => + { + var navStack = ((RoutingState)obj).NavigationStack; + for (var i = navStack.Count - 1; i >= 0; --i) + { + if (navStack[i].GetType() == typeof(EditViewModel)) + { + navStack.RemoveAt(i); + } + } + }; + } + + private static void UseDataContract(JsonTypeInfo typeInfo) + { + if (typeInfo.Type.GetCustomAttribute() is null) + { + return; + } + + foreach (var propertyInfo in typeInfo.Properties) + { + if (propertyInfo.AttributeProvider is not ICustomAttributeProvider provider + || !provider.GetCustomAttributes(true).Any(x => x is DataMemberAttribute)) + { + propertyInfo.ShouldSerialize = static (_, _) + => false; + } + } + } + + private static void UseTypeNamesForViewModels(JsonTypeInfo typeInfo) + { + if (typeInfo.Type != typeof(IRoutableViewModel)) + { + return; + } + + typeInfo.PolymorphismOptions = new() + { + DerivedTypes = + { + new(typeof(SongViewModel), "vm_song"), + new(typeof(AddViewModel), "vm_add"), + }, + }; + } +} + +public class RoutingStateWorkaround : RoutingState; \ No newline at end of file diff --git a/src/SongProcessor.UI/NewtonsoftJsonSkipThis.cs b/src/SongProcessor.UI/NewtonsoftJsonSkipThis.cs deleted file mode 100644 index 7c2d96e..0000000 --- a/src/SongProcessor.UI/NewtonsoftJsonSkipThis.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Newtonsoft.Json; - -namespace SongProcessor.UI; - -public sealed class NewtonsoftJsonSkipThis : JsonConverter -{ - public override bool CanConvert(Type objectType) - => true; - - public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) - => throw new NotSupportedException(); - - public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) - { - } -} \ No newline at end of file diff --git a/src/SongProcessor.UI/NewtonsoftJsonSuspensionDriver.cs b/src/SongProcessor.UI/NewtonsoftJsonSuspensionDriver.cs deleted file mode 100644 index 645f331..0000000 --- a/src/SongProcessor.UI/NewtonsoftJsonSuspensionDriver.cs +++ /dev/null @@ -1,116 +0,0 @@ -#define USE_NAV_STACK_FIX - -using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; - -using ReactiveUI; - -using SongProcessor.UI.ViewModels; - -using System.Reactive; -using System.Reactive.Linq; - -namespace SongProcessor.UI; - -public class NewtonsoftJsonSuspensionDriver(string Path) : ISuspensionDriver -{ - private readonly JsonSerializerSettings _Options = new() - { - ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor, - ContractResolver = new WritablePropertiesOnlyResolver(), - Formatting = Formatting.Indented, - ObjectCreationHandling = ObjectCreationHandling.Replace, - SerializationBinder = new NamespaceSerializationBinder(), - TypeNameHandling = TypeNameHandling.Auto, - }; - public bool DeleteOnInvalidState { get; set; } - - public IObservable InvalidateState() - { - if (DeleteOnInvalidState && File.Exists(Path)) - { - File.Delete(Path); - } - return Observable.Return(Unit.Default); - } - - public IObservable LoadState() - { - // ReactiveUI relies on this method throwing an exception - // to determine if CreateNewAppState should be called - var lines = File.ReadAllText(Path); - var state = JsonConvert.DeserializeObject(lines, _Options); - return Observable.Return(state)!; - } - - public IObservable SaveState(object state) - { - var lines = JsonConvert.SerializeObject(state, _Options); - File.WriteAllText(Path, lines); - return Observable.Return(Unit.Default); - } - - private sealed class NamespaceSerializationBinder : DefaultSerializationBinder - { - const string MyNamespace = nameof(SongProcessor); - - public override Type BindToType(string? assemblyName, string typeName) - { - if (!typeName.StartsWith(MyNamespace, StringComparison.OrdinalIgnoreCase)) - { - throw new JsonSerializationException($"Request type {typeName} not supported."); - } - return base.BindToType(assemblyName, typeName); - } - } - -#if USE_NAV_STACK_FIX - - private sealed class NavigationStackValueProvider(IValueProvider Original) : IValueProvider - { - public object? GetValue(object target) - => Original.GetValue(target); - - public void SetValue(object target, object? value) - { - var castedTarget = (RoutingState)target!; - var castedValue = (IEnumerable)value!; - - castedTarget.NavigationStack.Clear(); - foreach (var viewModel in castedValue) - { - castedTarget.NavigationStack.Add(viewModel); - } - } - } - -#endif - - private sealed class WritablePropertiesOnlyResolver : DefaultContractResolver - { - protected override IList CreateProperties(Type type, MemberSerialization memberSerialization) - { - var props = base.CreateProperties(type, memberSerialization); - for (var i = props.Count - 1; i >= 0; --i) - { - var prop = props[i]; - -#if USE_NAV_STACK_FIX - if (prop.DeclaringType == typeof(RoutingState) - && prop.PropertyName == nameof(RoutingState.NavigationStack)) - { - prop.Ignored = false; - prop.Writable = true; - prop.ValueProvider = new NavigationStackValueProvider(prop.ValueProvider!); - } - else -#endif - if (!prop.Writable) - { - props.RemoveAt(i); - } - } - return props; - } - } -} \ No newline at end of file diff --git a/src/SongProcessor.UI/SongProcessor.UI.csproj b/src/SongProcessor.UI/SongProcessor.UI.csproj index 01f158b..cbcc996 100644 --- a/src/SongProcessor.UI/SongProcessor.UI.csproj +++ b/src/SongProcessor.UI/SongProcessor.UI.csproj @@ -19,15 +19,14 @@ - - + + - - - + + + - - + diff --git a/src/SongProcessor.UI/ViewModels/AddViewModel.cs b/src/SongProcessor.UI/ViewModels/AddViewModel.cs index 8351466..3865d5d 100644 --- a/src/SongProcessor.UI/ViewModels/AddViewModel.cs +++ b/src/SongProcessor.UI/ViewModels/AddViewModel.cs @@ -10,6 +10,7 @@ using System.Collections.ObjectModel; using System.Reactive; using System.Runtime.Serialization; +using System.Text.Json.Serialization; namespace SongProcessor.UI.ViewModels; @@ -107,6 +108,7 @@ public AddViewModel( SelectDirectory = ReactiveCommand.CreateFromTask(SelectDirectoryAsync); } + [JsonConstructor] private AddViewModel() : this( Locator.Current.GetService()!, Locator.Current.GetService()!, diff --git a/src/SongProcessor.UI/ViewModels/EditViewModel.cs b/src/SongProcessor.UI/ViewModels/EditViewModel.cs index 8a21b13..8871a4a 100644 --- a/src/SongProcessor.UI/ViewModels/EditViewModel.cs +++ b/src/SongProcessor.UI/ViewModels/EditViewModel.cs @@ -1,6 +1,4 @@ -using Newtonsoft.Json; - -using ReactiveUI; +using ReactiveUI; using ReactiveUI.Validation.Abstractions; using ReactiveUI.Validation.Contexts; using ReactiveUI.Validation.Extensions; @@ -14,8 +12,6 @@ namespace SongProcessor.UI.ViewModels; -// Never serialize this view/viewmodel since this data is related to folder structure -[JsonConverter(typeof(NewtonsoftJsonSkipThis))] public sealed class EditViewModel : ReactiveObject, IRoutableViewModel, IValidatableViewModel { private readonly ObservableAnime _Anime; @@ -118,7 +114,7 @@ public string Start set => this.RaiseAndSetIfChanged(ref field, value); } public string UrlPathSegment => "/edit"; - public ValidationContext ValidationContext { get; } = new(); + public IValidationContext ValidationContext { get; } = new ValidationContext(); public int VideoTrack { get; diff --git a/src/SongProcessor.UI/ViewModels/MainViewModel.cs b/src/SongProcessor.UI/ViewModels/MainViewModel.cs index 981db80..b229229 100644 --- a/src/SongProcessor.UI/ViewModels/MainViewModel.cs +++ b/src/SongProcessor.UI/ViewModels/MainViewModel.cs @@ -1,5 +1,7 @@ using Avalonia.Input.Platform; +using DynamicData; + using ReactiveUI; using SongProcessor.FFmpeg; @@ -10,17 +12,25 @@ using System.Reactive; using System.Reactive.Linq; using System.Runtime.Serialization; +using System.Text.Json.Serialization; namespace SongProcessor.UI.ViewModels; [DataContract] public sealed class MainViewModel : ReactiveObject, IScreen { + public RoutingState Router => RouterWorkaround; [DataMember] - public RoutingState Router + public RoutingStateWorkaround RouterWorkaround { get; - set => this.RaiseAndSetIfChanged(ref field, value); + init + { + field.NavigationStack.Clear(); + field.NavigationStack.AddRange(value.NavigationStack); + this.RaisePropertyChanged(nameof(RouterWorkaround)); + this.RaisePropertyChanged(nameof(Router)); + } } = new(); #region Commands @@ -57,6 +67,7 @@ public MainViewModel( }, CanGoBack()); } + [JsonConstructor] private MainViewModel() : this( Locator.Current.GetService()!, Locator.Current.GetService()!, diff --git a/src/SongProcessor.UI/ViewModels/SongViewModel.cs b/src/SongProcessor.UI/ViewModels/SongViewModel.cs index 6d20741..4ff7a95 100644 --- a/src/SongProcessor.UI/ViewModels/SongViewModel.cs +++ b/src/SongProcessor.UI/ViewModels/SongViewModel.cs @@ -19,6 +19,7 @@ using System.Reactive; using System.Reactive.Linq; using System.Runtime.Serialization; +using System.Text.Json.Serialization; namespace SongProcessor.UI.ViewModels; @@ -188,6 +189,7 @@ public SongViewModel( CanNavigate = busy.CombineLatest(loaded, (x, y) => !(x || y)); } + [JsonConstructor] private SongViewModel() : this( Locator.Current.GetService()!, Locator.Current.GetService()!, diff --git a/tests/SongProcessor.Tests/SongProcessor.Tests.csproj b/tests/SongProcessor.Tests/SongProcessor.Tests.csproj index 1e8b4cf..b7c1008 100644 --- a/tests/SongProcessor.Tests/SongProcessor.Tests.csproj +++ b/tests/SongProcessor.Tests/SongProcessor.Tests.csproj @@ -23,7 +23,6 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all -