From 27e683ec027013ffe7e673e70036f5e5706bd35b Mon Sep 17 00:00:00 2001 From: Andrew Clinick <80841394+aclinick@users.noreply.github.com> Date: Mon, 8 Jun 2026 12:05:59 -0700 Subject: [PATCH] Fix #20 and #21: file association + clipboard copy #20: Register .msix/.msixbundle/.appx/.appxbundle as supported file types via a windows.fileTypeAssociation extension in Package.appxmanifest. Right- click `Open with MSIXplainer` now launches the app and loads the package. Wiring: * MainPageViewModel.LoadPackageFromPath(string) extracted as public so it can be reused by the picker and by file activation. * App.OnLaunched captures PendingFileActivationPath from AppInstance.GetCurrent().GetActivatedEventArgs() BEFORE new MainWindow() so the path is available when MainPage_Loaded fires. * MainPage_Loaded calls App.ConsumePendingFileActivationPath() and invokes the VM. Avoids a DispatcherQueue.TryEnqueue race where the lambda ran before Frame.Content was set. #21: WinUI 3 Clipboard.SetContent was throwing CO_E_NOTINITIALIZED (0x800401F0). Root cause: Program.cs Main was marked [MTAThread]. Because the .csproj sets DISABLE_XAML_GENERATED_MAIN, the auto-generated [STAThread] Main is suppressed and our hand-written one wins. Everything else (XAML, bindings, pickers) worked on MTA, but WinRT OLE-backed APIs like DataTransfer.Clipboard refuse to run there. One-line fix: [STAThread]. Added a comment in Program.cs so nobody silently flips it back. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- MSIXplainer/App.xaml.cs | 55 +++++++++++++++++++++ MSIXplainer/MainPage.xaml.cs | 31 +++++++++--- MSIXplainer/Package.appxmanifest | 16 +++++- MSIXplainer/Program.cs | 8 ++- MSIXplainer/ViewModels/MainPageViewModel.cs | 28 ++++++++--- 5 files changed, 122 insertions(+), 16 deletions(-) diff --git a/MSIXplainer/App.xaml.cs b/MSIXplainer/App.xaml.cs index 1550b83..3bc68a9 100644 --- a/MSIXplainer/App.xaml.cs +++ b/MSIXplainer/App.xaml.cs @@ -4,6 +4,7 @@ using Microsoft.UI.Xaml.Input; using Microsoft.UI.Xaml.Media; using Microsoft.UI.Xaml.Navigation; +using System.Linq; // To learn more about WinUI, the WinUI project structure, // and more about our project templates, see: http://aka.ms/winui-project-info. @@ -59,12 +60,29 @@ private void OnUnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExce e.Handled = true; } + /// + /// File path provided via file activation (right-click → Open with + /// MSIXplainer). MainPage reads this on construction and loads the + /// package. Null when the app was launched normally. + /// + public static string? PendingFileActivationPath { get; private set; } + + /// + /// Clears the pending file-activation path. Call after the path has been + /// loaded so it isn't re-applied on subsequent page reloads. + /// + public static void ConsumePendingFileActivationPath() => PendingFileActivationPath = null; + /// /// Invoked when the application is launched. /// /// Details about the launch request and process. protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args) { + // Capture file-activation arg BEFORE MainWindow is constructed so + // MainPage can pick it up during its own construction. See #20. + PendingFileActivationPath = TryGetFileActivationPath(); + Window = new MainWindow(); DispatcherQueue = Microsoft.UI.Dispatching.DispatcherQueue.GetForCurrentThread(); Window.Activate(); @@ -72,4 +90,41 @@ protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs ar // root exists. ThemeService.Apply no-ops if Content is still null. MSIXplainer.Services.ThemeService.Apply(MSIXplainer.Services.ThemeService.LoadPreference()); } + + /// + /// If the app was launched via a file activation, return the first file's + /// path; otherwise return null. Safe to call early in OnLaunched. + /// + private static string? TryGetFileActivationPath() + { + try + { + var activated = Microsoft.Windows.AppLifecycle.AppInstance + .GetCurrent() + .GetActivatedEventArgs(); + + System.Diagnostics.Debug.WriteLine( + $"[MSIXplainer] Activation kind: {activated.Kind}"); + + if (activated.Kind != Microsoft.Windows.AppLifecycle.ExtendedActivationKind.File) + return null; + + if (activated.Data is not Windows.ApplicationModel.Activation.IFileActivatedEventArgs fileArgs) + { + System.Diagnostics.Debug.WriteLine( + $"[MSIXplainer] Activation data is {activated.Data?.GetType().FullName ?? "null"}, not IFileActivatedEventArgs"); + return null; + } + + var firstFile = fileArgs.Files.FirstOrDefault(); + System.Diagnostics.Debug.WriteLine( + $"[MSIXplainer] File activation path: {firstFile?.Path ?? ""}"); + return firstFile?.Path; + } + catch (System.Exception ex) + { + System.Diagnostics.Debug.WriteLine($"[MSIXplainer] File activation failed: {ex.Message}"); + return null; + } + } } diff --git a/MSIXplainer/MainPage.xaml.cs b/MSIXplainer/MainPage.xaml.cs index 879ae5e..b61f4e4 100644 --- a/MSIXplainer/MainPage.xaml.cs +++ b/MSIXplainer/MainPage.xaml.cs @@ -30,6 +30,21 @@ public MainPage() ViewModel.InstalledPackages.CollectionChanged += InstalledPackages_CollectionChanged; ViewModel.PropertyChanged += ViewModel_PropertyChanged; BuildStaticNavItems(); + Loaded += MainPage_Loaded; + } + + private void MainPage_Loaded(object sender, RoutedEventArgs e) + { + // File activation: if the app was launched by right-clicking a .msix / + // .msixbundle in Explorer (see App.PendingFileActivationPath / issue #20), + // load it now that the page is fully wired up. Consume the path so a + // page reload doesn't reopen it. + var path = App.PendingFileActivationPath; + if (!string.IsNullOrEmpty(path)) + { + App.ConsumePendingFileActivationPath(); + ViewModel.LoadPackageFromPath(path); + } } private void BuildStaticNavItems() @@ -212,10 +227,9 @@ private void OnCloseFindingClick(object sender, RoutedEventArgs e) } /// - /// Copies a manifest property value to the clipboard. Wraps the clipboard - /// call in try/catch so a transient clipboard failure never bubbles up as - /// an unhandled exception. Bug fix for #10 — the built-in TextBlock - /// context-menu Copy was crashing the app on some Windows builds. + /// Copies a manifest property value to the clipboard. The 3-line WinUI + /// pattern works as long as the process's Main is marked [STAThread] — + /// see Program.cs and the comment there about issue #21. /// private void CopyPropertyValue_Click(object sender, RoutedEventArgs e) { @@ -224,13 +238,14 @@ private void CopyPropertyValue_Click(object sender, RoutedEventArgs e) try { - var package = new Windows.ApplicationModel.DataTransfer.DataPackage(); - package.SetText(value); - Windows.ApplicationModel.DataTransfer.Clipboard.SetContent(package); + var pkg = new Windows.ApplicationModel.DataTransfer.DataPackage(); + pkg.SetText(value); + Windows.ApplicationModel.DataTransfer.Clipboard.SetContent(pkg); } catch (System.Exception ex) { - System.Diagnostics.Debug.WriteLine($"[MSIXplainer] Clipboard copy failed: {ex.Message}"); + System.Diagnostics.Debug.WriteLine( + $"[MSIXplainer] Clipboard copy failed: {ex.GetType().Name} 0x{ex.HResult:X8} {ex.Message}"); } } diff --git a/MSIXplainer/Package.appxmanifest b/MSIXplainer/Package.appxmanifest index 5f32ded..a67c5ff 100644 --- a/MSIXplainer/Package.appxmanifest +++ b/MSIXplainer/Package.appxmanifest @@ -11,7 +11,7 @@ + Version="1.0.27.0" /> @@ -44,6 +44,20 @@ + + + + MSIX Package + Assets\Square44x44Logo.png + + .msix + .msixbundle + .appx + .appxbundle + + + + + /// Loads and analyzes a package from a file path. Used by both the file + /// picker flow and the file-activation flow (right-click → Open with + /// MSIXplainer) — see issue #20. + /// + public void LoadPackageFromPath(string path) + { + try + { + PackageFilePath = path; - if (ManifestParserService.IsBundleFile(result.Path)) + if (ManifestParserService.IsBundleFile(path)) { - var packages = ManifestParserService.ExtractFromBundle(result.Path); - // Analyze the first package in the bundle (typically the current platform arch) + var packages = ManifestParserService.ExtractFromBundle(path); var pkg = packages.First(); - PackageFilePath = $"{result.Path} ({pkg.Label})"; + PackageFilePath = $"{path} ({pkg.Label})"; AnalyzeManifest(pkg.RawXml, pkg.Info, pkg.Manifest); } else { - var (manifest, rawXml, info) = ManifestParserService.ExtractFromPackage(result.Path); + var (manifest, rawXml, info) = ManifestParserService.ExtractFromPackage(path); AnalyzeManifest(rawXml, info, manifest); } }