Desktop Bridge – Multiple desktop processes, a single UWP container


In the previous post we have learned how to expand a desktop application with a background task, which is a component that belongs to the Universal Windows Platform. For the first time, we didn’t have just a single process running inside the UWP container (like we’ve seen in this post, where we were leveraging some UWP APIs but, in the end, the only running process was the Win32 one), but multiple ones: the Windows Forms application and the backgroundTaskHost.exe process, which is the one that Windows uses to handle background tasks.

The purpose of this post is to help you to better understand, with two real world examples, that with the Desktop Bridge, even if we have multiple process inside our AppX, they’re actually running inside the same UWP container. like it’s highlighted in the following image.

 

The main benefit of this approach is that, even if we’re talking about separate processes, they share the same context and the same resources and they can easily exchange data between them. The simplest and most common example is the local storage: the AppData area, where desktop and UWP application can write and read data, is the same, so the two process can access to the same folders and the same files. Let’s make a real example by expanding the application we have used in the previous post and that you can download from my GitHub repository: https://github.com/qmatteoq/DesktopBridge/tree/master/5.%20Extend

The scenario described in the post was leveraging two different processes:

  • A Win32 one, which is a Windows Forms application that, by leveraging a set of UWP APIs, is registering a background task connected to the TimeZoneTrigger trigger.
  • A UWP one, which is a Windows Runtime Component that takes care of updating the tile of the application and to show a toast notifications.

By combining together the two processes we created a desktop application that, by registering the background task, was able to send a notification to the user and to update its tile every time the user changed the time zone of his computer in the Windows’ settings. In this post we’re going to expand the scenario by adding the following features, which will be made possible by the fact that the two applications are running inside the same container:

  1. In the Windows Forms application, we’re going to add a TextBox control, where the user will have the chance to write a custom message.
  2. By using the UWP APIs to handle settings, we’re going to save this text inside the local storage.
  3. The background task, before running, will try to load the data from the settings and it will include the custom message inside the text displayed in the live tile and in the toast notification sent to the user.

The Windows Forms application

The work to be done in the Windows Forms application is very simple: first, we need to add, above the button we have included in the previous post to register the background task, a TextBox control, where the user will be able to write his custom message. Then, we need to change a little bit the code that is executed when the user presses the button:

private async void OnRegisterTask(object sender, EventArgs e)
{
    if (!string.IsNullOrEmpty(txtMessage.Text))
    {
        ApplicationData.Current.LocalSettings.Values.Clear();
        ApplicationData.Current.LocalSettings.Values.Add("message", txtMessage.Text);
    }

    string triggerName = "TimeZoneTriggerTest";

    // Check if the task is already registered
    foreach (var cur in BackgroundTaskRegistration.AllTasks)
    {
        if (cur.Value.Name == triggerName)
        {
            // The task is already registered.
            return;
        }
    }

    BackgroundTaskBuilder builder = new BackgroundTaskBuilder();
    builder.Name = triggerName;
    builder.SetTrigger(new SystemTrigger(SystemTriggerType.TimeZoneChange, false));
    builder.TaskEntryPoint = "TileBackgroundTask.TileTask";
    var status = await BackgroundExecutionManager.RequestAccessAsync();
    if (status != BackgroundAccessStatus.DeniedByUser && status != BackgroundAccessStatus.DeniedBySystemPolicy)
    {
        builder.Register();
    }
}

We can spot the difference compared to the code we have seen in the previous post in the first lines of the event handler: if the Text property of the TextBox control we have added (identified by the name txtMessage) contains a value, we save it in the local storage using the settings APIs. These APIs are very simple to use: by using the static class ApplicationData.Current.LocalSettings we can access to a collection called Values, which is a dictionary where we can store a set of key / value pairs. In this case, we store an element identified by the key message which contains, as value, the custom message written by the user. The rest of the code is the same we’ve seen in the previous post and it’s the one that takes care of registering the background task in the proper way.

The background task

Inside the background task we’re going to perform the reverse operation: we’re going to check if, in the settings, there’s a value identified by the key called message and, if it’s positive, we read it as a string. Here is the new implementation of the Run() method inside the TileTask of the Windows Runtime Component:

public sealed class TileTask : IBackgroundTask
{
    public void Run(IBackgroundTaskInstance taskInstance)
    {
        string message = string.Empty;
        if (ApplicationData.Current.LocalSettings.Values.ContainsKey("message"))
        {
            message = ApplicationData.Current.LocalSettings.Values["message"].ToString();
        }


        string tileXml = $@"<tile>
                        <visual>
                            <binding template='TileMedium' branding='logo'>
                                <group>
                                    <subgroup>
                                        <text hint-style='caption'>Time zone changed!</text>
                                        <text hint-style='captionSubtle' hint-wrap='true'>{message}</text>
                                        <text hint-style='captionSubtle' hint-wrap='true'>Last update at {DateTime.Now}</text>
                                    </subgroup>
                                </group>
                            </binding>
                        </visual>
                    </tile>";

        XmlDocument tileDoc = new XmlDocument();
        tileDoc.LoadXml(tileXml);

        TileNotification notification = new TileNotification(tileDoc);
        TileUpdateManager.CreateTileUpdaterForApplication().Update(notification);

        string toastXml = $@"<toast>
                        <visual>
                            <binding template='ToastGeneric'>
                                <text>Time zone changed!</text>
                                <text>{message}</text>
                                <text>Last update at {DateTime.Now}</text>
                            </binding>
                        </visual>
                    </toast>";

        XmlDocument toastDoc = new XmlDocument();
        toastDoc.LoadXml(toastXml);

        ToastNotification toast = new ToastNotification(toastDoc);
        ToastNotificationManager.CreateToastNotifier().Show(toast);
    }
}

You can notice that we are using the same APIs that we have used in the desktop application. The only difference is that, before using the Applicationdata.Current.LocalSettings.Values collection to retrieve the value identified by the message key, we use the ContainsKey() method to make sure that such value really exists. If it’s positive, we retrieve it and we store it inside a variable. The rest of the code is the same we’ve seen in the previous post, with just one difference: in the XML payload that describes the tile and the toast notification we have added a new text element, which contains the message retrieved from the settings.

It’s testing time!

Our job is completed: now compile again the solution, launch the UWP deployment project included in your Visual Studio 17 RC’s solution and, this time, before pressing the button to register the task, write a message inside the TextBox. Now repeat the test we made in the previous post: open the Windows’ settings, choose Time & Language –> Date & Time and change the current time zone. You will notice that, this time, the tile and toast notifications will contain, other than the title and the date and time of the last update, also the custom message we have added in the Windows Forms application (in the below image, it’s the text Hello Desktop Bridge!):

 

Using the system registry

Let’s see another example of how multiple processes running inside the same UWP container share the same resources. This time, we’re going to use a more traditional approach: no more UWP APIs, but we’re going to use only standard .NET Framework APIs. As we have learned in the first post of the series, applications converted using the Desktop Bridge run inside a container, which we can consider as a virtualized environment: the file system and the registry will be isolated and merged, in real time, with the system ones. This way, the application will continue to have complete access to the registry and work as expected but, when uninstalled, it won’t be able to leave any leftover that, during time, can lead to slow down the operating system. This is made possible by the fact that, when a converted application tries to write some data in the registry, the operation won’t be performed against the system registry, but against a virtualized version (which is simply a special text file). Let’s see a real example to better understand this concept, by creating a new Visual Studio solution: also in this case, to make our life easier, we’re going to use Visual Studio 2017 RC and the UWP deployment extension.

Let’s start by creating a Windows Forms application with a Button control: when it’s pressed, we’re going to use the standard .NET Framework APIs to create a new registry key.

private void OnCreateRegistryKey(Object sender, EventArgs e)
{
    var RegKey = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\Matteo Pagani\DesktopBridge", true);

    if (RegKey == null)
    {
        RegKey = Registry.CurrentUser.CreateSubKey(@"SOFTWARE\Matteo Pagani\DesktopBridge", RegistryKeyPermissionCheck.ReadWriteSubTree);
        RegKey.SetValue("IsAppInstalled", true);
    }
}

The code uses the Registry class and the OpenSubKey() method to access to a specific registry key inside the hive of the current user (this way, the application will be able to run without requiring admin rights, which is a scenario not supported by converted apps). The key is called IsAppInstalled and it’s stored inside the path HKEY_CURRENT_USER\SOFTWARE\Matteo Pagani\DesktopBridge. If the key doesn’t exist, we create it by leveraging the SetValue() method, passing as parameters the name of the key and its value (true).

Let’s start, as a first test, to launch the application as a native Win32 process: let’s click on the button, then open the system registry (by opening the Start menu, writing regedit and pressing Enter). If we did everything correctly, we will find the key we have just created:

Now, from the registry editor, delete the key we have just created (in my case, the entire folder called Matteo Pagani) and now move on to the next step, which is adding a Desktop to UWP deployment project to our solution. Let’s configure it in a way that we will be able to launch the Windows Forms application we have just created inside the UWP container (I won’t repeat in this post all the required steps, since I’ve already explained this approach in one of the previous posts), then let’s try to launch it. The operation will complete just fine but, this time, if we repeat the previous steps to look for the HKEY_CURRENT_USER\SOFTWARE\Matteo Pagani\DesktopBridge path in the registry editor, we won’t be able to find it. The reason is that, this time, the application isn’t running as a native process but inside the UWP container and, consequently, it isn’t writing anymore inside the system registry, but inside the virtualized version offered by the container.

There’s an important takeaway that we can learn from this test: when it comes to converted applications, we can’t use the registry as a way to handle the communication between two different application if they aren’t running inside the same UWP container. We can easily see this blocker by adding, to our solution, a new Windows Forms project with another button that, this time, it will read the registry key instead of writing it:

private void OnReadRegistryKey(Object sender, EventArgs e)
{
    bool isAppInstalled = false;

    var RegKey = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\Matteo Pagani\DesktopBridge", true);
    if (RegKey != null)
    {
        string result = RegKey.GetValue("IsAppInstalled").ToString();
        isAppInstalled = result == "True" ? true : false;
    }

    string message = isAppInstalled ? "The app is installed" : "The app isn't installed";
    MessageBox.Show(message);
}

By using the same APIs we have learned in the previous sample (the Registry.CurrentUser class and the OpenSubKey() method), we check if the HKEY_CURRENT_USER\SOFTWARE\Matteo Pagani\DesktopBridge path exists. If it’s positive, we check if the key identified by the name IsAppInstalled exists and we display a message to the user, based on the outcome of the operation.

If we now try to launch both application as native (so without using the UWP container) and first we create the registry key using the first app, then we read it using the second app, everything will work as expected: we will see the message that the key has been found, since both applications are writing and reading from the system registry. However, if we try to use the UWP deployment project to launch the first app inside the UWP container and the second app, instead, as native, we will get an error message: the key doesn’t exist. The reason is that that the first application, since it runs inside the container, has written the key inside the virtualized registry, while the second application is running as native and, as such, it’s looking for it in the system registry.

image

How to handle these kind of scenarios? The best answer would be to start using a different approach and start leveraging some features offered by the Universal Windows Platform, like App Services or the new kind of shared storage introduced in the Anniversary Update that allows to read and write files by applications developed by the same publisher. However, we don’t always have the opportunity to refactor our application in the short term, because we may have some business and timing requirements to respect. As such, another approach we can leverage that doesn’t require us to partially rewrite our application is the same we’ve seen at the beginning of this post: when two or more processes are running inside the same container, they share the same resources. In the first example of the post, the resource that we shared was the local storage, but also the registry is one of these resources. As such, if we include all our Win32 processes inside the same AppX package, they will be able to read and write from the same virtualized registry, exactly in the same way in the previous example the Windows Forms application and the background task were able to read and write from the same local storage, since they were both stored in the same AppX package.

The first step to achieve this goal is to change the deployment project so that, when we deploy it, it will include both the Windows Forms applications and not just the original one we have created. Here is how we can configure the AppXPackageFileList.xml file so that it can retrieve, from the Visual Studio output folders, the executables of the two desktop applications and copy them inside the PackageLayout folder:

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <PropertyGroup>
    <MyProjectOutputPath>$(PackageLayout)\..\..\..\Registry</MyProjectOutputPath>
  </PropertyGroup>
  <ItemGroup>
    <LayoutFile Include="$(MyProjectOutputPath)\DesktopApp.WriteRegistry\bin\Debug\DesktopApp.WriteRegistry.exe">
      <PackagePath>$(PackageLayout)\DesktopApp.WriteRegistry.exe</PackagePath>
    </LayoutFile>
    <LayoutFile Include="$(MyProjectOutputPath)\DesktopApp.ReadRegistry\bin\Debug\DesktopApp.ReadRegistry.exe">
      <PackagePath>$(PackageLayout)\DesktopApp.ReadRegistry.exe</PackagePath>
    </LayoutFile>
  </ItemGroup>
</Project>

The approach is similar to the one we’ve seen in one of the previous posts, with the difference that in the previous sample we were including in the same package an executable (a Windows Forms application) and a DLL library (JSON.NET). This time, instead, we include two executable: the two Windows Forms apps we’ve created before (the first one that creates the registry key and the second one that reads the registry key). Since now both applications are part of the same package, they have access to the same resources, including the virtualized version of the registry. Let’s do a concrete example by adding a new button in the first application (the one called DesktopApp.WriteRegisty) that will invoke the following code:

private void OnOpenReadApp(Object sender, EventArgs e)
{
    string result = System.Reflection.Assembly.GetExecutingAssembly().Location;
    int index = result.LastIndexOf("\\");
    string processPath = $"{result.Substring(0, index)}\\DesktopApp.ReadRegistry.exe";
    Process.Start(processPath);
}

The previous code uses the .NET Framework APIs to retrieve, by using the reflection, the path of the running executable, so that it composes the proper path of the other executable which, instead, is called DesktopApp.ReadRegistry.exe. The reason why we used the reflection approach and not the specific API to retrieve the current working directory (the method Directory.GetCurrentDirectory()) is that, when a desktop application is running inside the UWP container, this API doesn’t return the real current directory, but the Windows’ system one. Consequently, this is one of the possible workarounds we can use to get access to the path of the other executable stored inside the same package. Once we have the full path, we can launch the second application by calling the Process.Start() method.

At this point, the second application will start: this time, if we create first the registry key using the button in the first app and then, in the second app, we press the button to read the same key, the operation will complete with success. The reason is that both applications are running inside the same UWP container and, as such, they are both reading and writing not only inside the same file system (like we’ve seen with the first sample), but also inside the same registry.

image

Wrapping up

In this post we have learned more about how the UWP container works and how we can share data between two different processes running in the same container. In the first sample, we demoed the local system as a shared resource: the Windows Forms application and the background task were able to access to the same settings stored in the file system. To demonstrate this feature, we have expanded the GitHub project we have created in the previous post, which you can download from https://github.com/qmatteoq/DesktopBridge/tree/master/5.%20Extend. In the second part of the post, instead, we demoed the registry as a shared resource: in this case, we have used two different Windows Forms applications running inside the UWP container that they were able to read and write data from the same virtualized registry. You can download the sample code of this second approach at the URL https://github.com/qmatteoq/DesktopBridge/tree/master/Extras/Container

Happy coding!

Comments (11)

  1. Norman from UK says:

    A very informative, well written article, thankyou Matteo. I have two questions that perhaps would also help other developers...

    Firstly, could a UWP packaged WPF app call Process.Start() on ANY ".exe" file contained within the UWP package?.
    I have a WPF chess app that calls Process.Start() on a third party chess engine ".exe", and that works very well, but would this work equally well in a packaged WPF app?

    Secondly, I am aware that packaged apps cannot write to files in the package, so how could user options be stored and retrieved?
    Currently in my WPF app I am storing / retrieveing user options to and from the app "exe.config" file, but how could this be achieved in a packaged WPF app?

    1. Thank you Normal, I'm really glad you appreciated it!

      Regarding your questions:

      1) Yes, it's possible. If you read the post, you'll notice that it's exactly what I'm doing in the second part of the post (where I explain the registry virtualization): the first process invokes a second process contained in the same AppX package, by using Process.Start(). This way, since they share the same identity, the have access to the same virtualized registry, local storage, ec.

      2) The standard approach suggested to Windows developers, when it comes to read and write user generated data (like a settings folder) is to leverage the AppData folder. The Desktop Bridge will automatically perform file system redirection, so every time your WPF app will try to write in the local AppData folder using the API Environment.GetFolderPath(Environment.SpecialFolders.ApplicationLocalData), under the hood the file will be written and read from the local storage of the converted app, meaning that when the app will be uninstalled, the content of the folder will be automatically deleted (instead of configuring your installer to do it on your behalf).

      Don't hesitate to reach me again if you have further questions!
      Best

      1. Norman from UK says:

        Thanks Matteo,

        Can this be done entirely manually, without using the "Desktop Bridge Debugging Project"?
        i.e. Can the "<LayoutFile Include..." statements be added to the "AppXManifest.xml" file?

        I have successfully packaged, signed and installed the Beta version my WPF App using the approach you described in this article,
        but I would rather not use the "Desktop Bridge Debugging Project" because I should only need to package my App once, when the release version is finished.

        But how can I specify the path to the third party Process() ".exe" in the "AppXManifest.xml" file?

  2. Hello Norman, I apologize but I think you misunderstood the purpose of the Desktop Bridge Debugging Project. Its goals are: 1) To allow debugging an application when it's launched as converted 2) To keep the folder that gets converted in an app package in sync with the native desktop application. As such, for your scenario, you don't have to specify in the AppxManifest.xml file the second process, the manifest must contain only the entry point (which is the main executable that is launched when you launch the app). If you need to launch another process, it's enough to include it in the same app package that contains the main executable. The layout file is just an helper to make sure that, when you compile the Desktop Bridge Debugging Project, every required file (in your case, your multiple executables with their dependencies) is copied into the app package. But it isn't needed to create the AppX, you need to use the makeappx tool or the WinJS project approach to achieve this goal. I hope it helps!

    1. Norman from UK says:

      Thanks Matteo,

      I am learning about the Desktop Bridge and of course yes you are right, I have misunderstood.
      I understand now that the "AppXPackageFileList.xml" is only used by Desktop Bridge Debugging Project
      and is not needed to package Apps manually.

      Your articles are very useful and thanks for answering my questions so quickly.
      I think perhaps that Microsoft will make Desktop Bridge App packaging easier in future versions of Visual Studio
      but I think its good to do it manually at the start so that one can understand the underlying concepts.

  3. This article is very helpful to me. Thanks Matteo !

    And I know some thing from the comment above is that the converted apps has only one entry point, It means that if I have two or more executable files in the same appx package, and I only can start the main executable file which is invoked in the AppxManifest.xml from the start menu. If I want to launch the second executable, the only way is using the main executable file, is it right?

    What's my concern is that If I want to launch all the executable files that in the same appx package from the start menu, how should I do?

    Thanks!

    1. shirley says:

      Hello Matteo,
      Could you please help me with the question?

      Thanks!

    2. Hello Matteo,

      Could you please help me with the above question?
      And if you have any confused, please let me know!

      Thanks!

      1. I apologize Shirley, but I totally missed your question! Unfortunately, your scenario isn't supported. Your main executable can invoke without issues other processes that belong to the same package and all of them will share the same identity. However, there may be only one entry point (the one declared in the manifest), so you can't have multiple Start menu entries, one for each process. You can have only a single Start menu entry for the main process.

        Sorry again for the delay!
        Best

        1. Thanks Matteo Pagani!

          As you say, if the windows desktop apps want to communicate with the converted appx, they should be build in the same package.
          But if we have a windows server and it want to communicate the converted appx, is it possible? and how?

          Thanks again!

          1. Hello Shirley, the converted app acts like a regular win32 app, so you can use most of the methods you were using with classic desktop apps, like a socket, a named pipe, a shared file. Only a few methods aren't supported because they are not suggested approach and, as such, they are blocked by the Bridge. For example, if you want to use the registry to share data between apps, it won't work because when a converted app writes data into the registry, he writes it into the private binary file and not in the system one, so the other apps can't see it.

Skip to main content