8
\$\begingroup\$

I am developing a WPF component that can be used to create interactive tours for WPF applications. To further improve the API in terms of usability and intuitiveness, the input of other developers is needed.

The following feedback would be helpful:

  • Do you see potential to improve naming?
  • Do you like that organization of the functionality?
  • Do you see essential requirements that are not supported?

NOTE: I know that Code Review requires working code. However, the complete code for that component is too complex. Therefore I just extracted some parts so it is possible to review at least one aspect (API) of the component. Hopefully that's OK.

NOTE2: Meanwhile the library is released and published as open source project on git hub: https://github.com/JanDotNet/ThinkSharp.FeatureTour

Introduction

A tour is an ordered list of steps, the user can pass through. Each step represents a popup that can be placed nearby a UIElement of the WPF application.

Steps may be passive or active. Passive steps contain an explanation and the user has to click next to go to the next step. Active steps require the user to change the state of the application (e.g. select a special value in a combo box, enter a specific text in a text box and so on) to go to the next step.

Popup - the visual representation of a step

Each step is represented by a popup that looks like the following (red elements are not part of the popup):

FeatureTour Popup

Header Area / Content Area

  • Displays the values of Step.Header / Step.Content (see class Step below).
  • Visual appearance of the areas are customizable via data templates.
  • Internally, each area is a ContentControl. Its Content property is set to the value of Step.Header / Step.Content. The default data template is a TextBlock.

Close Button

  • The [X]-button on top right can be used to close the tour.

Next Button

  • The [Next >>]-button on bottom right can be used to go to the next step (Visibility depends on property Step.ShowNextButton).

Do it! Button (not visible in figure above)

  • The [Do it!]-button is placed left to the next button and can be used to execute custom logic programmatically (e.g. fill a complex form).

Progess (Step N/M)

  • Just a read-only information about the tour progress.

API Description

This section describes the API for using the component.

Popup Placement

The location for each step's popup can be defined in XAML using the attached properties ElementID and Placement of the class TourHelper. The properties can be attached to all elements that derive from UIElement.

For instance, a TabItem:

<TabItem Header="Variables"
    tour:TourHelper.ElementID="Variables"
    tour:TourHelper.Placement="BottomCenter">
    <!—Content -->
</TabItem>

The Placement property defines the placement of the popup relative to the UIElement.

Supported placements:

  • Center (popup is displayed without arrows)
  • TopLeft, TopCenter, TopRight
  • LeftTop, LeftCenter, LeftBottom
  • BottomLeft, BottomCenter, BottomRight
  • RightTop, RightCenter, RightBottom

Tour Definition

The classes below are used to define the content of a tour.

The Step class contains all information related to one step.

public class Step
{
    public Step(string elementID, object header)
        : this(elementID, header, null, elementID)
    {}

    public Step(string elementID, object header, object content)
        : this(elementID, header, content, elementID)
    {}

    public Step(string elementID, object header, object content, string id)
    {
        Header = header;
        ElementID = elementID;
        ID = id;
        Content = content;
    }

    /// <summary>
    /// Gets the header object to show in the header area.
    /// </summary>
    public object Header { get; private set; }

    /// <summary>
    /// Gets the content to show in main area of the popup.
    /// </summary>
    public object Content { get; private set; }

    /// <summary>
    /// Gets the element ID that indicates the visual element where the step is shown. 
    /// </summary>
    /// <remarks>
    /// Use attached property <see cref="TourHelper.ElementIDProperty"/> to define a element id
    /// for an <see cref="UIElement"/>.
    /// </remarks>
    public string ElementID { get; private set; }

    /// <summary>
    /// Gets the ID of the step.
    /// </summary>
    /// <remarks>
    /// If ID is not set; it will be equal to the <see cref="ElementID"/>
    /// The ID can be used in <see cref="FeatureTourNavigator.ForStep"/> or <see cref="FeatureTourNavigator.IfCurrentStepEquals"/>
    /// to reference the step.
    /// </remarks>
    public string ID { get; private set; }

    /// <summary>
    /// Gets or sets a value that determines if next button is shown.
    /// If <see cref="ShowNextButton"/> is null (default), the value from <see cref="Tour.ShowNextButtonDefault"/> is used.
    /// </summary>
    public bool? ShowNextButton { get; set; }

    /// <summary>
    /// Gets or sets the key of the content data template.
    /// </summary>
    /// <remarks>
    /// A data template with that key must be accessible by <see cref="FrameworkElement.TryFindResource"/> 
    /// from the <see cref="UIElement"/> with the element id <see cref="ElementID"/>.
    /// If <see cref="ContentDataTemplateKey"/> is not set (null) or the template is not available, 
    /// the content will be displayed in a text block.
    /// </remarks>
    public string ContentDataTemplateKey { get; set; }

    /// <summary>
    /// Gets or sets the key of the header data template.
    /// </summary>
    /// <remarks>
    /// A data template with that key must be accessible by <see cref="FrameworkElement.TryFindResource"/> 
    /// from the <see cref="UIElement"/> with the element id <see cref="ElementID"/>.
    /// If <see cref="HeaderDataTemplateKey"/> is not set (null) or the template is not available, 
    /// the content will be displayed in a text block.
    /// </remarks>
    public string HeaderDataTemplateKey { get; set; }
}

The Tour class gets a list of steps and a color scheme. It can be used to start the tour.

public class Tour
{
    /// <summary>
    /// Gets or sets the name of the tour.
    /// </summary>
    public string Name { get; set; }

    /// <summary>
    /// Gets or sets a value that indicates if the next button for each step is shown or not.
    /// The value can be also specified for each step separately using <see cref="Step.ShowNextButton"/>.
    /// </summary>
    public bool ShowNextButtonDefault { get; set; }

    /// <summary>
    /// Gets or sets a list of steps.
    /// </summary>
    public Step[] Steps { get; set; }

    /// <summary>
    /// Gets or sets the color scheme for the popup.
    /// </summary>
    public ColorScheme ColorScheme { get; set; }

    /// <summary>
    /// Starts the tour.
    /// </summary>
    public void Start() { /* Logic to start the tour */ }
}

The ColorScheme class defines some colors that are customizable:

public class ColorScheme
{
    public ColorScheme(Color background, Color foreground, Color border)
    {
        Background = background;
        Foreground = foreground;
        Border = border;
    }

    public Color Background { get; set; }
    public Color Foreground { get; set; }
    public Color Border { get; set; }
}

The following example shows how to create and start a tour:

var tour = new Tour()
{
    Name = "My Demo Tour",
    ShowNextButtonDefault = true,
    Steps = new []
    {
        new Step("Element01", "Header 01", "Content 01", "StepID01"),
        new Step("Element02", new HeaderViewModel(), new ContentViewModel(), "StepID02")
        {
            ContentDataTemplateKey = "ContentTemplate02",
            HeaderDataTemplateKey = "HeaderTemplate02"
        },
        // ...
    }
};

tour.Start();

Tour Navigation

The FeatureTourNavigator class provides some static methods that can be used to control the tour during application logic and to control the application during tour logic.

# Navigate Programmatically

FeatureTourNavigator.IfCurrentStepEquals("StepIDXX").GoNext();
FeatureTourNavigator.IfCurrentStepEquals("StepIDXX").GoPrevious();
FeatureTourNavigator.IfCurrentStepEquals("StepIDXX").Close();

Use Case:

Navigate programmatically for certain steps.

Example:

Text on popup: Create a new item.

To move automatically to the next step if a new item was created, the application logic for creating an item can be extended with:

FeatureTourNavigator.IfCurrentStepEquals("StepIDXX").GoNext();

# Attach doable action (that makes the 'Do it!'-button visible for the specific step)

// Option 01: DoAction only
FeatureTourNavigator.ForStep("StepIDXX").AttachDoable(
            currentStep => { /* DoAction - will be executed when the 'Do it!'-button was pressed */ });
// Option 02: DoAction + CanDoAction (for disabling DO-button if needed)
FeatureTourNavigator.ForStep("StepIDXX").AttachDoable(
            currentStep => { /* DoAction - will be executed when the 'Do it!'-button was pressed */ }, 
            currentStep => { /* CanDoAction */ return true; });

Use Case:

Enter required input (like complex text) programmatically.

Example:

Text on popup: Enter the text: 'C:\Users\CurrentUser\AppData\Roaming\Company\App\ExampleData\ExampleFile.xml'

The code behind of the window (or also the VM) can be extended with:

FeatureTourNavigator.ForStep("StepIDXX").AttachDoable(currentStep => myTb.Text = "C:\\User...");

# Execute code on step changed / changing

FeatureTourNavigator.OnStepEntered("StepIDXX").Execute(enteredStep => { /* ActionToExecute */ });
FeatureTourNavigator.OnStepEntering("StepIDXX").Execute(enteringStep => { /* ActionToExecute */ });
FeatureTourNavigator.OnStepLeaved("StepIDXX").Execute(leavedStep => { /* ActionToExecute */ });

Use Case:

Create a specific application state programmatically.

Example:

Next step is attached to an element on a specific tab, so it is required that the tab is selected before the tour reaches that step.

The code behind of the window (or also the VM) can be extended with:

FeatureTourNavigator.OnStepEntering("StepIDXX").Execute(enteringStep => { myTabControl.SelectedIndex = 2; });
\$\endgroup\$
  • 1
    \$\begingroup\$ Historically, such questions have been deemed off topic. Albeit with a large amount of debate. Looks like a good question to me, but I am admittedly on the "on topic" side of the debate. Here is the relevant meta post \$\endgroup\$ – RubberDuck May 15 '16 at 13:39
  • \$\begingroup\$ Yes, I am aware of that. It is more a design review than a code review. However, IMHO CodeReview is the most appropriated StackExchange for that kind of question, isn't it?. \$\endgroup\$ – JanDotNet May 15 '16 at 14:01
  • \$\begingroup\$ I believe so. Others don't. I just wanted you to be aware that this question may be controversial. \$\endgroup\$ – RubberDuck May 15 '16 at 14:03
  • 2
    \$\begingroup\$ Is it possible to declaratively create a tour instead of by code? Also, can the tour easily to localized to other regions? Sweden for example... :) \$\endgroup\$ – Mattias Åslund May 15 '16 at 20:57
  • \$\begingroup\$ @MattiasÅslund: Good points - tanks! I note the declarative tour definition idea for version 2. However, localization is a must to have feature that is actually missing... Because there are just 2 text fragments ("Next" and "Do it!"), I'll add that configuration simply to the Tour class. \$\endgroup\$ – JanDotNet May 16 '16 at 11:45
3
\$\begingroup\$

If this control is intended for widespread use, then the main issue is poor support for customization. ColorScheme is not gonna cut it, as soon as I want to use my favorite gradient brush as background or change border color based on importance of the step. Even if you support that, next thing I am gonna ask you is "hey, I want to make this close button look just like this other close button in my metro-style application, how do I do that??!". Then I'm also gonna need to write different text on buttons depending on current step. Then I'm gonna need a button of my own. Etc.

I guess my point is, if you are going to write a library for WPF, then you should support one of the key WPF features: custom templates. ContentDataTemplateKey property is a half-measure, it's just not enough. Step class should be a fully customizable Control, which I should be able to declare in xaml and style however I want. Your current approach would be fine, if you were writing this for one-time use in your own application. But if you are writing a reusable UI library, then it's "go big or go home", IMHO. :)

As a result you have a poor MVVM support. For example, instead of calling ForStep("MyHardToMessUpStepName").AttachDoable(...) method for some step, I would rather bind ICommand to a command on my Step element in XAML. But I can't do that. I also don't like static classes. Why do I need a static dependency in my code just to set DoIt command? Why can't I set this command directly on my Step instance?

Another issue is that its hard to customize a tour. Lets say I have a plugin-based application. It will be really tricky to include only enabled plugins into the tour. If every plugin I have could somehow "add" or "register" its "steps" within existing tour, that would be awesome. But it looks like I can't do that easily with current API, as it asks me for fixed predefined list of steps in order to create a tour.

As a final note: I like the idea, and I would consider using such library in my projects, but not if I can't re-disign those popups to fit into existing themes.

Oh, and I guess it should be either OnStepLeft or OnStepLeave.

\$\endgroup\$
  • \$\begingroup\$ Thanks for the excellent feedback! You are right... theming is not supported right now. I'll fix that for version 1. However, IMHO the static API has nothing to do with poor MVVM support. It is just an easy to use mechanism for extending the tour without the need of creating additional view models. The first approach, was based of a view model based API - but imagine you have a tour with 10 or more steps, that makes you crazy. But I understand the averseness of static dependencies. Do you think something likeITourNavigator tourNav = TourNavigator.GetNavigator() whould be an approvment? \$\endgroup\$ – JanDotNet May 17 '16 at 8:07
  • \$\begingroup\$ Yes, poor support of MVVM is not a result of static API, but a result of Step being a second-class citizen as far as WPF is concerned. Step must inherit from DependencyObject in order to have a good MVVM support. At least that's the way I see it. But I still think that it should be possible to set Do command directly on Step class. You could then have some FeatureTourNavigator.GetStep("StepIDXX") method, which would return a Step instance. So you can call FeatureTourNavigator.GetStep("StepIDXX").DoCommand = .... \$\endgroup\$ – Nikita B May 17 '16 at 9:17
  • 1
    \$\begingroup\$ If I had to chose one or the other, yes I would prefer TourNavigator.GetNavigator(). Mainly because it implements ITourNavigator interface and static class does not. But I would rather just call new TourNavigator() unless it absolutely have to be a singleton. \$\endgroup\$ – Nikita B May 17 '16 at 9:20

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.