using System; using System.Collections.Generic; using System.ComponentModel; using System.Drawing; using System.Linq; using System.Reflection; using System.Windows.Forms; namespace Vanara.Windows.Forms { /// /// An input dialog that automatically creates controls to collect the values of the object supplied via the property. /// public class InputDialog : CommonDialog { private object data; /// /// Gets or sets the data for the input dialog box. The data type will determine the type of input mechanism displayed. For simple /// types, a with validation, or a or a will be displayed. For /// classes and structures, all of the public, top-level, fields and properties will have input mechanisms shown for each. See /// Remarks for more detail. /// /// The data for the input dialog box. /// TBD [DefaultValue(null), Browsable(false), Category("Data"), Description("The data for the input dialog box.")] public object Data { get => data; set => data = value; } /// /// Gets or sets the image to display on the top left corner of the dialog. This value can be null to display no image. /// /// The image to display on the top left corner of the dialog. [DefaultValue(null), Category("Appearance"), Description("The image to display on the top left corner of the dialog.")] [Localizable(true)] public Image Image { get; set; } /// Gets or sets the text prompt to display above all input options. This value can be null. /// The text prompt to display above all input options. [DefaultValue(null), Category("Appearance"), Description("The text prompt to display above all input options.")] [Localizable(true), Bindable(true), Editor(typeof(System.ComponentModel.Design.MultilineStringEditor), typeof(System.Drawing.Design.UITypeEditor))] public string Prompt { get; set; } /// Gets or sets the input dialog box title. /// The input dialog box title. The default value is an empty string (""). [DefaultValue(""), Category("Window"), Description("The input dialog box title.")] [Localizable(true), Bindable(true)] public string Title { get; set; } = ""; /// Displays an input dialog in front of the specified object and with the specified prompt, caption, data, and image. /// An implementation of that will own the modal dialog box. /// The text prompt to display above all input options. This value can be null. /// The caption for the dialog. /// /// The data for the input. The data type will determine the type of input mechanism displayed. For simple types, a /// with validation, or a or a will be displayed. For classes /// and structures, all of the public, top-level, fields and properties will have input mechanisms shown for each. See Remarks for /// more detail. /// /// /// The image to display on the top left corner of the dialog. This value can be null to display no image. /// /// The desired width of the . A value of 0 indicates a default width. /// /// Either or . On OK, the parameter will /// include the updated values from the . /// /// public static DialogResult Show(IWin32Window owner, string prompt, string caption, ref object data, Image image = null, int width = 0) { using var dlg = new InternalInputDialog(prompt, caption, image, data, width); var ret = owner == null ? dlg.ShowDialog() : dlg.ShowDialog(owner); if (ret == DialogResult.OK) data = dlg.Data; return ret; } /// Displays an input dialog with the specified prompt, caption, data, and image. /// The text prompt to display above all input options. This value can be null. /// The caption for the dialog. /// /// The data for the input. The data type will determine the type of input mechanism displayed. For simple types, a /// with validation, or a or a will be displayed. For classes /// and structures, all of the public, top-level, fields and properties will have input mechanisms shown for each. See Remarks for /// more detail. /// /// /// The image to display on the top left corner of the dialog. This value can be null to display no image. /// /// The desired width of the . A value of 0 indicates a default width. /// /// Either or . On OK, the parameter will /// include the updated values from the . /// /// public static DialogResult Show(string prompt, string caption, ref object data, Image image = null, int width = 0) => Show(null, prompt, caption, ref data, image, width); /// Resets all properties to their default values. public override void Reset() { } /// /// This API supports the.NET Framework infrastructure and is not intended to be used directly from your code. /// Specifies a common dialog box. /// /// A value that represents the window handle of the owner window for the common dialog box. /// true if the data was collected; otherwise, false. protected override bool RunDialog(IntPtr hwndOwner) => Show(NativeWindow.FromHandle(hwndOwner), Prompt, Title, ref data, Image) == DialogResult.OK; /// Get input based on automatic interpretation of Data object. internal class InternalInputDialog : Form { private const int prefWidth = 340; private static readonly Dictionary keyPressValidChars = new Dictionary { [typeof(byte)] = GetCultureChars(true, false, true), [typeof(sbyte)] = GetCultureChars(true, true, true), [typeof(short)] = GetCultureChars(true, true, true), [typeof(ushort)] = GetCultureChars(true, false, true), [typeof(int)] = GetCultureChars(true, true, true), [typeof(uint)] = GetCultureChars(true, false, true), [typeof(long)] = GetCultureChars(true, true, true), [typeof(ulong)] = GetCultureChars(true, false, true), [typeof(double)] = GetCultureChars(true, true, true, true, true, true), [typeof(float)] = GetCultureChars(true, true, true, true, true, true), [typeof(decimal)] = GetCultureChars(true, true, true, true, true), [typeof(TimeSpan)] = GetCultureChars(true, true, false, new[] { '-' }), [typeof(Guid)] = GetCultureChars(true, false, false, "-{}()".ToCharArray()), }; private static readonly Size minSize = new Size(193, 104); private static readonly Type[] simpleTypes = { typeof(Enum), typeof(decimal), typeof(DateTime), typeof(DateTimeOffset), typeof(string), typeof(TimeSpan), typeof(Guid) }; private static readonly Dictionary> validations = new Dictionary> { [typeof(byte)] = s => byte.TryParse(s, out var _), [typeof(sbyte)] = s => sbyte.TryParse(s, out var _), [typeof(short)] = s => short.TryParse(s, out var _), [typeof(ushort)] = s => ushort.TryParse(s, out var _), [typeof(int)] = s => int.TryParse(s, out var _), [typeof(uint)] = s => uint.TryParse(s, out var _), [typeof(long)] = s => long.TryParse(s, out var _), [typeof(ulong)] = s => ulong.TryParse(s, out var _), [typeof(char)] = s => char.TryParse(s, out var _), [typeof(double)] = s => double.TryParse(s, out var _), [typeof(float)] = s => float.TryParse(s, out var _), [typeof(decimal)] = s => decimal.TryParse(s, out var _), [typeof(DateTime)] = s => DateTime.TryParse(s, out var _), [typeof(TimeSpan)] = s => TimeSpan.TryParse(s, out var _), [typeof(Guid)] = s => { try { var n = new Guid(s); return true; } catch { return false; } }, }; private readonly List items = new List(); private Panel borderPanel; private TableLayoutPanel buttonPanel; private Button cancelBtn; private IContainer components; private object dataObj; private ErrorProvider errorProvider; private Image image; private Button okBtn; private string prompt; private TableLayoutPanel table; /// Initializes a new instance of the class. public InternalInputDialog() => InitializeComponent(); internal InternalInputDialog(string prompt, string caption, Image image, object data, int width) : this() { Width = width; this.prompt = prompt; Text = caption; this.image = image; Data = data; } /// Gets or sets the data. /// The data. [DefaultValue(null), Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public object Data { get => dataObj; set { if (value == null) value = string.Empty; items.Clear(); if (IsSimpleType(value.GetType())) items.Add(null); else { foreach (var mi in value.GetType().GetMembers(BindingFlags.Instance | BindingFlags.Public)) { if (GetAttr(mi)?.Hidden ?? false) continue; if (mi is FieldInfo fi && IsSupportedType(fi.FieldType)) { items.Add(fi); } else if (mi is PropertyInfo pi && IsSupportedType(pi.PropertyType) && pi.GetIndexParameters().Length == 0 && pi.CanWrite) { items.Add(pi); } } items.Sort((x, y) => (GetAttr(x)?.Order ?? int.MaxValue) - (GetAttr(y)?.Order ?? int.MaxValue)); } dataObj = value; BuildTable(); } } /// Gets or sets the image. /// The image. [DefaultValue(null)] public Image Image { get => image; set { if (image == value) return; image = value; BuildTable(); } } /// Gets or sets the prompt. /// The prompt. [DefaultValue(null)] public string Prompt { get => prompt; set { if (prompt == value) return; prompt = value; BuildTable(); } } /// Gets or sets the width of the control. public new int Width { get => base.Width; set { if (value == 0) value = prefWidth; value = Math.Max(minSize.Width, value); MinimumSize = new Size(value, minSize.Height); MaximumSize = new Size(value, int.MaxValue); } } private bool HasPrompt => !string.IsNullOrEmpty(Prompt); private static object ConvertFromStr(string value, Type destType) { if (destType == typeof(string)) return value; if (value.Trim() == string.Empty) return destType.IsValueType ? Activator.CreateInstance(destType) : null; if (typeof(IConvertible).IsAssignableFrom(destType)) try { return Convert.ChangeType(value, destType); } catch { } return TypeDescriptor.GetConverter(destType).ConvertFrom(value); } private static string ConvertToStr(object value) { return value switch { null => string.Empty, IConvertible _ => value.ToString(), _ => (string)TypeDescriptor.GetConverter(value).ConvertTo(value, typeof(string)), }; } private static int GetBestHeight(Control c) { using var g = c.CreateGraphics(); return TextRenderer.MeasureText(g, c.Text, c.Font, new Size(c.Width, 0), TextFormatFlags.WordBreak).Height; } private static char[] GetCultureChars(bool digits, bool neg, bool pos, bool dec = false, bool grp = false, bool e = false) { var c = System.Globalization.CultureInfo.CurrentCulture.NumberFormat; var l = new List(); if (digits) l.AddRange(c.NativeDigits); if (neg) l.Add(c.NegativeSign); if (pos) l.Add(c.PositiveSign); if (dec) l.Add(c.NumberDecimalSeparator); if (grp) l.Add(c.NumberGroupSeparator); if (e) l.Add("Ee"); var sb = new System.Text.StringBuilder(); foreach (var s in l) sb.Append(s); var ca = sb.ToString().ToCharArray(); Array.Sort(ca); return ca; } private static char[] GetCultureChars(bool timeChars, bool timeSep, bool dateSep, char[] other) { var c = System.Globalization.CultureInfo.CurrentCulture; var l = new List(); if (timeChars) l.AddRange(c.NumberFormat.NativeDigits); if (timeSep) { l.Add(c.DateTimeFormat.TimeSeparator); l.Add(c.NumberFormat.NumberDecimalSeparator); } if (dateSep) l.Add(c.DateTimeFormat.DateSeparator); if (other != null && other.Length > 0) l.Add(new string(other)); var sb = new System.Text.StringBuilder(); foreach (var s in l) sb.Append(s); var ca = sb.ToString().ToCharArray(); Array.Sort(ca); return ca; } private static bool IsSimpleType(Type type) => type.IsPrimitive || type.IsEnum || simpleTypes.Contains(type) || Convert.GetTypeCode(type) != TypeCode.Object || type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>) && IsSimpleType(type.GetGenericArguments()[0]); private static bool IsSupportedType(Type type) { if (typeof(IConvertible).IsAssignableFrom(type)) return true; var cvtr = TypeDescriptor.GetConverter(type); return cvtr.CanConvertFrom(typeof(string)) && cvtr.CanConvertTo(typeof(string)); } /// Binds input text values back to the Data object. private void BindToData() { for (var i = 0; i < items.Count; i++) { var item = items[i]; var itemType = GetItemType(item); // Get value from control var c = table.Controls[$"input{i}"]; var box = c as CheckBox; var val = box?.Checked ?? ConvertFromStr(c.Text, itemType); // Apply value to dataObj if (item == null) dataObj = val; else if (item is PropertyInfo) ((PropertyInfo)item).SetValue(dataObj, val, null); else ((FieldInfo)item).SetValue(dataObj, val); } } private Control BuildInputForItem(int i) { var item = items[i]; var itemType = GetItemType(item); // Get default text value object val; if (item == null) val = dataObj; else if (item is PropertyInfo) val = ((PropertyInfo)item).GetValue(dataObj, null); else val = ((FieldInfo)item).GetValue(dataObj); var t = ConvertToStr(val); // Build control type Control retVal; if (itemType == typeof(bool)) { retVal = new CheckBox { AutoSize = true, Checked = (bool)val, Margin = new Padding(0, 7, 0, 0), MinimumSize = new Size(0, 20) }; } else if (itemType.IsEnum) { var cb = new ComboBox { Dock = DockStyle.Fill, DropDownStyle = ComboBoxStyle.DropDownList }; cb.Items.AddRange(Enum.GetNames(itemType)); cb.Text = t; retVal = cb; } else { var tb = new TextBox { CausesValidation = true, Dock = DockStyle.Fill, Text = t, UseSystemPasswordChar = GetAttr(item)?.UsePasswordChar ?? false }; tb.Enter += (s, e) => tb.SelectAll(); if (itemType == typeof(char)) tb.KeyPress += (s, e) => e.Handled = !char.IsControl(e.KeyChar) && tb.TextLength > 0; else tb.KeyPress += (s, e) => e.Handled = IsInvalidKey(e.KeyChar, itemType); tb.Validating += (s, e) => { var invalid = TextIsInvalid(tb, itemType); e.Cancel = invalid; errorProvider.SetError(tb, invalid ? $"Text must be in a valid format for {itemType.Name}." : ""); }; tb.Validated += (s, e) => errorProvider.SetError(tb, ""); errorProvider.SetIconPadding(tb, -18); retVal = tb; } // Set standard props // TODO: Change out '7' for DPI specific spacing retVal.Margin = new Padding(items.Count == 1 && HasPrompt && items[0] == null ? 4 : 0, 7, 0, 0); retVal.Name = $"input{i}"; return retVal; } private Label BuildLabelForItem(int i) { var item = items[i]; var lbl = new Label { AutoSize = true, Dock = DockStyle.Left, Margin = new Padding(0, 0, 1, 0) }; if (item != null) { lbl.Text = (GetAttr(item)?.Label ?? item.Name) + ":"; // TODO: Change out '10' for spacing needed to align label text with TextBox and '4' for DPI specific spacing lbl.Margin = new Padding(0, 10, 4, 0); } return lbl; } private void BuildTable() { table.SuspendLayout(); // Clear out last layout table.Controls.Clear(); while (table.RowStyles.Count > 1) table.RowStyles.RemoveAt(1); table.RowCount = items.Count + (HasPrompt ? 1 : 0); // Icon if (Image != null) { table.Controls.Add(new PictureBox { Image = Image, Size = Image.Size, Margin = new Padding(0, 0, 7, 0), TabStop = false }, 0, 0); table.SetRowSpan(table.GetControlFromPosition(0, 0), table.RowCount); } var hrow = 0; // Add header row if needed Label lbl = null; if (HasPrompt) { lbl = new Label { AutoSize = true, Text = Prompt, Dock = DockStyle.Top, UseMnemonic = false, Font = new Font(Font.FontFamily, Font.Size * 4 / 3), ForeColor = Color.FromArgb(19, 112, 171), Margin = new Padding(items.Count == 1 && items[0] == null ? 1 : 0, 0, 0, 0) }; table.Controls.Add(lbl, 1, hrow++); table.SetColumnSpan(lbl, 2); } // Build rows for each item for (var i = 0; i < items.Count; i++) { if (i + hrow > 0) table.RowStyles.Add(new RowStyle(SizeType.AutoSize)); table.Controls.Add(BuildLabelForItem(i), 1, i + hrow); table.Controls.Add(BuildInputForItem(i), 2, i + hrow); } table.ResumeLayout(); if (HasPrompt && lbl != null && lbl.PreferredWidth > lbl.Width) lbl.MinimumSize = lbl.Size; } private void CancelBtn_Click(object sender, EventArgs e) => Close(); private InputDialogItemAttribute GetAttr(MemberInfo mi) => mi is null ? null : (InputDialogItemAttribute)Attribute.GetCustomAttribute(mi, typeof(InputDialogItemAttribute), true); private Type GetItemType(MemberInfo mi) => mi == null ? dataObj.GetType() : ((mi as PropertyInfo)?.PropertyType ?? ((FieldInfo)mi).FieldType); private void InitializeComponent() { components = new Container(); buttonPanel = new TableLayoutPanel(); okBtn = new Button(); cancelBtn = new Button(); borderPanel = new Panel(); table = new TableLayoutPanel(); errorProvider = new ErrorProvider(components); buttonPanel.SuspendLayout(); ((ISupportInitialize)errorProvider).BeginInit(); SuspendLayout(); // buttonPanel buttonPanel.AutoSize = true; buttonPanel.AutoSizeMode = AutoSizeMode.GrowAndShrink; buttonPanel.BackColor = SystemColors.Control; buttonPanel.ColumnCount = 3; buttonPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F)); buttonPanel.ColumnStyles.Add(new ColumnStyle()); buttonPanel.ColumnStyles.Add(new ColumnStyle()); buttonPanel.Controls.Add(okBtn, 1, 0); buttonPanel.Controls.Add(cancelBtn, 2, 0); buttonPanel.Dock = DockStyle.Bottom; buttonPanel.Location = new Point(0, 25); buttonPanel.Margin = new Padding(0); buttonPanel.MinimumSize = new Size(177, 40); buttonPanel.Name = "buttonPanel"; buttonPanel.Padding = new Padding(10, 8, 10, 9); buttonPanel.RowCount = 1; buttonPanel.RowStyles.Add(new RowStyle()); buttonPanel.Size = new Size(177, 40); buttonPanel.TabIndex = 1; // okBtn okBtn.Location = new Point(10, 8); okBtn.Margin = new Padding(0, 0, 7, 0); okBtn.MinimumSize = new Size(75, 23); okBtn.Name = "okBtn"; okBtn.Size = new Size(75, 23); okBtn.TabIndex = 1; okBtn.Text = "OK"; okBtn.UseVisualStyleBackColor = true; okBtn.Click += new EventHandler(OkBtn_Click); // cancelBtn cancelBtn.DialogResult = DialogResult.Cancel; cancelBtn.Location = new Point(92, 8); cancelBtn.Margin = new Padding(0); cancelBtn.MinimumSize = new Size(75, 23); cancelBtn.Name = "cancelBtn"; cancelBtn.Size = new Size(75, 23); cancelBtn.TabIndex = 2; cancelBtn.Text = "&Cancel"; cancelBtn.UseVisualStyleBackColor = true; cancelBtn.Click += new EventHandler(CancelBtn_Click); // borderPanel borderPanel.BackColor = Color.FromArgb(223, 223, 223); borderPanel.Dock = DockStyle.Bottom; borderPanel.Location = new Point(0, 24); borderPanel.Margin = new Padding(0); borderPanel.MinimumSize = new Size(0, 1); borderPanel.Name = "borderPanel"; borderPanel.Size = new Size(177, 1); borderPanel.TabIndex = 3; // table table.AutoSize = true; table.AutoSizeMode = AutoSizeMode.GrowAndShrink; table.ColumnCount = 3; table.ColumnStyles.Add(new ColumnStyle()); table.ColumnStyles.Add(new ColumnStyle()); table.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F)); table.Dock = DockStyle.Fill; table.Location = new Point(0, 0); table.Margin = new Padding(0); table.Name = "table"; table.Padding = new Padding(10); table.RowCount = 1; table.RowStyles.Add(new RowStyle()); table.Size = new Size(177, 24); table.TabIndex = 0; // errorProvider errorProvider.ContainerControl = this; // InternalInputDialog AcceptButton = okBtn; AutoSize = true; AutoSizeMode = AutoSizeMode.GrowAndShrink; BackColor = SystemColors.Window; CancelButton = cancelBtn; ClientSize = new Size(prefWidth, 65); Controls.Add(table); Controls.Add(borderPanel); Controls.Add(buttonPanel); Font = new Font("Segoe UI", 9F, FontStyle.Regular, GraphicsUnit.Point, 0); FormBorderStyle = FormBorderStyle.FixedDialog; MinimumSize = new Size(prefWidth, minSize.Height); MaximumSize = new Size(prefWidth, int.MaxValue); Name = "InternalInputDialog"; StartPosition = FormStartPosition.CenterParent; buttonPanel.ResumeLayout(false); ((ISupportInitialize)errorProvider).EndInit(); ResumeLayout(false); PerformLayout(); } private bool IsInvalidKey(char keyChar, Type itemType) { if (char.IsControl(keyChar)) return false; keyPressValidChars.TryGetValue(itemType, out var chars); if (chars != null) { var si = Array.BinarySearch(chars, keyChar); System.Diagnostics.Debug.WriteLine($"Processed key {keyChar} as {si} position."); if (si < 0) return true; } return false; } private void OkBtn_Click(object sender, EventArgs e) { BindToData(); DialogResult = DialogResult.OK; Close(); } private bool TextIsInvalid(TextBox tb, Type itemType) { if (string.IsNullOrEmpty(tb.Text)) return false; validations.TryGetValue(itemType, out var p); return p != null && !p(tb.Text); } private class RegexTextBox : TextBox { public string RegexPattern { get; set; } protected override void OnKeyPress(KeyPressEventArgs e) => //System.Text.RegularExpressions.Regex.IsMatch() base.OnKeyPress(e); } } } /// /// Allows a developer to attribute a property or field with text that gets shown instead of the field or property name in an . /// [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)] public sealed class InputDialogItemAttribute : Attribute { /// Initializes a new instance of the class. public InputDialogItemAttribute() { } /// Initializes a new instance of the class. /// The label to use in the as the label for this field or property. public InputDialogItemAttribute(string label) => Label = label; /// Gets or sets a value indicating whether this item is hidden and not displayed by the . /// true if hidden; otherwise, false. public bool Hidden { get; set; } = false; /// Gets or sets the label to use in the as the label for this field or property. /// The label for this item. public string Label { get; } /// Gets or sets the order in which to display the input for this field or property within the . /// The display order for this item. public int Order { get; set; } = int.MaxValue; /// Gets or sets a value indicating whether text boxes should display text using the system default password character. /// true if using a password character; otherwise, false. public bool UsePasswordChar { get; set; } = false; } }