Universal Windows Platform - Closing UWP-Win32 Gaps

Universal Windows Platform - Closing UWP-Win32 Gaps

By Andrew Whitechapel | May 2018 | Get the Code

One of the important themes for the latest update of Windows has been to close the gaps between the Universal Windows Platform (UWP) and traditional Win32 app models. As part of this effort, Microsoft introduced three major enhancements:

  • Multi-instancing
  • Console UWP apps
  • Broader file-system access

The features are related but largely independent. That is, you can create a multi-instanced app that’s either a regular windowed app or a console app—and it may or may not need broader file-system access. Equally, you can create a regular non-console, non-multi-instance app that has broad file-system access. One restriction is that a console app must be configured to support multi-instancing.

Multi-Instancing

In Win32, Linux and other app model environments, multi-­instancing has always been the default. In the UWP, in stark contrast, the default has always been single-instancing—and, in fact, multi-instancing wasn’t supported at all until now.

Without multi-instancing, some apps have resorted to a multi-windowing architecture instead, which typically involves significant work—and results in complexity and fragility. You have to spend a lot of effort in managing your windows, instead of focusing on your domain requirements. Single-process multi-windowing also suffers from reliability concerns: If the single instance crashes, it brings down all its windows; this isn’t true for multi-instancing, where each instance runs as a separate process.

In the single-instance model, the user can activate an app in a number of ways: via a tile-tap in Start; via a URL or protocol activation; by double-clicking a file with an extension that’s registered to the app; and so on. The first activation (of any kind) launches the app. After that, any subsequent activations simply call into the running instance of the app, which the app can handle by overriding the OnActivated method.

The new multi-instancing feature enables UWP apps to work like Win32 apps: If an instance of an app is running and a subsequent activation request is received, the platform won’t call in to activate the existing instance. Instead, it will create a new instance in a separate process.

Because this feature is largely driven by Win32 parity, it’s initially supported only on Desktop and IoT. Two levels of support for multi-instancing were introduced:

  • Multi-Instance UWP App: This is for the simple case, where the app just wants to declare that it should be multi-instanced.
  • Multi-Instance Redirection UWP App: This is for the complex case, where the app wants to be multi-instanced, but it also wants to have a say in exactly how each instance is activated.

For both cases, a Visual Studio project template is provided, as shown in Figure 1.

New Project Templates for Multi-Instanced Apps
Figure 1 New Project Templates for Multi-Instanced Apps

For the simple case, the project template generates code that’s almost identical to the Blank Application template code. The only difference is in the use of the SupportsMultipleInstances attribute in the app manifest. There are two additional declarations: The first is for the desktop4 and iot2 XML namespaces at the top of the manifest:

XML
Copy
xmlns:desktop4="http://schemas.microsoft.com/appx/manifest/desktop/windows10/4" xmlns:iot2="http://schemas.microsoft.com/appx/manifest/iot/windows10/2"   IgnorableNamespaces="uap mp desktop4 iot2">

The second adds the attribute on the <Application> element:

XML
Copy
<Application   Id="App8" Executable="$targetnametoken$.exe" EntryPoint="App8.App"   desktop4:SupportsMultipleInstances="true"   iot2:SupportsMultipleInstances="true">

If you’re updating existing app code rather than generating a new app, you can simply add these entries to the manifest manually. Once you’ve done this, you can build your app and launch multiple instances. With this manifest entry, every time your app is activated—whether from a tile-tap, or any other activation contract the app supports, such as file-association or protocol-launch—each activation will result in a separate instance. It’s as simple as that.

Multi-Instance Redirection

For most apps, all you need to do is add the manifest entry, but for an app that wants a finer degree of control over its instance activations, you can use the second template. This adds the exact same manifest entries, and also adds an additional file (Program.cs for C# apps, or Program.cpp for C++) that contains a standard Main function, as shown in Figure 2. This uses a new AppInstance class introduced in this release.

Figure 2 Standard Main Function for Multi-Instance Redirection
C#
Copy
static void Main(string[] args) {   IActivatedEventArgs activatedArgs = AppInstance.GetActivatedEventArgs();   if (AppInstance.RecommendedInstance != null)   {     AppInstance.RecommendedInstance.RedirectActivationTo();   }   else   {     uint number = CryptographicBuffer.GenerateRandomNumber();     string key = (number % 2 == 0) ? "even" : "odd";     var instance = AppInstance.FindOrRegisterInstanceForKey(key);     if (instance.IsCurrentInstance)     {       global::Windows.UI.Xaml.Application.Start((p) => new App());     }     else     {       instance.RedirectActivationTo();     }   } }

The first thing the function does is grab this instance’s activation arguments. The app would likely use the information held in these arguments as part of its redirection logic.

In some scenarios, the platform might indicate a recommended instance. If so, you can redirect this activation to that instance instead, if you wish, by using the AppInstance.RedirectActivationTo method. In other words, the app can choose to allow this activation request to be redirected to an existing instance instead. If it does redirect, then the target instance is activated and its OnActivated method is invoked—and this new instance is terminated.

If the platform doesn’t indicate a preferred instance, you go ahead and compose a key. The example code composes a key from a random number, but you’d normally replace this code and compose a key based on app-defined logic. Typically this would be based on the activation arguments retrieved earlier. For example, if the activation arguments were of type FileActivatedEventArgs, the app might use the specified file name as part of the key. Once you’ve composed the key, you pass it to the FindOrRegister­InstanceForKey method, which returns an AppInstance object that represents an instance of this app. To determine which instance to return, the method does two things:

  • It searches for an existing instance of the app that has already registered this key.
  • If no existing instance has already registered this key, it registers this current instance with this key.

If you’ve successfully registered this instance, you can now just go ahead and do the normal app initialization. For a XAML app, this means calling Application.Start with a new instance of the App class. If some other instance has already registered this key, you can now redirect this activation to that instance instead, and allow this instance to terminate. For example, consider an app that edits files. If the user has Foo.doc open for edits in the app, and then attempts to open Foo.doc again, the app might choose to redirect the second activation to the instance that already has Foo.doc open—and prevent the user from opening the same file in multiple instances. The logic for deciding whether or not to redirect, and which instance to select as the redirection target, is entirely app-defined.

For XAML apps, the Main method is normally auto-generated and hidden from the developer. This behavior is suppressed in the “multi-instance with redirection” template. If you’re updating an existing app, you can suppress the default Main by adding DISABLE_XAML_GENERATED_MAIN to the list of conditional compilation symbols in the build properties for the app. In some app types—for example, a C++ DirectX app—the Main function isn’t hidden. Apart from that, a DirectX app’s use of the new APIs follows the same patterns as in the XAML example.

Note that an app can only use the GetActivatedEventArgs and RedirectActivationTo methods during Main; if these are called anywhere else, they will fail. This is because if you want to take part in activation redirection, you need to do this extremely early in the life of the app process, and certainly before any windows are created.

On the other hand, you can use the remaining AppInstance methods and properties at any time. In particular, you can use FindOrRegisterInstanceForKey to update the key for the current instance whenever you need to. For example, if your key was based on a file name, and you later close this file, you’d update your key registration at that time. You can also use the Unregister method to unregister completely if for some reason you no longer want this particular instance to take part in activation redirection. Also, at any time, you can use the AppInstance.GetInstances method to get a list of all registered instances of your app, including their keys, so that you can reason about their state.

Additional Considerations

Multi-instancing is a major enhancement, and the initial release covers only the major scenarios. Specifically, support is included for multi-instancing a foreground app, console apps, and most out-of-process background tasks including app services. However, there’s no support in this release for ApplicationTrigger tasks or any in-proc background tasks.

During development, Microsoft spent considerable time testing a broad range of existing Store apps to see how they’d perform when multi-instanced. From this, Microsoft learned that apps fall into three broad categories:

  • Apps that have no reason to be multi-instanced. These apps just won’t opt in to the feature.
  • Apps that want to be multi-instanced, and continue to function correctly without any code changes. These apps can simply opt in to multi-instancing and call it done.
  • Apps that want to be multi-instanced, but need to do work to allow for the differences in execution model.

The common issue with apps in the third category is that they’re using some central resource—perhaps a cache, or a database, or other file—and when single-instanced they’ve been safely assuming that the app has exclusive access to this resource. Once they opt in to multi-instancing, there may be multiple instances attempting to access the resource. In this scenario, the app needs to do work to synchronize access, locking reads and writes, and so on—in other words, all the usual synchronization issues that traditional Win32 apps need to consider.

As an obvious example, consider the use of the app’s local storage. This is an example of a resource where access is constrained on a package basis, not a process basis—and of course all instances of an app share the same package. Although each instance of the app is running as a separate process, they will all use the same local storage and settings, as represented by the Application­Data.Current API. If you’re performing data access operations in local storage, you should consider how to protect against clashes. One option is to use instance-unique files, where one instance’s operations can’t conflict with any other’s. Alternatively, if you want to use a common file across multiple instances, you should lock and unlock access to the file appropriately. You can use standard mechanisms such as a named Mutex for this.

Console UWP Apps

Another obvious gap in the UWP landscape is the ability to create a headless console app. In Win32 and other environments, you can create a command-line tool that uses the console window for input and output. So, we added this support also. Again, there’s a new Visual Studio project template, and as with multi-instanced apps, this generates additional manifest entries. This feature is also restricted to Desktop and IoT—not least because only those SKUs actually have console windows right now. The same XML namespaces are declared. The <Application> element includes both Supports­MultipleInstances and Subsystem attributes, with Subsystem set to “console”. Console apps must be multi-instanced—this is the expected model for apps moving from traditional Win32 console apps. In addition, the app includes an AppExecutionAlias—and this also has the new Subsystem attribute, as shown in Figure 3.

Figure 3 Additional Manifest Entries for a Console App
XML
Copy
<Application Id="App"   Executable="$targetnametoken$.exe"   EntryPoint="App9.App"   desktop4:Subsystem="console"   desktop4:SupportsMultipleInstances="true"   iot2:Subsystem="console"   iot2:SupportsMultipleInstances="true"> ...   <Extensions>     <uap5:Extension       Category="windows.appExecutionAlias"       Executable="App9.exe"       EntryPoint="App9.App">       <uap5:AppExecutionAlias          desktop4:Subsystem="console"          iot2:Subsystem="console">         <uap5:ExecutionAlias Alias="App9.exe"/>       </uap5:AppExecutionAlias>     </uap5:Extension>   </Extensions> </Application>

You can change the Alias value to something appropriate for your app. Again, as with multi-instancing, the code-generation includes a Program.cs or Program.cpp file. The generated code provides an example of how you could implement the required main function, as shown in the C++ example in Figure 4. You can replace all the code inside main with your own custom code.

Figure 4 Template-Generated Code for a Console App Main Function
C#
Copy
int __cdecl main() {   // You can get parsed command-line arguments from the CRT globals.   wprintf(L"Parsed command-line arguments:\n");   for (int i = 0; i < __argc; i++)   {     wprintf(L"__argv[%d] = %S\n", i, __argv[i]);   }   wprintf(L"Press Enter to continue:");   getchar(); }

Once you’ve built and deployed the app, you can execute it from a regular command prompt, PowerShell window, or Windows-R, as shown in Figure 5. Note that because the app uses the console window, it’s not expected to create any other windows—and indeed, this is not supported. Instead, the app can now use all the System.Console APIs, plus many traditional Win32 APIs that have now been added to the approved list specifically to support console apps.

Executing a Console UWP App from the Command Line
Figure 5 Executing a Console UWP App from the Command Line

With this feature, you can finally build command-line console apps that take advantage of the benefits of the UWP, including APPX packaging, Store publication, easy updates and so on.

Broader File-System Access

Until now, a UWP app has only been able to access certain specific folders, such as the Pictures library and the Music library—and then only if the app declares these as capabilities in its manifest. Beyond that, the app could get access to anywhere else in the file system by raising a FilePicker dialog and prompting the user to choose a location, which grants the app permissions.

Now, the third major feature added for Win32 parity increases the level of file-system access for UWP apps. This was done in two ways by including:

  • Implicit access to the current working directory.
  • Broad file-system access gated by a restricted capability.

Any UWP app (either a regular windowed app or a console app) that declares an AppExecutionAlias is now granted implicit access to the files and folders in the current working directory and downward, when it’s activated from a command line. The current working directory is from whatever file-system location the user chooses to execute your AppExecutionAlias. This was debated for a long time, as the UWP model has always been very cautious about granting file-system access to apps. On balance, it was decided that the user choosing to execute the app from a particular location is equivalent to the user choosing a location in a FilePicker dialog, in terms of granting permissions.

It’s important to note that the app has exactly the same file permissions as the user who’s running the app—so there might still be files or folders the app can’t access, because the user can’t access them, either. For example, if the user can’t see a hidden file when they execute a dir command, the app also won’t be able to see that hidden file.

To take advantage of this feature, you can code the OnActivated override to look for CommandLineActivatedEventArgs. This will include the CurrentDirectoryPath, which in this case will be the file-system location from which the user executed your AppExecutionAlias. Figure 6 shows an example; here, the app extracts the current directory and passes it on to the MainPage.

Figure 6 Overriding OnActivated for Command-Line Activation
C#
Copy
protected override void OnActivated(IActivatedEventArgs args) {   switch (args.Kind)   {     case ActivationKind.CommandLineLaunch:       CommandLineActivatedEventArgs cmdLineArgs =          args as CommandLineActivatedEventArgs;       CommandLineActivationOperation operation = cmdLineArgs.Operation;       string activationPath = operation.CurrentDirectoryPath;       Frame rootFrame = Window.Current.Content as Frame;       if (rootFrame == null)       {         rootFrame = new Frame();         Window.Current.Content = rootFrame;       }       rootFrame.Navigate(typeof(MainPage), activationPath);       Window.Current.Activate();       break;   } }

You could then code your MainPage OnNavigatedTo override to retrieve this path from the incoming NavigationEventArgs, as shown in Figure 7. In this example, the app is initializing a StorageFolder from this path, and then building a TreeView control for the files and folders from here downward.

Figure 7 Building a File-System Tree from the Current Working Directory
C#
Copy
protected async override void OnNavigatedTo(NavigationEventArgs e) {   string activationPath = e.Parameter as string;   argumentsText.Text = activationPath;   fileTreeView.RootNodes.Clear();   try   {     StorageFolder folder =        await StorageFolder.GetFolderFromPathAsync(activationPath);     if (folder != null)     {       TreeViewNode rootNode = new TreeViewNode() { Content = folder.Name };       IReadOnlyList<StorageFolder> folders = await folder.GetFoldersAsync();       GetDirectories(folders, rootNode);       fileTreeView.RootNodes.Add(rootNode);     }   }   catch (Exception ex)   {     Debug.WriteLine(ex.Message);   } }
New Capability

The second way that more file-system access is being provided is via a new restricted capability. To use this, you must declare the restrictedcapabilities XML namespace at the top of your app manifest, and include broadFileSystemAccess in your <Capabilities> list:

XML
Copy
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"   IgnorableNamespaces="uap mp uap5 rescap"> ...   <Capabilities>     <rescap:Capability Name="broadFileSystemAccess" />   </Capabilities>

If you declare any restricted capability, this triggers additional scrutiny at the time you submit your package to the Store for publication. If the app is granted this capability, it will have the same access to the file-system as the user running the app. Not just from the current working directory but everywhere that the user has access. You don’t need an AppExecutionAlias if you have this capability. Because this is such a powerful feature, Microsoft will grant the capability only if the app developer provides compelling reasons for the request, a description of how this will be used, and an explanation of how this benefits the user.

If you declare the broadFileSystemAccess capability, you don’t need to declare any of the more narrowly scoped file-system capabilities (Documents, Pictures or Videos); indeed, an app must not declare both broadFileSystemAccess and any of the other three file-system capabilities.

Even after the app has been granted the capability, there’s also a runtime check, because this constitutes a privacy concern for the user. Just like other privacy issues, the app will trigger a user-consent prompt on first use. If the user chooses to deny permission, the app must be resilient to this. The user can also change her mind at any time, by going to the relevant File system page under the Privacy list in Settings, as shown in Figure 8.

New File System Page in Settings
Figure 8 New File System Page in Settings

Note that to take advantage of both the current working directory access and the broadFileSystemAccess permissions, your code must use the WinRT Windows.Storage APIs for file handling.

Wrapping Up

One of the long-term strategies with the UWP is to close the gaps with earlier app technologies—especially Win32—so that the UWP is a viable option for more and more different app types over time. With the introduction of support for true multi-instancing, console UWP apps, and broader file-system access, three more large steps have been taken on this journey. Sample code is available at bit.ly/2GtzM3T, and you’ll find the Visual Studio project templates at bit.ly/2HApmii and bit.ly/2FEIAXu.

Andrew Whitechapel is a program manager in the Microsoft Windows division, responsible for the app activation workflow for the Universal Windows Platform.

Thanks to the following technical experts for reviewing this article: Jason Holmes, Tim Kurtzman, Anis Mohammed Khaja Mohideen

Nguồn: msdn.microsoft.com