Public/Add-MSIXFileTypeAssociation.ps1
|
function Add-MSIXFileTypeAssociation { <# .SYNOPSIS Adds a windows.fileTypeAssociation (FTA) to an Application in an MSIX package. .DESCRIPTION Creates a uap3:FileTypeAssociation under the target Application so double-clicking a registered file extension activates the packaged app. Required namespaces (uap, uap2, uap3 and uap7 for a default verb) are declared automatically. The cmdlet is additive and idempotent: calling it again with the same -Name appends missing file extensions and adds/updates the given verb (matched by -VerbId) instead of creating a duplicate block. One verb per call - run it again or pipe to add more verbs. .PARAMETER MSIXFolderPath Expanded MSIX package folder. Pipeline by property name (e.g. from Get-MSIXApplications). .PARAMETER AppId Target Application Id. Binds from Get-MSIXApplications (Id). Omit to auto-detect when the package has exactly one Application. .PARAMETER Name FTA group name. Forced to lower case; must not contain whitespace (schema rule). .PARAMETER FileType One or more file extensions (a leading dot is added if missing). .PARAMETER VerbId Verb id (default 'open' - the verb invoked on a double-click). .PARAMETER VerbParameters Verb parameters passed to the app (default '"%1"' - the selected file path). .PARAMETER VerbDisplayName Context-menu display text for the verb (defaults to VerbId). .PARAMETER ExtendedVerb Show the verb only with Shift held down (uap3:Verb Extended="true"). .PARAMETER DefaultVerb Mark the verb as the default (uap7:Default="true"). .PARAMETER Logo Package-relative path to a logo image (uap:Logo, e.g. Assets\ext.png). .PARAMETER InfoTip Tooltip text for the file type (uap:InfoTip). .PARAMETER MultiSelectModel Activation model for multi-file selection: Player, Document or Single. .EXAMPLE Get-MSIXApplications -MSIXFolder $pkg | Where-Object { $_.Executable -like '*putty.exe' } | Add-MSIXFileTypeAssociation -Name putty -FileType .putty .NOTES Andreas Nick, 2026 #> [CmdletBinding(SupportsShouldProcess = $true)] param( [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, Position = 0)] [Alias('MSIXFolder')] [System.IO.DirectoryInfo] $MSIXFolderPath, [Parameter(ValueFromPipelineByPropertyName = $true)] [Alias('Id')] [string] $AppId, [Parameter(Mandatory = $true, Position = 1)] [string] $Name, [Parameter(Mandatory = $true, Position = 2)] [string[]] $FileType, [string] $VerbId = 'open', [string] $VerbParameters = '"%1"', [string] $VerbDisplayName, [switch] $ExtendedVerb, [switch] $DefaultVerb, [string] $Logo, [string] $InfoTip, [ValidateSet('Player', 'Document', 'Single')] [string] $MultiSelectModel ) process { # --- Validate / normalize inputs -------------------------------------------- if ($Name -match '\s') { Write-Error "FileTypeAssociation Name '$Name' must not contain whitespace (schema rule)." return } $Name = $Name.ToLowerInvariant() if ($Name.Length -lt 1 -or $Name.Length -gt 64) { Write-Error "FileTypeAssociation Name must be 1-64 characters." return } $normFileTypes = @() foreach ($ext in $FileType) { $e = $ext.Trim() if (-not $e.StartsWith('.')) { $e = ".$e" } $normFileTypes += $e.ToLowerInvariant() } $manifestPath = Join-Path $MSIXFolderPath 'AppxManifest.xml' if (-not (Test-Path $manifestPath)) { Write-Error "AppxManifest.xml not found in: $($MSIXFolderPath.FullName)" return } $manifest = New-Object System.Xml.XmlDocument $manifest.Load($manifestPath) # Ensure namespaces before building the namespace manager. $prefixes = @('uap', 'uap2', 'uap3') if ($DefaultVerb) { $prefixes += 'uap7' } Add-MSIXManifestNamespace -Manifest $manifest -Prefixes $prefixes $nsmgr = New-Object System.Xml.XmlNamespaceManager($manifest.NameTable) $AppXNamespaces.GetEnumerator() | ForEach-Object { $null = $nsmgr.AddNamespace($_.Key, $_.Value) } $uapUri = $AppXNamespaces['uap'] $uap2Uri = $AppXNamespaces['uap2'] $uap3Uri = $AppXNamespaces['uap3'] $uap7Uri = $AppXNamespaces['uap7'] # --- Resolve target Application --------------------------------------------- if ($AppId) { $app = $manifest.SelectSingleNode("//ns:Package/ns:Applications/ns:Application[@Id='$AppId']", $nsmgr) if ($null -eq $app) { Write-Error "Application '$AppId' not found in AppxManifest.xml." return } } else { $apps = @($manifest.SelectNodes('//ns:Package/ns:Applications/ns:Application', $nsmgr)) if ($apps.Count -eq 0) { Write-Error "No Application found in AppxManifest.xml." return } if ($apps.Count -gt 1) { $ids = ($apps | ForEach-Object { $_.GetAttribute('Id') }) -join ', ' Write-Error "Multiple Applications found ($ids). Specify -AppId." return } $app = $apps[0] $AppId = $app.GetAttribute('Id') } if (-not $PSCmdlet.ShouldProcess($AppId, "Add file type association '$Name' ($($normFileTypes -join ', '))")) { return } # --- Ensure Extensions node ------------------------------------------------- $extensionsNode = $app.SelectSingleNode('ns:Extensions', $nsmgr) if ($null -eq $extensionsNode) { $extensionsNode = $manifest.CreateElement('Extensions', $AppXNamespaces['ns']) $null = $app.AppendChild($extensionsNode) } # --- Find existing FTA for this Name (merge) or create a new block ---------- $ftaNode = $app.SelectSingleNode("ns:Extensions/uap3:Extension[@Category='windows.fileTypeAssociation']/uap3:FileTypeAssociation[@Name='$Name']", $nsmgr) if ($null -eq $ftaNode) { $extNode = $manifest.CreateElement('uap3:Extension', $uap3Uri) $null = $extNode.SetAttribute('Category', 'windows.fileTypeAssociation') $ftaNode = $manifest.CreateElement('uap3:FileTypeAssociation', $uap3Uri) $null = $ftaNode.SetAttribute('Name', $Name) if ($MultiSelectModel) { $null = $ftaNode.SetAttribute('MultiSelectModel', $MultiSelectModel) } # Child order per schema: Logo, InfoTip, SupportedFileTypes, SupportedVerbs. if ($Logo) { $logoEl = $manifest.CreateElement('uap:Logo', $uapUri) $logoEl.InnerText = $Logo $null = $ftaNode.AppendChild($logoEl) } if ($InfoTip) { $itEl = $manifest.CreateElement('uap:InfoTip', $uapUri) $itEl.InnerText = $InfoTip $null = $ftaNode.AppendChild($itEl) } $sftEl = $manifest.CreateElement('uap:SupportedFileTypes', $uapUri) $null = $ftaNode.AppendChild($sftEl) $null = $extNode.AppendChild($ftaNode) $null = $extensionsNode.AppendChild($extNode) Write-Verbose "Created FTA '$Name' on Application '$AppId'." } else { if ($MultiSelectModel) { $null = $ftaNode.SetAttribute('MultiSelectModel', $MultiSelectModel) } Write-Verbose "Merging into existing FTA '$Name' on Application '$AppId'." } # --- Ensure file extensions ------------------------------------------------- $sftEl = $ftaNode.SelectSingleNode('uap:SupportedFileTypes', $nsmgr) if ($null -eq $sftEl) { $sftEl = $manifest.CreateElement('uap:SupportedFileTypes', $uapUri) $null = $ftaNode.AppendChild($sftEl) } foreach ($e in $normFileTypes) { $exists = $false foreach ($ft in $sftEl.SelectNodes('uap:FileType', $nsmgr)) { if ($ft.InnerText -eq $e) { $exists = $true; break } } if (-not $exists) { $ftEl = $manifest.CreateElement('uap:FileType', $uapUri) $ftEl.InnerText = $e $null = $sftEl.AppendChild($ftEl) } } # --- Ensure the verb -------------------------------------------------------- $svEl = $ftaNode.SelectSingleNode('uap2:SupportedVerbs', $nsmgr) if ($null -eq $svEl) { $svEl = $manifest.CreateElement('uap2:SupportedVerbs', $uap2Uri) $null = $ftaNode.AppendChild($svEl) } $verbEl = $svEl.SelectSingleNode("uap3:Verb[@Id='$VerbId']", $nsmgr) if ($null -eq $verbEl) { $verbEl = $manifest.CreateElement('uap3:Verb', $uap3Uri) $null = $verbEl.SetAttribute('Id', $VerbId) $null = $svEl.AppendChild($verbEl) } if ($VerbParameters) { $null = $verbEl.SetAttribute('Parameters', $VerbParameters) } if ($ExtendedVerb) { $null = $verbEl.SetAttribute('Extended', 'true') } if ($DefaultVerb) { $null = $verbEl.SetAttribute('Default', $uap7Uri, 'true') } $verbEl.InnerText = if ($VerbDisplayName) { $VerbDisplayName } else { $VerbId } $manifest.Save($manifestPath) Write-Verbose "Saved $manifestPath" } } |