diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets index 0678793..1ad1a97 100644 --- a/src/Directory.Build.targets +++ b/src/Directory.Build.targets @@ -18,7 +18,7 @@ Morten Nielsen - https://xaml.dev Morten Nielsen - https://xaml.dev logo.png - 2.0.1 + 2.1.0 2.0.0 diff --git a/src/WinUIEx/WinUIEx.csproj b/src/WinUIEx/WinUIEx.csproj index c72d5e0..84fc7fe 100644 --- a/src/WinUIEx/WinUIEx.csproj +++ b/src/WinUIEx/WinUIEx.csproj @@ -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). diff --git a/src/WinUIEx/WindowManager.cs b/src/WinUIEx/WindowManager.cs index 3998b54..df3b2ea 100644 --- a/src/WinUIEx/WindowManager.cs +++ b/src/WinUIEx/WindowManager.cs @@ -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; @@ -22,7 +21,7 @@ public partial class WindowManager : IDisposable private readonly Window _window; private OverlappedPresenter overlappedPresenter; private readonly static Dictionary> managers = new Dictionary>(); - 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) { @@ -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; @@ -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; } @@ -294,34 +293,64 @@ private struct MINMAXINFO #region Persistence - /// - /// Gets or sets a unique ID used for saving and restoring window size and position - /// across sessions. - /// /// /// 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 storage, and therefore is currently only functional for + /// packaged applications. If you're using an unpackaged application, you must also set the + /// property and manage persisting this across application settings. /// + /// 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 + + /// + /// Gets or sets the persistence storage for maintaining window settings across application settings. + /// + /// + /// For a packaged application, this will be initialized automatically for you, and saved with the application identity using . + /// 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. + /// + /// + public static IDictionary? PersistenceStorage { get; set; } + + private static IDictionary? 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); } @@ -354,9 +383,9 @@ 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 { } } @@ -364,35 +393,37 @@ private void LoadPersistence() 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 diff --git a/src/WinUIExSample/App.xaml.cs b/src/WinUIExSample/App.xaml.cs index fc6b38d..38dd38c 100644 --- a/src/WinUIExSample/App.xaml.cs +++ b/src/WinUIExSample/App.xaml.cs @@ -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. @@ -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"); + } } /// @@ -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 + { + private readonly Dictionary _data = new Dictionary(); + 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(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 Keys => _data.Keys; + + public ICollection 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 item) + { + _data.Add(item.Key, item.Value); Save(); + } + + public void Clear() + { + _data.Clear(); Save(); + } + + public bool Contains(KeyValuePair item) => _data.Contains(item); + + public bool ContainsKey(string key) => _data.ContainsKey(key); + + public void CopyTo(KeyValuePair[] array, int arrayIndex) => throw new NotImplementedException(); // TODO + + public IEnumerator> GetEnumerator() => throw new NotImplementedException(); // TODO + + public bool Remove(string key) => throw new NotImplementedException(); // TODO + + public bool Remove(KeyValuePair 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 + } } }