Fondest greetings, and welcome to the blog home of the Merlin Wizard Framework! In spite of some tangential posts, this blog will be geared toward getting you up and running with the slickest, simplest, and richest WinForms wizard framework for .NET. If you read the blog posts in chronological order, it may even read like a tutorial.

Tuesday, December 23, 2008

Delays, Delays

Let's go back to the demo from the previous post, for a moment. Once you got to the "Advanced User Information" step, you may have noticed an unsightly disabled text box at the top of the screen:

Screen Shot

The purpose of that text box is to display the name of the user entered in the previous step. To do that, I need to populate the text box only when the controller reaches this step and not before. The cleanest (but not the only) way to do that is by setting the step's StepReachedHandler property to a void delegate that will do the initialization for you. Here's the modified Main method from the previous post's demo demonstrating how this would be done (changes in bold):

var steps = new List<IStep>();
var simpleEntryUI = new SimpleEntryUI();
var simpleEntryStep =
    new TemplateStep(simpleEntryUI, "User Information");
var advancedEntryUI = new AdvancedEntryUI();
var advancedEntryStep =
    new TemplateStep(advancedEntryUI,
"Advanced User Information");
advancedEntryStep.StepReachedHandler = () =>
{
    advancedEntryUI.FullName = simpleEntryUI.FullName;
};
steps.Add(simpleEntryStep);
steps.Add(new ConditionalStep(
    () => { return simpleEntryUI.ShowAdvancedOptions; },
    advancedEntryStep));

Label farewellMessage = new Label();
farewellMessage.Text = "Well, done. Bye, now!";
steps.Add(new TemplateStep(farewellMessage, 10, "Goodbye!"));
new WizardController(steps).StartWizard("User Data Wizard");

The StepReachedHandler does not get evaluated until the controller reaches the step, hence it has access to all the data provided by the preceding steps. Note, that if you run the example above but do not check the "Show Advanced Settings" checkbox, the StepReachedHandler will not run at all. This is what you would expect: if a step is inside a conditional and the condition is false, the StepReachedHandler of that step will not run.

Maybes and What-ifs

In Merlin, we represent a wizard as a sequence of steps. Of course, a wizard isn't always a fully-defined sequence - sometimes the end user's input determines what step comes next or whether a step occurs at all. In this post we will investigate the simplest technique Merlin offers for dynamically determining whether a step occurs.

It's called a ConditionalStep, and as the name implies, it occurs only if some condition is true. Creating a ConditionalStep is easier than gaining weight on a cruise ship:

var conditionalStep = new ConditionalStep(condition, actual step);

The condition above is a boolean delegate. The actual step is another step. If (and only if, for the mathematical purists) condition is true, actual step occurs. If the condition is false, we skip on down to the next step. This implies, of course, that a conditional step cannot be the last step in the step sequence. If you try to give such a sequence to the wizard controller, it will throw a ConditionalStepAtEndException.

I put a small demo together to show off this functionality. You can get the complete code here, but here's the gist: For our first step, we have a simple form (shown below). If the user clicks next, she goes straight to our farewell message. But, if the user checks the "Show Advanced Options" checkbox, she gets to enter some more data, and then sees the farewell message.

image 

And here's the code that makes all this magic possible:

var steps = new List<IStep>();
var simpleEntryUI = new SimpleEntryUI();
var simpleEntryStep = new TemplateStep(simpleEntryUI, "User Information");
var advancedEntryStep =
    new TemplateStep(new AdvancedEntryUI(), "Advanced User Information");
steps.Add(simpleEntryStep);
steps.Add(new ConditionalStep(
    () => { return simpleEntryUI.ShowAdvancedOptions; },
    advancedEntryStep));


Label farewellMessage = new Label();
farewellMessage.Text = "Well, done. Bye, now!";
steps.Add(new TemplateStep(farewellMessage, 10, "Goodbye!"));
new WizardController(steps).StartWizard("User Data Wizard");

When the wizard runs, the condition does not get evaluated until the controller reaches the conditional step, which allows you to use data from previous steps in your conditions. That data will be available by the time the condition gets evaluated.

If you put a breakpoint inside the condition, you will also notice that the condition does not get evaluated on the way back. In other words, if skip down to the farewell message and click the "back" button, the condition will not be evaluated to determine whether or not you see the advanced data step. Instead, the result of the last condition evaluation will be used. But, if you go backward through a conditional step and then go forward again, the condition will be reevaluated. So to summarize: conditions are evaluated only when the conditional step is reached by clicking the "next" button.

Monday, December 22, 2008

Getting Picky

Just like covering up the wall sockets in a home with small children, Merlin lets you disable certain elements and validate others, so as to prevent your users from doing unsavory things.

Let's go back to the example in the previous post. There, we have a step whose UI consists of a single text box. Let's now make sure that the user cannot advance to the next step without having typed something in the box. We rewrite the main method as follows:

var steps = new List<IStep>(); //We'll define wizard steps here.
var t = new TextBox();
var myStep = new TemplateStep(t, 10, "Name Entry");
myStep.NextHandler = () => {
    if (string.IsNullOrEmpty(t.Text))
    {
        MessageBox.Show("You gotta enter your name before I let you continue");
        return false;
    }
    else return true;
};
steps.Add(myStep);
var result = new WizardController(steps).StartWizard("My SuperCoolWizard");
if (result == WizardController.WizardResult.Finish)
MessageBox.Show("Hello, " + t.Text);

That's a bit more code than we had before. Let's look at the new stuff:

myStep.NextHandler = () => {
    if (string.IsNullOrEmpty(t.Text))
    {
        MessageBox.Show("You gotta enter your name before I let you continue");
        return false;
    }
    else return true;
};

The TemplateStep class offers the NextHandler, PreviousHandler, and CancelHandler properties to handle the "Next/Finish", "Previous", and "Cancel" buttons, respectively. The first two are boolean delegates. If they return false, the controller will not leave the current step. Since the purpose of a cancel button is to drop everything and abort, it does not have a return value, and a cancellation will occur no matter what.

But why wait for the user to hit the "Next/Finish" button? Why not just gray it out altogether? I'm glad I asked!

var steps = new List<IStep>();
var t = new TextBox();
var myStep = new TemplateStep(t, 10, "Name Entry");
myStep.AllowNextStrategy = () => {
    return !string.IsNullOrEmpty(t.Text);
};
t.KeyUp += (object sender, KeyEventArgs keyArgs) => {
   myStep.StateUpdated();
};
steps.Add(myStep);
var result = new WizardController(steps).StartWizard("My Wizard");
if (result == WizardController.WizardResult.Finish)
    MessageBox.Show("Hello, " + t.Text);

This time, in lieu of a handler for the next button, we provided a strategy (in the form of a boolean delegate) to determine if the Next button should be enabled. But we needed one more piece:

t.KeyUp += (object sender, KeyEventArgs keyArgs) => {
   myStep.StateUpdated();
};

We need to know when to recheck the strategy so that the buttons accurately reflect the validity of the user's input. To do this, we fire TemplateStep's StateUpdated method whenever something in our UI has changed.

That does it for this lesson. Please leave any questions or feedback in the comments, we'll do our best to answer.

The obligatory Hello, World

Those who prefer to beat around the bush best step off here, for we shall plunge head-first into the magical world of Merlin. All examples here, like Merlin itself, are written in C# 3.0.

Starting with a new winforms application, let's add a reference to Merlin.dll and

Using Merlin;

to the top of the main class file. Now we go inside the Main(...) method.

var steps = new List<IStep>(); //We'll define wizard steps here.
var t = new TextBox();
steps.Add(new TemplateStep(t, "Name Entry"));
new WizardController(steps).StartWizard("My SuperCoolWizard");
MessageBox.Show("Hello, " + t.Text);

When you run this, you should see something like this:
screen

Enter your name into the sole textbox in the page, click "Finish", and you will be greeted plainly but warmly. So what exactly happened?

var steps = new List<IStep>();

A "step" in the wizard correlates to a single screen that you see. The "Next" and "Previous" buttons navigate the user from one step to another. Because the step we see in the wizard is the last one, it appears with a "Finish" button instead of a "Next" button.

var t = new TextBox();
steps.Add(new TemplateStep(t, "Name Entry"));

The UI of a step is represented by a control. TemplateStep, a simple step implementation, requires (at a minimum) that control as its constructor argument. It attempts to size this control to take up all the space available. Of course, we don't always want the control to be flushed against the edges of the step area. So we can provide a margin around it by changing the statement above to:

steps.Add(new TemplateStep(t, 10, "Name Entry"));

This creates a margin of 10 pixels from each edge of the control. But let's go on examining our code:

new WizardController(steps).StartWizard("My SuperCoolWizard");

Once we have our sequence of steps, we need to execute them. The WizardController object does just that. It accepts as its constructor an IList of steps. When the StartWizard method is invoked, it goes through the steps as the user clicks the "Back" and "Next/Finish" buttons.

Ok, simple enough, but now let's run this wizard again, but instead of the "Finish" button, click the "Cancel" button. Whoops! You still get greeted. The cancel button aborts the wizard, but you still have to check afterward to make sure the wizard exited successfully. So let's rewrite the last two lines in our example as follows:

var result = new WizardController(steps).StartWizard("My SuperCoolWizard");
if (result == WizardController.WizardResult.Finish)
{
    MessageBox.Show("Hello, " + t.Text);
}

Of course, there's a lot more to wizards than single-control steps. Join us next time for more Merlin magic.