using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Drawing.Design; using System.Globalization; using System.Reflection; using System.Runtime.InteropServices; using Telerik.Data.Expressions; using Telerik.WinControls; using Telerik.WinControls.Data; using Telerik.WinControls.Design; using Telerik.WinControls.UI; namespace Pilz.UI.Telerik.Controls.RadValidationProvider; /// /// Provides a validation management for RadControl descendant editors. /// [ToolboxItem(true)] [TelerikToolboxCategory(ToolboxGroupStrings.EditorsGroup)] [Designer(DesignerConsts.RadValidationProviderDesignerString)] [ProvideProperty("ValidationRule", typeof(RadControl))] [ProvideProperty("IconAlignment", typeof(RadControl))] [ProvideProperty("IconPadding", typeof(RadControl))] public class RadValidationProviderEx : Component, ISupportInitialize, IExtenderProvider { #region Externs [DllImport("user32.dll", EntryPoint = "GetClassLong")] private static extern int GetClassLong(nint hWnd, int nIndex); [DllImport("user32.dll", EntryPoint = "SetClassLong")] private static extern int SetClassLong(nint hWnd, int nIndex, int dwNewLong); #endregion #region Fields private FilterDescriptorCollection validationRules = []; private ValidationMode validationMode = ValidationMode.OnValidating; private Dictionary errorIconAlignments = []; private Dictionary errorIconPadding = []; private Dictionary controlsToToolTips = []; #endregion #region Properties /// /// Occurs before a RadControl is being validated. /// public event RadValidationEventHandlerEx ControlValidation; /// /// Occurs when the ValidionMode property changed. /// public event EventHandler ValidationModeChanged; /// /// Gets or Sets the ValidationMode. /// [DefaultValue(typeof(ValidationMode), "OnValidating")] public ValidationMode ValidationMode { get { return validationMode; } set { if (validationMode != value) { validationMode = value; CallOnValidationChanged(); } } } /// /// Gets or Sets the collection of ValidationRules that belongs to this RadValidationProvider. /// [DesignerSerializationVisibility(DesignerSerializationVisibility.Content), Editor(DesignerConsts.RadValidationProviderItemCollectionDesignerString, typeof(UITypeEditor)), Category(RadDesignCategory.DataCategory)] [Description("Gets a collection representing the Conditions in this ValidationProvider.")] public FilterDescriptorCollection ValidationRules { get { return validationRules; } set { validationRules = value; } } #endregion #region Cstor /// /// Initializes a new instance of the RadValidationProvider class. /// public RadValidationProviderEx() { validationRules.PropertyChanged += ValidationRules_PropertyChanged; } /// /// Initializes a new instance of the RadValidationProvider class with the specified container control. /// /// An object that implements the interface, and owns the created object. public RadValidationProviderEx(IContainer container) : this() { container.Add(this); } #endregion #region Public methods /// /// Remove a specific RadControl from the validation rules. /// /// A RadControl descendant. public void RemoveControlFromRules(RadControl editorControl) { foreach (IRadValidationRuleEx rule in ValidationRules) { RemoveValidationRule(editorControl, rule); } } /// /// Remove a validation rule associated with the specified RadControl descendant. /// /// A RadControl descendant. /// Rule to remove. public void RemoveValidationRule(RadControl editorControl, IRadValidationRuleEx ruleToRemove) { ruleToRemove.RemoveControl(editorControl); } public void BeginInit() { } public void EndInit() { EnsureEventSubscribe(); } /// /// Validates all editors associated with the RadControl. /// /// true if all editors has been successfully validated; otherwise false. public bool ValidateAll() { var valid = true; AssociatedControls.ForEach(control => { if (!Validate(control)) valid = false; }); return valid; } /// /// Validates the specified editor associated with the RadControl. /// /// A RadControl or descendant that represents the editor to be validated. /// true if the editor has been successfully validated; otherwise false. public bool Validate(RadControl control) { return ValidateCore(control, EventArgs.Empty); } /// /// Sets the alignment of an error icon for the specified control. /// /// A target RadControl. /// An value that specifies the alignment to be set for the RadControl. public void SetIconAlignment(RadControl control, ErrorIconAlignment errorIconAlignment) { if (!errorIconAlignments.ContainsKey(control)) errorIconAlignments.Add(control, errorIconAlignment); else { errorIconAlignments[control] = errorIconAlignment; } } /// /// Get the alignment of an error icon for the specified RadControl. /// /// A target control. /// An value. [DefaultValue(ErrorIconAlignment.MiddleRight)] public ErrorIconAlignment GetIconAlignment(RadControl control) { if (errorIconAlignments.ContainsKey(control)) return errorIconAlignments[control]; return ErrorIconAlignment.MiddleRight; } /// Sets the amount of extra space to leave between the specified control and the error icon. /// The to set the padding for. /// The padding to add between the icon and the . public void SetIconPadding(RadControl control, Padding errorIconPadding) { if (!this.errorIconPadding.ContainsKey(control)) this.errorIconPadding.Add(control, errorIconPadding); else { this.errorIconPadding[control] = errorIconPadding; } } /// Returns the amount of extra space to leave next to the error icon. /// The padding to leave between the icon and the control. /// The control to get the padding for. [DefaultValue(typeof(Padding), "1, 1, 1, 1")] public Padding GetIconPadding(RadControl control) { if (errorIconPadding.ContainsKey(control)) return errorIconPadding[control]; return new Padding(1); } protected virtual void OnControlValidation(RadValidationEventArgsEx e) { if (ControlValidation != null) ControlValidation(this, e); } private void ValidationRules_PropertyChanged(object sender, PropertyChangedEventArgs e) { if (e.PropertyName == "Item[]") EnsureEventSubscribe(); } /// /// Removes a rules associated with the control. /// /// A RadControl descendant public void RemoveRules(RadControl control) { for (var i = validationRules.Count - 1; i >= 0; i--) { if (((IRadValidationRuleEx)validationRules[i]).Controls.Contains(control)) validationRules.RemoveAt(i); } } /// /// Gets the collection of the controls whose values are validated. /// [Browsable(false)] public List AssociatedControls { get => [.. ValidationRules.OfType().SelectMany(n => n.Controls).OfType()]; } /// /// Clear the visual indication for the validation error. /// public virtual void ClearErrorStatus() { var controls = AssociatedControls; for (var i = 0; i < controls.Count; i++) { ClearErrorStatus(controls[i]); } } /// /// Clear the visual indication for the validation error. /// /// A RadControl descendant public virtual void ClearErrorStatus(RadControl associatedControl) { var children = associatedControl.RootElement.Children; if (children.Count == 0) return; var editorControl = associatedControl; var controlElement = TryFindControlElement(children);//first non ValidationIconElement is a Main Element for the Control. if (controlElement == null) return; var border = ValidationHelperElement.GetBorder(children); ValidationHelperElement.RestoreBorderColor(border); associatedControl.ElementTree.ApplyThemeToElementTree(); for (var i = children.Count - 1; i >= 0; --i) { if (associatedControl.RootElement.Children[i] is ValidationHelperElement) associatedControl.RootElement.Children.RemoveAt(i); } controlElement.PositionOffset = new SizeF(); controlElement.MaxSize = new Size(); associatedControl.RootElement.InvalidateMeasure(true); associatedControl.RootElement.UpdateLayout(); } #endregion #region Private methods private void AssociatedControl_TextChanged(object sender, EventArgs e) { if (ValidationMode == ValidationMode.OnTextChange) ValidateCore(sender, e); } private void AssociatedControl_Validating(object sender, CancelEventArgs e) { if (ValidationMode == ValidationMode.OnValidating) ValidateCore(sender, e); } internal static object GetSubPropertyValue(object control, string fieldName) { var names = fieldName.Split('.'); if (names.Length < 2) return control.GetType().GetProperty(fieldName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance).GetValue(control, null); var innerValue = control.GetType().GetProperty(names[0], BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance).GetValue(control, null); var index = 1; while (index < names.Length && innerValue != null) { innerValue = innerValue.GetType().GetProperty(names[index], BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance).GetValue(innerValue, null); index++; } return innerValue; } private bool ValidateCore(object sender, EventArgs e) { var control = sender as Control; foreach (IRadValidationRuleEx ruleToEvaluete in ValidationRules) { if (!ruleToEvaluete.Controls.Contains(control)) continue; var context = new ExpressionContext(); context.Clear(); var value = GetSubPropertyValue(control, ruleToEvaluete.PropertyName);//control.GetType().GetProperty(ruleToEvaluete.PropertyName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance).GetValue(control, null); if (value != null && ruleToEvaluete.Value != null) { try { // Fix for 441010 // Try to retrieve Culture from the control var cultureInfo = RetrieveCulture(control); value = Convert.ChangeType(value, ruleToEvaluete.Value.GetType(), cultureInfo); } catch { } } context.Add(ruleToEvaluete.PropertyName, value); if (string.IsNullOrEmpty(ruleToEvaluete.Expression)) continue; var node = DataUtils.Parse(ruleToEvaluete.Expression, ruleToEvaluete.CaseSensitive); var result = node.Eval(null, context); if (result is bool) { var boolResult = (bool)result; var validationEventArgs = FireValidationEvent(!boolResult, (RadControl)sender, ruleToEvaluete); if (validationEventArgs.DisplayIconAndToolTip) boolResult = AddOrRemoveImage(validationEventArgs, (RadControl)sender, ruleToEvaluete); else { boolResult = validationEventArgs.IsValid; } var cancelEventArgs = e as CancelEventArgs; if (cancelEventArgs != null) cancelEventArgs.Cancel = !boolResult; if (!boolResult) return false; } } return true; } private bool AddOrRemoveImage(RadValidationEventArgsEx validationEventArgs, RadControl associatedControl, IRadValidationRuleEx rule) { var children = associatedControl.RootElement.Children; if (children.Count == 0) return true; var editorControl = associatedControl; var controlElement = TryFindControlElement(children);//first non ValidationIconElement is a Main Element for the Control. if (controlElement == null) return true; var element = validationEventArgs.ValidationHelperElement; var border = ValidationHelperElement.GetBorder(children); var truncateSize = false; if (!validationEventArgs.IsValid) { element ??= new ValidationHelperElement(); if (!children.Contains(element)) truncateSize = true;//size of the control should be truncate element.ToolTipText = validationEventArgs.ErrorText; element.AutoToolTip = true; element.StretchHorizontally = element.StretchVertically = false; element.Image = validationEventArgs.ErrorImage; element.SvgImage = validationEventArgs.ErrorSvgImage; element.Alignment = ToContentAligment(GetIconAlignment(editorControl)); element.Padding = GetIconPadding(editorControl); if (truncateSize) { var elementSize = TelerikDpiHelper.ScaleSizeF(MeasurementControl.ThreadInstance.GetDesiredSize(element, new SizeF(float.MaxValue, float.MaxValue)), controlElement.DpiScaleFactor); var width = (controlElement.Size.Width - elementSize.Width) / controlElement.DpiScaleFactor.Width; controlElement.MaxSize = new Size((int)width, controlElement.MaxSize.Height); associatedControl.RootElement.ResetValue(VisualElement.BackColorProperty, ValueResetFlags.Style); if (element.IsRightLocated) children.Add(element); else { controlElement.PositionOffset = new SizeF(elementSize.Width, 0); children.Insert(0, element); } } ValidationHelperElement.SetBorderColor(border); if (rule.AutoToolTip) { var toolTipX = validationEventArgs.ToolTipX.HasValue ? validationEventArgs.ToolTipX.Value : 0; var toolTipY = validationEventArgs.ToolTipY.HasValue ? validationEventArgs.ToolTipY.Value : associatedControl.Height + 1; var toolTipDuration = validationEventArgs.ToolTipDuration.HasValue ? validationEventArgs.ToolTipDuration.Value : 2000; if (validationEventArgs.ToolTip != null) { associatedControl.Disposed -= AssociatedControl_Disposed; associatedControl.Disposed += AssociatedControl_Disposed; if (controlsToToolTips.ContainsKey(associatedControl)) controlsToToolTips[associatedControl] = validationEventArgs.ToolTip; else { controlsToToolTips.Add(associatedControl, validationEventArgs.ToolTip); } if (!validationEventArgs.EnableToolTipShadow) ToolTipRemoveShadow(validationEventArgs.ToolTip); validationEventArgs.ToolTip.Show(element.ToolTipText, associatedControl, toolTipX, toolTipY, toolTipDuration); } else { ShowToolTip(associatedControl, element.ToolTipText, validationEventArgs.ErrorTitle, toolTipX, toolTipY, toolTipDuration, validationEventArgs.EnableToolTipShadow); } } } else { ValidationHelperElement.RestoreBorderColor(border); associatedControl.ElementTree.ApplyThemeToElementTree(); for (var i = children.Count - 1; i >= 0; --i) { if (associatedControl.RootElement.Children[i] is ValidationHelperElement) associatedControl.RootElement.Children.RemoveAt(i); } controlElement.PositionOffset = new SizeF(); controlElement.MaxSize = new Size(); } associatedControl.RootElement.InvalidateMeasure(true); associatedControl.RootElement.UpdateLayout(); associatedControl.Refresh(); return validationEventArgs.IsValid; } private void ToolTipRemoveShadow(ToolTip toolTip) { var hwnd = (nint)typeof(ToolTip).GetProperty("Handle", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(toolTip, null); var cs = GetClassLong(hwnd, NativeMethods.GCL_STYLE); if ((cs & NativeMethods.CS_DROPSHADOW) == NativeMethods.CS_DROPSHADOW) { cs &= ~NativeMethods.CS_DROPSHADOW; SetClassLong(hwnd, NativeMethods.GCL_STYLE, cs); } } private void AssociatedControl_Disposed(object sender, EventArgs e) { var radControl = sender as RadControl; radControl.Disposed -= AssociatedControl_Disposed; if (radControl != null && controlsToToolTips.ContainsKey(radControl)) { var toolTip = controlsToToolTips[radControl]; if (toolTip != null) { controlsToToolTips.Remove(radControl); toolTip.Dispose(); toolTip = null; } } } protected virtual void ShowToolTip(RadControl associatedControl, string toolTipText, string toolTipTitle, int toolTipX, int toolTipY, int toolTipDuration, bool enableToolTipShadow) { var toolTip = new ToolTip { ToolTipTitle = toolTipTitle, BackColor = Color.Red, ForeColor = Color.White, OwnerDraw = true, InitialDelay = 0, AutoPopDelay = toolTipDuration }; if (!enableToolTipShadow) ToolTipRemoveShadow(toolTip); toolTip.Draw += delegate (object sender, DrawToolTipEventArgs e) { e.DrawBackground(); e.DrawBorder(); using (var brush = new SolidBrush(Color.White)) using (var titleFont = new Font("Segoe UI", 8f, FontStyle.Bold)) using (var textFont = new Font("Segoe UI", 8f, FontStyle.Regular)) { e.Graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality; if (!string.IsNullOrEmpty(toolTipTitle)) { e.Graphics.DrawString(toolTipTitle, titleFont, brush, new Point(2, 2)); e.Graphics.DrawString(toolTipText, textFont, brush, new Point(2, 19)); } else { e.Graphics.DrawString(toolTipText, textFont, brush, new Point(2, 2)); } } }; if (controlsToToolTips.ContainsKey(associatedControl)) controlsToToolTips[associatedControl] = toolTip; else { controlsToToolTips.Add(associatedControl, toolTip); } associatedControl.Disposed -= AssociatedControl_Disposed; associatedControl.Disposed += AssociatedControl_Disposed; toolTip.Show(toolTipText, associatedControl, toolTipX, toolTipY, toolTipDuration); } private ContentAlignment ToContentAligment(ErrorIconAlignment errorIconAlignment) { switch (errorIconAlignment) { case ErrorIconAlignment.TopLeft: return ContentAlignment.TopLeft; case ErrorIconAlignment.TopRight: return ContentAlignment.TopRight; case ErrorIconAlignment.MiddleLeft: return ContentAlignment.MiddleLeft; case ErrorIconAlignment.MiddleRight: return ContentAlignment.MiddleRight; case ErrorIconAlignment.BottomLeft: return ContentAlignment.BottomLeft; case ErrorIconAlignment.BottomRight: return ContentAlignment.BottomRight; default: return ContentAlignment.MiddleRight; } } private static RadElement TryFindControlElement(RadElementCollection children) { RadElement controlElement = null; foreach (var child in children) { if (child is not ValidationHelperElement) { controlElement = child; break; } } return controlElement; } protected virtual RadValidationEventArgsEx FireValidationEvent(bool isNotValid, RadControl associatedControl, IRadValidationRuleEx rule) { Image image = ResourceHelper.ImageFromResource(typeof(global::Telerik.WinControls.UI.RadValidationProvider), "Telerik.WinControls.UI.Resources.error-icon.png"); var errorText = string.IsNullOrEmpty(rule.ToolTipText) ? rule.Expression : rule.ToolTipText; var validationEventArgs = new RadValidationEventArgsEx(associatedControl, image, errorText, rule.ToolTipTitle, rule, isNotValid) { ValidationHelperElement = ValidationHelperElement.GetValidationElement(associatedControl.RootElement.Children) }; validationEventArgs.ValidationHelperElement ??= new ValidationHelperElement(); OnControlValidation(validationEventArgs); return validationEventArgs; } private void EnsureEventSubscribe() { foreach (IRadValidationRuleEx rule in ValidationRules) { var controls = rule.Controls; foreach (var control in controls) { if (control != null) { control.Validating -= AssociatedControl_Validating; control.Validating += AssociatedControl_Validating; control.TextChanged -= AssociatedControl_TextChanged; control.TextChanged += AssociatedControl_TextChanged; } } } } private void CallOnValidationChanged() { if (ValidationModeChanged != null) ValidationModeChanged(this, EventArgs.Empty); } private CultureInfo RetrieveCulture(Control control) { var cultureProperty = control.GetType().GetProperty("Culture", BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance); if (cultureProperty != null) { var cultureObject = cultureProperty.GetValue(control, null); if (cultureObject != null && cultureObject is CultureInfo) return cultureObject as CultureInfo; } return CultureInfo.CurrentCulture; } #endregion #region ExtenderProvider Interface implementation /// /// Indicates whether a control can be extended. /// /// The control to be extended. /// true if the control can be extended otherwise false. public bool CanExtend(object extendee) { return extendee is RadControl; } /// /// Associates a validation rule with the specified RadControl descendant. /// /// A RadControl descendant that represents the editor. /// A RadValidationRule descendant that represents the validation rule. [Editor(DesignerConsts.RadValidationRuleEditorString, typeof(UITypeEditor))] public void SetValidationRule(RadControl control, FilterDescriptor rule) { if (rule is not IRadValidationRuleEx radValidationRule) RemoveControlFromRules(control); else radValidationRule.AddControl(control); } /// /// Associates a validation rule with the specified RadControls descendant. /// /// A RadControl collection descendant that represents the editors. /// A RadValidationRule descendant that represents the validation rule. [Editor(DesignerConsts.RadValidationRuleEditorString, typeof(UITypeEditor))] public void SetValidationRule(IEnumerable controls, FilterDescriptor rule) { SetValidationRule(controls.OfType(), rule); } /// /// Associates a validation rule with the specified RadControls descendant. /// /// A RadControl collection descendant that represents the editors. /// A RadValidationRule descendant that represents the validation rule. [Editor(DesignerConsts.RadValidationRuleEditorString, typeof(UITypeEditor))] public void SetValidationRule(IEnumerable controls, FilterDescriptor rule) { foreach (var control in controls) SetValidationRule(control, rule); } /// /// Returns a validation rule associated with the specified RadControl descendant. /// /// A RadControl descendant. /// A RadValidationRule descendant that represents the validation rule associated with the editor. Null if no validation rule is associated with the specified control. [Editor(DesignerConsts.RadValidationRuleEditorString, typeof(UITypeEditor))] public FilterDescriptor GetValidationRule(RadControl control) { IRadValidationRuleEx ruleToEvaluete = null; foreach (IRadValidationRuleEx rule in ValidationRules) { if (rule.Controls.Contains(control)) { ruleToEvaluete = rule; break; } } return ruleToEvaluete as FilterDescriptor; } #endregion }