view ServerMonitor/Forms/CheckForm.cs @ 23:3866c19535fd

Fix NullReferenceException when checks are executed on a brand new server.
author Brad Greco <brad@bgreco.net>
date Thu, 30 May 2019 21:40:27 -0400
parents 48044d9ac000
children e59ec1585616
line wrap: on
line source

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading;
using System.Windows.Forms;
using ServerMonitorApp.Properties;

namespace ServerMonitorApp
{
    /// <summary>Form for creating and editing a check.</summary>
    /// <remarks>
    /// This form contains controls common to editing all types of checks.
    /// Additional controls for specific check types may be shown in a panel below.
    /// </remarks>
    public partial class CheckForm : Form
    {
        private bool helpShown;
        private CancellationTokenSource cancellationTokenSource;
        private Check workCheck;
        private CheckControl checkControl;
        private QuickHelpForm helpForm;
        private Server server;
        private ServerMonitor monitor;

        /// <summary>Raised when the Help button's location on the screen changes.</summary>
        public event EventHandler<HelpLocationChangedEventArgs> HelpLocationChanged;

        /// <summary>The check being edited by the form.</summary>
        /// <remarks>This check object is not updated with the form values until the OK button is clicked.</remarks>
        public Check Check { get; private set; }

        /// <summary>The ID of the check being edited.</summary>
        public int CheckId { get; private set; }

        /// <summary>The Help button's location on the screen.</summary>
        public Point HelpLocation => TypeHelpPictureBox.PointToScreen(Point.Empty);

        /// <summary>Creates a check form to edit an existing check.</summary>
        /// <param name="monitor">The server monitor.</param>
        /// <param name="check">The check to edit.</param>
        public CheckForm(ServerMonitor monitor, Check check)
        {
            InitializeComponent();

            Check = check;
            CheckId = Check.Id;
            server = Check.Server;
            this.monitor = monitor;
        }

        /// <summary>Creates a check form for creating a new check.</summary>
        /// <param name="monitor">The server monitor.</param>
        /// <param name="server">The server the new check will run on.</param>
        public CheckForm(ServerMonitor monitor, Server server)
        {
            InitializeComponent();
            this.server = server;
            this.monitor = monitor;
        }

        private void CheckForm_Load(object sender, EventArgs e)
        {
            // Set up control default values.
            CheckTypeComboBox.Items.AddRange(Check.CheckTypes);
            SeverityComboBox.Items.AddRange(new object[] { CheckStatus.Error, CheckStatus.Warning, CheckStatus.Information });
            SeverityComboBox.SelectedIndex = 1;
            FrequencyUnitsComboBox.DataSource = Enum.GetValues(typeof(FrequencyUnits));
            Helpers.FormatImageButton(RunButton);
            Helpers.FormatImageButton(CancelRunButton);
            Icon = Resources.icon;

            // Bind event listeners.
            Move += CheckForm_Move;
            CheckTypePanel.LocationChanged += CheckTypePanel_LocationChanged;
            GeneralGroupBox.Click += Control_Click;
            CheckSettingsPanel.Click += Control_Click;

            // Set control values from the check.
            CheckTypeComboBox.SelectedItem = Check?.GetType();
            SetTitle();
            if (Check != null)
                LoadCheck(Check);
        }

        /// <summary>Sets the form title from the check, or a default value for a new check.</summary>
        private void SetTitle()
        {
            string name = NameTextBox.Text.IsNullOrEmpty() ? "New Check" : NameTextBox.Text;
            Text = string.Format("{0}: {1}", server.Name, name);
        }

        /// <summary>Handles the check type combo box changing.</summary>
        private void CheckTypeComboBox_SelectedIndexChanged(object sender, EventArgs e)
        {
            // Show the check control for the selected check type.
            ShowCheckControl();
            // Create a temporary instance of the selected check type that will be used for validation
            // and execution that can be discarded if the Cancel button is clicked.
            workCheck = (Check)Activator.CreateInstance((Type)CheckTypeComboBox.SelectedItem);
        }

        /// <summary>Shows the display name of each check type in the combo box.</summary>
        private void CheckTypeComboBox_Format(object sender, ListControlConvertEventArgs e)
        {
            e.Value = Helpers.GetDisplayName((Type)e.ListItem);
        }

        /// <summary>Shows a check control containing settings for the selected check type.</summary>
        private void ShowCheckControl()
        {
            // Hide the existing check control.
            IEnumerable<CheckControl> checkControls = CheckSettingsPanel.Controls.OfType<CheckControl>();
            foreach (CheckControl control in checkControls)
                control.Hide();
            // Show the check control for the selected check type if it has already been created.
            // Allows switching check types without losing data.
            Type type = (Type)CheckTypeComboBox.SelectedItem;
            checkControl = checkControls.FirstOrDefault(c => c.CheckType == type);
            if (checkControl == null)
            {
                // Create a new check control if one has not been created yet for this check type.
                checkControl = CheckControl.Create(type);
                if (checkControl != null)
                {
                    checkControl.Anchor = AnchorStyles.Left | AnchorStyles.Top | AnchorStyles.Right;
                    checkControl.Click += Control_Click;
                    CheckSettingsPanel.Controls.Add(checkControl);
                }
            }
            if (checkControl != null)
            {
                checkControl.Show();
            }
        }

        /// <summary>Populates common check controls from a check.</summary>
        private void LoadCheck(Check check)
        {
            Icon = Check.LastRunStatus.GetIcon();
            NameTextBox.Text = Check.Name;
            EnabledCheckBox.Checked = check.Enabled;
            TimeoutInput.Value = check.Timeout;
            SeverityComboBox.SelectedItem = check.FailStatus;
            FailuresInput.Value = check.MaxConsecutiveFailures;
            FrequencyUnitsComboBox.SelectedItem = check.Schedule.Units;
            FrequencyUpDown.Value = check.Schedule.Frequency;
            StartTimePicker.Value = new DateTime(1970, 1, 1) + check.Schedule.StartTime;
            EndTimePicker.Value = new DateTime(1970, 1, 1) + check.Schedule.EndTime;
            // Populate the controls specific to this check type.
            checkControl?.LoadCheck(check);
        }

        /// <summary>Updates a check with user inputs.</summary>
        /// <param name="check">The check to be updated.</param>
        /// <param name="saving">
        /// Whether the check is being saved.
        /// Checks are validated before being saved and before being executed. This parameter allows
        /// them to distinguish between these cases.
        /// </param>
        /// <returns>Whether the check was updated successfully.</returns>
        private bool UpdateCheck(Check check, bool saving = true)
        {
            // The validation result message. An empty value indicates that validation succeeded.
            string result;
            // Make sure we have a valid check type before doing anything else.
            if (CheckTypeComboBox.SelectedIndex == -1)
            {
                result = "Check type cannot be blank.";
            }
            else
            {
                check.Id = CheckId;
                check.Server = server;
                check.Name = NameTextBox.Text;
                check.Enabled = EnabledCheckBox.Checked;
                check.Timeout = (int)TimeoutInput.Value;
                check.FailStatus = (CheckStatus)SeverityComboBox.SelectedItem;
                check.MaxConsecutiveFailures = (int)FailuresInput.Value;
                check.Schedule = new Schedule((FrequencyUnits)FrequencyUnitsComboBox.SelectedItem, (int)FrequencyUpDown.Value, StartTimePicker.Value.TimeOfDay, EndTimePicker.Value.TimeOfDay);
                // Attempt to update the check from the check control inputs.
                try
                {
                    checkControl?.UpdateCheck(check);
                    // If the update succeeded, run the check's own validation.
                    result = check.Validate(saving);
                }
                catch (UpdateCheckException e)
                {
                    // If the update failed, set the validation message to the error message.
                    result = e.Message;
                }
            }
            if (!result.IsNullOrEmpty())
            {
                MessageBox.Show(result, "Error validating check", MessageBoxButtons.OK, MessageBoxIcon.Error);
                return false;
            }
            return true;
        }

        /// <summary>Saves the check and closes the form.</summary>
        private void OkButton_Click(object sender, EventArgs e)
        {
            if (!UpdateCheck(workCheck))
                return;
            if (Check == null)
            {
                // If this is a new check, just use the check object created by this form.
                Check = workCheck;
            }
            else
            {
                // When editing an existing check, update it now that we know the validation will succeed.
                // Don't replace it with the temporary check object because the temporary check
                // is not a complete copy; some properties are missing such as LastRunStatus.
                UpdateCheck(Check);
            }
            UpdateCheck(Check);
            server.UpdateCheck(Check);
            monitor.SaveServers();
            Close();
        }

        /// <summary>Cancels the form.</summary>
        private void CancelCheckButton_Click(object sender, EventArgs e)
        {
            Close();
        }

        /// <summary>Executes the check with the current user inputs.</summary>
        private async void RunButton_Click(object sender, EventArgs e)
        {
            // If the the inputs are invalid, show the error and return.
            if (!UpdateCheck(workCheck, false))
                return;

            // Update controls with the running status.
            RunButton.Enabled = false;
            RunButton.Text = "Running";
            ResultLabel.Visible = ResultIconPictureBox.Visible = false;
            CancelRunButton.Visible = true;

            // Create a CancellationTokenSource for this execution, and set it to both variables.
            // localCancellationTokenSource will mantain a reference to the token for this execution,
            // while cancellationTokenSource might end up referencing a different token if the Cancel
            // button is clicked (but the Task itself is unable to be cancelled), then Run is clicked
            // again.
            CancellationTokenSource localCancellationTokenSource = new CancellationTokenSource();
            cancellationTokenSource = localCancellationTokenSource;
            // Do not update the check status or execution history when the check is run interactively.
            // No need to pollute the history with manual executions that will likely fail as a
            // check is being configured.
            CheckResult result = await workCheck.ExecuteAsync(cancellationTokenSource.Token, false);
            if (!localCancellationTokenSource.IsCancellationRequested)
                OnRunFinished(result);
            localCancellationTokenSource.Dispose();
            localCancellationTokenSource = null;
        }

        /// <summary>Cancels a check execution.</summary>
        private void CancelRunButton_Click(object sender, EventArgs e)
        {
            // Request the check to cancel. Some check types are not cancellable.
            cancellationTokenSource.Cancel();
            // Immediately update the UI so the user doesn't have to wait
            // if a check is uncancellable.
            OnRunFinished();
        }

        /// <summary>Updates the UI after a check is finished running.</summary>
        /// <param name="result">Result of the check execution. If null, the check was cancelled before it completed.</param>
        private void OnRunFinished(CheckResult result = null)
        {
            RunButton.Enabled = true;
            RunButton.Text = "Run";
            CancelRunButton.Visible = false;
            // If the check was not cancelled, show the results.
            if (result != null)
            {
                ResultLabel.Text = result.Message;
                ResultIconPictureBox.Image = result.CheckStatus.GetImage();
                ResultLabel.Visible = ResultIconPictureBox.Visible = true;
            }
        }

        /// <summary>Gathers the descriptions of all check types to show in the Help window.</summary>
        private string BuildHelpText()
        {
            // Build the string as RTF to allow bold text.
            StringBuilder rtf = new StringBuilder(@"{\rtf1\ansi ");
            foreach (Type checkType in Check.CheckTypes)
            {
                // Show the check type name in bold, and the check type description.
                // Arguments to AppendLine() must end with a \ for the RichTextBox to render the newline character.
                rtf.Append(@"\b ").Append(Helpers.GetDisplayName(checkType)).AppendLine(@"\b0 \")
                    .Append(checkType.GetAttribute<DescriptionAttribute>()?.Description ?? "No description provided.")
                    .AppendLine(@"\").AppendLine(@"\");
            }
            return rtf.ToString().TrimEnd(' ', '\r', '\n', '\\');
        }

        /// <summary>Shows or hides the Help popup window when the Help button is clicked.</summary>
        private void TypeHelpPictureBox_Click(object sender, EventArgs e)
        {
            if (helpShown)
            {
                helpForm.Close();
            }
            else
            {
                helpForm = new QuickHelpForm(BuildHelpText());
                helpForm.FormClosed += QuickHelpForm_FormClosed;
                helpForm.Show(this);
                // Keep focus on this form.
                Activate();
                helpShown = true;
                // Trigger the location changed event so the popup will appear in the correct location.
                OnHelpLocationChanged();
            }
        }

        /// <summary>Handles the closing of the Help popup.</summary>
        private void QuickHelpForm_FormClosed(object sender, FormClosedEventArgs e)
        {
            helpShown = false;
            helpForm.FormClosed -= QuickHelpForm_FormClosed;
            helpForm.Dispose();
        }

        /// <summary>Notifies event subscribers that the location of the Help button on the screen changed.</summary>
        private void OnHelpLocationChanged()
        {
            HelpLocationChanged?.Invoke(this, new HelpLocationChangedEventArgs(HelpLocation));
        }

        /// <summary>The Help button location changes if its panel location changes (such as when the window is resized).</summary>
        private void CheckTypePanel_LocationChanged(object sender, EventArgs e)
        {
            OnHelpLocationChanged();
        }

        /// <summary>The Help button location changes if the window is moved.</summary>
        private void CheckForm_Move(object sender, EventArgs e)
        {
            OnHelpLocationChanged();
        }

        /// <summary>Closes the Help popup when another control is clicked.</summary>
        private void Control_Click(object sender, EventArgs e)
        {
            if (helpShown)
                helpForm.Close();
        }

        /// <summary>Hides the Help popup when ESC is pressed.</summary>
        protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
        {
            if (keyData == Keys.Escape && helpShown)
            {
                helpForm.Close();
                return true;
            }
            return base.ProcessCmdKey(ref msg, keyData);
        }

        /// <summary>Shows appropriate schedule controls depending on the time interval selected.</summary>
        private void FrequencyUnitsComboBox_SelectedIndexChanged(object sender, EventArgs e)
        {
            // Show a single time input for daily schedules.
            // For more frequent schedules, show a time range.
            ScheduleBetweenPanel.Visible = !(ScheduleAtPanel.Visible = FrequencyUnitsComboBox.SelectedIndex == 3);
        }

        /// <summary>Formats the FrequencyUnits enum as a string.</summary>
        private void FrequencyUnitsComboBox_Format(object sender, ListControlConvertEventArgs e)
        {
            e.Value = e.Value.ToString().ToLower() + "s";
        }

        /// <summary>Updates the form title when the check's name is changed..</summary>
        private void NameTextBox_TextChanged(object sender, EventArgs e)
        {
            SetTitle();
        }
    }

    /// <summary>Event arguments containing the location of the Help button.</summary>
    public class HelpLocationChangedEventArgs : EventArgs
    {
        public Point HelpLocation { get; private set; }

        public HelpLocationChangedEventArgs(Point helpLocation)
        {
            HelpLocation = helpLocation;
        }
    }
}