Skip to content

Commit 5e179c2

Browse files
author
Morten Nielsen
committed
Add support for custom window persistence storage. #61
1 parent d0cd0f4 commit 5e179c2

File tree

4 files changed

+166
-56
lines changed

4 files changed

+166
-56
lines changed

src/Directory.Build.targets

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
<Authors>Morten Nielsen - https://xaml.dev</Authors>
1919
<Company>Morten Nielsen - https://xaml.dev</Company>
2020
<PackageIcon>logo.png</PackageIcon>
21-
<Version>2.0.1</Version>
21+
<Version>2.1.0</Version>
2222
<PackageValidationBaselineVersion>2.0.0</PackageValidationBaselineVersion>
2323
</PropertyGroup>
2424

src/WinUIEx/WinUIEx.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
- Don't attempt to use window persistence in un-packaged applications.
2727
- WebAuthenticator: now supports cancellation tokens.
2828
- WebAuthenticator: Avoids an issue where state parameters are not always correctly handled/preserved correctly by OAuth services (reported in PR #92).
29+
- Persistence: Add support for custom Window state persistence storage, for use by unpackaged applications (Issue #61).
2930
</PackageReleaseNotes>
3031
</PropertyGroup>
3132

src/WinUIEx/WindowManager.cs

Lines changed: 74 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
using Windows.Storage;
66
using WinUIEx.Messaging;
77
using Windows.Win32.UI.WindowsAndMessaging;
8-
using Microsoft.UI;
98
using System.Collections.Generic;
109
using System.Diagnostics.CodeAnalysis;
1110

@@ -22,7 +21,7 @@ public partial class WindowManager : IDisposable
2221
private readonly Window _window;
2322
private OverlappedPresenter overlappedPresenter;
2423
private readonly static Dictionary<IntPtr, WeakReference<WindowManager>> managers = new Dictionary<IntPtr, WeakReference<WindowManager>>();
25-
private bool _isInitialized; // Set to true on first activation. Used to track persistance restore
24+
private bool _isInitialized; // Set to true on first activation. Used to track persistence restore
2625

2726
private static bool TryGetWindowManager(Window window, [MaybeNullWhen(false)] out WindowManager manager)
2827
{
@@ -254,7 +253,7 @@ private unsafe void OnWindowMessage(object? sender, Messaging.WindowMessageEvent
254253
{
255254
case WindowsMessages.WM_GETMINMAXINFO:
256255
{
257-
if (_restoringPersistance)
256+
if (_restoringPersistence)
258257
break;
259258
// Restrict min-size
260259
MINMAXINFO* rect2 = (MINMAXINFO*)e.Message.LParam;
@@ -269,7 +268,7 @@ private unsafe void OnWindowMessage(object? sender, Messaging.WindowMessageEvent
269268
break;
270269
case WindowsMessages.WM_DPICHANGED:
271270
{
272-
if (_restoringPersistance)
271+
if (_restoringPersistence)
273272
e.Handled = true; // Don't let WinUI resize the window due to a dpi change caused by restoring window position - we got this.
274273
break;
275274
}
@@ -294,34 +293,64 @@ private struct MINMAXINFO
294293

295294
#region Persistence
296295

297-
/// <summary>
298-
/// Gets or sets a unique ID used for saving and restoring window size and position
299-
/// across sessions.
300-
/// </summary>
301296
/// <remarks>
302297
/// The ID must be set before the window activates. The window size and position
303298
/// will only be restored if the monitor layout hasn't changed between application settings.
304299
/// The property uses ApplicationData storage, and therefore is currently only functional for
305300
/// packaged applications.
301+
/// By default the property uses <see cref="ApplicationData"/> storage, and therefore is currently only functional for
302+
/// packaged applications. If you're using an unpackaged application, you must also set the <see cref="PersistenceStorage"/>
303+
/// property and manage persisting this across application settings.
306304
/// </remarks>
305+
/// <seealso cref="PersistenceStorage"/>
307306
public string? PersistenceId { get; set; }
308307

309-
private bool _restoringPersistance; // Flag used to avoid WinUI DPI adjustment
308+
private bool _restoringPersistence; // Flag used to avoid WinUI DPI adjustment
309+
310+
/// <summary>
311+
/// Gets or sets the persistence storage for maintaining window settings across application settings.
312+
/// </summary>
313+
/// <remarks>
314+
/// For a packaged application, this will be initialized automatically for you, and saved with the application identity using <see cref="ApplicationData"/>.
315+
/// However for an unpackaged application, you will need to set this and serialize the property to/from disk between
316+
/// application sessions. The provided dictionary is automatically written to when the window closes, and should be initialized
317+
/// before any window with persistence opens.
318+
/// </remarks>
319+
/// <seealso cref="PersistenceId"/>
320+
public static IDictionary<string, object>? PersistenceStorage { get; set; }
321+
322+
private static IDictionary<string, object>? GetPersistenceStorage(bool createIfMissing)
323+
{
324+
if (PersistenceStorage is not null)
325+
return PersistenceStorage;
326+
if (Helpers.IsApplicationDataSupported)
327+
{
328+
try
329+
{
330+
if(ApplicationData.Current?.LocalSettings.Containers.TryGetValue("WinUIEx", out var container) == true)
331+
return container.Values!;
332+
else if (createIfMissing)
333+
return ApplicationData.Current?.LocalSettings?.CreateContainer("WinUIEx", ApplicationDataCreateDisposition.Always)?.Values;
334+
335+
}
336+
catch { }
337+
}
338+
return null;
339+
}
310340

311341
private void LoadPersistence()
312342
{
313-
if (!string.IsNullOrEmpty(PersistenceId) && Helpers.IsApplicationDataSupported)
343+
if (!string.IsNullOrEmpty(PersistenceId))
314344
{
315345
try
316346
{
317-
if (ApplicationData.Current?.LocalSettings?.Containers is null ||
318-
!ApplicationData.Current.LocalSettings.Containers.ContainsKey("WinUIEx"))
347+
var winuiExSettings = GetPersistenceStorage(false);
348+
if (winuiExSettings is null)
319349
return;
320350
byte[]? data = null;
321-
var winuiExSettings = ApplicationData.Current.LocalSettings.CreateContainer("WinUIEx", ApplicationDataCreateDisposition.Existing);
322-
if (winuiExSettings is not null && winuiExSettings.Values.ContainsKey($"WindowPersistance_{PersistenceId}"))
351+
if (winuiExSettings.ContainsKey($"WindowPersistance_{PersistenceId}"))
323352
{
324-
var base64 = winuiExSettings.Values[$"WindowPersistance_{PersistenceId}"] as string;
353+
var base64 = winuiExSettings[$"WindowPersistance_{PersistenceId}"] as string;
325354
if(base64 != null)
326355
data = Convert.FromBase64String(base64);
327356
}
@@ -354,45 +383,47 @@ private void LoadPersistence()
354383
retobj.showCmd = SHOW_WINDOW_CMD.SW_MAXIMIZE;
355384
else if (retobj.showCmd != SHOW_WINDOW_CMD.SW_MAXIMIZE)
356385
retobj.showCmd = SHOW_WINDOW_CMD.SW_NORMAL;
357-
_restoringPersistance = true;
386+
_restoringPersistence = true;
358387
Windows.Win32.PInvoke.SetWindowPlacement(new Windows.Win32.Foundation.HWND(_window.GetWindowHandle()), in retobj);
359-
_restoringPersistance = false;
388+
_restoringPersistence = false;
360389
}
361390
catch { }
362391
}
363392
}
364393

365394
private void SavePersistence()
366395
{
367-
if (!string.IsNullOrEmpty(PersistenceId) && Helpers.IsApplicationDataSupported)
396+
if (!string.IsNullOrEmpty(PersistenceId))
368397
{
369-
// Store monitor info - we won't restore on original screen if original monitor layout has changed
370-
using var data = new System.IO.MemoryStream();
371-
using var sw = new System.IO.BinaryWriter(data);
372-
var monitors = MonitorInfo.GetDisplayMonitors();
373-
sw.Write(monitors.Count);
374-
foreach (var monitor in monitors)
398+
var winuiExSettings = GetPersistenceStorage(true);
399+
if (winuiExSettings is not null)
375400
{
376-
sw.Write(monitor.Name);
377-
sw.Write(monitor.RectMonitor.Left);
378-
sw.Write(monitor.RectMonitor.Top);
379-
sw.Write(monitor.RectMonitor.Right);
380-
sw.Write(monitor.RectMonitor.Bottom);
401+
// Store monitor info - we won't restore on original screen if original monitor layout has changed
402+
using var data = new System.IO.MemoryStream();
403+
using var sw = new System.IO.BinaryWriter(data);
404+
var monitors = MonitorInfo.GetDisplayMonitors();
405+
sw.Write(monitors.Count);
406+
foreach (var monitor in monitors)
407+
{
408+
sw.Write(monitor.Name);
409+
sw.Write(monitor.RectMonitor.Left);
410+
sw.Write(monitor.RectMonitor.Top);
411+
sw.Write(monitor.RectMonitor.Right);
412+
sw.Write(monitor.RectMonitor.Bottom);
413+
}
414+
var placement = new WINDOWPLACEMENT();
415+
Windows.Win32.PInvoke.GetWindowPlacement(new Windows.Win32.Foundation.HWND(_window.GetWindowHandle()), ref placement);
416+
417+
int structSize = Marshal.SizeOf(typeof(WINDOWPLACEMENT));
418+
IntPtr buffer = Marshal.AllocHGlobal(structSize);
419+
Marshal.StructureToPtr(placement, buffer, false);
420+
byte[] placementData = new byte[structSize];
421+
Marshal.Copy(buffer, placementData, 0, structSize);
422+
Marshal.FreeHGlobal(buffer);
423+
sw.Write(placementData);
424+
sw.Flush();
425+
winuiExSettings[$"WindowPersistance_{PersistenceId}"] = Convert.ToBase64String(data.ToArray());
381426
}
382-
var placement = new WINDOWPLACEMENT();
383-
Windows.Win32.PInvoke.GetWindowPlacement(new Windows.Win32.Foundation.HWND(_window.GetWindowHandle()), ref placement);
384-
385-
int structSize = Marshal.SizeOf(typeof(WINDOWPLACEMENT));
386-
IntPtr buffer = Marshal.AllocHGlobal(structSize);
387-
Marshal.StructureToPtr(placement, buffer, false);
388-
byte[] placementData = new byte[structSize];
389-
Marshal.Copy(buffer, placementData, 0, structSize);
390-
Marshal.FreeHGlobal(buffer);
391-
sw.Write(placementData);
392-
sw.Flush();
393-
var winuiExSettings = ApplicationData.Current?.LocalSettings?.CreateContainer("WinUIEx", ApplicationDataCreateDisposition.Always);
394-
if (winuiExSettings != null)
395-
winuiExSettings.Values[$"WindowPersistance_{PersistenceId}"] = Convert.ToBase64String(data.ToArray());
396427
}
397428
}
398429
#endregion

src/WinUIExSample/App.xaml.cs

Lines changed: 90 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,13 @@
11
using System;
2+
using System.Collections;
23
using System.Collections.Generic;
4+
using System.Collections.Immutable;
5+
using System.Diagnostics.CodeAnalysis;
36
using System.IO;
47
using System.Linq;
5-
using System.Runtime.InteropServices.WindowsRuntime;
8+
using System.Text.Json.Nodes;
69
using Microsoft.UI.Xaml;
7-
using Microsoft.UI.Xaml.Controls;
8-
using Microsoft.UI.Xaml.Controls.Primitives;
9-
using Microsoft.UI.Xaml.Data;
10-
using Microsoft.UI.Xaml.Input;
11-
using Microsoft.UI.Xaml.Media;
12-
using Microsoft.UI.Xaml.Navigation;
13-
using Microsoft.UI.Xaml.Shapes;
14-
using Windows.ApplicationModel;
15-
using Windows.ApplicationModel.Activation;
16-
using Windows.Foundation;
17-
using Windows.Foundation.Collections;
10+
using System.Runtime.InteropServices;
1811

1912
// To learn more about WinUI, the WinUI project structure,
2013
// and more about our project templates, see: http://aka.ms/winui-project-info.
@@ -33,6 +26,14 @@ public partial class App : Application
3326
public App()
3427
{
3528
this.InitializeComponent();
29+
int length = 0;
30+
var sb = new System.Text.StringBuilder(0);
31+
int result = GetCurrentPackageFullName(ref length, sb);
32+
if(result == 15700L)
33+
{
34+
// Not a packaged app. Configure file-based persistence instead
35+
WinUIEx.WindowManager.PersistenceStorage = new FilePersistence("WinUIExPersistence.json");
36+
}
3637
}
3738

3839
/// <summary>
@@ -48,5 +49,82 @@ protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs ar
4849
}
4950

5051
private Window m_window;
52+
53+
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
54+
private static extern int GetCurrentPackageFullName(ref int packageFullNameLength, System.Text.StringBuilder packageFullName);
55+
56+
private class FilePersistence : IDictionary<string, object>
57+
{
58+
private readonly Dictionary<string, object> _data = new Dictionary<string, object>();
59+
private readonly string _file;
60+
61+
public FilePersistence(string filename)
62+
{
63+
_file = filename;
64+
try
65+
{
66+
if (File.Exists(filename))
67+
{
68+
var jo = System.Text.Json.Nodes.JsonObject.Parse(File.ReadAllText(filename)) as JsonObject;
69+
foreach(var node in jo)
70+
{
71+
if (node.Value is JsonValue jvalue && jvalue.TryGetValue<string>(out string value))
72+
_data[node.Key] = value;
73+
}
74+
}
75+
}
76+
catch { }
77+
}
78+
private void Save()
79+
{
80+
JsonObject jo = new JsonObject();
81+
foreach(var item in _data)
82+
{
83+
if (item.Value is string s) // In this case we only need string support. TODO: Support other types
84+
jo.Add(item.Key, s);
85+
}
86+
File.WriteAllText(_file, jo.ToJsonString());
87+
}
88+
public object this[string key] { get => _data[key]; set { _data[key] = value; Save();} }
89+
90+
public ICollection<string> Keys => _data.Keys;
91+
92+
public ICollection<object> Values => _data.Values;
93+
94+
public int Count => _data.Count;
95+
96+
public bool IsReadOnly => false;
97+
98+
public void Add(string key, object value)
99+
{
100+
_data.Add(key, value); Save();
101+
}
102+
103+
public void Add(KeyValuePair<string, object> item)
104+
{
105+
_data.Add(item.Key, item.Value); Save();
106+
}
107+
108+
public void Clear()
109+
{
110+
_data.Clear(); Save();
111+
}
112+
113+
public bool Contains(KeyValuePair<string, object> item) => _data.Contains(item);
114+
115+
public bool ContainsKey(string key) => _data.ContainsKey(key);
116+
117+
public void CopyTo(KeyValuePair<string, object>[] array, int arrayIndex) => throw new NotImplementedException(); // TODO
118+
119+
public IEnumerator<KeyValuePair<string, object>> GetEnumerator() => throw new NotImplementedException(); // TODO
120+
121+
public bool Remove(string key) => throw new NotImplementedException(); // TODO
122+
123+
public bool Remove(KeyValuePair<string, object> item) => throw new NotImplementedException(); // TODO
124+
125+
public bool TryGetValue(string key, [MaybeNullWhen(false)] out object value) => throw new NotImplementedException(); // TODO
126+
127+
IEnumerator IEnumerable.GetEnumerator() => throw new NotImplementedException(); // TODO
128+
}
51129
}
52130
}

0 commit comments

Comments
 (0)