A WinUI 2 Reference App

In this article we’ll walk through a UWP app that heavily relies on WinUI 2. WinUI 2.x (currently version 2.4) is the production-ready predecessor of the ambitious WinUI 3 platform. Version 3.x aims to become -in a not too distant future- the native runtime UI library for all apps that run on Windows – think Win32, UWP, Xamarin, Uno, and React Native.

To prepare ourselves for this WinUI 3.x we created a small but –hopefully- representative UWP app using WinUI 2.4. It fetches and displays live financial data (stock prices and news) from IEX Cloud. Here’s how the app looks like:

PortfolioPage

The app

It is our intention to upgrade this app with every new version of one of the underlying dependencies (specifically WinUI). This will give us an idea on how hard or easy it is to upgrade existing UWP apps to the new platform. In the mean time this reference app may also help you getting started with WinUI in UWP.

The app displays stock market data that is returned from services by IEXCloud, a company that publishes (and we quote from their home page) “institutional grade data, including fundamentals, ownership, international equities, mutual funds, options, real-time data, and alternative data – all in one API“. The data is exposed for free (if you stay below 50.000 core calls per month) but you need to register an account. The app’s Settings page allows you to enter the access keys that come with that account.

To become familiar with the financials API, we started to play around with IEXSharp – an Open Source C# IEXCloud client. We rapidly decided to continue to use the client: it’s complete, easy to work with, and regularly updated.

Here’s the full list of the app’s dependencies, with WinUI v2.5.0 already lurking as a preview:

Dependencies

Let’s dive into the source code.

The Shell Page

The root page of the app hosts the navigation infrastructure and shared UI assets such as the background image. Due to our long history as Prism framework user, we keep calling this type of page the Shell.

Look and feel

The main control in the Shell page is a NavigationView. We’ll use ‘winui’ as namespace alias for the WinUI-specific controls:

xmlns:winui="using:Microsoft.UI.Xaml.Controls"

Here’s the XAML declaration of a NavigationView with its menu on the left, a page header and no back button:

<winui:NavigationView 
	x:Name="NavigationView"
	Header="My stocks"
	IsBackButtonVisible="Collapsed">
	<winui:NavigationView.MenuItems>
		<!-- menu items -->
	</winui:NavigationView.MenuItems>
</winui:NavigationView>

The menu is a list of NavigationViewItem instances. You can nest these to obtain hierarchical navigation. Here’s the XAML definition of the History menu item, demonstrating not only a hierarchical construct but also three types of icons to use (symbol, SVG path, and multicolor bitmap):

<winui:NavigationViewItem Content="History">
    <winui:NavigationViewItem.Icon>
        <FontIcon Glyph="" />
    </winui:NavigationViewItem.Icon>
    <winui:NavigationViewItem.MenuItems>
        <winui:NavigationViewItem Content="AAPL">
            <winui:NavigationViewItem.Icon>
                <PathIcon Data="M32.295, etcetera ..." />
            </winui:NavigationViewItem.Icon>
        </winui:NavigationViewItem>
        <winui:NavigationViewItem Content="MSFT">
            <winui:NavigationViewItem.Icon>
                <BitmapIcon UriSource="/Assets/microsoft.png"
                            ShowAsMonochrome="False" />
            </winui:NavigationViewItem.Icon>
        </winui:NavigationViewItem>
    </winui:NavigationViewItem.MenuItems>
</winui:NavigationViewItem>

Here’s how the Shell page looks like. The Fluent reveal highlight effect is by default enabled in the control, as well as the acrylic background (we did override the BackgroundSource however):

Shell

The emptiness on the right of the menu is a Frame control to host the different user pages.

Navigation

Navigation is very straightforward: each NavigationViewItem keeps the class name of the target content page in its Tag property:

<winui:NavigationViewItem 
	Content="Watchlist"
        Tag="XamlBrewer.UWP.IEXCloud.Sample.Views.WatchListPage" />

The menu conveniently responds to ItemInvoked (click) as well as SelectionChanged (click on a unselected item). Here’s how we Navigate the Frame to the selected content page, and update the Header with the text of the (root node of the) selected item:

private void NavigationView_SelectionChanged(
	WinUI.NavigationView sender, 
	WinUI.NavigationViewSelectionChangedEventArgs args)
{
    if (args.IsSettingsSelected)
    {
        ContentFrame.Navigate(typeof(SettingsPage));
        NavigationView.Header = "Settings";
        return;
    }

    var item = args.SelectedItemContainer as WinUI.NavigationViewItem;

    if (item.Tag != null)
    {
        ContentFrame.Navigate(Type.GetType(item.Tag.ToString()), item.Content);
        NavigationView.Header = sender.SelectedItemsPath().First().Content;
    }
}

The NavigationView makes a distinction between the Settings menu item and the others, in case you would want to open a settings dialog or ye olde Windows 8 SettingsPane. The current guidelines for app settings however specify that “the app settings window should open full-screen and fill the whole window”. Here’s the corresponding from the (by the way excellent) docs:

appsettings-layout-navpane-desktop

It looks like we’re supposed to treat the Settings page not different from the other pages. If that’s the case for all devices, then API members such as IsSettingsSelected are actually obsolete.

NavigationView 2.4 supports a hierarchical menu but is missing a SelectedItems property – representing the path from the root menu to the selected leaf menu. This would be extremely helpful in some common scenario’s, e.g. when

  • you want to display a bread crumb, or
  • your navigation logic requires information from different levels.

In our sample app, the root menu defines the target class for navigation while the leaf menu provides the stock symbol to show. We created a SelectedItemsPath extension method that goes recursively through the selected menu items and returns the full path as a list:

public static List<WinUI.NavigationViewItem> SelectedItemsPath(
	this WinUI.NavigationView navigationView)
{
    var result = new List<WinUI.NavigationViewItem>();
    GetSelectedItems(navigationView.MenuItems, ref result);

    return result;
}

private static void GetSelectedItems(
	IList<object> items, 
	ref List<WinUI.NavigationViewItem> result)
{
    foreach (WinUI.NavigationViewItem item in items)
    {
        if (item.IsSelected)
        {
            result.Insert(0, item);
        }
        else
        {
            if (item.MenuItems?.Count > 0)
            {
                var count = result.Count;
                GetSelectedItems(item.MenuItems, ref result);
                if (result.Count > count)
                {
                    result.Insert(0, item);
                }
            }
        }
    }
}

Teaching tip

All of the content pages visualize stock related data from the IEXCloud services. This requires a (free) account. The Shell page hosts a TeachingTip control to inform you of this:

<winui:TeachingTip 
	x:Name="SettingsTip"
	Title="IEX Cloud Account Required"
	CloseButtonClick="SettingsTip_CloseButtonClick"
	IsOpen="False">
   <winui:TeachingTip.Content>
      <!-- ... -->
   </winui:TeachingTip.Content>
</winui:TeachingTip>

Here’s how it looks like:

TeachingTip

The TeachingTip is only opened when the required tokens are not found in the settings, and its arrow points to the center of the Settings menu item:

private void Shell_Loaded(
	object sender, 
	RoutedEventArgs e)
{
    var settings = new Settings();
    if (String.IsNullOrEmpty(settings.PublishableKey) && 
	String.IsNullOrEmpty(settings.PublishableSandBoxKey))
    {
        SettingsTip.Target = NavigationView.SettingsItem as FrameworkElement;
        SettingsTip.PreferredPlacement = WinUI.TeachingTipPlacementMode.TopLeft;
        SettingsTip.IsOpen = true;
    }
}

When the teaching tip is closed, the app autonavigates to the Settings page.

The Settings page

In the Settings page the user can enter (and test) the different tokens that are required to call the IEXCloud services, and also choose between ‘production’ mode (limited number of calls, but real data) and ‘sandbox’ mode (unlimited calls, but fake data). During development and testing it definitely made sense to activate ‘sandbox’ mode – some of the diagrams eat a lot of data.

Using Observable Settings

A while ago we came across this very elegant ObservableSettings base class for bindable settings in UWP. Here are the settings for our WinUI reference app – two pairs of token keys, and an indicator for sandbox mode:

public class Settings : ObservableSettings
{
    public static Settings Default { get; } = new Settings();

    public Settings()
        : base(ApplicationData.Current.LocalSettings)
    {}

    public string PublishableKey
    {
        get { return Get<string>(); }
        set { Set(value); }
    }

    public string SecretKey
    {
        get { return Get<string>(); }
        set { Set(value); }
    }

    // Similar code for sandbox keys.
   // ...

    [DefaultSettingValue(Value = true)]
    public bool UseSandBox
    {
        get { return Get<bool>(); }
        set { Set(value); }
    }
}

All you need to do is create a Settings instance …

private Settings Settings => new Settings();

… and bind its properties it to the corresponding UI elements:

<TextBox Header="Publishable key"
            Text="{x:Bind Settings.PublishableKey, Mode=TwoWay}" />
<PasswordBox Header="Secret key"
                Password="{x:Bind Settings.SecretKey, Mode=TwoWay}" />

Changes are automatically saved to local settings. How convenient!

Here’s how the Settings page looks like:

Settingspage

Applying ThemeShadow

The panels in the Settings page have shadow around them. ThemeShadow is defined as a shared resource and then applied to each panel:

<Grid Background="Transparent">
    <Grid.Resources>
        <ThemeShadow x:Name="SharedShadow" />
    </Grid.Resources>
    <Grid x:Name="ShadowCatcher"
            Margin="-8" />
    <VariableSizedWrapGrid x:Name="SettingsGrid"
                            Orientation="Horizontal">
        <StackPanel x:Name="ContentGrid"
                    Shadow="{StaticResource SharedShadow}">
            <!-- Production Content -->
        </StackPanel>
        <!-- Gets the animation -->
        <Grid>
            <!-- Casts the shadow -->
            <StackPanel x:Name="SandboxContentGrid"
                        Shadow="{StaticResource SharedShadow}">
                <!-- Sandbox Content -->
            </StackPanel>
        </Grid>
    </VariableSizedWrapGrid>
</Grid>

When the app starts, the panels are lifted by a Translation on the z-axis:

public SettingsPage()
{
    this.InitializeComponent();
    ContentGrid.Translation += new Vector3(0, 0, 6);
    // ...
}

The control underneath the WrapGrid is added to the shadow’s Receivers:

private void SettingsPage_Loaded(object sender, RoutedEventArgs e)
{
    SharedShadow.Receivers.Add(ShadowCatcher);
}

Applying Implicit Transformations

Since smooth transition is not (yet) the default, we let the different panels in the Settings page fluently respond to changes in window size via implicit animations:

SettingsAnimation

We had to add an extra Grid in the panel template to make this happen: adding ThemeShadow and implicit animations to the same control did not work …

Let’s dive into the real content pages now.

The Watchlist page

The Watchlist page hosts a list of high level properties of some stocks. We admit that the current Telerik Rad Datagrid would have been an ideal container for this collection, but we wanted to get some more experience with the WinUI ItemsRepeater. Here’s the XAML structure of the Watchlist page:

<winui:ItemsRepeater x:Name="Quotes">
    <winui:ItemsRepeater.Layout>
        <winui:StackLayout 
	Orientation="Vertical"
	Spacing="20" />
    </winui:ItemsRepeater.Layout>
    <winui:ItemsRepeater.ItemTemplate>
        <DataTemplate x:DataType="response:Quote">
            <!-- template content -->
        </DataTemplate>
    </winui:ItemsRepeater.ItemTemplate>
</winui:ItemsRepeater>

Here’s what the page looks like:

WatchlistPage

There’s nothing more to report on this page, except that –after all those years- we learned that you can format positive, negative, and zero values with a single expression:

<TextBlock Text="{x:Bind sys:String.Format('{0:+0.00%;-0.00%;0%}', changePercent)}" />

And again, we’re using an items repeater to mimic a data grid here. As soon as this app will make the move to WinUI 3, this page will become an ideal host for testing the upcoming Telerik WinUI 3 Data Grid.

The News page

The News page displays the latest news for a stock symbol in an ItemsRepeater. The symbol is taken from the Content of the hierarchical menu item and passed via the navigation logic:

protected override void OnNavigatedTo(NavigationEventArgs e)
{
    base.OnNavigatedTo(e);

    _symbol = e.Parameter.ToString();
}

Here’s how the page looks like:NewsPage

We observed that the RelativePanel is the best host for data templates that contain images with different source sizes.

The History page

The main beef of the History page is a Telerik RadCartesianChart that displays stock price history with

  • the closing price as a LineSeries,
  • a graphical representation of the OHLC (open-high-low-close) data as a CandleStickSeries, and
  • a customized ChartTrackBall that reveals the OHLC data when hovering over the chart with the mouse.

Here’s how the History page looks like:

HistoryPage

The focus of the reference app –and this article- is not on the charts but on WinUI 2, so let’s take a look on the TabView on top of the page. It starts with TabItems for the two predefined stock symbols from the menu (AAPL and MSPF). It also allows to add (removable) custom tabs, and it comes with a TabStripFooter with a (not-yet-implemented) Save button.

Here’s its XAML definition:

<winui:TabView 
	x:Name="SymbolsTab"
	SelectionChanged="SymbolsTab_SelectionChanged"
	AddTabButtonClick="SymbolsTab_AddTabButtonClick"
	TabCloseRequested="SymbolsTab_TabCloseRequested">
    <winui:TabViewItem Header="AAPL"
                        IsClosable="False" />
    <winui:TabViewItem Header="MSFT"
                        IsClosable="False" />
    <winui:TabView.TabStripFooter>
        <!-- Save Button -->
    </winui:TabView.TabStripFooter>
</winui:TabView>

When we navigate to the History page, we pick up the stock symbol – just like in the News page- and then use it to select the appropriate Tab item:

protected override void OnNavigatedTo(NavigationEventArgs e)
{
    base.OnNavigatedTo(e);

    _symbol = e.Parameter.ToString();

    try
    {
        // Synchronize menu to tab.
        SymbolsTab.SelectedItem = SymbolsTab
                                    .TabItems
                                    .Where(ti => (ti as WinUI.TabViewItem).Header.ToString() == _symbol)
                                    .First();
    }
    catch (Exception)
    {
        // Synchronization failed.
        SymbolsTab.SelectedIndex = 0;
    }
}

When the user selects another Tab –that’s a SelectionChanged– we fetch its symbol and refresh the page’s data:

private async void SymbolsTab_SelectionChanged(
	object sender, 
	SelectionChangedEventArgs e)
{
    if (!e.AddedItems.Any())
    {
        return;
    }

    _symbol = (e.AddedItems.First() as WinUI.TabViewItem).Header.ToString();

    // Here's where we should try to sync from tab to menu without starting a loop.
    // ...

    ProgressRing.IsActive = true;

    using (var iexCloudClient = IEXCloudService.GetClient())
    {
        try
        {
            var response = await iexCloudClient.StockPrices.HistoricalPriceAsync(_symbol);
            if (response.ErrorMessage != null)
            {
                HistoricPrices.ItemsSource = null;
                CandleSticks.ItemsSource = null;
            }
            else
            {
                HistoricPrices.ItemsSource = response.Data;
                CandleSticks.ItemsSource = response.Data;
            }
        }
        catch (Exception ex)
        {
            HistoricPrices.ItemsSource = null;
            CandleSticks.ItemsSource = null;
        }
    }

    ProgressRing.IsActive = false;
}

When the ‘plus’ button is hit, we open a small ContentDialog to request a new stock symbol:

HistoryPageAdd

When the user confirms the new stock symbol, we add a new TabViewItem and select it:

if (await dialog.ShowAsync() == ContentDialogResult.Primary)
{
    var newTab = new WinUI.TabViewItem();
    newTab.Header = textBox.Text;
    sender.TabItems.Add(newTab);
    sender.SelectedIndex = sender.TabItems.Count - 1;
}

Here’s another straightforward piece of code. When the user closes one of the custom tabs, we simply remove it:

private void SymbolsTab_TabCloseRequested(
	WinUI.TabView sender, 
	WinUI.TabViewTabCloseRequestedEventArgs args)
{
    sender.TabItems.Remove(args.Item);
}

The Portfolio page

All the controls and techniques that were used in the previous pages were just a warming-up for the Portfolio page. It displays a GridView with (fictional) stock positions: original value, current value, turnover, and a history sparkline (technicaly another Telerik RadChart, this time showing a SplineSeries). Here’s how the page looks like:

PortfolioPage

We’ll focus on the menu on top of the page. It allows to specify the time period for the sparklines. It is another NavigationView instance, this time demonstrating a NavigationViewItemHeader and a different PaneDisplayMode:

<winui:NavigationView 
	x:Name="TopNavigationView"
	PaneDisplayMode="Top"
	IsSettingsVisible="False"
	IsBackButtonVisible="Collapsed"
	SelectionChanged="TopNavigationView_SelectionChanged">
    <winui:NavigationView.MenuItems>
        <winui:NavigationViewItemHeader Content="  Chart period: " />
        <winui:NavigationViewItem Content="1 Month"
                                    Tag="OneMonth" />
        <winui:NavigationViewItem Content="3 Months"
                                    Tag="ThreeMonths"
                                    IsSelected="True" />
        <winui:NavigationViewItem Content="1 Year"
                                    Tag="OneYear" />
    </winui:NavigationView.MenuItems>
</winui:NavigationView>

A navigation view menu comes with its own styling – including a default acrylic background. We needed to override one of its many resources to make that background transparent to blend in the page:

<SolidColorBrush x:Key="NavigationViewTopPaneBackground"
                    Color="Transparent" />

Selecting a menu refreshes the query and the page, just like the tab in the History page.

For the sake of completeness, here’s how the implicit animations look like (this should be the default) on this page:

PortfolioAnimation

The Source

Our WinUI 2 reference app lives here on GitHub. You may expect regular updates to it.

Enoy!

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