Handle Protocol Activation and Redirection in Packaged Apps
Posted: May 10, 2019 Filed under: .NET, MSIX, WPF | Tags: .NET, C#, Desktop Bridge, MSIX, UWP, WPF 3 CommentsAn example of how to handle Uniform Resource Identifier (URI) protocol and file extension activation in a packaged WPF application.
You can easily register a desktop bridge or MSIX packaged WPF or Windows Forms app to become the default handler for a URI scheme name or a file extension in Windows 10 by adding an element to the Package.appxmanifest
file of the Windows Application Packaging Project:
<Extensions> <uap:Extension Category="windows.protocol"> <uap:Protocol Name="mysampleuri"> <uap:DisplayName>Sample URI Scheme</uap:DisplayName> </uap:Protocol> </uap:Extension> <uap:Extension Category="windows.fileTypeAssociation"> <uap:FileTypeAssociation Name="mysamplefileextension"> <uap:SupportedFileTypes> <uap:FileType>.mysamplefileextension</uap:FileType> </uap:SupportedFileTypes> </uap:FileTypeAssociation> </uap:Extension> </Extensions>
In this example, I register for the “mysampleuri” URI scheme name and the “.mysamplefileextension” file extension. You can choose any name you want except for the ones that are reserved by the operating system. If you then install the packaged app, you will be able to activate it from another (or the same) app using the Launcher.LaunchUriAsync
API, or by opening a .mysamplefileextension
file in Windows:
async void Button_Click(object sender, RoutedEventArgs e) { await Windows.System.Launcher.LaunchUriAsync(new Uri("mysampleuri:")); }
To be able to use the LaunchUriAsync
API in a .NET application, you should add references to C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETCore\v4.5\System.Runtime.WindowsRuntime.dll
and C:\Program Files (x86)\Windows Kits\10\UnionMetadata\Windows.winmd
.
In a pure Universal Windows Platform (UWP) app that is implemented using C#, you can simply override the OnActivated
or OnFileActivated
methods of the Windows.UI.Xaml.Application
class to handle your activation logic. In a packaged WPF application, there are no such methods of the System.Windows.Application
class to override though. What you can do instead is to implement your own custom Main
method and check the values of the command-line arguments that get passed to it. The first argument contains the value that was passed to the LaunchUriAsync
method. In case of a file activation, the first argument contains the path to the activated file:
class Program { [STAThread] static void Main(string[] args) { if (args != null && args.Length > 0) { if (args[0].StartsWith("mysampleuri:")) { //handle URI activation } else if (args[0].EndsWith(".mysamplefileextension")) { //handle file activation } } App application = new App(); application.InitializeComponent(); application.Run(); } }
Remember to change the Build Action
property of the App.xaml
file from ApplicationDefinition
to Page
to prevent the compiler from generating a default Main
method for you:
If your packaging project targets Windows 10 version 1809 (build 17763) or later, you can use the AppInstance.GetActivatedEventArgs
method to get the actual IActivatedEventArgs
that is passed in to the OnActivated
method of a UWP app:
[STAThread] static void Main() { App application = new App(); application.InitializeComponent(); application.OnActivated(Windows.ApplicationModel.AppInstance.GetActivatedEventArgs()); application.Run(); }
This enables you to implement your own custom OnActivated
method – called OnProtocolActivated
in the sample code below – almost exactly like you would do in a UWP app:
using System.Windows; using Windows.ApplicationModel.Activation; namespace Mm.ProtocolActivationSample { public partial class App : Application { public void OnProtocolActivated(IActivatedEventArgs args) { switch (args.Kind) { case ActivationKind.Protocol: //handle URI activation break; case ActivationKind.File: //handle file activation break; } } } }
You need to add a reference to C:\Program Files (x86)\Windows Kits\10\References\10.0.17763.0\Windows.Foundation.UniversalApiContract\7.0.0.0\Windows.Foundation.UniversalApiContract.winmd
from the WPF application to be able to call GetActivatedEventArgs()
. If you have also added a reference to Windows.winmd
and Visual Studio complains that “IActivatedEventArgs
exists in both ‘Windows.Foundation.UniversalApiContract
‘ and ‘Windows
‘”, you can change the alias of the Windows.winmd
reference to something else than “global”:
The above implementation of the Main
method creates a new instance of the App
class before it first calls InitializeComponent()
to load and parse the XAML markup in App.xaml
and then Run()
to start the application and begin processing windows messages using a dispatcher. This is exactly what the compiler generated code does if you don’t provide your own entry point. Using this implementation, you will get a new instance of the app opened for each activation.
If you want to handle your activation logic in an existing already running instance of your app, things get a bit more complicated. There is indeed a FindOrRegisterInstanceForKey
method that lets you register and retrieve a specific app instance using a key, and a RedirectActivationTo
method that redirects activation to a specified instance. Unfortunately, both these APIs and the redirection feature they provide are only supported for pure UWP apps. If you try to mimic the behavior of the official multi-instance UWP code sample in a packaged desktop application, you will get an exception when calling the FindOrRegisterInstanceForKey
method.
A common approach to ensure that there is only a single instance of a desktop application running on a computer is to use a named mutex:
class Program { const string AppUniqueGuid = "9da112cb-a929-4c50-be53-79f31b2135ca"; [STAThread] static void Main(string[] args) { using (System.Threading.Mutex mutex = new System.Threading.Mutex(false, AppUniqueGuid)) { if (mutex.WaitOne(0, false)) { App application = new App(); application.InitializeComponent(); application.Run(); } else { MessageBox.Show("Instance already running!"); } } } }
Since a Mutex
supports interprocess synchronization, the above code ensures that there will only be one instance of the application activated. You still need a way of passing the IActivatedEventArgs
from the Main
method to the existing App
instance that lives in another process. There are various ways of communicating between processes on a machine. One of them is to use named pipes. The idea is that you set up a single pipe server and one or more pipe clients and communicate between them by serializing data to and reading and deserializing data from a stream. In the below sample code, the single instance of the App
acts as the server and any subsequent activation creates a NamedPipeClientStream
that connects to the server and serializes the IActivatedEventArgs
that is returned from the GetActivatedEventArgs()
method:
class Program { const string AppUniqueGuid = "9da112cb-a929-4c50-be53-79f31b2135ca"; const string NamedPipeServerName = "."; static readonly int s_connectionTimeout = TimeSpan.FromSeconds(3).Milliseconds; static readonly BinaryFormatter s_formatter = new BinaryFormatter() { Binder = new CustomBinder() }; static App s_application; [STAThread] static void Main(string[] args) { IActivatedEventArgs activatedEventArgs = AppInstance.GetActivatedEventArgs(); using (Mutex mutex = new Mutex(false, AppUniqueGuid)) { if (mutex.WaitOne(0, false)) { new Thread(CreateNamedPipeServer) { IsBackground = true } .Start(); s_application = new App(); s_application.InitializeComponent(); if (activatedEventArgs != null) s_application.OnProtocolActivated(activatedEventArgs); s_application.Run(); } else if (activatedEventArgs != null) { //instance already running using (NamedPipeClientStream namedPipeClientStream = new NamedPipeClientStream(NamedPipeServerName, AppUniqueGuid, PipeDirection.Out)) { try { namedPipeClientStream.Connect(s_connectionTimeout); SerializableActivatedEventArgs serializableActivatedEventArgs = Serializer.Serialize(activatedEventArgs); s_formatter.Serialize(namedPipeClientStream, serializableActivatedEventArgs); } catch (Exception ex) { MessageBox.Show(ex.Message, string.Empty, MessageBoxButton.OK, MessageBoxImage.Error); } } } } } static void CreateNamedPipeServer() { using (NamedPipeServerStream pipeServer = new NamedPipeServerStream(AppUniqueGuid, PipeDirection.In, 1, PipeTransmissionMode.Message)) { while (true) { pipeServer.WaitForConnection(); IActivatedEventArgs activatedEventArgs; do { activatedEventArgs = s_formatter.Deserialize(pipeServer) as IActivatedEventArgs; } while (!pipeServer.IsMessageComplete); if (activatedEventArgs != null) s_application.Dispatcher .BeginInvoke(new Action(() => s_application.OnProtocolActivated(activatedEventArgs))); pipeServer.Disconnect(); } } } }
A BinaryFormatter
is used to perform the serialization and deserialization of the IActivatedEventArgs
across the process boundaries. The native implementations of IActivatedEventArgs
cannot be serialized as-is. To overcome this, I’ve defined a general SerializableActivatedEventArgs
base class and two derived types that implement the IProtocolActivatedEventArgs
and IFileActivatedEventArgs
interfaces respectively:
[Serializable] public class SerializableActivatedEventArgs : IActivatedEventArgs { public ActivationKind Kind { get; set; } public ApplicationExecutionState PreviousExecutionState { get; set; } [NonSerialized] private SplashScreen _splashScreen; public SplashScreen SplashScreen { get => _splashScreen; set => _splashScreen = value; } } [Serializable] public class SerializableProtocolActivatedEventArgs : SerializableActivatedEventArgs, IProtocolActivatedEventArgs { public Uri Uri { get; set; } } [Serializable] public class SerializableFileActivatedEventArgs : SerializableActivatedEventArgs, IFileActivatedEventArgs { public string Verb { get; set; } public IReadOnlyList<IStorageItem> Files { get; set; } }
These classes are decorated with the SerializableAttribute
. This is required for them to be compatible with the BinaryFormatter
. Also note that you may omit properties that you don’t intend to use or don’t want to serialize by decorating the backing field with the NonSerializedAttribute
. For the BinaryFormatter
to be able handle the types that are defined in Windows.Foundation.UniversalApiContract.winmd
, I’ve also defined a custom SerializationBinder
and set the Binder
property of the BinaryFormatter
to an instance of this one:
public class CustomBinder : SerializationBinder { public override Type BindToType(string assemblyName, string typeName) { if (typeName == typeof(Windows.ApplicationModel.Activation.ActivationKind).FullName) return typeof(Windows.ApplicationModel.Activation.ActivationKind); else if (typeName == typeof(Windows.ApplicationModel.Activation.ApplicationExecutionState).FullName) return typeof(Windows.ApplicationModel.Activation.ApplicationExecutionState); else if (typeName == typeof(Windows.Storage.FileAttributes).FullName) return typeof(Windows.Storage.FileAttributes); else if (typeName == typeof(Windows.Storage.IStorageItem).FullName) return typeof(SerializableStorageItem); return Type.GetType(typeName); } }
The static Serializer.Serialize
method that is called from the Main
method uses a type pattern matching switch
statement to determine the type of the serializable object to create and return:
public class Serializer { public static SerializableActivatedEventArgs Serialize(IActivatedEventArgs activatedEventArgs) { SerializableActivatedEventArgs serializableActivatedEventArgs = null; switch (activatedEventArgs) { case IProtocolActivatedEventArgs protocolActivatedEventArgs: serializableActivatedEventArgs = new SerializableProtocolActivatedEventArgs() { Kind = activatedEventArgs.Kind, PreviousExecutionState = activatedEventArgs.PreviousExecutionState, SplashScreen = activatedEventArgs.SplashScreen, Uri = protocolActivatedEventArgs.Uri }; break; case IFileActivatedEventArgs fileActivatedEventArgs: IList<IStorageItem> serializableFiles = fileActivatedEventArgs.Files? .Select<IStorageItem, IStorageItem>(x => new SerializableStorageItem() { Attributes = x.Attributes, DateCreated = x.DateCreated, Name = x.Name, Path = x.Path }) .ToList(); serializableActivatedEventArgs = new SerializableFileActivatedEventArgs() { Kind = activatedEventArgs.Kind, PreviousExecutionState = activatedEventArgs.PreviousExecutionState, SplashScreen = activatedEventArgs.SplashScreen, Verb = fileActivatedEventArgs.Verb, Files = serializableFiles != null ? new ReadOnlyCollection<IStorageItem>(serializableFiles) : null }; break; default: serializableActivatedEventArgs = new SerializableProtocolActivatedEventArgs() { Kind = activatedEventArgs.Kind, PreviousExecutionState = activatedEventArgs.PreviousExecutionState, SplashScreen = activatedEventArgs.SplashScreen }; break; } return serializableActivatedEventArgs; } } [Serializable] public class SerializableStorageItem : IStorageItem { public FileAttributes Attributes { get; set; } public DateTimeOffset DateCreated { get; set; } public string Name { get; set; } public string Path { get; set; } public IAsyncAction RenameAsync(string desiredName) => throw new NotImplementedException(); public IAsyncAction RenameAsync(string desiredName, NameCollisionOption option) => throw new NotImplementedException(); public IAsyncAction DeleteAsync() => throw new NotImplementedException(); public IAsyncAction DeleteAsync(StorageDeleteOption option) => throw new NotImplementedException(); public IAsyncOperation<BasicProperties> GetBasicPropertiesAsync() => throw new NotImplementedException(); public bool IsOfType(StorageItemTypes type) => throw new NotImplementedException(); }
The IStorageFile
type that provides information about files and folders in UWP and the Windows Runtime (WinRT) exposes several methods that return an IAsyncAction
. To be able to implement a serializable version of this interface, like the SerializableStorageItem
class above does, you should reference C:\Program Files (x86)\Windows Kits\10\References\10.0.17763.0\Windows.Foundation.FoundationContract\3.0.0.0\Windows.Foundation.FoundationContract.winmd
.
The sample code in this post can be downloaded on GitHub. The window of the packaged “SingleInstanceSampleApp” WPF application displays all activations in an ItemsControl
that is data bound to an ObservableCollection<string>
of the App
class:
public partial class App : Application { const string TimeFormat = "HH:mm:ss"; public ObservableCollection<string> Activations { get; } = new ObservableCollection<string>(); public void OnProtocolActivated(IActivatedEventArgs args) { switch (args.Kind) { case ActivationKind.Protocol: Activations.Add($"Protocol activation triggered at {DateTime.Now.ToString(TimeFormat)}." + (args is IProtocolActivatedEventArgs protocolArgs ? $" URI: {protocolArgs.Uri.ToString()}" : string.Empty)); break; case ActivationKind.File: Activations.Add($"File activation triggered at {DateTime.Now.ToString(TimeFormat)}." + (args is IFileActivatedEventArgs fileArgs && fileArgs.Files != null && fileArgs.Files.Count > 0 ? $" Files:{Environment.NewLine}{string.Join(Environment.NewLine, fileArgs.Files.Select(x => $"- {x.Path}"))}" : string.Empty)); break; } } }
You can click on the “URI Activation” button to call the LaunchUriAsync
method and activate the app using an URI, or you can create a “test.mysamplefileextension” file somewhere on your hard drive and double-click on it to try out the file activation functionality.
<DockPanel Margin="10"> <Button Content="URI Activation" Click="Button_Click" DockPanel.Dock="Bottom" /> <ItemsControl ItemsSource="{Binding Activations, Source={x:Static local:App.Current}}" /> </DockPanel>
Project file?
AppInstance.GetActivatedEventArgs() gives error in my case, deatails of error:
System.Exception
HResult=0xD0000225
Message=The text associated with this error code could not be found.
The text associated with this error code could not be found.
Source=Windows.ApplicationModel
StackTrace:
at Windows.ApplicationModel.AppInstance.GetActivatedEventArgs()
at MultipleInstancesSampleApp.Program.Main() in %UserDir%\Downloads\Sample Uwp\CodeSamples-master\ActivationAndRedirectionSample\MultipleInstancesSampleApp\Program.cs:line 13
Named pipes are in a global namespace so this may collide if you have user switching or terminal service users trying to use the same app.