Click here to Skip to main content
15,886,362 members
Articles / Desktop Programming / Windows Forms

Building multi-control components using IExtenderProvider

Rate me:
Please Sign up or sign in to vote.
4.50/5 (2 votes)
10 Feb 2009BSD5 min read 29.4K   276   23   6
In this article, I will show you a technique for building multi-control components using the IExtenderProvider.

Image 1

Introduction

This article is about creating multi-control components, but this time. instead of inheriting from an existing control (which I showed here), we will use IExtenderProvider. I ask you for your opinion about which technique is better, pros and cons etc...

Why am I writing this article...

After my article about multi-control components, I got a feedback from a guy named BarCode to try the LabelProvider from 'Robert Verpalen a.k.a Hotdog' (by the way, if have some spare time, check his page, he's got some good code there, but lacks documentation quite a bit). So, I downloaded and tried it... and I got some errors in the designer. I tried to fix them, but without any documentation, it was more like trial-end-error type of coding. Then, I found nice article on CP written by James T. Johnson, and after that, I decided to write my own LabelProvider.

Perquisites

First of all, my extender uses techniques that I explained here, so prior to reading this, you should be familiar with that article. In brief, I'm not using painting with labels... I actually put labels on the form. Also, you should be familiar with James' article about IExtenderProvider.

LabelProvider - How does it work

I will break it down into pieces, and after that, I will give the whole code of the component:

  1. Contructors, helper classes, lists, hashtables.
  2. We're going to have only one property, and that will be:

    C#
    // pretty much standard
    [ProvideProperty("AfpLabel", typeof(Control))]

    Next, we have a helper class. This is very important because this is the core for our control <-> label mapping. I decided that I will base my mapping on GetHashCode(). I know this is not 100 % secure, but to this day, I could not figure out a better way for doing this. I have been thinking about some combinations like hashcode + name, but the name disappears on some events like Dispose. I mean, I have some control, and after I fire some events, the hash code stays the same, but the name == String.Empty and I really don't know why. So, if you have a better technique in making some unique footprint of the control, send me some feedback.

    Next, we will make a List of objects and a variable called activeControl. This variable will be used to keep track of what control fired events, which one is now moving etc. You don't need to worry about handling two controls at once, because Windows will take their events sequentially (so no need for a List or something like that).

    C#
    protected class AfpLabelInfo
    {
        // label that will be drawn next to control
        protected Label _lab = new Label();
        // hash code of control so we know which labels goes to wchich control
        protected int _controlHash;
        public Label lab
        {
            get { return _lab; }
            set { _lab = value; }
        }
        public int controlHash
        {
            get { return _controlHash; }
            set { _controlHash = value; }
        }
        // we will take hashcode of each control
        public AfpLabelInfo(int AcontrolName)
        {
            _controlHash = AcontrolName;
        }
    }
    // remember to use one HashTable per property
    protected Hashtable AfpLabelProps = new Hashtable();
    protected List<afplabelinfo> _labels = new List<afplabelinfo>();
    protected Control activeControl;
  3. Control <-> label mapping methods.
  4. All thes methods operate on our AfpLabelInfo objects. These can be also implemented as public interfaces of the AfpLabelInfo class.

    • getIndexFromLabels takes the active control and returns the index in the list so we know which label is associated with Control.
    • RemoveControlFromLabels is fired when Dispose takes place and makes sure there are no phantoms on the Form.
    • AddOrRenewLabel is fired when you type something inside a property. The label is mapped to Control through a hash code.
    C#
    protected int getIndexFromLabels(Control cnt)
    {
        for (int i = 0; i < _labels.Count; i++)
        {
            if (_labels[i].controlHash == (cnt.GetHashCode()) )
                return i;
        }
        return -1;
    }
    protected void RemoveControlFromLabels(Control cnt)
    {
        int i = getIndexFromLabels(cnt);
        if (i != -1)
        {
            _labels[i].lab.Dispose();
            _labels.RemoveAt(i);
        }
    }
    protected void AddOrRenewLabel(Control cnt)
    {
        int dummy = cnt.GetHashCode();
        for (int i = 0; i < _labels.Count; i++)
        {
            if (_labels[i].controlHash == (dummy))
            {
                if (_labels[i].lab == null)
                {
                    _labels[i].lab = new Label();
                    return;
                }
            }
        }
        
        _labels.Add(new AfpLabelInfo(dummy));
        // if there was no control we add it manually
    }
  5. Getters and setters.
  6. These are pretty much standard, except that when you type something, it will hook three events: ParentChanged, LocationChanged, and Dispose. activeControl is the key to keeping track of which control actually fired event. We have to manually fire a method that hooks to ParentChanged since when we add a property, the parent change has already occurred...

    C#
    public string GetAfpLabel(Control c)
    {
        string text = (string)AfpLabelProps[c];
        if (text == null)
        {
            text = String.Empty;
        }
        return text;
    }
    public void SetAfpLabel(Control c, string value)
    {
        activeControl = c;
        AfpLabelProps[c] = value;
        if (value == String.Empty)
        {
            c.Disposed -= new EventHandler(c_Disposed);
            c.ParentChanged -= new EventHandler(val_ParentChanged);
            c.LocationChanged -= new EventHandler(val_LocationChanged);    
        }
        else
        {
            c.ParentChanged += new EventHandler(val_ParentChanged);
            c.LocationChanged += new EventHandler(val_LocationChanged);    
            c.Disposed += new EventHandler(c_Disposed);
    
            val_ParentChanged(activeControl, null);
        }
    }
  7. Methods that will actually draw, reposition, and dispose.
  8. This is explained in my previous article. The only difference is we use activeControl to figure out on what label we need to operate.

    C#
    void c_Disposed(object sender, EventArgs e)
    {
        activeControl = (Control)sender;
        if (_labels[getIndexFromLabels(activeControl)].lab != null)
        {    
            RemoveControlFromLabels(activeControl);
        }
    }
    void val_LocationChanged(object sender, EventArgs e)
    {
        int i = getIndexFromLabels(activeControl);
        activeControl = (Control)sender;
        if (i != -1)
        {
            setControlsPosition(_labels[getIndexFromLabels(activeControl)].lab);
        }
    }
    void val_ParentChanged(object sender, EventArgs e)
    {
        activeControl = (Control)sender;
        AddOrRenewLabel(activeControl);
        if (activeControl.Parent != null)
        {
            activeControl.Parent.Controls.Add(
                        _labels[getIndexFromLabels(activeControl)].lab);
            setControlsPosition(_labels[getIndexFromLabels(activeControl)].lab);
        }
                
    }
    protected virtual void setControlsPosition(Label someLab)
    {
        if (someLab != null)
        {
            // setting text
            someLab.Text = GetAfpLabel(activeControl);
            // autosize is important cause it saves us a lot of code
            someLab.AutoSize = true;
            // little bit to the right
            someLab.Left = activeControl.Left - someLab.Width - 5;
            // and little bit below top 
            someLab.Top = activeControl.Top + 3;
        }
    }

All together

C#
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Collections;

namespace AfpComponents
{
    [ProvideProperty("AfpLabel", typeof(Control))]
    
    [ToolboxBitmap(typeof(Label))]
    public partial class AfpLabelProvider : Component, IExtenderProvider
    {
        #region contructorz and protectorz
        protected class AfpLabelInfo
        {
            // label that will be drawn next to control
            protected Label _lab = new Label();
            // hash code of control so we know which
            // labels goes to wchich control
            protected int _controlHash;
            public Label lab
            {
                get { return _lab; }
                set { _lab = value; }
            }
            public int controlHash
            {
                get { return _controlHash; }
                set { _controlHash = value; }
            }
            // we will take hashcode of each control
            public AfpLabelInfo(int AcontrolName)
            {
                _controlHash = AcontrolName;
            }
        }

        protected Hashtable AfpLabelProps = new Hashtable();
        protected List<afplabelinfo> _labels = new List<afplabelinfo>();
        protected Control activeControl;
        public AfpLabelProvider()
        {
            InitializeComponent();

        } 
        #endregion
        #region Label mapper methods

        protected int getIndexFromLabels(Control cnt)
        {
            for (int i = 0; i < _labels.Count; i++)
            {
                if (_labels[i].controlHash == (cnt.GetHashCode()) )
                    return i;
            }
            return -1;
        }
        protected void RemoveControlFromLabels(Control cnt)
        {
            int i = getIndexFromLabels(cnt);
            if (i != -1)
            {
                _labels[i].lab.Dispose();
                _labels.RemoveAt(i);
            }
        }
        protected void AddOrRenewLabel(Control cnt)
        {
            int dummy = cnt.GetHashCode();
            for (int i = 0; i < _labels.Count; i++)
            {
                if (_labels[i].controlHash == (dummy))
                {
                    if (_labels[i].lab == null)
                    {
                        _labels[i].lab = new Label();
                        return;
                    }
                }
            }
            
            _labels.Add(new AfpLabelInfo(dummy));
            // if there was no control we add it manually
        }

        #endregion

        #region Getters and setterrs

        public string GetAfpLabel(Control c)
        {
            string text = (string)AfpLabelProps[c];
            if (text == null)
            {
                text = String.Empty;
            }
            return text;
        }
        public void SetAfpLabel(Control c, string value)
        {
            activeControl = c;
            AfpLabelProps[c] = value;
            if (value == String.Empty)
            {
                c.Disposed -= new EventHandler(c_Disposed);
                c.ParentChanged -= new EventHandler(val_ParentChanged);
                c.LocationChanged -= new EventHandler(val_LocationChanged);    
            }
            else
            {
                c.ParentChanged += new EventHandler(val_ParentChanged);
                c.LocationChanged += new EventHandler(val_LocationChanged);    
                c.Disposed += new EventHandler(c_Disposed);

                val_ParentChanged(activeControl, null);
            }
        }
        #endregion
        void c_Disposed(object sender, EventArgs e)
        {
            activeControl = (Control)sender;
            if (_labels[getIndexFromLabels(activeControl)].lab != null)
            {    
                RemoveControlFromLabels(activeControl);
            }
        }

        void val_LocationChanged(object sender, EventArgs e)
        {
            int i = getIndexFromLabels(activeControl);
            activeControl = (Control)sender;
            if (i != -1)
                setControlsPosition(_labels[getIndexFromLabels(activeControl)].lab);
        }

        void val_ParentChanged(object sender, EventArgs e)
        {
            activeControl = (Control)sender;
            AddOrRenewLabel(activeControl);
            if (activeControl.Parent != null)
            {
                activeControl.Parent.Controls.Add(
                        _labels[getIndexFromLabels(activeControl)].lab);
                setControlsPosition(_labels[getIndexFromLabels(activeControl)].lab);
            }
            
        }
        protected virtual void setControlsPosition(Label someLab)
        {
            if (someLab != null)
            {
                // setting text
                someLab.Text = GetAfpLabel(activeControl);
                // autosize is important cause it saves us a lot of code
                someLab.AutoSize = true;
                // little bit to the right
                someLab.Left = activeControl.Left - someLab.Width - 5;
                // and little bit below top 
                someLab.Top = activeControl.Top + 3;
            }
        }
        
        #region IExtenderProvider Members

        public bool CanExtend(object extendee)
        {
            if (extendee is Control)
                return true;
            else
                return false;
        }

        #endregion
    }
}

OK. Fine... but what about buttons?

And now, we come to the part where the troubles begin. I browsed the net and couldn't find any information on how to add events. Adding a button is really not a problem, adding an event of the button to the control is a problem. That's when I need your help. So far, I tried (or am thinking about) this:

  • Cloning some other, unused event - yes, it's nice until you're not using this event. It would be a pain if you build a project and all of a sudden you really need that event. Then what? Altering all base classes... a bad idea.
  • Making events manually - I was thinking about making an event on the provider that would be fired on the creation of the form, and inside of it, there would be like control1.acompButton.click += new ....... because I can map a button just as I'm mapping labels. The only problem is deleting. Just as you manually add an event, you have to manually delete it.
  • Hooking up to a used event (i.e. Click) with some State Machine - well, this seems crazy, but if we make some switches with the sender (the sender will be the control or the button), it might work.

So, I'm counting on your feedback. Sent e-mails or leave a comment.

Possible errors

As I was trying this component and testing it, a weird thing happened. Somehow, Control.Parent (DateTimePicker in my case) was null. I tried, but I could not repeat it. So, if anyone has this problem, please e-mail me or leave a comment.

Final words

I must admit IExtenderProvider is an excellent tool. I learned quite a bit about it when I wrote this component. Now, my question to you: what technique do you prefer? Inheriting? Or extending? I know that extending is much more convenient (one component to rule all controls... :-)), but it also creates problems (events). Send feedback and tell me what you think...

History

  • 11.Feb.2009 - Bug hunting.
  • 7.Feb.2009 - First version.

License

This article, along with any associated source code and files, is licensed under The BSD License


Written By
Software Developer Agilion Consulting
Poland Poland
I specialize at C#, developing Enterprise solutions. I have some knowledge of ASP.NET MVC - looking forward to use it together with Typescript.

Comments and Discussions

 
GeneralSuggestion Pin
Xmen Real 10-Feb-10 23:07
professional Xmen Real 10-Feb-10 23:07 
Generalgot exception :( Pin
HiMik200311-Feb-09 0:57
HiMik200311-Feb-09 0:57 
I've got an exception in val_LocationChanged procedure (Index was out of range... ) in design and at runtime.
I have a form with several controls on it and afpLabels.

VCSharp 2008 Express Edition.
GeneralRe: got exception :( Pin
Adrian Pasik11-Feb-09 1:45
Adrian Pasik11-Feb-09 1:45 
Generalhopefully fixed Pin
Adrian Pasik11-Feb-09 2:34
Adrian Pasik11-Feb-09 2:34 
GeneralRe: hopefully fixed Pin
Adrian Pasik11-Feb-09 2:40
Adrian Pasik11-Feb-09 2:40 
GeneralRe: hopefully fixed Pin
HiMik200311-Feb-09 21:47
HiMik200311-Feb-09 21:47 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.