Settings - Programming Windows Store Apps with C# (2014)

Programming Windows Store Apps with C# (2014)

Chapter 9. Settings

The last charm-based feature that we’re going to look at is the settings charm, which allows you to define a set of commands that are presented along with one or two standard options within each app. Although originally defined within the Windows 8 experience vision as a common way to provide access for settings, a de facto standard has emerged whereby apps use it to provide access to their Help options. There is also a store requirement to provide easy access to a “privacy policy,” and this should be done through the settings charm.

In this chapter, we’re going to look at the standard options first and then add an option to jump out to the web browser to display the privacy policy. We’ll build a flyout that can be used to host normal settings. (A flyout is a panel that winds in from the right side of the screen, similar to a pop up.) Within this flyout, we’ll look at taking some marked-up text and rendering it in a “prettified” fashion. This will show us how we can render help content within the app if we aren’t using HTML, and also show how we can render more richly formatted text within the app, where it’s often impractical to host IE and render HTML.

Adding Options

Let’s now look at the basics of how to add options. As you may have guessed, this is an issue of asking WinRT to return a handler, whereupon we bind to events and feed back the information. In this case, we use WinRT’s SettingsPane class and respond to the CommandsRequestedevent. When we do this, we need to create instances of SettingsCommand objects, one for each item that we want to appear in the settings view before the standard options.

Standard Options

Each Windows Store app can display one or two default options in the settings charm. The one that you’ll always get is the Permissions option. This will show the name of the app, provide an option for turning off push notifications (if applicable), and list any permissions that the app has.Figure 9-1 shows the permissions view for StreetFoo thus far.

The standard permissions view

Figure 9-1. The standard permissions view

If your app is installed via the store, you will also get a “Rate and review” option. You can see an example of this by looking in the settings for any built-in or downloaded app that you have.

Adding Custom Options

The bare minimum that we can do to satisfy the requirement to provide a privacy statement within the app is to add an option that navigates to a web page.

NOTE

The Store requirement is such that the user should be able to access the statement from the app, but the whole text doesn’t necessarily have to be in the app. I talk more about store requirements in Chapter 15.

Much like we did in the last chapter, we’ll create a new class called SettingsInteractionHelper in the UI-agnostic project and wire it up to handlers in the Windows Store app−specific project.

Navigating to a URL is just a matter of using the Windows.System.Launcher class. This will dereference the default handler for a given protocol—in this case, http:, but it could equally be mailto: in order to launch the default email client—and then navigate to it. That sameLauncher class can also open files using the LaunchFileAsync option.

The next code details a SettingsInteractionHelper, rigged with a method to display a “privacy statement.” As the comment says, that’s not a real privacy statement. I’ve also included an option to display some web-based help. Similarly, this is not a real help site.

public static class SettingsInteractionHelper

{

public static async Task ShowPrivacyStatementAsync()

{

// this will just take the user off to a web page...

// this isn't a real privacy statement, btw...

await Launcher.LaunchUriAsync(new Uri

("http://programmingwindowsstoreapps.com/"));

}

internal static async Task ShowWebHelpAsync()

{

// again, not a real website...

await Launcher.LaunchUriAsync(new Uri

("http://programmingwindowsstoreapps.com/"));

}

}

As is by now a traditional approach, we’ll hook up the settings charm handler from the OnLaunched method in App. The code is as follows (I’ve removed much of the code from the OnLaunched method for brevity):

// Modify method in App, add new handler method...

protected override async void OnLaunched(LaunchActivatedEventArgs args)

{

// code omitted...

// settings...

var settings = SettingsPane.GetForCurrentView();

settings.CommandsRequested += settings_CommandsRequested;

}

void settings_CommandsRequested(SettingsPane sender,

SettingsPaneCommandsRequestedEventArgs args)

{

args.Request.ApplicationCommands.Add(new SettingsCommand

("PrivacyStatement", "Privacy Statement",

async (e) => { await

SettingsInteractionHelper.ShowPrivacyStatementAsync(); }));

}

If you now run the app, you’ll see the Privacy Statement option appear in the settings charm. Select it, and IE will spring into life and navigate the user away. Figure 9-2 illustrates.

The Privacy Statement option

Figure 9-2. The Privacy Statement option

BEST PRACTICE

It’s worth calling out a couple of points about best practice.

There are no explicit guidelines about the casing of options on the charm. There isn’t much in the way of consistency in this. OneNote MX uses sentence casing—for example, “Privacy and terms of use.” IE uses title casing (e.g., Internet Options). Seeing as the default “Rate and review” is sentence-cased and also seeing that you can’t change that, I’d go with sentence casing.

Microsoft’s published guidelines also state that when you select an option, the current view should not be navigated—that is, the user should stay where he or she is. In the next section, we’re going to create a settings flyout, which does exactly this. However, some of the built-in apps do navigate the page. The Store app is especially exuberant in this department.

In LOB apps, the settings charm is a decent place to put support tools and information—for example, functions that dump out diagnostic information, or that reset local caching. The same is possibly also true of consumer apps, although in consumer apps we have to be more careful not to blind the user with science.

Implementing the Settings Flyout

The most common thing to do with the settings charm is to create a flyout that contains more options. I am going to present this here, even though we don’t have much in the way of settings! What we’ll do is present a view that isn’t functional, concentrating on the process of building the flyout. Luckily for us, the WinRT API contains a SettingsFlyout control that implements the basic behavior of winding in from the righthand side of the screen. All we have to do is create an instance of this control that is able to containerize specific settings panes. In this chapter, we’re going to build two settings panes—one that displays some settings and another that displays a help message.

Building a Settings Pane

The settings pane is a standard XAML surface. A lot of the examples you’ll see of this on the Web use the UserControl class. However, we have to consider that right now our MVVM implementation is tied into using StreetFooPage by virtue of its implementation ofIViewModelHost. Although the view-models don’t know anything about the StreetFooPage, they do need to be able to “poke” back into the UI, which is why we’ve created the IViewModelHost route. If we choose to base our settings pane on something other than StreetFooPage, we’ll need to extend that support forward into some expansion of UserControl. (Some people might argue that there’s no need for any coupling like this at all—pragmatically, I think it’s easier to have a degree of coupling while maintaining the spirit of a well-managed separation.)

I’m going to propose that we build an MvvmAwareControl based on UserControl. All that we really have to do here is implement IViewModelHost. The view-model implementations themselves won’t care what they’re based on. That’s the point of the abstraction, after all.

You may recall that thus far we’ve used extension methods on the Page to drive additional functionality, such as displaying message boxes and initializing and dereferencing the view-model. We now have two classes that we need to provide this functionality to: StreetFooPage (which we did before) and MvvmAwareControl (which is new). This means we need to change those extension methods.

Weirdly—at least it seems slightly weird to me—we can just change the extension methods to extend IViewModel rather than Page, and suddenly all of that behavior becomes available to MvvmAwareControl. (This sort of thing is just one reason why extension methods are one of my favorite C# language features. What we’re really doing here is creating a type of quasi-multiple-inheritance support.)

Now take a look at the revised version of PageExtender that contains the extension methods. (It may be worth renaming that class, but I’ve left it with the original name to save confusion for readers who may be comparing code across chapters.)

internal static class PageExtender

{

internal static Task ShowAlertAsync(this IViewModelHost page, ErrorBucket

errors)

{

return ShowAlertAsync(page, errors.GetErrorsAsString());

}

internal static Task ShowAlertAsync(this IViewModelHost page, string

message)

{

// show...

MessageDialog dialog = new MessageDialog(message != null ? message :

string.Empty);

return dialog.ShowAsync().AsTask();

}

internal static void InitializeModel(this IViewModelHost page,

IViewModel model)

{

// set up the data context...

((Control)page).DataContext = model;

}

internal static IViewModel GetModel(this IViewModelHost page)

{

return ((Control)page).DataContext as IViewModel;

}

}

Once we’ve done that, building MvvmAwareControl is a cinch. Here’s the code:

public class MvvmAwareControl : UserControl, IViewModelHost

{

public MvvmAwareControl()

{

}

Task IViewModelHost.ShowAlertAsync(ErrorBucket errors)

{

return PageExtender.ShowAlertAsync(this, errors);

}

Task IViewModelHost.ShowAlertAsync(

string message)

{

return PageExtender.ShowAlertAsync(this, message);

}

public void ShowView(Type viewModelInterfaceType)

{

throw new NotImplementedException();

}

public void HideAppBar()

{

throw new NotImplementedException();

}

}

}

I’ve assumed in this code that supporting ShowView and HideAppBar is out of scope for this class.

We have to decide what we’re going to call these controls. There’s precedent for calling them panes—thus far, we’ve seen SearchPane and SettingsPane, which sit in the same physical place in the real estate. However, we can’t call it SettingsPane, as this would clash with the WinRT class of the same name. My proposition, then, is that we call our new control MySettingsPane.

Using the Add New Item dialog, add a new UserControl called MySettingsPane. You’ll get some XAML that looks like this:

<UserControl

x:Class="StreetFoo.Client.UI.MySettingsPane"

xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

xmlns:local="using:StreetFoo.Client.UI"

xmlns:d="http://schemas.microsoft.com/expression/blend/2008"

xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"

mc:Ignorable="d"

d:DesignHeight="300"

d:DesignWidth="400">

<Grid>

</Grid>

</UserControl>

There are two things we need to change in that default XAML. We need to change the UserControl declaration to local:MvvmAwareControl. (We’ll also need to change the base class, which we’ll do in a moment.) We also need to change the width. Settings panes can be 346 pixels or 646 pixels in width. I find that 346 tends to be a bit narrow, so set this to 646. Here’s the amended code:

<local:MvvmAwareControl

x:Class="StreetFoo.Client.UI.MySettingsPane"

xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

xmlns:local="using:StreetFoo.Client.UI"

xmlns:d="http://schemas.microsoft.com/expression/blend/2008"

xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"

mc:Ignorable="d"

d:DesignHeight="646"

d:DesignWidth="400">

<Grid>

</Grid>

</local:MvvmAwareControl>

As mentioned, you’ll also need to change the base class in the codebehind file, like so:

[ViewModel(typeof(IMySettingsPaneViewModel))]

public sealed partial class MySettingsPane : MvvmAwareControl

{

private IMySettingsPaneViewModel ViewModel { get; set; }

public MySettingsPane()

{

this.InitializeComponent();

this.InitializeViewModel();

}

}

All of these styles let us build our layout properly. As mentioned, we’re not putting real controls on here—I’ve used a ToggleSwitch because it’s one of the cooler new controls in Windows 8. Notice that the back button has a command binding to DismissCommand; we’ll get to that soon. Here’s the layout of MySettingsPane in its entirety:

<local:MvvmAwareControl

x:Class="StreetFoo.Client.UI.MySettingsPane"

IsTabStop="false"

xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

xmlns:local="using:StreetFoo.Client.UI"

xmlns:d="http://schemas.microsoft.com/expression/blend/2008"

xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"

mc:Ignorable="d"

Width="646" Height="200">

<Border Style="{StaticResource SettingsBorderStyle}">

<Grid>

<Grid.RowDefinitions>

<RowDefinition Height="50"></RowDefinition>

<RowDefinition Height="*"></RowDefinition>

</Grid.RowDefinitions>

<Grid Style="{StaticResource SettingsCaptionStyle}">

<StackPanel Orientation="Horizontal">

<Button Style="{StaticResource SettingsBackButtonStyle}"

Command="{Binding DismissCommand}"/>

<TextBlock Grid.Row="1" Style="{StaticResource

SettingsCaptionTextStyle}">Settings</TextBlock>

</StackPanel>

</Grid>

<Grid Grid.Row="2" Margin="10,10,10,10">

<StackPanel>

<ToggleSwitch Grid.Row="2" Header="The cows look small

because..." OnContent="They are small" OffContent=

"They are far away"></ToggleSwitch>

</StackPanel>

</Grid>

</Grid>

</Border>

</local:MvvmAwareControl>

Building MySettingsFlyout

To do this, add a new SettingsFlyout item to the project. Call it MySettingsFlyout.

This flyout will contain some default XAML. We need to change this so that the default text within the StackPanel instance doesn’t exist, and give the StackPanel instance an x:Name attribute so that we can address it from code. Here’s the XAML:

<SettingsFlyout

x:Class="StreetFoo.Client.UI.MySettingsFlyout"

xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

xmlns:local="using:StreetFoo.Client.UI"

xmlns:d="http://schemas.microsoft.com/expression/blend/2008"

xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"

mc:Ignorable="d"

IconSource="Assets/SmallLogo.png"

Title="SettingsFlyout1"

d:DesignWidth="346">

<!-- This StackPanel acts as a root panel for vertical layout of the content

sections -->

<StackPanel VerticalAlignment="Stretch" HorizontalAlignment="Stretch" >

<!-- The StackPanel(s) below define individual content sections -->

<!-- Content Section 1-->

<StackPanel x:Name="StackPanel" Style="{StaticResource

SettingsFlyoutSectionStyle}">

</StackPanel>

<!-- Define more Content Sections below as necessary -->

</StackPanel>

</SettingsFlyout>

In terms of the code, when we create a new instance of the settings flyout, we’ll pass in an instance of one of the user controls that drives the pane. We’ll then dynamically add this control to the StackPanel instance defined in the XAML. Here’s the code:

public sealed partial class MySettingsFlyout : SettingsFlyout

{

private UserControl UserControl { get; set; }

public MySettingsFlyout()

{

this.InitializeComponent();

}

public MySettingsFlyout(UserControl control)

: this()

{

// set the user control...

this.UserControl = control;

this.StackPanel.Children.Add(control);

// subscribe...

this.Loaded += OnLoaded;

}

private void OnLoaded(object sender, RoutedEventArgs routedEventArgs)

{

// set the title and the width...

this.Title = ((IViewModel)this.UserControl.DataContext).Caption;

this.Width = this.UserControl.Width;

}

}

All that remains now is to rig up a way of showing the pane. We’ll do this by adding another command to the settings command collection. Here’s the change to App:

// Modify method in App...

void settings_CommandsRequested(SettingsPane sender,

SettingsPaneCommandsRequestedEventArgs args)

{

args.Request.ApplicationCommands.Add(new SettingsCommand

("PrivacyStatement", "Privacy Statement",

async (e) => { await SettingsInteractionHelper.

ShowPrivacyStatementAsync(); }));

args.Request.ApplicationCommands.Add(new

SettingsCommand("MySettings", "My Settings",

(e) => {

var flyout = new MySettingsFlyout(new MySettingsPane());

flyout.Show();

}));

}

Again, run the project and you’ll now be able to access the new pane through the settings charm.

Developing a Help Screen

Now, let’s go through the relatively basic requirement of displaying help content on the screen. It’s emerging as a standard approach within Windows Store apps to put a Help option on the settings pane, at least for those apps that provide help. We will rig Help to display when the user presses F1. (Although pressing F1 to access a help function is not necessarily current fashion, I want to show how you can handle keyboard events.)

Although we’re going to see how to use this to represent help text, the rendering portion of this can be used anywhere in your app to render richer blocks of bigger text. For example, in LOB apps you may want to render product descriptions, or summaries of a customer’s order history. In retail apps, you may want to render downloaded content.

Hypothetically, if you want to present text within the app, you want some formatting control. HTML is an obvious choice for this. And you can render HTML content using the WebView control. (This control containerizes IE.) This supports a NavigateToString method into which you can feed a string containing the HTML to render. Or, you can use the Navigate method and give it a URL. (However, the ms-appx and ms-appdata protocols are not supported by WebView, so you need to load the data first and feed it in through NavigateToString.)

The problem with the IE-based approach is that it’s a little blunt. Back in the olden days, this would not have been at all pretty. It’s a better and more lightweight approach now, but it doesn’t provide the sort of granularity and control you might need when rendering small portions of text.

Let’s go down a different route and show you how to build up formatted content without using WebView. We’re going to build a control called a MarkupViewer to which we can give some marked-up text and have it create XAML objects that present the text in a “prettified” way.

Creating a Help Pane

I’m not going to go through how you create the structure of HelpPane in detail, as it’s the selfsame job as creating the MySettingsPane. The more interesting aspect is in creating a control called MarkupViewer that will be responsible for rendering the help content.

What will normally happen with help content is that you’ll have it loaded locally on the device, most likely in a Windows 8 world by having the content available in the ~/Assets folder of the project. (What we’re not going to do here is build a complex, context-sensitive help system—all we’re going to do is put some help content on the screen.) I’m assuming that we have one file called ~/Assets/HelpText.txt. You can put whatever you like in this file.

What we’re going to do with our IHelpPaneViewModel is have it expose a command whereby the user can jump off to a website to get proper help, and a Markup property that will have the content to render. Here’s the code:

public interface IHelpPaneViewModel : IViewModel, IDismissCommandSource

{

ICommand WebHelpCommand { get; set; }

string Markup { get; }

}

The implementation, then, looks like this:

public class HelpPaneViewModel : ViewModel, IHelpPaneViewModel

{

// commands...

public ICommand DismissCommand {

get { return this.GetValue<ICommand>(); }

set { this.SetValue(value); } }

public ICommand WebHelpCommand {

get { return this.GetValue<ICommand>(); }

private set { this.SetValue(value); } }

public HelpPaneViewModel()

{

WebHelpCommand = new DelegateCommand(async (args) => await

SettingsInteractionHelper.ShowWebHelpAsync());

}

// property for holding the markup...

public string Markup { get { return this.GetValue<string>(); } set

{ this.SetValue(value); } }

// loads the markup from disk when we're activated...

public override async void Activated(object args)

{

base.Activated(args);

// load...

var file = await StorageFile.GetFileFromApplicationUriAsync(new Uri

("ms-appx:///Assets/HelpText.txt"));

this.Markup = await FileIO.ReadTextAsync(file);

}

}

You’ll notice that we have the FileIO class read the help contents from disk.

In the next section, we’re going to build a control called MarkupViewer. This will have a Markup property that’s bound to the Markup property on the view-model. Similarly, we’ll have a HyperlinkButton that will have its Command property bound to the WebHelpCommand property on the view-model. Here’s the XAML that shows those two controls within HelpPane.xaml:

<!-- snippet from HelpPane... -->

<Grid Grid.Row="1">

<StackPanel Margin="10,10,10,10">

<local:MarkupViewer Markup="{Binding Markup}" />

<HyperlinkButton

Content="Visit our website to get more help"

Command="{Binding WebHelpCommand}"></HyperlinkButton>

</StackPanel>

</Grid>

As is common practice, you’ll need to modify the constructor of HelpPage so that it obtains and sets up the view-model. You’ll also need to add a command into the settings_CommandRequested handler in App. I’m proposing creating a static method for showing help, as we’re going to activate this view through more mechanisms than just the settings pane. Here’s the code:

// Modify method and add new method in App...

void settings_CommandsRequested(SettingsPane sender,

SettingsPaneCommandsRequestedEventArgs args)

{

args.Request.ApplicationCommands.Add(new SettingsCommand

("PrivacyStatement", "Privacy Statement",

async (e) => { await SettingsInteractionHelper.

ShowPrivacyStatementAsync(); }));

args.Request.ApplicationCommands.Add(

new SettingsCommand("MySettings", "My Settings",

(e) => {

var flyout = new MySettingsFlyout(new MySettingsPane());

flyout.Show();

}));

args.Request.ApplicationCommands.Add(new SettingsCommand("Help",

"Help", (e) => { ShowHelp(); }));

}

internal static void ShowHelp()

{

var flyout = new MySettingsFlyout(new HelpPane());

flyout.Show();

}

You won’t be able to compile yet, as we still have to build the MarkupViewer control.

Handling the F1 Key

Handling the keypress is very easy to do—just override the OnKeyUp method in StreetViewPage to respond to the F1 key. Here’s the code:

// Add method to StreetViewPage…

protected override void OnKeyUp(Windows.UI.Xaml.Input.

KeyRoutedEventArgs e)

{

if (e.Key == VirtualKey.F1)

App.ShowHelp();

else

base.OnKeyUp(e);

}

That’s it—if we could compile and run the app, we could then see that.

Rendering Markup

Before we build the MarkupViewer control, let’s consider how we render the content.

As we know by now, XAML is definitely not HTML. Whereas HTML is designed with text/document rendering as its primary function, XAML is not. If we want to render flowing text with formatting, we have to build up a control tree to do it. We can use RichTextBox to create a container for the text, and then put Paragraph and Run objects in it to build up the representation. For example, the following results in the representation shown in Figure 9-3:

<RichTextBlock FontSize="16">

<Paragraph>So, this is some text. And this word is

<Run FontWeight="Bold">bold</Run>.

</Paragraph>

<Paragraph>And this text is

<Run Foreground="Pink">pink</Run>.

</Paragraph>

</RichTextBlock>

Example RichTextBlock rendering

Figure 9-3. Example RichTextBlock rendering

What we’re going to do is take the contents of our help text file and create Paragraph instances for each line. If we were building a more sophisticated markup processor, we would just have to add a more complex control structure depending on the directives in the markup. The principle, though, would remain the same.

We need somewhere to put the markup. For our control, we’ll extend ContentControl. (Ultimately, we’ll put a RichTextBlock instance in the Content property. We can’t extend RichTextBlock because it’s sealed.) Here’s the code:

public class MarkupViewer : ContentControl

{

// dependency property...

public static readonly DependencyProperty MarkupProperty =

DependencyProperty.Register("Markup", typeof(string), typeof(MarkupViewer),

new PropertyMetadata(null, (d, e) => ((MarkupViewer)d).Markup =

(string)e.NewValue));

public MarkupViewer()

{

}

public string Markup

{

get

{

return (string)GetValue(MarkupProperty);

}

set

{

SetValue(MarkupProperty, value);

this.RefreshView();

}

}

private void RefreshView()

{

// tbd...

}

}

If you recall back in Chapter 5, that’s the same pattern that we’ve been using for adding properties to our controls using dependency properties.

Before we run it, let’s make it do something so that we can prove it works. Modify RefreshView so that it creates a button, like this:

// Modify method in MarkupViewer...

private void RefreshView()

{

var button = new Button();

button.Content = this.Markup;

// set...

this.Content = button;

}

Run the app and summon the help, and it’ll render our text in a giant button. Figure 9-4 illustrates.

Demonstrating that we’ve loaded the text and passed it through to MarkupViewer

Figure 9-4. Demonstrating that we’ve loaded the text and passed it through to MarkupViewer

Now we can do some processing on the text.

You’ll notice from Figure 9-4 that the second line is a set of equals signs. I’ve borrowed this convention from Markdown. In Markdown, this notation is used to indicate that the preceding line should be a heading.

As I’ve mentioned, we’re not going to do a proper Markdown implementation, but I want to do more than rendering flat text, hence the heading—we will support that.

The bits that we need to render the text are all in the Windows.UI.Xaml.Documents namespace. It works by combining blocks and inlines, all of which can be styled.

We’ll create a root RichTextBlock instance, and add Paragraph instances to it. (Paragraphs are blocks.) To each paragraph we’ll then add Run instances, a run being an inline.

When we start building our view, we’ll take the CR+LF delimited text and break it down into lines. We’ll walk through each line and look ahead to see if the next line is a heading. If it is, we’ll adjust the styling of the paragraph that we’re on and then skip the line. When we’re done, we’ll set the Content property to be the RichTextBlock instance that we created. Here’s the code:

// Modify method in MarkupViewer...

private void RefreshView()

{

// anything?

if (string.IsNullOrEmpty(Markup))

{

this.Content = null;

return;

}

// get the lines...

var lines = new List<string>();

using (var reader = new StringReader(this.Markup))

{

while(true)

{

string buf = reader.ReadLine();

if (buf == null)

break;

lines.Add(buf);

}

}

// walk...

var block = new RichTextBlock();

for (int index = 0; index < lines.Count; index++)

{

string nextLine = null;

if (index < lines.Count - 1)

nextLine = lines[index + 1];

// create a paragraph... and add it to the block...

var para = new Paragraph();

block.Blocks.Add(para);

// create a "run" and add it to the paragraph...

var run = new Run();

run.Text = lines[index];

para.Inlines.Add(run);

// heading?

if (nextLine != null && nextLine.StartsWith("="))

{

// make it bigger, and then skip the next line...

para.FontSize = 20;

index++;

}

else if (nextLine != null && nextLine.StartsWith("-"))

{

para.FontSize = 18;

index++;

}

}

// set...

this.Content = block;

}

Run the code and summon the help option, and you’ll see something like Figure 9-5.

“Prettified” markup

Figure 9-5. “Prettified” markup

There you go: nicely rendered text using a custom markup format, albeit quite a limited custom format. As I mentioned before, the easiest win here is that hopefully someone will port over Markdown or something similar. Either way, don’t forget that this approach is not just valid for help text. It also applies to displays of complex text data in all sorts of data that you might present in your app. In fact, in the next chapter we’ll use this same control to “prettify” rendering of the report description.