14

I'm experimenting around with writing custom WinForms components and I wrote a couple of simple validator components for use with a subclass of ErrorProvider that automatically hooks up validation events. All these components can be added to a form and hooked up using only the designer, thanks to IExtenderProvider.

Now in trying to go up one level, I'm trying to get a composite validator to be usable with the designer. I can get it up and working with code, but that's really easy. I'd like to get it to work in a designer-only way.

My difficulty resides in exposing a property that is a collection of other validators that are in the same form. The validators all inherit directly from Component, and implement a IControlValidator interface. I'm open to changing this to have them inherit from a ValidatorComponent base class if it helps.

I thought of a couple solutions, but either I don't like them, or I can't get them to work:

  1. Make the validators into invisible controls, and the have composite validator contain them, similar to what a Panel does;

    This one I don't like because it is more of a hack, and having to juggle them among true controls just feels wrong;

  2. Use a collection editor, as you use for toolbars;

    I looked around the web and found a couple of articles about this, but I couldn't get it to work. At least without building my own editor form, which would be too much of a hassle for an experiment project.

    I admit I didn't spend much time trying this, because I realized using the standard CollectionEditor would lock me down to using a fixed set of validator types (it would, wouldn't it?).

    I also thought of creating a simple ValidatorReference class with a single property of type IControlValidator and use that as the element type for a simple collection editor. I would then add one of these, and in its property grid set the property to an existing validator component. This one seems easy to get working, but loses its appeal because it is such an obvious hack.

Anyone has any other ideas? Is there something I'm missing and this is actually something simple?

R. Martinho Fernandes
  • 228,013
  • 71
  • 433
  • 510
  • You need to write a custom designer or UITypeEditor. Not so sure you'll find this easy going, this sounds awfully close to what's practical to do. – Hans Passant Apr 13 '11 at 00:45
  • Do you want to build a collection of validators from a predefined list of validators, similar to how Columns are added in DataGridView? Do you want it to be a drop target for implementers of IControlValidator, similar to Panel or GroupBox? Or do you want to have it auto-connect to components (that implement IControlValidator) that are dropped on a from? Or do you have something else in mind? – Andre Artus Apr 18 '11 at 19:48
  • Have you tried overrriding `CollectionEditor.CreateNewItemTypes` to return a base class implementation of IControlValidator? – Andre Artus Apr 18 '11 at 19:55
  • @Andre: I would be happy with the first option, but if possible I would like the versatility of using whatever validator I want. Ideally I would have something like the surface where you drop components like Timers and ErrorProviders, where I would drop IControlValidators. Yes, I overrode `CreateNewItemTypes` to return three different types, but the collection editor "Add" button didn't show a drop-down like columns in a DGV:( – R. Martinho Fernandes Apr 18 '11 at 19:55
  • @Martinho: I need to get hold of some of my old code and familarize myself with WinForms components & controls again. I will see what I can do to help. – Andre Artus Apr 18 '11 at 20:37
  • @Andre: thanks for making the effort. You have three days if you want the award :) – R. Martinho Fernandes Apr 18 '11 at 21:26
  • @Martinho: No problem. But while rewards are nice it's not why I answer questions. If I don't make the cut-off I'll still help if I can. – Andre Artus Apr 18 '11 at 21:58
  • Would you like your validators to be a bridge between editor controls and the ErrorProvider component? I.e. you drop a validator component onto the design surface (it's a component; so it does not show on the form) it decorates (IExtenderProvider) suitable editor controls with one or more properties that you use to specify the validation criteria (e.g. Required, RegEx, MinLength, etc.) depending on the validator. And gives you a property to connect to an ErrorProvider. Validator components hook up to OnValidating & OnValidated for suitable controls, calling SetError if validation fails. – Andre Artus Apr 19 '11 at 21:27
  • Using the above method means you dont have to build dialogs. And if you build a new validator you just need to plonk it onto a form where you want to use it. Would that solve your problem? – Andre Artus Apr 19 '11 at 21:30

2 Answers2

7

Why not creating an editor to do this??? You think it sounds an overkill, but actually it is not.

I will demonstrate with a sample.

Sample description

In this sample I will be creating a control named ButtonActivityControl that is abled to make multiple references to other controls in the same form, using a property called Buttons, that is an array of type Button (i.e. Button[]).

The property is marked with a custom editor, that makes it easy to reference the controls in the page. The editor shows a form that consists of a checked list box, that is used to select multiple controls that are in the very same form.

Steps to create the sample

1) a Form called ReferencesCollectionEditorForm

  • place a CheckedListBox inside it,
  • place an 'Ok' button
  • place the following code in the form class

Code of ReferencesCollectionEditorForm:

public partial class ReferencesCollectionEditorForm : Form
{
    public ReferencesCollectionEditorForm(Control[] available, Control[] selected)
    {
        this.InitializeComponent();
        List<Control> sel = new List<Control>(selected);
        this.available = available;
        if (available != null)
            foreach (var eachControl in available)
                this.checkedListBox1.Items.Add(new Item(eachControl),
                    selected != null && sel.Contains(eachControl));
    }

    class Item
    {
        public Item(Control ctl) { this.control = ctl; }
        public Control control;
        public override string ToString()
        {
            return this.control.GetType().Name + ": " + this.control.Name;
        }
    }

    Control[] available;

    public Control[] Selected
    {
        get
        {
            List<Control> selected = new List<Control>(this.available.Length);
            foreach (Item eachItem in this.checkedListBox1.CheckedItems)
                selected.Add(eachItem.control);
            return selected.ToArray();
        }
    }
}

2) an UITypeEditor

Code of ReferencesCollectionEditor:

public class ReferencesCollectionEditor : UITypeEditor
{
    public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value)
    {
        List<Control> available = new List<Control>();

        ButtonActivityControl control = context.Instance as ButtonActivityControl;
        IDesignerHost host = provider.GetService(typeof(IDesignerHost)) as IDesignerHost;
        IComponent componentHost = host.RootComponent;
        if (componentHost is ContainerControl)
        {
            Queue<ContainerControl> containers = new Queue<ContainerControl>();
            containers.Enqueue(componentHost as ContainerControl);
            while (containers.Count > 0)
            {
                ContainerControl container = containers.Dequeue();
                foreach (Control item in container.Controls)
                {
                    if (item != null && context.PropertyDescriptor.PropertyType.GetElementType().IsAssignableFrom(item.GetType()))
                        available.Add(item);
                    if (item is ContainerControl)
                        containers.Enqueue(item as ContainerControl);
                }
            }
        }

        // collecting buttons in form
        Control[] selected = (Control[])value;

        // show editor form
        ReferencesCollectionEditorForm form = new ReferencesCollectionEditorForm(available.ToArray(), selected);

        form.ShowDialog();

        // save new value
        Array result = Array.CreateInstance(context.PropertyDescriptor.PropertyType.GetElementType(), form.Selected.Length);
        for (int it = 0; it < result.Length; it++)
            result.SetValue(form.Selected[it], it);
        return result;
    }
}

3) a control that uses other controls in the same form

Code of custom control:

public class ButtonActivityControl : Control, ISupportInitialize
{
    [Editor(typeof(ReferencesCollectionEditor), typeof(UITypeEditor))]
    public Button[] Buttons { get; set; }

    Dictionary<Button, bool> map = new Dictionary<Button, bool>();

    protected override void OnPaint(PaintEventArgs e)
    {
        e.Graphics.FillRectangle(Brushes.White, e.ClipRectangle);
        if (this.Site != null) return; // this code is needed otherwise designer crashes when closing
        int h = e.ClipRectangle.Height / this.Buttons.Length;
        int top = 0;
        foreach (var button in this.Buttons)
        {
            e.Graphics.FillRectangle(map[button] ? Brushes.Black : Brushes.White, new Rectangle(0, top, e.ClipRectangle.Width, h));
            top += h;
        }
        base.OnPaint(e);
    }

    void ISupportInitialize.BeginInit()
    {
    }

    void ISupportInitialize.EndInit()
    {
        if (this.Site != null) return; // this is needed so that designer does not change the colors of the buttons in design-time
        foreach (var button in this.Buttons)
        {
            button.Click += new EventHandler(button_Click);
            button.ForeColor = Color.Blue;
            map[button] = false;
        }
    }

    void button_Click(object sender, EventArgs e)
    {
        map[(Button)sender] = !map[(Button)sender];
        this.Invalidate();
    }
}

Now create a form that will contain the custom control, place some buttons on it, and then place a ButtonActivityControl on it. The custom control has a property called Buttons, that is editable.

That's it!!

No reason to fear custom Editors... and not so complex.... dit it in half an hour.

I think this is the answer... that is, I think it is! =) Maybe I didn't understand the question well... but thats the best one can do: trying to help others!

EDIT

This code is needed in the ReferencesCollectionEditor:

    public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context)
    {
        return UITypeEditorEditStyle.Modal;
    }
    public override bool GetPaintValueSupported(ITypeDescriptorContext context)
    {
        return false;
    }
Miguel Angelo
  • 23,796
  • 16
  • 59
  • 82
1

This is not production code, I tried to keep it short so it's just enough to illustrate the idea. Initialization and disposal is handled in the .Designer file created by VS2010.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Text.RegularExpressions;
using System.Windows.Forms;

namespace ValidationControls
{
  [ProvideProperty("ErrorMessage", typeof (TextBoxBase))]
  [ProvideProperty("RegEx", typeof (TextBoxBase))]
  public partial class ValidationComponent : Component, IExtenderProvider
  {
    private readonly Dictionary<Control, string> _errorMessages =
      new Dictionary<Control, string>();

    private readonly Dictionary<Control, string> _regExDictionary =
      new Dictionary<Control, string>();

    private TextBoxBase _activeControl;
    private ErrorProvider _errorProvider;

    public ValidationComponent()
    {
      InitializeComponent();
    }

    public ValidationComponent(IContainer container)
    {
      container.Add(this);

      InitializeComponent();
    }

    public ErrorProvider ErrorProvider
    {
      get { return _errorProvider; }
      set { _errorProvider = value; }
    }

    #region IExtenderProvider Members

    public bool CanExtend(object extendee)
    {
      return extendee is TextBoxBase;
    }

    #endregion

    [DefaultValue("")]
    [Category("Validation")]
    public string GetRegEx(TextBoxBase control)
    {
      string value;
      return _regExDictionary.TryGetValue(control, out value) ? value : string.Empty;
    }

    [Category("Validation")]
    public void SetRegEx(TextBoxBase control, string value)
    {
      if (string.IsNullOrWhiteSpace(value))
      {
        _regExDictionary.Remove(control);

        control.Validating -= OnControlValidating;
        control.Validated -= OnControlValidated;
      }
      else
      {
        _regExDictionary[control] = value;

        control.Validating += OnControlValidating;
        control.Validated += OnControlValidated;
      }
    }

    [Category("Validation")]
    public string GetErrorMessage(TextBoxBase control)
    {
      string value;
      return _errorMessages.TryGetValue(control, out value) ? value : string.Empty;
    }

    [Category("Validation")]
    public void SetErrorMessage(TextBoxBase control, string value)
    {
      if (string.IsNullOrWhiteSpace(value))
      {
        _errorMessages.Remove(control);
      }
      else
      {
        _errorMessages[control] = value;
      }
    }

    private void OnControlValidating(object sender, CancelEventArgs e)
    {
      _activeControl = (TextBoxBase) sender;
      var regExPattern = GetRegEx(_activeControl);

      if (Regex.IsMatch(_activeControl.Text, regExPattern, RegexOptions.Singleline))
        return;
      e.Cancel = true;

      var errorMsg = GetErrorMessage(_activeControl);

      if (_errorProvider != null)
        _errorProvider.SetError(_activeControl, errorMsg);
    }

    private void OnControlValidated(object sender, EventArgs e)
    {
      if (sender != _activeControl)
        return;
      if (_errorProvider != null)
        _errorProvider.SetError(_activeControl, "");
      _activeControl = null;
    }
  }
}
Andre Artus
  • 1,850
  • 15
  • 21