Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I make a composite component usable with the designer?

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?

like image 378
R. Martinho Fernandes Avatar asked Apr 12 '11 23:04

R. Martinho Fernandes


2 Answers

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;
    }
like image 119
Miguel Angelo Avatar answered Oct 22 '22 20:10

Miguel Angelo


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;
    }
  }
}
like image 36
Andre Artus Avatar answered Oct 22 '22 21:10

Andre Artus