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):
Header Area / Content Area
- Displays the values of
Step.Header
/Step.Content
(see classStep
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 ofStep.Header
/Step.Content
. The default data template is aTextBlock
.
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; });
1 Answer 1
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
.
-
\$\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 like
ITourNavigator tourNav = TourNavigator.GetNavigator()
whould be an approvment? \$\endgroup\$JanDotNet– JanDotNet2016年05月17日 08:07:26 +00:00Commented May 17, 2016 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 fromDependencyObject
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 setDo
command directly on Step class. You could then have someFeatureTourNavigator.GetStep("StepIDXX")
method, which would return aStep
instance. So you can callFeatureTourNavigator.GetStep("StepIDXX").DoCommand = ...
. \$\endgroup\$Nikita B– Nikita B2016年05月17日 09:17:19 +00:00Commented May 17, 2016 at 9:17 -
1\$\begingroup\$ If I had to chose one or the other, yes I would prefer
TourNavigator.GetNavigator()
. Mainly because it implementsITourNavigator
interface and static class does not. But I would rather just callnew TourNavigator()
unless it absolutely have to be a singleton. \$\endgroup\$Nikita B– Nikita B2016年05月17日 09:20:37 +00:00Commented May 17, 2016 at 9:20
Tour
class. \$\endgroup\$