Skip to content

Commit

Permalink
JsonSuspensionDriver no longer modifies NavigationStack while saving …
Browse files Browse the repository at this point in the history
…settings.
  • Loading branch information
natekford committed Jan 2, 2025
1 parent a482a28 commit ec30f33
Show file tree
Hide file tree
Showing 3 changed files with 53 additions and 38 deletions.
11 changes: 5 additions & 6 deletions src/SongProcessor.UI/App.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ public override void OnFrameworkInitializationCompleted()
Locator.CurrentMutable.RegisterConstant<ISongProcessor>(processor);
Locator.CurrentMutable.RegisterConstant<IEnumerable<IAnimeGatherer>>(gatherers);

Locator.CurrentMutable.Register<IViewFor<SongViewModel>>(() => new SongView());
Locator.CurrentMutable.Register<IViewFor<AddViewModel>>(() => new AddView());
Locator.CurrentMutable.Register<IViewFor<EditViewModel>>(() => new EditView());

// Set up suspension to save view model information
var suspension = new AutoSuspendHelper(ApplicationLifetime!);
var driver = new JsonSuspensionDriver("appstate.json")
Expand All @@ -71,12 +75,7 @@ public override void OnFrameworkInitializationCompleted()
RxApp.SuspensionHost.SetupDefaultSuspendResume(driver);
suspension.OnFrameworkInitializationCompleted();

var state = screenWrapper.Screen = RxApp.SuspensionHost.GetAppState<MainViewModel>();
Locator.CurrentMutable.Register<IViewFor<SongViewModel>>(() => new SongView());
Locator.CurrentMutable.Register<IViewFor<AddViewModel>>(() => new AddView());
Locator.CurrentMutable.Register<IViewFor<EditViewModel>>(() => new EditView());

window.DataContext = state;
window.DataContext = screenWrapper.Screen = RxApp.SuspensionHost.GetAppState<MainViewModel>();
window.Show();
base.OnFrameworkInitializationCompleted();
}
Expand Down
71 changes: 43 additions & 28 deletions src/SongProcessor.UI/JsonSuspensionDriver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@

using SongProcessor.UI.ViewModels;

using System.Collections.ObjectModel;
using System.Reactive;
using System.Reactive.Linq;
using System.Reflection;
using System.Runtime.Serialization;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;

namespace SongProcessor.UI;
Expand All @@ -18,15 +20,19 @@ public class JsonSuspensionDriver(string Path) : ISuspensionDriver
IgnoreReadOnlyFields = true,
IgnoreReadOnlyProperties = true,
WriteIndented = true,
Converters =
{
new NavStackConverter(),
},
TypeInfoResolver = new DefaultJsonTypeInfoResolver
{
Modifiers =
{
UseDataContract,
UseTypeNamesForViewModels,
IgnoreCertainViewModels,
UseRoutingStateConstructor,
}
}
},
};
public bool DeleteOnInvalidState { get; set; }

Expand All @@ -50,40 +56,19 @@ public IObservable<object> LoadState()

public IObservable<Unit> SaveState(object state)
{
using var fs = File.Create(Path);
JsonSerializer.Serialize(fs, state, _Options);
var text = JsonSerializer.Serialize(state, _Options);
File.WriteAllText(Path, text);
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<DataContractAttribute>() is null)
{
return;
}

// System.Text.Json does not automatically abide by DataContract/Member
foreach (var propertyInfo in typeInfo.Properties)
{
if (propertyInfo.AttributeProvider is not ICustomAttributeProvider provider
Expand All @@ -95,13 +80,26 @@ private static void UseDataContract(JsonTypeInfo typeInfo)
}
}

private static void UseRoutingStateConstructor(JsonTypeInfo typeInfo)
{
if (typeInfo.Type != typeof(RoutingState))
{
return;
}

// System.Text.Json does not consider a ctor with only optional parameters
// to be a parameterless constructor
typeInfo.CreateObject = () => new RoutingState();
}

private static void UseTypeNamesForViewModels(JsonTypeInfo typeInfo)
{
if (typeInfo.Type != typeof(IRoutableViewModel))
{
return;
}

// This actually is an improvement over Newtonsoft
typeInfo.PolymorphismOptions = new()
{
DerivedTypes =
Expand All @@ -111,6 +109,23 @@ private static void UseTypeNamesForViewModels(JsonTypeInfo typeInfo)
},
};
}
}

public class RoutingStateWorkaround : RoutingState;
private sealed class NavStackConverter : JsonConverter<ObservableCollection<IRoutableViewModel>>
{
public override ObservableCollection<IRoutableViewModel>? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> new(JsonSerializer.Deserialize<IRoutableViewModel[]>(ref reader, options) ?? []);

public override void Write(Utf8JsonWriter writer, ObservableCollection<IRoutableViewModel> value, JsonSerializerOptions options)
{
writer.WriteStartArray();
foreach (var vm in value)
{
if (vm is not EditViewModel)
{
JsonSerializer.Serialize(writer, vm, options);
}
}
writer.WriteEndArray();
}
}
}
9 changes: 5 additions & 4 deletions src/SongProcessor.UI/ViewModels/MainViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,17 @@ namespace SongProcessor.UI.ViewModels;
[DataContract]
public sealed class MainViewModel : ReactiveObject, IScreen
{
public RoutingState Router => RouterWorkaround;
[DataMember]
public RoutingStateWorkaround RouterWorkaround
public RoutingState Router
{
get;
// Due to certain design choices I have made, changing the RoutingState
// instance causes routing to not work. Buttons can be clicked which
// causes the NavigationStack to update but the UI does not.
init
{
field.NavigationStack.Clear();
field.NavigationStack.AddRange(value.NavigationStack);
this.RaisePropertyChanged(nameof(RouterWorkaround));
field.NavigationStack.AddRange(value?.NavigationStack ?? []);
this.RaisePropertyChanged(nameof(Router));
}
} = new();
Expand Down

0 comments on commit ec30f33

Please sign in to comment.