A Lap around the WinUI TeachingTip Control

In this article we will run through a couple of scenarios using the UWP TeachingTip control. This relatively new control is an animated flyout that according to the documentation “draws the user’s attention on new or important updates and features, remind a user of nonessential options that would improve the experience, or teach a user how a task should be completed”.

The high quality of this official documentation made us decide to skip a high-level introduction and to immediately expose the TeachingTip control to some more challenging ‘enterprise-ish’ scenarios. Here are the things you can expect us to cover in this article:

  • programmatically creating a TeachingTip,
  • precision targeting a XAML control,
  • state management,
  • auto-hiding a TeachingTip on time-out and navigation, and
  • building an inherited control.

We also added a sample that expresses our concerns on the light dismiss behavior, and identified an interesting use case for the TeachingTip as ‘Form Field Wizard’. Since all the official samples use the light theme, we decided to go for dark – with a custom highlighted border.

As usual we built a small sample app, this is how it looks like:

HomePage

The TeachingTip control is shipped as a part of the Windows UI Library (a.k.a. WinUI) which is mostly known for its down-level compatibility versions of the official native UWP controls. The Windows UI Library also hosts brand new controls that aren’t shipped as part of the default Windows platform. Here are some of the controls that were shipped with WinUI 2.1 in April 2019:

The latest WinUI 2.2 release from September 2019 introduced a very promising TabView control – and slightly rounded corners for all controls. In the beginning of 2020 also an Edge Chromium based WebView is expected. 

Getting started with the Windows UI Library

The WinUI toolkit is available as NuGet packages that can be added to any existing or new project. Make sure not to forget to add the Windows UI Theme Resources to your App.xaml resources – as explained in the Getting Started guide. Here’s how this is done in the sample app:

<Application.Resources>
    <ResourceDictionary>
        <ResourceDictionary.MergedDictionaries>
            <!-- Win UI Controls -->
            <XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
            <!-- Other Dictionaries and Styles -->
            <!-- ... -->
        </ResourceDictionary.MergedDictionaries>
    </ResourceDictionary>
</Application.Resources>

WinUI exposes an extended API and a set of new controls. Currently these controls live in the Microsoft.UI.Xaml namespace to distinct them from the classic (future legacy?) UWP controls in Windows.UI.Xaml. IntelliSense will reveal some of the some doubles:

ControlsList

According to the roadmap WinUI’s intention is to decouple the entire XAML stack from the rest of UWP, to ship it as a NuGet package. UWP developers will observe that existing UWP XAML APIs (currently shipping as part of the OS) will fade out to no longer receive new feature updates – only security updates and critical fixes. Other UWP features such as application and security model, media pipeline, shell integrations, and broad device support will continue to evolve. WPF, Windows Forms and MFC developers will observe that WinUI is going to absorb (or render obsolete?) XAML Islands, a feature that is currently available through the Windows Community Toolkit.

Here’s an illustration of the current and target architecture:

roadmap_winui2

roadmap_winui3

The TeachingTip Control

A teaching tip is a control that provides contextual information, often used for informing, reminding, and teaching users about important and new features. Visually the TeachingTip is a Flyout with a Tail (an arrow pointing to its Target) and smooth opening and closing animations (love these!). The Teaching Tip is truly an Open Source control: it has been on GitHub from its proposal and specifications to its C++ source code. The TeachingTip documentation will get you started on the fly, and the API is simple. It’s safe to infer from the state of the related GitHub Issues that the control is new but stable.

It’s time to make our hands dirty.

Programmatically instantiating a TeachingTip

Most of our sample pages come with a Home page that describes the sample, and a Main page with all the action. One of the TeachingTip’s missions is to get the user started or to point out new features, so we decided to immediately open one on the Home page when the app starts. Here’s the C# code to define a TeachingTip programmatically, and hook it into the visual tree:

_mainMenuTeachingTip = new TeachingTip
{
    Target = glyph,
    Title = "Welcome",
    Content = "The Main page is where all the action is.",
    HeroContent = new Image
    {
        Source = new BitmapImage(new Uri("ms-appx:///Assets/MindFlayer.jpg"))
    },
    PreferredPlacement = TeachingTipPlacementMode.BottomRight,
    IsOpen = true,
    BorderThickness = new Thickness(.5),
    BorderBrush = new SolidColorBrush(Colors.DarkRed)
};
_mainMenuTeachingTip.Closed += MainMenuTeachingTip_Closed;

contentGrid.Children.Add(_mainMenuTeachingTip);

Precision Targeting

A TeachingTip is a Flyout, with an optional arrow that points to its Target. The positioning sample on the Main page displays what the impact is of PreferredPlacement for targeted and non-targeted teaching tips. Here’s a screenshot of it:

Positioning

Keep in mind that the TeachingTip will find its own place when there is not enough room for it. Here’s what happens when we specify LeftBottom when there’s no space there:

PositioningOverride

If you aim for visual perfection, you have to identify the exact Target that you want your TeachingTip to point its arrow to. In our sample app we wanted to target a menu item – a templated list item. Targeting the list item itself produced a rather fuzzy result, because of the default position calculation and the fact that TeachingTip never touches its Target. So we decided to target the icon inside the menu item instead of the menu item itself.

In most samples the Target is declared in XAML referencing a static control on the page. In most real life scenarios you will probably programmatically dig through a complex dynamic XAML structure, relying on things like the VisualTreeHelper, the GetChild() and FindName() methods, and looking up a XAML element that corresponds to an item in a list, with ContainerFromIndex().

Here’s how the sample app looks up the menu icon to be set as Target:

// Find the Main menu item and the Content grid.
var shell = (Window.Current.Content as Frame)?.Content as FrameworkElement;
var contentGrid = shell?.FindName("ContentGrid") as Grid;
var menu = shell?.FindName("Menu") as ListView;
var mainPageMenu = menu?.ContainerFromIndex(1) as ListViewItem;

// Find the Icon.
var itemsPresenter = ((VisualTreeHelper.GetChild(mainPageMenu, 0)) as FrameworkElement);
var stackPanel = ((VisualTreeHelper.GetChild(itemsPresenter, 0)) as FrameworkElement);
var glyph = stackPanel?.FindName("Glyph") as FrameworkElement;

State management

We assume that you liked the animated Teachingtip when opening the app for the first time, and perhaps the second time too. Just be aware that automatically opening TeachingTips becomes boring and annoying very rapidly to the end user. It really makes sense to let your remember that it showed a tip a couple of times, and then hide it forever.

To enhance this type of annoyment we made a second TeachingTip that opens when the first one raises its Closed event:

ReplayTip

We remember whether or not the TeachingTip has been displayed in an entry in the LocalSettings of the current ApplicationData:

public static void DisplayReplayButtonTip()
{
    var localSettings = Windows.Storage.ApplicationData.Current.LocalSettings;

    if (localSettings.Values["replayButtonTeachingTipDisplayed"] != null &&
        localSettings.Values["replayButtonTeachingTipDisplayed"].ToString() == "True")
    {
        return;
    }

    // ...

    _replayButtonTeachingTip = new TeachingTip
    {
        Target = replayButton,
        // ...
    };

    localSettings.Values["replayButtonTeachingTipDisplayed"] = "True";

    (homePage.Content as Grid).Children.Add(_replayButtonTeachingTip);
}

Here’s the code to remove the state again, e.g. in case of a reset-to-factory-settings situation:

private void ResetButton_Click(object sender, Windows.UI.Xaml.RoutedEventArgs e)
{
    var containerSettings = (ApplicationDataContainerSettings)ApplicationData.Current.LocalSettings.Values;
    var keys = containerSettings.Keys;
    foreach (var key in keys)
    {
        ApplicationData.Current.LocalSettings.Values.Remove(key);
    }

    ResetButtonTeachingTip.IsOpen = true;
}

A Word about Light-Dismiss

The IsLightDismissEnabled property makes an open teaching tip dismiss when the user scrolls or interacts with other elements of the application. In this mode, the PopupRoot –the top layer that hosts Flyouts, Dialogs, and TeachingTips- invisibly covers the whole page and swallows all UI events. Except for the system buttons in the top right corner, none of the app’s buttons will respond to a click or tap – including the Back button, the Hamburger menu and all Navigation items in our sample app.

We’re not a big fan of this mode. If you want to see for yourself, there’s one light-dismiss enabled TeachingTip in the sample app:

LightDismiss

Auto-Hiding a TeachingTip Control

It makes sense for a TeachingTip to hide itself when it’s no longer relevant, like when its Target is not displayed anymore (e.g. on Navigation) or when we can assume that the user had enough time to admire it. Let’s write some code to support such scenarios.

Hiding on Navigation

By default an open TeachingTip remains open when the user navigates to another page. When the code behind that TeachingTip (e.g. a Close event handler) refers to its Target or other items on the original page, your app will crash:

PopupRoot

On the left hand side in the above screenshot you see the Visual Studio Live Visual Tree. It reveals how the PopupRoot holding the TeachingTip is completely separated from the RootScrollViewer holding its Frame, Page, and Target. It’s up to you to synchronize these.

It’s easy for a Page to close a TeachingTip in its OnNavigatingFrom event:

protected override void OnNavigatingFrom(NavigatingCancelEventArgs e)
{
    PositioningTip.IsOpen = false;
    base.OnNavigatingFrom(e);
}

A more reusable solution is to enumerate all open Popup elements (Dialogs, Flyouts, ToolTips, TeachingTips) with GetOpenPopups() and then close each of these:

var openPopups = VisualTreeHelper.GetOpenPopups(Window.Current);
foreach (var popup in openPopups)
{
    popup.IsOpen = false;
}

Here’s an alternative for the fans of one-liners:

VisualTreeHelper.GetOpenPopups(Window.Current).ToList().ForEach(p => p.IsOpen = false);

Hiding on Timeout

If you want to close a TeachingTip after a certain time interval then you need to start a DispatcherTimer when you open it:

var timer = new DispatcherTimer();
timer.Tick += MainMenuTimer_Tick;
timer.Interval = TimeSpan.FromSeconds(5);
timer.Start();

The code that you write in its Tick() event executes on the UI Thread, so it can visually impact controls. You can set the IsOpen property of the TeachingTip to false there, and do more cleanup if you wish. Don’t forget to stop the timer:

private static void MainMenuTimer_Tick(object sender, object e)
{
    (sender as DispatcherTimer).Stop();

    if (_mainMenuTeachingTip == null)
    {
        return;
    }

    // Close and cleanup the TeachingTip
    _mainMenuTeachingTip.IsOpen = false;
}

Building an Inherited Control

Some time ago most Microsoft (and Open Source) teams that build UWP controls stopped making these controls sealed. Since then it is relatively easy to create inherited controls – such as a TeachingTip that auto-closes itself when its AutoCloseInterval elapses.

The sample app hosts such a control, here’s how an instance is declared in one of the XAML pages:

<xbcontrols:AutoCloseTeachingTip x:Name="ResetButtonTeachingTip"
                                    AutoCloseInterval="3000"
                                    Title="Reset"
                                    Content="Your app was reset to factory settings."
                                    PreferredPlacement="Right">
    <controls:TeachingTip.IconSource>
        <controls:SymbolIconSource Symbol="Repair" />
    </controls:TeachingTip.IconSource>
</xbcontrols:AutoCloseTeachingTip>

Here are the steps to create the control:

  1. Create a class that inherits from TeachingTip (no shit, Sherlock),
  2. add a property to hold the timeout value -it does not need to be a dependency property– with a decent default value,
  3. call RegisterPropertyChangedCallback() to define a listener for changes in the IsOpen dependency property,
  4. implement the DispatcherTimer based auto-close algorithm, and
  5. make sure to unregister all event handlers to avoid memory leaks.

Here’s the entire class definition:

/// <summary>
/// A teaching tip that closes itself after an interval.
/// </summary>
public class AutoCloseTeachingTip : Microsoft.UI.Xaml.Controls.TeachingTip
{
    private DispatcherTimer _timer;
    private long _token;

    public AutoCloseTeachingTip() : base()
    {
        this.Loaded += AutoCloseTeachingTip_Loaded;
        this.Unloaded += AutoCloseTeachingTip_Unloaded;
    }

    /// <summary>
    /// Gets or sets the auto-close interval, in milliseconds.
    /// </summary>
    public int AutoCloseInterval { get; set; } = 5000;

    private void AutoCloseTeachingTip_Loaded(object sender, RoutedEventArgs e)
    {
        _token = this.RegisterPropertyChangedCallback(IsOpenProperty, IsOpenChanged);
        if (IsOpen)
        {
            Open();
        }
    }

    private void AutoCloseTeachingTip_Unloaded(object sender, RoutedEventArgs e)
    {
        this.UnregisterPropertyChangedCallback(IsOpenProperty, _token);
    }

    private void IsOpenChanged(DependencyObject o, DependencyProperty p)
    {
        var that = o as AutoCloseTeachingTip;
        if (that == null)
        {
            return;
        }

        if (p != IsOpenProperty)
        {
            return;
        }

        if (that.IsOpen)
        {
            that.Open();
        }
        else
        {
            that.Close();
        }
    }

    private void Open()
    {
        _timer = new DispatcherTimer();
        _timer.Tick += Timer_Tick;
        _timer.Interval = TimeSpan.FromMilliseconds(AutoCloseInterval);
        _timer.Start();
    }

    private void Close()
    {
        if (_timer == null)
        {
            return;
        }

        _timer.Stop();
        _timer.Tick -= Timer_Tick;
    }

    private void Timer_Tick(object sender, object e)
    {
        this.IsOpen = false;
    }
}

For the sake of completeness, here’s how an instance of the control is created programmatically:

var _closeTeachingTip = new AutoCloseTeachingTip
{
    Title = "Then what are you still doing there?",
    Content = "We are all waiting for you in the factory.",
    HeroContent = new Image
    {
        Source = new BitmapImage(new Uri("ms-appx:///Assets/BrimbornSteelworks.png"))
    },
    PreferredPlacement = TeachingTipPlacementMode.Right,
    IsOpen = true
};

Using a TeachingTip as Form Field Wizard

Out of the box the TeachingTip supports an optional button: the Action Button. When one is defined, the cross icon in the upper left corner is replaced by a genuine Close Button at the bottom. The corresponding action can be defined in a traditional ActionButtonClick handler or in a more MVVM way through an ActionButtonCommand property. The action button does not close the TeachingTip.

Here’s how a TeachingTip with two buttons is declared in the sample app:

<controls:TeachingTip x:Name="ButtonsTip"
                        Target="{x:Bind ButtonsButton}"
                        Title="Were you already Flayed?"
                        CloseButtonContent="Yes"
                        CloseButtonCommand="{x:Bind CloseCommand}"
                        ActionButtonContent="Not sure"
                        ActionButtonCommand="{x:Bind ActionCommand}" />

And this is how it looks like – the action button was clicked, so the TeachingTip remained open when we opened the untargeted one on the left:

ActionButton

TeachingTip inherits from ContentControl, so it supports rich content. Together with its titles, its buttons, and its tail, this makes it an ideal host for a local wizard that can help the end user filling out a specific input field on a form – as a small dialog box that provides options or a calculation tool. This functionality is typically visually represented by decorating the form field with a tiny button showing an ellipsis or a magic wand or a calculator icon.

Our UWP app contains an example of using the TeachingTip as a form field wizard. The ellipsis button next to the TextBox opens a teaching tip presenting some options. The standard Action and Close buttons allow the user to select one of the options, or ignore the suggestion:

WizardTip

This is a scenario in which the TeachingTip could really shine, and we’re definitely planning to use it like this in some of our apps.

TeachingTip is a useful new control in the UWP ecosystem, with a simple API and smooth animations.

The Code

The sample app lives here of GitHub.

Enjoy!

Advertisements

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