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:
- Contructors, helper classes, lists, hashtables.
We're going to have only one property, and that will be:
[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).
protected class AfpLabelInfo
{
protected Label _lab = new Label();
protected int _controlHash;
public Label lab
{
get { return _lab; }
set { _lab = value; }
}
public int controlHash
{
get { return _controlHash; }
set { _controlHash = value; }
}
public AfpLabelInfo(int AcontrolName)
{
_controlHash = AcontrolName;
}
}
protected Hashtable AfpLabelProps = new Hashtable();
protected List<afplabelinfo> _labels = new List<afplabelinfo>();
protected Control activeControl;
- Control <-> label mapping methods.
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.
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));
}
- Getters and setters.
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...
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);
}
}
- Methods that will actually draw, reposition, and dispose.
This is explained in my previous article. The only difference is we use activeControl
to figure out on what label we need to operate.
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)
{
someLab.Text = GetAfpLabel(activeControl);
someLab.AutoSize = true;
someLab.Left = activeControl.Left - someLab.Width - 5;
someLab.Top = activeControl.Top + 3;
}
}
All together
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
{
protected Label _lab = new Label();
protected int _controlHash;
public Label lab
{
get { return _lab; }
set { _lab = value; }
}
public int controlHash
{
get { return _controlHash; }
set { _controlHash = value; }
}
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));
}
#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)
{
someLab.Text = GetAfpLabel(activeControl);
someLab.AutoSize = true;
someLab.Left = activeControl.Left - someLab.Width - 5;
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.