Adding An Interactive Tutorial To My Programs

The other day I watched a video by Patrick McKenzie.  It talks about making the user’s first use of your program as simple as possible.  In this case, he shows how to do it by having an interactive tutorial that gets them through doing their first <whatever you program does>.

One of my recent programs is Decision Assistant, a tool to help you easily make the best decision for complex problems.  I sent it out to some testers to get their feedback on it.  One of them mentioned that it wasn’t intuitive what he should do when he first started it up.

So, I started to create an interactive tutorial for it.

It starts out telling the user how to create a new decision.  When they click on the “Create New Decision” menu option, the tutorial shows them the next thing they need to do – in this case, enter in the decision name.  When they enter the decision name, the tutorial shows them how to enter in the options for the decision.  And so on, and so on.

It’s not complete yet, but this is the basic way I’m handling the code.

First, I created an InteractiveTutorialEventArgs class.  This class has an enum, with each value being a step the user will go through.

using System;

namespace Engine.Utilities
{
    public class InteractiveTutorialEventArgs : EventArgs
    {
        public enum Step
        {
            TutorialStarted,
            CreateNewDecision,
            EnterDecisionName,
            EnterFirstOption,
            DisplayOptionDeleteMenu,
            SelectFactorTab,
            EnterFactor,
            DisplayFactorDeleteMenu,
            ChangeFactorWeighting,
            SelectComparisonsTab,
            AnswerFirstComparison,
            AnswerFinalComparison,
            ChangeComparison
        }

        public Step CurrentStep { get; private set; }

        public InteractiveTutorialEventArgs(Step currentStep)
        {
            CurrentStep = currentStep;
        }
    }
}

Next, I created an InteractiveTutorial class.  This class has a collection of all the steps.  It’s a SortedList, with each enum value from InteractiveTutorialEventArgs, along with a Boolean, for whether or not the user has performed the action that relates to a screen in the tutorial.

There’s also a MarkTutorialStepCompleted method that lets my program notify the tutorial that the step has been performed by the user.  This method will raise an event that the Tutorial page is watching for.  The rest of the program calls this method, whenever the user reaches a point where they’ve done something the tutorial cares about.

using System.Collections.Generic;
using System.Linq;

namespace Engine.Utilities
{
    public static class InteractiveTutorial
    {
        private static readonly SortedList<InteractiveTutorialEventArgs.Step, bool> Steps = new SortedList<InteractiveTutorialEventArgs.Step, bool>();

        public static event TutorialStepChangedHandler OnTutorialStepChanged;

        public delegate void TutorialStepChangedHandler(object obj, InteractiveTutorialEventArgs e);

        static InteractiveTutorial()
        {
            Steps.Add(InteractiveTutorialEventArgs.Step.TutorialStarted, false);
            Steps.Add(InteractiveTutorialEventArgs.Step.CreateNewDecision, false);
            Steps.Add(InteractiveTutorialEventArgs.Step.EnterDecisionName, false);
            Steps.Add(InteractiveTutorialEventArgs.Step.EnterFirstOption, false);
            Steps.Add(InteractiveTutorialEventArgs.Step.DisplayOptionDeleteMenu, false);
            Steps.Add(InteractiveTutorialEventArgs.Step.SelectFactorTab, false);
            Steps.Add(InteractiveTutorialEventArgs.Step.EnterFactor, false);
            Steps.Add(InteractiveTutorialEventArgs.Step.DisplayFactorDeleteMenu, false);
            Steps.Add(InteractiveTutorialEventArgs.Step.ChangeFactorWeighting, false);
            Steps.Add(InteractiveTutorialEventArgs.Step.SelectComparisonsTab, false);
            Steps.Add(InteractiveTutorialEventArgs.Step.AnswerFirstComparison, false);
            Steps.Add(InteractiveTutorialEventArgs.Step.AnswerFinalComparison, false);
            Steps.Add(InteractiveTutorialEventArgs.Step.ChangeComparison, false);
        }

        public static void MarkTutorialStepCompleted(InteractiveTutorialEventArgs.Step completedStep)
        {
            if(Steps.ContainsKey(completedStep))
            {
                if(!Steps[completedStep])
                {
                    Steps[completedStep] = true;

                    if(OnTutorialStepChanged != null)
                    {
                        InteractiveTutorialEventArgs.Step currentStep = Steps.First(x => x.Value == false).Key;
                        OnTutorialStepChanged(new object(), new InteractiveTutorialEventArgs(currentStep));
                    }
                }
            }
        }

    }
}

Here’s a sample of how I use a call to the InteractiveTutorial MarkTutorialStepCompleted method.  In this case, it’s to make sure that the user saw how to open a context menu that lets them delete an option.

        private void cmnuOptions_Opened(object sender, EventArgs e)
        {
            InteractiveTutorial.MarkTutorialStepCompleted(InteractiveTutorialEventArgs.Step.DisplayOptionDeleteMenu);
        }

The Tutorial code is fairly simple.  It watches for event notifications from the InteractiveTutorial class and displays the correct text and image on the Tutorial form.

using System;
using System.Drawing;
using System.Reflection;
using System.Windows.Forms;

using Engine.Utilities;

namespace DecisionAssistant
{
    public partial class Tutorial : Form
    {
        public Tutorial()
        {
            InitializeComponent();

            InteractiveTutorial.OnTutorialStepChanged += UpdateTutorialText;

            InteractiveTutorial.MarkTutorialStepCompleted(InteractiveTutorialEventArgs.Step.TutorialStarted);
        }

        private void UpdateTutorialText(object obj, InteractiveTutorialEventArgs e)
        {
            rtbTutorialInstructions.Text = "";
            picTutorial.Image = null;

            Assembly thisExe = Assembly.GetExecutingAssembly();

            switch(e.CurrentStep)
            {
                case InteractiveTutorialEventArgs.Step.CreateNewDecision:
                    rtbTutorialInstructions.Text += Paragraph("This tutorial will take you through creating your first decision.  If you ever want to run the tutorial again, you can click on the \"Help\" menu option and select \"Run Interactive Tutorial\".");
                    rtbTutorialInstructions.Text += Environment.NewLine;
                    rtbTutorialInstructions.Text += Paragraph("Think about a decision you need to make - one that has several options to choose from, and several different factors you'd use to rank them.");
                    rtbTutorialInstructions.Text += Paragraph("Let's say you want help with choosing where to go on vacation.  Your options might be \"Las Vegas\", \"London\", and \"The Bahamas\".  Your factors might the cost, weather, time to travel, etc.");
                    rtbTutorialInstructions.Text += Environment.NewLine;
                    rtbTutorialInstructions.Text += Paragraph("When you know what decision you want to make, click on the menu option \"File\", and then on the option \"Create New Decision\".");
                    picTutorial.Image = Image.FromStream(thisExe.GetManifestResourceStream("DecisionAssistant.TutorialImages.SelectCreateNewDecisionMenuOption.png"));
                    break;
                case InteractiveTutorialEventArgs.Step.EnterDecisionName:
                    rtbTutorialInstructions.Text += Paragraph("Now you're on the decision screen.");
                    rtbTutorialInstructions.Text += Paragraph("Enter the decision you want to make into the box.  For example \"Where to go for vacation\".");
                    rtbTutorialInstructions.Text += Paragraph("You must enter in something.  If you leave this blank, a warning message will pop up.");
                    picTutorial.Image = Image.FromStream(thisExe.GetManifestResourceStream("DecisionAssistant.TutorialImages.EnterDecisionName.png"));
                    break;
                case InteractiveTutorialEventArgs.Step.EnterFirstOption:
                    rtbTutorialInstructions.Text += Paragraph("Now it's time to enter the options you're considering.");
                    rtbTutorialInstructions.Text += Paragraph("Just type in the option and click on the \"Add Option\" button.");
                    rtbTutorialInstructions.Text += Paragraph("As you add options, they will appear in the list.  If you accidentally type in an option you've alreay entered, a warning message will pop up, and prevent the duplicate option from being added.");
                    picTutorial.Image = Image.FromStream(thisExe.GetManifestResourceStream("DecisionAssistant.TutorialImages.EnterFirstOption.png"));
                    break;
                case InteractiveTutorialEventArgs.Step.DisplayOptionDeleteMenu:
                    rtbTutorialInstructions.Text += Paragraph("If you want to remove an option you entered, you can always do that.");
                    rtbTutorialInstructions.Text += Paragraph("Just right-click on the option you want to remove.  A menu will pop up.  The only choice on the menu is \"Delete Option\".");
                    rtbTutorialInstructions.Text += Paragraph("If you want to delete the option, click on \"Delete Option\".");
                    rtbTutorialInstructions.Text += Paragraph("If you don't want to delete that option, click anywhere else on the program.");
                    rtbTutorialInstructions.Text += Paragraph("Right-click on an option now, to see how the menu works, and to get to the next step in the tutorial.");
                    picTutorial.Image = Image.FromStream(thisExe.GetManifestResourceStream("DecisionAssistant.TutorialImages.DeleteOptionMenuDisplayed.png"));
                    break;
                case InteractiveTutorialEventArgs.Step.SelectFactorTab:
                    rtbTutorialInstructions.Text += Paragraph("Continue adding the other options you are considering.  You can add as many as you want.");
                    rtbTutorialInstructions.Text += Paragraph("If you think of any other options later on, you can always come back later and add them in.");
                    rtbTutorialInstructions.Text += Paragraph("When you've added the final option, click on the \"Factors\" tab.");
                    picTutorial.Image = Image.FromStream(thisExe.GetManifestResourceStream("DecisionAssistant.TutorialImages.SelectFactorsTabWhenDoneAddingOptions.png"));
                    break;
            }
        }

        private static string Paragraph(string text)
        {
            return text + Environment.NewLine + Environment.NewLine;
        }

        #region Form Events

        private void btnCloseTutorial_Click(object sender, EventArgs e)
        {
            // TODO: Set Universe.Settings.InteractiveTutorialViewed to true
            Close();
        }

        private void Tutorial_FormClosing(object sender, FormClosingEventArgs e)
        {
            Hide();
            e.Cancel = true;
        }

        #endregion
    }
}

Starting the interactive tutorial is simple.

In the constructor of the main form, I create an instance of the Tutorial class and set it to a local variable.  I do this, so that if the user closes the Tutorial form, and re-opens if later (from the Help menu), it will be the same form and continue from where the user needs to be (since it’s been watching all the events for the user’s actions).

I display the Tutorial form in the main form’s Shown event.

If the user hasn’t already gone through the interactive tutorial (based on a Boolean value saved in the settings file) then the program will display it.  Within the InteractiveTutorial class, I’ll change the Settings.InteractiveTutorialViewed to ‘true’ after the user has performed the last step in the tutorial.

        private void DecisionAssistant_Shown(object sender, EventArgs e)
        {
            if(!Universe.Settings.InteractiveTutorialViewed)
            {
                if(MessageBox.Show("Would you like to view the interactive tutorial?", "Tutorial", MessageBoxButtons.YesNo) == DialogResult.Yes)
                {
                    _tutorial.Show();
                }
            }
        }

In the future, I may change this to include one of those ‘always show on startup’ checkboxes.

The Ultimate Goal

As I create more programs for general use (not just custom, in-house applications), I want to make the initial experience with them as easy as possible for the users.  That means they’ll quickly get more value from the program, since they’ll know what it can do.  It should also reduce the amount of support I need to perform – leaving me with more time to write more programs.

Leave a Reply

Your email address will not be published. Required fields are marked *