Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions MSIXplainer/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -59,17 +60,71 @@ private void OnUnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExce
e.Handled = true;
}

/// <summary>
/// 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.
/// </summary>
public static string? PendingFileActivationPath { get; private set; }

/// <summary>
/// Clears the pending file-activation path. Call after the path has been
/// loaded so it isn't re-applied on subsequent page reloads.
/// </summary>
public static void ConsumePendingFileActivationPath() => PendingFileActivationPath = null;

/// <summary>
/// Invoked when the application is launched.
/// </summary>
/// <param name="args">Details about the launch request and process.</param>
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();
// Apply persisted theme after Window.Content is set so the FrameworkElement
// root exists. ThemeService.Apply no-ops if Content is still null.
MSIXplainer.Services.ThemeService.Apply(MSIXplainer.Services.ThemeService.LoadPreference());
}

/// <summary>
/// If the app was launched via a file activation, return the first file's
/// path; otherwise return null. Safe to call early in OnLaunched.
/// </summary>
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 ?? "<null>"}");
return firstFile?.Path;
}
catch (System.Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[MSIXplainer] File activation failed: {ex.Message}");
return null;
}
}
}
31 changes: 23 additions & 8 deletions MSIXplainer/MainPage.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -212,10 +227,9 @@ private void OnCloseFindingClick(object sender, RoutedEventArgs e)
}

/// <summary>
/// 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.
/// </summary>
private void CopyPropertyValue_Click(object sender, RoutedEventArgs e)
{
Expand All @@ -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}");
}
}

Expand Down
16 changes: 15 additions & 1 deletion MSIXplainer/Package.appxmanifest
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<Identity
Name="Clinick.msixplainer"
Publisher="CN=46604BD4-AFD9-4B23-8EB3-10EAF66872A5"
Version="1.0.24.0" />
Version="1.0.27.0" />

<mp:PhoneIdentity PhoneProductId="3a09e9f9-55b1-418a-9e63-9a662f1a29bc" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>

Expand Down Expand Up @@ -44,6 +44,20 @@
<uap:DefaultTile Wide310x150Logo="Assets\Wide310x150Logo.png" />
<uap:SplashScreen Image="Assets\SplashScreen.png" />
</uap:VisualElements>
<Extensions>
<uap:Extension Category="windows.fileTypeAssociation">
<uap:FileTypeAssociation Name="msixplainer.packages">
<uap:DisplayName>MSIX Package</uap:DisplayName>
<uap:Logo>Assets\Square44x44Logo.png</uap:Logo>
<uap:SupportedFileTypes>
<uap:FileType>.msix</uap:FileType>
<uap:FileType>.msixbundle</uap:FileType>
<uap:FileType>.appx</uap:FileType>
<uap:FileType>.appxbundle</uap:FileType>
</uap:SupportedFileTypes>
</uap:FileTypeAssociation>
</uap:Extension>
</Extensions>
</Application>
<Application Id="Cli"
Executable="MSIXplainer.Cli.exe"
Expand Down
8 changes: 7 additions & 1 deletion MSIXplainer/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,13 @@ namespace MSIXplainer;

public static class Program
{
[MTAThread]
// [STAThread] is REQUIRED. We disable XamlGeneratedMain
// (DISABLE_XAML_GENERATED_MAIN in the .csproj) and hand-write Main, so we
// have to set the apartment ourselves. With [MTAThread] the app appears
// to work — XAML, bindings, navigation, even file pickers all run — but
// WinRT OLE-backed APIs like Windows.ApplicationModel.DataTransfer.Clipboard
// throw CO_E_NOTINITIALIZED (0x800401F0). See issue #21.
[STAThread]
static void Main(string[] args)
{
ComWrappersSupport.InitializeComWrappers();
Expand Down
28 changes: 22 additions & 6 deletions MSIXplainer/ViewModels/MainPageViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -180,19 +180,35 @@ private async Task OpenPackageAsync()
var result = await picker.PickSingleFileAsync();
if (result is null) return;

PackageFilePath = result.Path;
LoadPackageFromPath(result.Path);
}
catch (Exception ex)
{
ShowError($"Failed to open package: {ex.Message}");
}
}

/// <summary>
/// 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.
/// </summary>
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);
}
}
Expand Down
Loading