Mercurial > servermonitor
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; } } }