My blog about software development on the Microsoft® stack.

Handle Protocol Activation and Redirection In Packaged Apps

An 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>



Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s