Skip to content

Commit

Permalink
Add support for custom window persistence storage. #61
Browse files Browse the repository at this point in the history
  • Loading branch information
Morten Nielsen committed Jan 3, 2023
1 parent d0cd0f4 commit 5e179c2
Show file tree
Hide file tree
Showing 4 changed files with 166 additions and 56 deletions.
2 changes: 1 addition & 1 deletion src/Directory.Build.targets
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
<Authors>Morten Nielsen - https://xaml.dev</Authors>
<Company>Morten Nielsen - https://xaml.dev</Company>
<PackageIcon>logo.png</PackageIcon>
<Version>2.0.1</Version>
<Version>2.1.0</Version>
<PackageValidationBaselineVersion>2.0.0</PackageValidationBaselineVersion>
</PropertyGroup>

Expand Down
1 change: 1 addition & 0 deletions src/WinUIEx/WinUIEx.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
- Don't attempt to use window persistence in un-packaged applications.
- WebAuthenticator: now supports cancellation tokens.
- WebAuthenticator: Avoids an issue where state parameters are not always correctly handled/preserved correctly by OAuth services (reported in PR #92).
- Persistence: Add support for custom Window state persistence storage, for use by unpackaged applications (Issue #61).
</PackageReleaseNotes>
</PropertyGroup>

Expand Down
117 changes: 74 additions & 43 deletions src/WinUIEx/WindowManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
using Windows.Storage;
using WinUIEx.Messaging;
using Windows.Win32.UI.WindowsAndMessaging;
using Microsoft.UI;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;

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

private static bool TryGetWindowManager(Window window, [MaybeNullWhen(false)] out WindowManager manager)
{
Expand Down Expand Up @@ -254,7 +253,7 @@ private unsafe void OnWindowMessage(object? sender, Messaging.WindowMessageEvent
{
case WindowsMessages.WM_GETMINMAXINFO:
{
if (_restoringPersistance)
if (_restoringPersistence)
break;
// Restrict min-size
MINMAXINFO* rect2 = (MINMAXINFO*)e.Message.LParam;
Expand All @@ -269,7 +268,7 @@ private unsafe void OnWindowMessage(object? sender, Messaging.WindowMessageEvent
break;
case WindowsMessages.WM_DPICHANGED:
{
if (_restoringPersistance)
if (_restoringPersistence)
e.Handled = true; // Don't let WinUI resize the window due to a dpi change caused by restoring window position - we got this.
break;
}
Expand All @@ -294,34 +293,64 @@ private struct MINMAXINFO

#region Persistence

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

private bool _restoringPersistance; // Flag used to avoid WinUI DPI adjustment
private bool _restoringPersistence; // Flag used to avoid WinUI DPI adjustment

/// <summary>
/// Gets or sets the persistence storage for maintaining window settings across application settings.
/// </summary>
/// <remarks>
/// For a packaged application, this will be initialized automatically for you, and saved with the application identity using <see cref="ApplicationData"/>.
/// However for an unpackaged application, you will need to set this and serialize the property to/from disk between
/// application sessions. The provided dictionary is automatically written to when the window closes, and should be initialized
/// before any window with persistence opens.
/// </remarks>
/// <seealso cref="PersistenceId"/>
public static IDictionary<string, object>? PersistenceStorage { get; set; }

private static IDictionary<string, object>? GetPersistenceStorage(bool createIfMissing)
{
if (PersistenceStorage is not null)
return PersistenceStorage;
if (Helpers.IsApplicationDataSupported)
{
try
{
if(ApplicationData.Current?.LocalSettings.Containers.TryGetValue("WinUIEx", out var container) == true)
return container.Values!;
else if (createIfMissing)
return ApplicationData.Current?.LocalSettings?.CreateContainer("WinUIEx", ApplicationDataCreateDisposition.Always)?.Values;

}
catch { }
}
return null;
}

private void LoadPersistence()
{
if (!string.IsNullOrEmpty(PersistenceId) && Helpers.IsApplicationDataSupported)
if (!string.IsNullOrEmpty(PersistenceId))
{
try
{
if (ApplicationData.Current?.LocalSettings?.Containers is null ||
!ApplicationData.Current.LocalSettings.Containers.ContainsKey("WinUIEx"))
var winuiExSettings = GetPersistenceStorage(false);
if (winuiExSettings is null)
return;
byte[]? data = null;
var winuiExSettings = ApplicationData.Current.LocalSettings.CreateContainer("WinUIEx", ApplicationDataCreateDisposition.Existing);
if (winuiExSettings is not null && winuiExSettings.Values.ContainsKey($"WindowPersistance_{PersistenceId}"))
if (winuiExSettings.ContainsKey($"WindowPersistance_{PersistenceId}"))
{
var base64 = winuiExSettings.Values[$"WindowPersistance_{PersistenceId}"] as string;
var base64 = winuiExSettings[$"WindowPersistance_{PersistenceId}"] as string;
if(base64 != null)
data = Convert.FromBase64String(base64);
}
Expand Down Expand Up @@ -354,45 +383,47 @@ private void LoadPersistence()
retobj.showCmd = SHOW_WINDOW_CMD.SW_MAXIMIZE;
else if (retobj.showCmd != SHOW_WINDOW_CMD.SW_MAXIMIZE)
retobj.showCmd = SHOW_WINDOW_CMD.SW_NORMAL;
_restoringPersistance = true;
_restoringPersistence = true;
Windows.Win32.PInvoke.SetWindowPlacement(new Windows.Win32.Foundation.HWND(_window.GetWindowHandle()), in retobj);
_restoringPersistance = false;
_restoringPersistence = false;
}
catch { }
}
}

private void SavePersistence()
{
if (!string.IsNullOrEmpty(PersistenceId) && Helpers.IsApplicationDataSupported)
if (!string.IsNullOrEmpty(PersistenceId))
{
// Store monitor info - we won't restore on original screen if original monitor layout has changed
using var data = new System.IO.MemoryStream();
using var sw = new System.IO.BinaryWriter(data);
var monitors = MonitorInfo.GetDisplayMonitors();
sw.Write(monitors.Count);
foreach (var monitor in monitors)
var winuiExSettings = GetPersistenceStorage(true);
if (winuiExSettings is not null)
{
sw.Write(monitor.Name);
sw.Write(monitor.RectMonitor.Left);
sw.Write(monitor.RectMonitor.Top);
sw.Write(monitor.RectMonitor.Right);
sw.Write(monitor.RectMonitor.Bottom);
// Store monitor info - we won't restore on original screen if original monitor layout has changed
using var data = new System.IO.MemoryStream();
using var sw = new System.IO.BinaryWriter(data);
var monitors = MonitorInfo.GetDisplayMonitors();
sw.Write(monitors.Count);
foreach (var monitor in monitors)
{
sw.Write(monitor.Name);
sw.Write(monitor.RectMonitor.Left);
sw.Write(monitor.RectMonitor.Top);
sw.Write(monitor.RectMonitor.Right);
sw.Write(monitor.RectMonitor.Bottom);
}
var placement = new WINDOWPLACEMENT();
Windows.Win32.PInvoke.GetWindowPlacement(new Windows.Win32.Foundation.HWND(_window.GetWindowHandle()), ref placement);

int structSize = Marshal.SizeOf(typeof(WINDOWPLACEMENT));
IntPtr buffer = Marshal.AllocHGlobal(structSize);
Marshal.StructureToPtr(placement, buffer, false);
byte[] placementData = new byte[structSize];
Marshal.Copy(buffer, placementData, 0, structSize);
Marshal.FreeHGlobal(buffer);
sw.Write(placementData);
sw.Flush();
winuiExSettings[$"WindowPersistance_{PersistenceId}"] = Convert.ToBase64String(data.ToArray());
}
var placement = new WINDOWPLACEMENT();
Windows.Win32.PInvoke.GetWindowPlacement(new Windows.Win32.Foundation.HWND(_window.GetWindowHandle()), ref placement);

int structSize = Marshal.SizeOf(typeof(WINDOWPLACEMENT));
IntPtr buffer = Marshal.AllocHGlobal(structSize);
Marshal.StructureToPtr(placement, buffer, false);
byte[] placementData = new byte[structSize];
Marshal.Copy(buffer, placementData, 0, structSize);
Marshal.FreeHGlobal(buffer);
sw.Write(placementData);
sw.Flush();
var winuiExSettings = ApplicationData.Current?.LocalSettings?.CreateContainer("WinUIEx", ApplicationDataCreateDisposition.Always);
if (winuiExSettings != null)
winuiExSettings.Values[$"WindowPersistance_{PersistenceId}"] = Convert.ToBase64String(data.ToArray());
}
}
#endregion
Expand Down
102 changes: 90 additions & 12 deletions src/WinUIExSample/App.xaml.cs
Original file line number Diff line number Diff line change
@@ -1,20 +1,13 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Text.Json.Nodes;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Navigation;
using Microsoft.UI.Xaml.Shapes;
using Windows.ApplicationModel;
using Windows.ApplicationModel.Activation;
using Windows.Foundation;
using Windows.Foundation.Collections;
using System.Runtime.InteropServices;

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

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

private Window m_window;

[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern int GetCurrentPackageFullName(ref int packageFullNameLength, System.Text.StringBuilder packageFullName);

private class FilePersistence : IDictionary<string, object>
{
private readonly Dictionary<string, object> _data = new Dictionary<string, object>();
private readonly string _file;

public FilePersistence(string filename)
{
_file = filename;
try
{
if (File.Exists(filename))
{
var jo = System.Text.Json.Nodes.JsonObject.Parse(File.ReadAllText(filename)) as JsonObject;
foreach(var node in jo)
{
if (node.Value is JsonValue jvalue && jvalue.TryGetValue<string>(out string value))
_data[node.Key] = value;
}
}
}
catch { }
}
private void Save()
{
JsonObject jo = new JsonObject();
foreach(var item in _data)
{
if (item.Value is string s) // In this case we only need string support. TODO: Support other types
jo.Add(item.Key, s);
}
File.WriteAllText(_file, jo.ToJsonString());
}
public object this[string key] { get => _data[key]; set { _data[key] = value; Save();} }

public ICollection<string> Keys => _data.Keys;

public ICollection<object> Values => _data.Values;

public int Count => _data.Count;

public bool IsReadOnly => false;

public void Add(string key, object value)
{
_data.Add(key, value); Save();
}

public void Add(KeyValuePair<string, object> item)
{
_data.Add(item.Key, item.Value); Save();
}

public void Clear()
{
_data.Clear(); Save();
}

public bool Contains(KeyValuePair<string, object> item) => _data.Contains(item);

public bool ContainsKey(string key) => _data.ContainsKey(key);

public void CopyTo(KeyValuePair<string, object>[] array, int arrayIndex) => throw new NotImplementedException(); // TODO

public IEnumerator<KeyValuePair<string, object>> GetEnumerator() => throw new NotImplementedException(); // TODO

public bool Remove(string key) => throw new NotImplementedException(); // TODO

public bool Remove(KeyValuePair<string, object> item) => throw new NotImplementedException(); // TODO

public bool TryGetValue(string key, [MaybeNullWhen(false)] out object value) => throw new NotImplementedException(); // TODO

IEnumerator IEnumerable.GetEnumerator() => throw new NotImplementedException(); // TODO
}
}
}

0 comments on commit 5e179c2

Please sign in to comment.