Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support WindowEx Persistence in unpackaged apps #61

Closed
mikebattista opened this issue Jul 5, 2022 · 4 comments
Closed

Support WindowEx Persistence in unpackaged apps #61

mikebattista opened this issue Jul 5, 2022 · 4 comments
Labels
enhancement New feature or request help wanted Extra attention is needed

Comments

@mikebattista
Copy link

Setting WindowEx.PersistenceId in an MSIX app preserves window size and position. This isn't currently supported for unpackaged apps, though.

Would be great if we could support this in unpackaged apps as well that don't use MSIX. I understand that the implementation of Persistence today relies on the ApplicationData API which is only available to packaged apps. How could we support unpackaged apps as well?

@dotMorten
Copy link
Owner

How could we support unpackaged apps as well?

According to microsoft/WindowsAppSDK#1721 (comment), we are supposed to just use file storage in the AppData folder. However this folder is shared among all apps, and if WinUIEx need to "just work" it would have to somehow uniquely identify each app in a way where they won't overwrite each other - as well as somehow handle cleanup on uninstall. I don't think that is feasible.

If unpackaged apps needs to be supported, we'd need some sort of extension point where the developer can take over and save the settings somewhere, but then most of what WinUIEx does for you wrt this would go away.

ApplicationData is just one of a lot of benefit you get by packaging your apps.

@dotMorten dotMorten added enhancement New feature or request help wanted Extra attention is needed labels Jul 5, 2022
@mikebattista
Copy link
Author

We generally strive for parity between MSIX and non-MSIX apps. At least in Template Studio's case, we do provide an unpackaged implementation for local settings that writes to LocalAppData using configurable paths. Not providing settings storage at all because MSIX has a better way to do it is not really an option there, and LocalAppData has been the standard way to do it. Yes I understand the drawbacks compared to MSIX ApplicationData but developers have been dealing with this for years. I wouldn't expect the library to clean up on uninstall, I would expect the app to continue to manage its own LocalAppData.

If WinUIEx contained a similar supporting implementation, I guess one option would be to expose a PersistencePath property that the user could set to instruct the library where to save the settings. The value of the library would be that it's handling a good amount of code and P/Invokes vs. requiring apps to do all that themselves.

private void LoadPersistence()
{
if (!string.IsNullOrEmpty(PersistenceId))
{
try
{
if (ApplicationData.Current?.LocalSettings?.Containers is null ||
!ApplicationData.Current.LocalSettings.Containers.ContainsKey("WinUIEx"))
return;
byte[]? data = null;
var winuiExSettings = ApplicationData.Current.LocalSettings.CreateContainer("WinUIEx", ApplicationDataCreateDisposition.Existing);
if (winuiExSettings is not null && winuiExSettings.Values.ContainsKey($"WindowPersistance_{PersistenceId}"))
{
var base64 = winuiExSettings.Values[$"WindowPersistance_{PersistenceId}"] as string;
if(base64 != null)
data = Convert.FromBase64String(base64);
}
if (data is null)
return;
// Check if monitor layout changed since we stored position
var monitors = MonitorInfo.GetDisplayMonitors();
System.IO.BinaryReader br = new System.IO.BinaryReader(new System.IO.MemoryStream(data));
int monitorCount = br.ReadInt32();
if (monitorCount < monitors.Count)
return; // Don't restore - list of monitors changed
for (int i = 0; i < monitorCount; i++)
{
var pMonitor = monitors[i];
if (pMonitor.Name != br.ReadString() ||
pMonitor.RectMonitor.Left != br.ReadDouble() ||
pMonitor.RectMonitor.Top != br.ReadDouble() ||
pMonitor.RectMonitor.Right != br.ReadDouble() ||
pMonitor.RectMonitor.Bottom != br.ReadDouble())
return; // Don't restore - Monitor layout changed
}
int structSize = Marshal.SizeOf(typeof(WINDOWPLACEMENT));
byte[] placementData = br.ReadBytes(structSize);
IntPtr buffer = Marshal.AllocHGlobal(structSize);
Marshal.Copy(placementData, 0, buffer, structSize);
var retobj = (WINDOWPLACEMENT)Marshal.PtrToStructure(buffer, typeof(WINDOWPLACEMENT))!;
Marshal.FreeHGlobal(buffer);
// Ignore anything by maximized or normal
if (retobj.showCmd == SHOW_WINDOW_CMD.SW_INVALIDATE && retobj.flags == WINDOWPLACEMENT_FLAGS.WPF_RESTORETOMAXIMIZED)
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;
Windows.Win32.PInvoke.SetWindowPlacement(new Windows.Win32.Foundation.HWND(_window.GetWindowHandle()), in retobj);
_restoringPersistance = false;
}
catch { }
}
}
private void SavePersistence()
{
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)
{
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();
var winuiExSettings = ApplicationData.Current?.LocalSettings?.CreateContainer("WinUIEx", ApplicationDataCreateDisposition.Always);
if (winuiExSettings != null)
winuiExSettings.Values[$"WindowPersistance_{PersistenceId}"] = Convert.ToBase64String(data.ToArray());
}
}
#endregion

Are there other ways we could allow apps to reuse your code here for saving and restoring window state? In Template Studio's case, the app will have its own local settings storage implementation already, what could a solution look like that didn't require you to implement your own unpackaged implementation for settings storage?

@dotMorten
Copy link
Owner

I could provide some sort of property bag interface on the window manager - something along the lines of:

public class WindowManager
{
    public static IDictionary<string,object>? PersistanceStorage { get; set; }

Would this work? On a packaged app it would just default to ApplicationData.Current?.LocalSettings.CreateContainer("WinUIEx", ApplicationDataCreateDisposition.Always)?.Values

So in your code you could just initialize it if it is null.

@dotMorten
Copy link
Owner

Fixed. See

WinUIEx.WindowManager.PersistenceStorage = new FilePersistence("WinUIExPersistence.json");
for example

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request help wanted Extra attention is needed
Projects
None yet
Development

No branches or pull requests

2 participants