public ScriptInputGroupPage(InputGroup inputGroup, List<InputGroup> previousInputGroups)
        {
            Title = "Input Group";

            StackLayout contentLayout = new StackLayout
            {
                Orientation = StackOrientation.Vertical,
                VerticalOptions = LayoutOptions.FillAndExpand
            };

            foreach (StackLayout stack in UiProperty.GetPropertyStacks(inputGroup))
                contentLayout.Children.Add(stack);

            Button editInputsButton = new Button
            {
                Text = "Edit Inputs",
                FontSize = 20,
                HorizontalOptions = LayoutOptions.FillAndExpand
            };

            editInputsButton.Clicked += async (o, e) =>
            {
                await Navigation.PushAsync(new ScriptInputsPage(inputGroup, previousInputGroups));
            };

            contentLayout.Children.Add(editInputsButton);

            Content = new ScrollView
            {
                Content = contentLayout
            };
        }
        public ScriptInputsPage(InputGroup inputGroup)
        {
            _inputGroup = inputGroup;

            Title = "Inputs";

            _inputsList = new ListView();
            _inputsList.ItemTemplate = new DataTemplate(typeof(TextCell));
            _inputsList.ItemTemplate.SetBinding(TextCell.TextProperty, new Binding(".", stringFormat: "{0}"));
            _inputsList.ItemTapped += async (o, e) =>
            {
                if (_inputsList.SelectedItem == null)
                    return;

                Input selectedInput = _inputsList.SelectedItem as Input;
                int selectedIndex = inputGroup.Inputs.IndexOf(selectedInput);

                List<string> actions = new string[] { "Edit", "Delete" }.ToList();

                if (selectedIndex < inputGroup.Inputs.Count - 1)
                    actions.Insert(0, "Move Down");

                if (selectedIndex > 0)
                    actions.Insert(0, "Move Up");

                string selectedAction = await DisplayActionSheet(selectedInput.Name, "Cancel", null, actions.ToArray());

                if (selectedAction == "Move Up")
                    inputGroup.Inputs.Move(selectedIndex, selectedIndex - 1);
                else if (selectedAction == "Move Down")
                    inputGroup.Inputs.Move(selectedIndex, selectedIndex + 1);
                else if (selectedAction == "Edit")
                {
                    ScriptInputPage inputPage = new ScriptInputPage(selectedInput);
                    inputPage.Disappearing += (oo, ee) =>
                    {
                        Bind();
                    };

                    await Navigation.PushAsync(inputPage);
                    _inputsList.SelectedItem = null;
                }
                else if (selectedAction == "Delete")
                {
                    if (await DisplayAlert("Delete " + selectedInput.Name + "?", "This action cannot be undone.", "Delete", "Cancel"))
                    {
                        _inputGroup.Inputs.Remove(selectedInput);
                        _inputsList.SelectedItem = null;  // manually reset, since it isn't done automatically.
                    }
                }
            };

            ToolbarItems.Add(new ToolbarItem(null, "plus.png", async () =>
                    {
                        List<Input> inputs = Assembly.GetExecutingAssembly()
                            .GetTypes()
                            .Where(t => !t.IsAbstract && t.IsSubclassOf(typeof(Input)))
                            .Select(t => Activator.CreateInstance(t))
                            .Cast<Input>()
                            .OrderBy(i => i.Name)
                            .ToList();

                        string cancelButtonName = "Cancel";
                        string selected = await DisplayActionSheet("Select Input Type", cancelButtonName, null, inputs.Select((input, index) => (index + 1) + ") " + input.Name).ToArray());
                        if (!string.IsNullOrWhiteSpace(selected) && selected != cancelButtonName)
                        {
                            Input input = inputs[int.Parse(selected.Substring(0, selected.IndexOf(")"))) - 1];

                            if (input is VoiceInput && inputGroup.Inputs.Count > 0 || !(input is VoiceInput) && inputGroup.Inputs.Any(i => i is VoiceInput))
                                UiBoundSensusServiceHelper.Get(true).FlashNotificationAsync("Voice inputs must reside in groups by themselves.");
                            else
                            {
                                inputGroup.Inputs.Add(input);

                                ScriptInputPage inputPage = new ScriptInputPage(input);
                                inputPage.Disappearing += (o, e) =>
                                {
                                    Bind();
                                };

                                await Navigation.PushAsync(inputPage);
                            }
                        }
                    }));

            Bind();
            Content = _inputsList;
        }
        public PromptForInputsPage(InputGroup inputGroup, int stepNumber, int totalSteps, bool showCancelButton, string nextButtonTextOverride, CancellationToken? cancellationToken, string cancelConfirmation, string incompleteSubmissionConfirmation, string submitConfirmation, bool displayProgress, DateTimeOffset? firstPromptTimestamp, Action<Result> disappearanceCallback)
        {
            _displayedInputCount = 0;

            StackLayout contentLayout = new StackLayout
            {
                Orientation = StackOrientation.Vertical,
                VerticalOptions = LayoutOptions.FillAndExpand,
                Padding = new Thickness(10, 20, 10, 20),
                Children =
                {
                    new Label
                    {
                        Text = inputGroup.Name,
                        FontSize = 20,
                        HorizontalOptions = LayoutOptions.CenterAndExpand
                    }
                }
            };

            if (displayProgress)
            {
                float progress = (stepNumber - 1) / (float)totalSteps;

                contentLayout.Children.Add(new Label
                    {
                        Text = "Progress:  " + Math.Round(100 * progress) + "%",
                        FontSize = 15,
                        HorizontalOptions = LayoutOptions.CenterAndExpand
                    });

                contentLayout.Children.Add(new ProgressBar
                    {
                        Progress = progress,
                        HorizontalOptions = LayoutOptions.FillAndExpand
                    });
            }

            if (firstPromptTimestamp.HasValue)
            {
                DateTime firstDisplayDateTime = firstPromptTimestamp.Value.ToLocalTime().DateTime;

                string displayLapseDayDesc;
                if (firstDisplayDateTime.Date == DateTime.Now.Date)
                    displayLapseDayDesc = "earlier today";
                else if (firstDisplayDateTime.Date == DateTime.Now.AddDays(-1).Date)
                    displayLapseDayDesc = "yesterday";
                else
                    displayLapseDayDesc = ((int)(DateTime.Now - firstDisplayDateTime).TotalDays) + " days ago (" + firstDisplayDateTime.ToShortDateString() + ")";

                contentLayout.Children.Add(new Label
                    {
                        Text = "These fields were first displayed " + displayLapseDayDesc + " at " + firstDisplayDateTime.ToShortTimeString() + ".",
                        FontSize = 20,
                        HorizontalOptions = LayoutOptions.Start
                    });
            }

            if (inputGroup.Inputs.Any(input => input.Display && input.Required))
                contentLayout.Children.Add(new Label
                    {
                        Text = "Required fields are indicated with *",
                        FontSize = 15,
                        TextColor = Color.Red,
                        HorizontalOptions = LayoutOptions.Start
                    });

            List<Input> displayedInputs = new List<Input>();
            int viewNumber = 1;
            int inputSeparatorHeight = 10;
            foreach (Input input in inputGroup.Inputs)
                if (input.Display)
                {
                    View inputView = input.GetView(viewNumber);
                    if (inputView != null)
                    {
                        if (input.Enabled && input.Frame)
                        {
                            inputView = new Frame
                            {
                                Content = inputView,
                                OutlineColor = Color.Accent,
                                VerticalOptions = LayoutOptions.Start,
                                HasShadow = true,
                                Padding = new Thickness(10)
                            };
                        }

                        // add some vertical separation between inputs
                        if (_displayedInputCount > 0)
                            contentLayout.Children.Add(new BoxView { Color = Color.Transparent, HeightRequest = inputSeparatorHeight });

                        contentLayout.Children.Add(inputView);
                        displayedInputs.Add(input);

                        if (input.DisplayNumber)
                            ++viewNumber;

                        ++_displayedInputCount;
                    }
                }

            if (_displayedInputCount > 0)
                contentLayout.Children.Add(new BoxView { Color = Color.Transparent, HeightRequest = inputSeparatorHeight });

            #region previous/next buttons

            StackLayout previousNextStack = new StackLayout
            {
                Orientation = StackOrientation.Horizontal,
                HorizontalOptions = LayoutOptions.FillAndExpand
            };

            #region previous button

            bool previousButtonTapped = false;

            if (stepNumber > 1)
            {
                Button previousButton = new Button
                {
                    HorizontalOptions = LayoutOptions.FillAndExpand,
                    FontSize = 20,
                    Text = "Previous"
                };

                previousButton.Clicked += async (o, e) =>
                {
                    previousButtonTapped = true;
                    await Navigation.PopModalAsync(false);
                };

                previousNextStack.Children.Add(previousButton);
            }

            #endregion

            #region next button

            Button nextButton = new Button
            {
                HorizontalOptions = LayoutOptions.FillAndExpand,
                FontSize = 20,
                Text = stepNumber < totalSteps ? "Next" : "Submit"

                #if UNIT_TESTING
                // set style id so that we can retrieve the button when unit testing
                , StyleId = "NextButton"
                #endif
            };

            if (nextButtonTextOverride != null)
                nextButton.Text = nextButtonTextOverride;

            bool nextButtonTapped = false;

            nextButton.Clicked += async (o, e) =>
            {
                string confirmationMessage = "";

                if (!string.IsNullOrWhiteSpace(incompleteSubmissionConfirmation) && !inputGroup.Valid)
                    confirmationMessage += incompleteSubmissionConfirmation;
                else if (nextButton.Text == "Submit" && !string.IsNullOrWhiteSpace(submitConfirmation))
                    confirmationMessage += submitConfirmation;

                if (string.IsNullOrWhiteSpace(confirmationMessage) || await DisplayAlert("Confirm", confirmationMessage, "Yes", "No"))
                {
                    // if the cancellation token was cancelled while the dialog was up, then we should ignore the dialog. the token
                    // will have already popped this page off the navigation stack.
                    if (!cancellationToken.GetValueOrDefault().IsCancellationRequested)
                    {
                        nextButtonTapped = true;
                        await Navigation.PopModalAsync(stepNumber == totalSteps);
                    }
                }
            };

            previousNextStack.Children.Add(nextButton);

            #endregion

            #endregion

            StackLayout navigationStack = new StackLayout
            {
                Orientation = StackOrientation.Vertical,
                HorizontalOptions = LayoutOptions.FillAndExpand,
                Children = { previousNextStack }
            };

            #region cancel button

            bool cancelButtonTapped = false;

            if (showCancelButton)
            {
                Button cancelButton = new Button
                {
                    HorizontalOptions = LayoutOptions.FillAndExpand,
                    FontSize = 20,
                    Text = "Cancel"
                };

                navigationStack.Children.Add(new BoxView { Color = Color.Gray, HorizontalOptions = LayoutOptions.FillAndExpand, HeightRequest = 0.5 });
                navigationStack.Children.Add(cancelButton);

                cancelButton.Clicked += async (o, e) =>
                {
                    string confirmationMessage = "";

                    if (!string.IsNullOrWhiteSpace(cancelConfirmation))
                        confirmationMessage += cancelConfirmation;

                    if (string.IsNullOrWhiteSpace(confirmationMessage) || await DisplayAlert("Confirm", confirmationMessage, "Yes", "No"))
                    {
                        // if the cancellation token was cancelled while the dialog was up, then we should ignore the dialog. the token
                        // will have already popped this page off the navigation stack (see below).
                        if (!cancellationToken.GetValueOrDefault().IsCancellationRequested)
                        {
                            cancelButtonTapped = true;
                            await Navigation.PopModalAsync(true);
                        }
                    }
                };
            }

            #endregion

            contentLayout.Children.Add(navigationStack);

            #region cancellation token

            bool cancellationTokenCanceled = false;

            if (cancellationToken != null)
            {
                // if the cancellation token is cancelled, pop this page off the stack.
                cancellationToken.GetValueOrDefault().Register(() =>
                    {
                        SensusServiceHelper.Get().Logger.Log("Cancellation token was cancelled. Will pop pages.", LoggingLevel.Normal, GetType());

                        cancellationTokenCanceled = true;

                        Device.BeginInvokeOnMainThread(async() =>
                            {
                                SensusServiceHelper.Get().Logger.Log("On UI thread. Ready to pop page.", LoggingLevel.Normal, GetType());

                                if (Navigation.ModalStack.Count > 0 && Navigation.ModalStack.Last() == this)
                                {
                                    await Navigation.PopModalAsync(true);
                                    SensusServiceHelper.Get().Logger.Log("Popped page.", LoggingLevel.Normal, GetType());
                                }
                                else
                                    SensusServiceHelper.Get().Logger.Log("No page to pop.", LoggingLevel.Normal, GetType());
                            });
                    });
            }

            #endregion

            Appearing += (o, e) =>
            {
                foreach (Input input in displayedInputs)
                    input.Viewed = true;
            };

            Disappearing += (o, e) =>
            {
                if (previousButtonTapped)
                    disappearanceCallback(Result.NavigateBackward);
                else if (cancelButtonTapped || cancellationTokenCanceled)
                    disappearanceCallback(Result.Cancel);
                else if (nextButtonTapped)
                    disappearanceCallback(Result.NavigateForward);
                else
                    disappearanceCallback(Result.Cancel);  // the user navigated back, or another activity started and covered the window
            };

            Content = new ScrollView
            {
                Content = contentLayout
            };
        }
        public void PromptForInputsAsync(string windowTitle, IEnumerable<Input> inputs, CancellationToken? cancellationToken, bool showCancelButton, string nextButtonText, string cancelConfirmation, string incompleteSubmissionConfirmation, string submitConfirmation, bool displayProgress, Action<List<Input>> callback)
        {
            InputGroup inputGroup = new InputGroup(windowTitle);

            foreach (Input input in inputs)
                inputGroup.Inputs.Add(input);

            PromptForInputsAsync(false, DateTimeOffset.MinValue, new InputGroup[] { inputGroup }, cancellationToken, showCancelButton, nextButtonText, cancelConfirmation, incompleteSubmissionConfirmation, submitConfirmation, displayProgress, null, inputGroups =>
                {
                    if (inputGroups == null)
                        callback(null);
                    else
                        callback(inputGroups.SelectMany(g => g.Inputs).ToList());
                });
        }
Exemple #5
0
        public ScriptInputsPage(InputGroup inputGroup, List<InputGroup> previousInputGroups)
        {
            _inputGroup = inputGroup;

            Title = "Inputs";

            _inputsList = new ListView();
            _inputsList.ItemTemplate = new DataTemplate(typeof(TextCell));
            _inputsList.ItemTemplate.SetBinding(TextCell.TextProperty, new Binding(".", stringFormat: "{0}"));
            _inputsList.ItemTapped += async (o, e) =>
            {
                if (_inputsList.SelectedItem == null)
                    return;

                Input selectedInput = _inputsList.SelectedItem as Input;
                int selectedIndex = inputGroup.Inputs.IndexOf(selectedInput);

                List<string> actions = new List<string>();

                if (selectedIndex > 0)
                    actions.Add("Move Up");

                if (selectedIndex < inputGroup.Inputs.Count - 1)
                    actions.Add("Move Down");

                actions.Add("Edit");

                if (previousInputGroups != null && previousInputGroups.Select(previousInputGroup => previousInputGroup.Inputs.Count).Sum() > 0)
                    actions.Add("Add Display Condition");

                if (selectedInput.DisplayConditions.Count > 0)
                    actions.AddRange(new string[]{ "View Display Conditions" });

                actions.Add("Delete");

                string selectedAction = await DisplayActionSheet(selectedInput.Name, "Cancel", null, actions.ToArray());

                if (selectedAction == "Move Up")
                    inputGroup.Inputs.Move(selectedIndex, selectedIndex - 1);
                else if (selectedAction == "Move Down")
                    inputGroup.Inputs.Move(selectedIndex, selectedIndex + 1);
                else if (selectedAction == "Edit")
                {
                    ScriptInputPage inputPage = new ScriptInputPage(selectedInput);
                    inputPage.Disappearing += (oo, ee) =>
                    {
                        Bind();
                    };

                    await Navigation.PushAsync(inputPage);
                    _inputsList.SelectedItem = null;
                }
                else if (selectedAction == "Add Display Condition")
                {
                    string abortMessage = "Condition is not complete. Abort?";

                    SensusServiceHelper.Get().PromptForInputsAsync("Display Condition", new Input[]
                        {
                            new ItemPickerPageInput("Input:", previousInputGroups.SelectMany(previousInputGroup => previousInputGroup.Inputs.Cast<object>()).ToList()),
                            new ItemPickerPageInput("Condition:", Enum.GetValues(typeof(InputValueCondition)).Cast<object>().ToList()),
                            new ItemPickerPageInput("Conjunctive/Disjunctive:", new object[] { "Conjunctive", "Disjunctive" }.ToList())
                        },
                        null,
                        true,
                        null,
                        null,
                        abortMessage,
                        null,
                        false,
                        inputs =>
                        {
                            if (inputs == null)
                                return;

                            if (inputs.All(input => input.Valid))
                            {
                                Input conditionInput = ((inputs[0] as ItemPickerPageInput).Value as IEnumerable<object>).First() as Input;
                                InputValueCondition condition = (InputValueCondition)((inputs[1] as ItemPickerPageInput).Value as IEnumerable<object>).First();
                                bool conjunctive = ((inputs[2] as ItemPickerPageInput).Value as IEnumerable<object>).First().Equals("Conjunctive");

                                if (condition == InputValueCondition.IsComplete)
                                    selectedInput.DisplayConditions.Add(new InputDisplayCondition(conditionInput, condition, null, conjunctive));
                                else
                                {
                                    Regex uppercaseSplitter = new Regex(@"
                                    (?<=[A-Z])(?=[A-Z][a-z]) |
                                    (?<=[^A-Z])(?=[A-Z]) |
                                    (?<=[A-Za-z])(?=[^A-Za-z])", RegexOptions.IgnorePatternWhitespace);

                                    // show the user a required copy of the condition input and prompt for the condition value
                                    Input conditionInputCopy = conditionInput.Copy();
                                    conditionInputCopy.DisplayConditions.Clear();
                                    conditionInputCopy.LabelText = "Value that " + conditionInputCopy.Name + " " + uppercaseSplitter.Replace(condition.ToString(), " ").ToLower() + ":";
                                    conditionInputCopy.Required = true;

                                    SensusServiceHelper.Get().PromptForInputAsync("Display Condition",
                                        conditionInputCopy,
                                        null,
                                        true,
                                        "OK",
                                        null,
                                        abortMessage,
                                        null,
                                        false,
                                        input =>
                                        {
                                            if (input == null)
                                                return;

                                            if (input.Valid)
                                                selectedInput.DisplayConditions.Add(new InputDisplayCondition(conditionInput, condition, input.Value, conjunctive));
                                        });
                                }
                            }
                        });
                }
                else if (selectedAction == "View Display Conditions")
                {
                    await Navigation.PushAsync(new ViewTextLinesPage("Display Conditions", selectedInput.DisplayConditions.Select(displayCondition => displayCondition.ToString()).ToList(), null, async () =>
                            {
                                selectedInput.DisplayConditions.Clear();
                            }));
                }
                else if (selectedAction == "Delete")
                {
                    if (await DisplayAlert("Delete " + selectedInput.Name + "?", "This action cannot be undone.", "Delete", "Cancel"))
                    {
                        _inputGroup.Inputs.Remove(selectedInput);
                        _inputsList.SelectedItem = null;  // manually reset, since it isn't done automatically.
                    }
                }
            };

            ToolbarItems.Add(new ToolbarItem(null, "plus.png", async () =>
                    {
                        List<Input> inputs = Assembly.GetExecutingAssembly()
                            .GetTypes()
                            .Where(t => !t.IsAbstract && t.IsSubclassOf(typeof(Input)))
                            .Select(t => Activator.CreateInstance(t))
                            .Cast<Input>()
                            .OrderBy(i => i.Name)
                            .ToList();

                        string cancelButtonName = "Cancel";
                        string selected = await DisplayActionSheet("Select Input Type", cancelButtonName, null, inputs.Select((input, index) => (index + 1) + ") " + input.Name).ToArray());
                        if (!string.IsNullOrWhiteSpace(selected) && selected != cancelButtonName)
                        {
                            Input input = inputs[int.Parse(selected.Substring(0, selected.IndexOf(")"))) - 1];

                            if (input is VoiceInput && inputGroup.Inputs.Count > 0 || !(input is VoiceInput) && inputGroup.Inputs.Any(i => i is VoiceInput))
                                SensusServiceHelper.Get().FlashNotificationAsync("Voice inputs must reside in groups by themselves.");
                            else
                            {
                                inputGroup.Inputs.Add(input);

                                ScriptInputPage inputPage = new ScriptInputPage(input);
                                inputPage.Disappearing += (o, e) =>
                                {
                                    Bind();
                                };

                                await Navigation.PushAsync(inputPage);
                            }
                        }
                    }));

            Bind();
            Content = _inputsList;
        }
        public PromptForInputsPage(InputGroup inputGroup, int stepNumber, int totalSteps, bool showCancelButton, string nextButtonTextOverride, CancellationToken? cancellationToken, string cancelConfirmation, string incompleteSubmissionConfirmation, string submitConfirmation, bool displayProgress, Action<Result> callback)
        {            
            _displayedInputCount = 0;

            StackLayout contentLayout = new StackLayout
            {
                Orientation = StackOrientation.Vertical,
                VerticalOptions = LayoutOptions.FillAndExpand,
                Padding = new Thickness(0, 20, 0, 0),
                Children =
                {
                    new Label
                    {
                        Text = inputGroup.Name,
                        FontSize = 20,
                        HorizontalOptions = LayoutOptions.CenterAndExpand
                    }
                }
            };

            if (displayProgress)
            {
                float progress = (stepNumber - 1) / (float)totalSteps;

                contentLayout.Children.Add(new Label
                    {
                        Text = "Progress:  " + Math.Round(100 * progress) + "%",
                        FontSize = 15,
                        HorizontalOptions = LayoutOptions.CenterAndExpand
                    });

                contentLayout.Children.Add(new ProgressBar
                    {
                        Progress = progress,
                        HorizontalOptions = LayoutOptions.FillAndExpand
                    });
            }

            int inputSeparatorHeight = 10;

            int viewNumber = 1;
            bool anyRequired = false;
            List<Input> displayedInputs = new List<Input>();
            foreach (Input input in inputGroup.Inputs)
                if (input.Display)
                {
                    View inputView = input.GetView(viewNumber);
                    if (inputView != null)
                    {
                        if (input.Enabled && input.Frame)
                        {
                            inputView = new Frame
                            {
                                Content = inputView,
                                OutlineColor = Color.Accent,
                                VerticalOptions = LayoutOptions.Start,
                                HasShadow = true,
                                Padding = new Thickness(10)
                            };
                        }
                        
                        if (_displayedInputCount > 0)
                            contentLayout.Children.Add(new BoxView { Color = Color.Transparent, HeightRequest = inputSeparatorHeight });
                        
                        contentLayout.Children.Add(inputView);
                        displayedInputs.Add(input);

                        if (input.DisplayNumber)
                            ++viewNumber;

                        if (input.Required)
                            anyRequired = true;

                        ++_displayedInputCount;
                    }
                }

            if (_displayedInputCount > 0)
                contentLayout.Children.Add(new BoxView { Color = Color.Transparent, HeightRequest = inputSeparatorHeight });

            if (anyRequired)
                contentLayout.Children.Add(new Label
                    {
                        Text = "* Required Field",
                        FontSize = 15,
                        TextColor = Color.Red,
                        HorizontalOptions = LayoutOptions.FillAndExpand
                    });

            StackLayout navigationStack = new StackLayout
            {
                Orientation = StackOrientation.Horizontal,
                HorizontalOptions = LayoutOptions.FillAndExpand
            };

            #region previous button

            bool previousButtonTapped = false;

            // step numbers are 1-based -- if we're beyond the first, provide a previous button
            if (stepNumber > 1)
            {
                Button previousButton = new Button
                {
                    HorizontalOptions = LayoutOptions.FillAndExpand,
                    FontSize = 20,
                    Text = "Previous"
                };

                navigationStack.Children.Add(previousButton);

                previousButton.Clicked += async (o, e) =>
                {
                    previousButtonTapped = true;
                    await Navigation.PopModalAsync(false);
                };                      
            }

            #endregion

            #region cancel button

            bool cancelButtonTapped = false;

            if (showCancelButton)
            {
                Button cancelButton = new Button
                {
                    HorizontalOptions = LayoutOptions.FillAndExpand,
                    FontSize = 20,
                    Text = "Cancel"
                };

                navigationStack.Children.Add(cancelButton);

                cancelButton.Clicked += async (o, e) =>
                {
                    string confirmationMessage = "";

                    if (!string.IsNullOrWhiteSpace(cancelConfirmation))
                        confirmationMessage += cancelConfirmation;

                    if (string.IsNullOrWhiteSpace(confirmationMessage) || await DisplayAlert("Confirm", confirmationMessage, "Yes", "No"))
                    {
                        // if the cancellation token was cancelled while the dialog was up, then we should ignore the dialog. the token
                        // will have already popped this page off the navigation stack.
                        if (!cancellationToken.GetValueOrDefault().IsCancellationRequested)
                        {
                            cancelButtonTapped = true;
                            await Navigation.PopModalAsync(true);
                        }
                    }
                };
            }

            #endregion

            #region next button

            Button nextButton = new Button
            {
                HorizontalOptions = LayoutOptions.FillAndExpand,
                FontSize = 20,
                Text = stepNumber < totalSteps ? "Next" : "Submit"

                #if UNIT_TESTING
                // set style id so that we can retrieve the button when unit testing
                , StyleId = "NextButton"
                #endif
            };

            if (nextButtonTextOverride != null)
                nextButton.Text = nextButtonTextOverride;

            navigationStack.Children.Add(nextButton);

            bool nextButtonTapped = false;

            nextButton.Clicked += async (o, e) =>
            {
                string confirmationMessage = "";

                if (!string.IsNullOrWhiteSpace(incompleteSubmissionConfirmation) && !inputGroup.Valid)
                    confirmationMessage += incompleteSubmissionConfirmation;
                else if (nextButton.Text == "Submit" && !string.IsNullOrWhiteSpace(submitConfirmation))
                    confirmationMessage += submitConfirmation;
                    
                if (string.IsNullOrWhiteSpace(confirmationMessage) || await DisplayAlert("Confirm", confirmationMessage, "Yes", "No"))
                {
                    // if the cancellation token was cancelled while the dialog was up, then we should ignore the dialog. the token
                    // will have already popped this page off the navigation stack.
                    if (!cancellationToken.GetValueOrDefault().IsCancellationRequested)
                    {
                        nextButtonTapped = true;
                        await Navigation.PopModalAsync(stepNumber == totalSteps);
                    }
                }
            };

            #endregion
                
            contentLayout.Children.Add(navigationStack);

            #region cancellation token

            bool cancellationTokenCanceled = false;

            if (cancellationToken != null)
            {
                // if the cancellation token is cancelled, pop this page off the stack.
                cancellationToken.GetValueOrDefault().Register(() =>
                    {                        
                        cancellationTokenCanceled = true;

                        Device.BeginInvokeOnMainThread(async() =>
                            {
                                if (Navigation.ModalStack.Count > 0 && Navigation.ModalStack.Last() == this)
                                    await Navigation.PopModalAsync(true);
                            });
                    });
            }

            #endregion

            Appearing += (o, e) =>
            {
                foreach (Input input in displayedInputs)
                    input.Viewed = true;
            };
            
            Disappearing += (o, e) =>
            {
                if (previousButtonTapped)
                    callback(Result.NavigateBackward);
                else if (cancelButtonTapped || cancellationTokenCanceled)
                    callback(Result.Cancel);
                else if (nextButtonTapped)
                    callback(Result.NavigateForward);
                else
                    callback(Result.Cancel);  // the user navigated back, or another activity started
            };                    

            Content = new ScrollView
            {
                Content = contentLayout
            };                        
        }
        public PromptForInputsPage(InputGroup inputGroup, int stepNumber, int totalSteps, Action<Result> callback)
        {
            Title = inputGroup.Name;

            StackLayout contentLayout = new StackLayout
            {
                Orientation = StackOrientation.Vertical,
                VerticalOptions = LayoutOptions.FillAndExpand,
                Padding = new Thickness(0, 20, 0, 0)
            };

            contentLayout.Children.Add(new Label
                {
                    Text = "Step " + stepNumber + " of " + totalSteps,
                    FontSize = 15,
                    HorizontalOptions = LayoutOptions.CenterAndExpand
                });

            contentLayout.Children.Add(new ProgressBar
                {
                    Progress = stepNumber / (double)totalSteps,
                    HorizontalOptions = LayoutOptions.FillAndExpand
                });

            foreach (Input input in inputGroup.Inputs)
                if (input.View != null)
                    contentLayout.Children.Add(input.View);

            StackLayout navigationStack = new StackLayout
            {
                Orientation = StackOrientation.Horizontal,
                HorizontalOptions = LayoutOptions.FillAndExpand
            };

            bool previousButtonTapped = false;

            // step numbers are 1-based -- if we're beyond the first, provide a previous button
            if (stepNumber > 1)
            {
                Button previousButton = new Button
                {
                    HorizontalOptions = LayoutOptions.FillAndExpand,
                    FontSize = 20,
                    Text = "Previous"
                };

                navigationStack.Children.Add(previousButton);

                previousButton.Clicked += async (o, e) =>
                {
                    previousButtonTapped = true;
                    await Navigation.PopModalAsync(false);
                };
            }

            Button cancelButton = new Button
            {
                HorizontalOptions = LayoutOptions.FillAndExpand,
                FontSize = 20,
                Text = "Cancel"
            };

            navigationStack.Children.Add(cancelButton);

            bool cancelButtonTapped = false;

            cancelButton.Clicked += async (o, e) =>
            {
                cancelButtonTapped = true;
                await Navigation.PopModalAsync(true);
            };

            Button nextButton = new Button
            {
                HorizontalOptions = LayoutOptions.FillAndExpand,
                FontSize = 20,
                Text = stepNumber < totalSteps ? "Next" : "Submit"
            };

            navigationStack.Children.Add(nextButton);

            bool nextButtonTapped = false;

            nextButton.Clicked += async (o, e) =>
            {
                nextButtonTapped = true;
                await Navigation.PopModalAsync(stepNumber == totalSteps);
            };

            contentLayout.Children.Add(navigationStack);

            Disappearing += (o, e) =>
            {
                if (previousButtonTapped)
                    callback(Result.NavigateBackward);
                else if (cancelButtonTapped)
                    callback(Result.Cancel);
                else if (nextButtonTapped)
                    callback(Result.NavigateForward);
                else
                    callback(Result.Cancel);  // the user navigated back, or another activity started
            };

            Content = new ScrollView
            {
                Content = contentLayout
            };
        }