Category Archives: Windows Template Studio

Milestone User Prompts in UWP

In this article we describe how to put the logic and the UI in a UWP application to execute some code and display a dialog on important milestones in its lifetime. We provide an implementation for the following milestones:

  • the application was run for the first time,
  • the application updated to a new release,
  • a trial version was upgraded to a purchase,
  • it’s time to rate and review.

Here’s one of the dialogs in action, in a sample application:

FirstUseDialog

The architecture of the solution comes with one helper service and one content dialog for each milestone, and one central activation Service to rule them all. Exactly the same constituents are found in a Windows Template Studio based project (in Basic MVVM and Code Behind style projects). The logic to detect whether or not a milestone is hit, is powered by helper classes from UWP Community Toolkit v2.1.

Milestone Prompts in Windows Template Studio

Service and Dialogs

The requirement for ‘First Run Prompt’ and ‘What’s New Prompt’ dialogs is often only identified when your application is almost ready to be shipped. If you created your project with Windows Template Studio that’s not a problem. Template Studio allows you to add features to your generated solution as an afterthought:

TemplateStudio

After you selected the feature(s) to be added, there’s even an overview of all the changes that come with them so you can bail out when there’s an unexpected modification of your existing code:

TemplateStudioCode

Here’s a screenshot of the dialog from the  ‘First Run Prompt’ feature. All you need to do is replace its content of with your own:

TemplateStudioDialog

The logic to open these dialogs is implemented around a central Activation Service. This service brings your application startup code –which is usually hidden in app.xaml.cs– conveniently together with the rest of your services code. The UI element for each milestone feature is a ContentDialog. These dialogs are added to the Views section:

TemplateStudioArchitecture

When the application is launched, the activation service navigates to the main page, and then calls StartupAsync:

// Ensure the current window is active
Window.Current.Activate();

// Tasks after activation
await StartupAsync();

This anchor method is the place where the dialog services hook in their calls:

private async Task StartupAsync()
{
    await WhatsNewDisplayService.ShowIfAppropriateAsync();
    await FirstRunDisplayService.ShowIfAppropriateAsync();
}

Windows Template Studio provides a clean architecture and a straightforward way of introducing new user prompt services and any other service that needs to run on application startup.

Let’s now take a look at the implementation of the logic. Here’s how the What’s New Prompt feature determines whether or not to pop up the dialog:

public class WhatsNewDisplayService
{
    internal static async Task ShowIfAppropriateAsync()
    {
        var currentVersion = PackageVersionToReadableString(Package.Current.Id.Version);

        var lastVersion = await Windows.Storage.ApplicationData.Current.LocalSettings.ReadAsync<string>(nameof(currentVersion));

        if (lastVersion == null)
        {
            await Windows.Storage.ApplicationData.Current.LocalSettings.SaveAsync(nameof(currentVersion), currentVersion);
        }
        else
        {
            if (currentVersion != lastVersion)
            {
                await Windows.Storage.ApplicationData.Current.LocalSettings.SaveAsync(nameof(currentVersion), currentVersion);

                var dialog = new WhatsNewDialog();
                await dialog.ShowAsync();
            }
        }
    }

    private static string PackageVersionToReadableString(PackageVersion packageVersion)
    {
        return $"{packageVersion.Major}.{packageVersion.Minor}.{packageVersion.Build}.{packageVersion.Revision}";
    }
}

It also looks very straightforward, but allow me to show you how this code would look like when using the latest version of UWP Community Toolkit:

if (SystemInformation.IsAppUpdated)
{
    var dialog = new WhatsNewDialog();
    await dialog.ShowAsync();
}

Let’s take a look at some other relevant goodies that this toolkit provides.

Milestone Prompts in UWP Community Toolkit

Logic and Persistency

UWP Community Toolkit is well-known for its controls, its animations and its services. But it also contains a huge amount of helper classes for different scenarios. Notably the SystemInformation and LocalObjectStorageHelper have some members that may help in determining whether or not an application lifetime milestone was reached, based on information that was stored in local settings. Here’s the UML for these two classes:

UwpCommunityToolkitHelpers

Here’s the subset of SystemInformation members that may help to detect milestones:

Property Purpose
ApplicationVersion Gets the application’s version as a PackageVersion
IsFirstRun Gets a value indicating whether the app is being used for the first time since it was installed.
IsAppUpdated Gets a value indicating whether the app is being used for the first time since being upgraded from an older version.
LaunchTime Gets the DateTime (in UTC) that this instance of the app was launched.
LastLaunchTime Gets the DateTime (in UTC) that this was previously launched.
LaunchCount Gets the number of times the app has been launched.
AppUptime Gets the length of time this instance of the app has been running.
FirstVersionInstalled Gets the first version of the app that was installed.
FirstUseTime Gets the DateTime (in UTC) that the app as first used.
Method Description
LaunchStoreForReviewAsync() Launch the store app so the user can leave a review.
TrackAppUse() Track app launch time and count.

And here’s the list for LocalObjectStorageHelper:

Property Purpose
Settings Gets or sets settings container
Method Description
bool KeyExists(string key) Detect if a setting already exists
bool KeyExists(string compositeKey, string key) Detect if a setting already exists in composite.
T Read<T>(string key, T) Retrieve single item by its key.
T Read<T>(string compositeKey, string key, T) Retrieve single item by its key in composite.
void Save<T>(string compositeKey, IDictionary values) Retrieve single item by its key in composite.
Save(String, IDictionary) Save a group of items by its key in a composite.
void Save<T>(string key, T value) Save single item by its key.

These classes will definitely allow you to write the complex milestone scenarios that your application may require.

UWP Community Toolkit provides the helpers to implement the logic and persist the settings for user prompt services and any other service that needs to run on application startup.

Let’s say you want to execute some stuff the first time the application is started after a milestone, but you want to keep on opening the user prompt dialog on startup until the user explicitly opts out via the don’t-show-this-again checkbox. Well, that’s exactly what we’re going to do.

Rolling your own Milestone Prompts

Here’s the structure of my sample application, with a central activation service, and a service-and-dialog pair per milestone (just like in Windows Template Studio):

MySolutionArchitecture

Activation Service

The core member of the Activation Service is LaunchAsync which is called by app.xaml.cs when the application is launched. It allows other services to execute some initialization code, then navigates to the main page (using a nice fluent API that I couldn’t resist writing), and then allows the services to execute the rest of the startup code:

public async Task LaunchAsync(LaunchActivatedEventArgs e)
{
    // Custom pre-launch service calls.
    await PreLaunchAsync(e);

    // Navigate
    Window
      .Current
      .EnsureRootFrame()
      .NavigateIfAppropriate(typeof(Shell), e.Arguments)
      .Activate();

    // Custom post-launch service calls.
    await PostLaunchAsync(e);
}

The PreLaunchAsync() method hosts the initialization code, which is executed before the UI is ready. Use it for things like

  • selecting the culture,
  • setting the theme,
  • starting to log, and
  • determining the page to navigate to.

Here’s how it looks like in the sample application:

private async Task PreLaunchAsync(LaunchActivatedEventArgs e)
{
    Theme.ApplyToContainer(); // await ThemingService.PreLaunchAsync(e);

    await FirstUseActivationService.PreLaunchAsync(e);
    await NewReleaseActivationService.PreLaunchAsync(e);
    await TrialToPurchaseActivationService.PreLaunchAsync(e);
    await RateAndReviewActivationService.PreLaunchAsync(e);
}

The PostLauncAsync() method is the anchor for the startup code that requires the visual elements, like

  • moving around and resizing controls, and
  • opening a Content Dialog.

This is the code from the sample application:

private async Task PostLaunchAsync(LaunchActivatedEventArgs e)
{
    // await ThemingService.PostLaunchAsync(e);
    CoreApplication.GetCurrentView().TitleBar.ExtendViewIntoTitleBar = true; 

    await FirstUseActivationService.ShowIfAppropriateAsync(e);
    await NewReleaseActivationService.ShowIfAppropriateAsync(e);
    await TrialToPurchaseActivationService.ShowIfAppropriateAsync(e);
    await RateAndReviewActivationService.ShowIfAppropriateAsync(e);
}

Dialog Services

The milestone dialog services that I present in this article all come with these PreLaunchAsync() and PostLaunchAsync() methods. On top of that, they all have an instance of a LocalObjectStorageHelper to read and write local Settings, and a variable to hold whether or not the user still wants to see the dialog. Last but not least all dialogs have a Reset() and a Deactivate() method:

private const string HasShown = "FirstUseActivation_HasShown";
private static readonly LocalObjectStorageHelper _localSettings = new LocalObjectStorageHelper();

internal static void Reset()
{
    _localSettings.Save(HasShown, false);
}

internal static void Deactivate()
{
    _localSettings.Save(HasShown, true);
}

Each milestone service also comes with a Content Dialog. I added an extra buttons to the dialogs that I copied from Windows Template Studio, since I want to reshow the dialog until the user opts out of it:

DialogDetail

Let’s dive in the details for each milestone.

First Use Prompt Dialog

The SystemInformation helper from UWP Community Toolkit has an IsFirsRun() method to detect whether or not the application is started for the first time. So we’ll call this in our PreLaunchAsync() together with the other initialization code, including a call to Reset() to initialize the dialog logic:

internal static async Task PreLaunchAsync(LaunchActivatedEventArgs e)
{
    if (SystemInformation.IsFirstRun)
    {
        // First-time app initialization.
        // ...
        Reset();
    }

    await Task.CompletedTask;
}

After navigating to the main page, we call the ShowIfAppropriateAsync() where we verify if we should open the dialog based on the saved Settings. When the user closes that dialog with the ‘Never Again’ button, we deactivate the service:

internal static async Task ShowIfAppropriateAsync(LaunchActivatedEventArgs e)
{
    bool hasShownFirstRun = _localSettings.Read(HasShown, false);

    if (!hasShownFirstRun)
    {
        var dialog = new FirstUseDialog();
        var response = await dialog.ShowAsync();
        if (response == ContentDialogResult.Secondary)
        {
            Deactivate();
        }
    }
}

Here’s how the dialog looks like in the sample application:

FirstUseDialog

All other milestone services follow the same pattern.

New Release Prompt Dialog

The ‘New Release Prompt’ service is identical to the ‘First Use Prompt’ service, except that it uses SystemInformation’s IsAppUpdated() method:

internal static async Task PreLaunchAsync(LaunchActivatedEventArgs e)
{
    if (SystemInformation.IsAppUpdated)
    {
        // New release app initialization.
        // ...
        NewReleaseActivationService.Reset();
    }

    await Task.CompletedTask;
}

internal static async Task ShowIfAppropriateAsync(LaunchActivatedEventArgs e)
{
    var currentVersion = SystemInformation.ApplicationVersion;

    if (currentVersion.ToFormattedString() == SystemInformation.FirstVersionInstalled.ToFormattedString())
    {
        // Original version. Ignore.
        return;
    }

    var hasShown = _localSettings.Read(HasShown, false);

    if (!hasShown)
    {
        // New release dialog.
        var dialog = new NewReleaseDialog();
        var response = await dialog.ShowAsync();
        if (response == ContentDialogResult.Secondary)
        {
            Deactivate();
        }
    }
}

[For a brand new application, the comparison between ApplicationVersion and FirstVersionInstalled is probably obsolete, but I’m planning to add these services to existing applications so I’m playing a bit with alternative algorithms.]

Here’s how the dialog looks like in the sample application:

NewReleaseDialog

Trial-to-Purchase Prompt Dialog

You don’t need UWP Community Toolkit to figure out if a trial version of the application is running and when that version upgrades to an officially purchased one.  That’s because there are already two ways of doing that in the current SDKs. You can use the members of the (now becoming legacy) Windows.ApplicationModel.Store namespace or the ones in the newer Windows.Services.Store namespace. I’m going for the former version, which is based on the LicenceInformation that you get from CurrentAppSimulator during development and from CurrentApp in the deployed version of your application:

_licenseInformation = CurrentAppSimulator.LicenseInformation;

The pattern is again the same as for the other milestone services, with one extension. The user may decide to buy your application when it is running, so the service is not only called on launch, but also in the LicenseChanged event handler:

internal static async Task PreLaunchAsync(LaunchActivatedEventArgs e)
{
    if (_licenseInformation.IsTrial)
    {
        // Trial: set HasShown to false to trigger detection.
        Reset();
        _licenseInformation.LicenseChanged += LicenseInformation_LicenseChanged;
    }
    else
    {
        // Purchased: set HasShown to true to inhibit detecting, but only if we were not detecting already.
        var hasShown = _localSettings.Read(HasShown, true);
        if (hasShown)
        {
            Deactivate();
        }
    }

    await Task.CompletedTask;
}
private async static void LicenseInformation_LicenseChanged()
{
    await ShowIfAppropriateAsync(null);
}
internal static async Task ShowIfAppropriateAsync(LaunchActivatedEventArgs e)
{
    if (_licenseInformation.IsTrial)
    {
        return;
    }

    var hasShown = _localSettings.Read(HasShown, true);

    if (!hasShown)
    {
        // New trial-to-purchase dialog.
        var dialog = new TrialToPurchaseDialog();
        var response = await dialog.ShowAsync();
        if (response == ContentDialogResult.Secondary)
        {
            Deactivate();
        }
    }
}

You can easily test the service by simulating a purchase with RequestAppPurchaseAsync(), like this:

internal async static Task SimulatePurchase()
{
    try
    {
        var result = await CurrentAppSimulator.RequestAppPurchaseAsync(false);

        // Purchased.

    }
    catch (Exception)
    {
        // Purchase failed.
    }

    await Task.CompletedTask;
}

The simulator will then open a new dialog where you can specify the answer to test (purchase or not, or an error):

TrialToPurchaseDialogTest

Here’s the Trial-to-Purchase dialog in the sample application:

TrialToPurchaseDialog

Rate and Review Prompt Dialog

For the last milestone service in this article, the UWP Community is back in full force. There’s no ideal moment for popping up a dialog to send the user to the store to rate or review. But for some applications this is important, and the SystemInformation helper has all the members to brew your own logic: when the application was first launched, how many times it has been launched, how long the current instance is running, and so on.

These settings are only maintained if you activate application tracking with a call to TrackAppUse(). So that’s what we do in PreLauncAsync(). The following code snippet also shows a small hack to transform the standard AppUptime to a cumulative version:

internal static async Task PreLaunchAsync(LaunchActivatedEventArgs e)
{
    // Start tracking app usage (launch count, uptime, ...)
    try
    {
        // Make the AppUptime cumulative over sessions.
        var uptimeSoFar = TimeSpan.FromTicks(new LocalObjectStorageHelper().Read<long>("AppUptime", 0));

        SystemInformation.TrackAppUse(e); // Resets AppUptime to 0.
        SystemInformation.AddToAppUptime(uptimeSoFar);
    }
    catch (Exception)
    {
        SystemInformation.TrackAppUse(e);
    }

    await Task.CompletedTask;
}

I’m not going to dive into the logic to open the dialog. It will be different for each application. The dialog itself has a hyperlink that points to the Store’s Rate and Review page for the application:

<TextBlock TextWrapping="WrapWholeWords">
    <Run>Replace the content of this dialog with whatever content is appropriate to your app.</Run>
    <LineBreak /><LineBreak />
    <Hyperlink Click="Hyperlink_Click">Rate and Review this app</Hyperlink>
    <LineBreak /><LineBreak />
    <Run>Don't feel restricted to just text. You can also include images and animations if you wish too.</Run>
</TextBlock>

When that link is clicked UWP Community Toolkit comes to the rescue again, with LaunchStoreForReviewAsync():

private async void Hyperlink_Click(Hyperlink sender, HyperlinkClickEventArgs args)
{
    await SystemInformation.LaunchStoreForReviewAsync();
}

Of course this only works when the application is associated with the store. Here’s the corresponding dialog:

RateAndReviewDialog

And that concludes the list of milestone user prompt services. I think that the architecture and implementation is generic enough so you can use some of it in your own applications.

Styling the Dialog

To give the dialogs in the sample application a more contemporary look, I extended the ContentDialog Style to have a DropShadowPanel (again from UWP Community Toolkit) around the border. After all, Depth is one of the building blocks of Fluent Design:

<!-- Added -->
<toolkit:DropShadowPanel Color="{StaticResource CustomDialogBorderColor}"
                            BlurRadius="20"
                            HorizontalContentAlignment="Stretch"
                            VerticalContentAlignment="Stretch">

All the code and more

The sample application lives here on GitHub. Feel free to reuse it or launch pull requests to improve and enhance it. Also don’t hesitate to dive into the source code of Windows Template Studio and UWP Community Toolkit. There’s a lot of gems in there…

Enjoy!

Advertisements