diff --git a/TrayApp.Test/TrayAppBuilderTests.cs b/TrayApp.Test/TrayAppBuilderTests.cs index 44607aa..2f72183 100644 --- a/TrayApp.Test/TrayAppBuilderTests.cs +++ b/TrayApp.Test/TrayAppBuilderTests.cs @@ -200,6 +200,67 @@ public async Task LoadPreferences_RestoresOnlyThePersistedToggles() Assert.IsFalse(forgotten); } + [TestMethod] + [DataRow(false)] + [DataRow(true)] + public async Task LoadPreferences_WhenTheToggleIsAlreadyInTheRememberedState_DoesNotCallTheSetter(bool state) + { + InMemoryPersistenceProvider provider = new(); + + using (DebouncedPreferenceStore seed = new(provider, "tray", Debounce)) + { + await seed.LoadAsync().ConfigureAwait(false); + seed.Set("awake", state); + await seed.FlushAsync().ConfigureAwait(false); + } + + bool awake = state; + int setterCalls = 0; + + // The setter ignores its argument and flips, as the class example's does. + TrayAppDefinition app = TrayAppBuilder.Create("demo") + .Toggle("Keep awake", () => awake, _ => + { + setterCalls++; + awake = !awake; + }, persistAs: "awake") + .Preferences(provider) + .Build(); + + using DebouncedPreferenceStore store = new(provider, "tray", Debounce); + + Assert.IsNull(TrayAppBuilder.LoadPreferences(app, store)); + Assert.AreEqual(state, awake); + Assert.AreEqual(0, setterCalls); + } + + [TestMethod] + public async Task LoadPreferences_WhenTheGetterThrows_KeepsGoingAndHandsBackTheMessage() + { + InMemoryPersistenceProvider provider = new(); + + using (DebouncedPreferenceStore seed = new(provider, "tray", Debounce)) + { + await seed.LoadAsync().ConfigureAwait(false); + seed.Set("awake", true); + seed.Set("display", true); + await seed.FlushAsync().ConfigureAwait(false); + } + + bool display = false; + + TrayAppDefinition app = TrayAppBuilder.Create("demo") + .Toggle("Keep awake", () => throw new InvalidOperationException("no inhibitor here"), _ => { }, persistAs: "awake") + .Toggle("Keep display awake", () => display, value => display = value, persistAs: "display") + .Preferences(provider) + .Build(); + + using DebouncedPreferenceStore store = new(provider, "tray", Debounce); + + Assert.AreEqual("no inhibitor here", TrayAppBuilder.LoadPreferences(app, store)); + Assert.IsTrue(display); + } + [TestMethod] public async Task LoadPreferences_ThenOptions_LetsAnExplicitSwitchWin() { diff --git a/TrayApp/Menu/TrayToggleItem.cs b/TrayApp/Menu/TrayToggleItem.cs index d3650d2..4232ecf 100644 --- a/TrayApp/Menu/TrayToggleItem.cs +++ b/TrayApp/Menu/TrayToggleItem.cs @@ -63,6 +63,12 @@ public override void Refresh() /// public void Set(bool value) => setState(value); + /// + /// Reads what the tool currently reports, without touching . + /// + /// The getter's value. The getter belongs to the tool and may throw. + internal bool ReadState() => getState(); + /// /// Sets the toggle to the opposite of what the tool currently reports. /// diff --git a/TrayApp/TrayAppBuilder.cs b/TrayApp/TrayAppBuilder.cs index 73cf5ca..df99fd8 100644 --- a/TrayApp/TrayAppBuilder.cs +++ b/TrayApp/TrayAppBuilder.cs @@ -535,7 +535,13 @@ private static async Task FinishAsync(int exitCode, DebouncedPreferenceStor try { - toggle.Set(remembered); + // A restore asks for a state, where a click asks for a change, so a toggle already in the + // remembered state is left alone: a setter written as a flip (as the class example's is) would + // otherwise invert it, and one that takes a resource would take it a second time. + if (toggle.ReadState() != remembered) + { + toggle.Set(remembered); + } } catch (Exception ex) when (FatalError.IsNotFatal(ex)) {