… and implementing it, can be the difference between your book being on time, and being, er, ah, well… late.
I’ve written chapters on custom controls at least a dozen times, and I’ve explained how to write custom controls at least a hundred times. You’d think I could do it in my sleep. In this first of a series of blog entries about Silverlight custom controls I’ll try to convey why creating custom controls is not difficult, but explaining it, or at least explaining it well, is.
There were custom controls in ASP.NET, in Windows Forms, in many other GUI platforms, and they’ve always been just about the same idea. You typically had three options:
Option One
The first way to create a custom control is to aggregate two or more existing controls into a new type; to make something new (or something monstrous). My favorite example is to combine a button with a DropDown to make a button whose action can change.
Option Two
The second approach is to derive from an existing control but to add a little new functionality. My standard example is Clickable button, a button that knows how many times it has been clicked.
The code shown below is taken from Programming ASP.NET but the concepts are the same across many GUI frameworks.
1: using System;
2: using System.ComponentModel;
3: using System.Web.UI;
4:
5: namespace CustomControls
6: {
7: [DefaultProperty("Text")]
8: [ToolboxData("<{0}:CountedButton2 runat=server></{0}:CountedButton2>")]
9: public class CountedButton : System.Web.UI.WebControls.Button
10: {
11: private string displayString;
12:
13: public CountedButton( )
14: {
15: displayString = "clicks";
16: InitValues( );
17: }
18:
19: private void InitValues( )
20: {
21: if (ViewState["Count"] == null)
22: ViewState["Count"] = 0;
23: this.Text = "Click me";
24: }
25:
26: public int Count
27: {
28: get { return (int) ViewState["Count"]; }
29: set { ViewState["Count"] = value; }
30: }
31:
32: protected override void OnClick(EventArgs e)
33: {
34: ViewState["Count"] = ((int)ViewState["Count"]) + 1;
35: this.Text = ViewState["Count"] + " " + displayString;
36: base.OnClick(e);
37: }
38: }
39: }
40:
Option Three
And the third, typically most labor-intensive approach is to derive from a root Control object and (typically) draw the entire control and write all its logic yourself. An example that I’ve used a number of times is a custom clock face where the date travels around the circumference in Ferris Wheel like fashion and where you must draw the numbers, and the hour and minute hands and the position for the seconds indicator essentially “by hand”
This kind of control requires extensive coding in some environments, as shown in this cut down code listing (full and annotated listing in Programming WinForms.)
1: public class ClockFaceCtrl : System.Windows.Forms.Control
2: {
3: public ClockFaceCtrl()
4: { }
5:
6: public void OnTimer(Object source, ElapsedEventArgs e)
7: {
8: Graphics g = this.CreateGraphics();
9: SetScale(g);
10: DrawFace(g);
11: DrawTime(g,false);
12: DrawDate(g);
13: g.Dispose();
14: }
15:
16: protected override void OnPaint ( PaintEventArgs e )
17: { }
18:
19: private void SetScale(Graphics g)
20: {
21: g.TranslateTransform(Width / 2, Height / 2);
22: float inches = Math.Min(Width / g.DpiX, Height / g.DpiX);
23: g.ScaleTransform(inches * g.DpiX / 2000, inches * g.DpiY / 2000);
24: }
25:
26: private static float GetSin(float degAngle)
27: { return (float) Math.Sin(Math.PI * degAngle / 180f); }
28:
29: private static float GetCos(float degAngle)
30: { return (float) Math.Cos(Math.PI * degAngle / 180f); }
31:
32: private void DrawFace(Graphics g)
33: {
34: for (int i = 1; i <= numHours; i++)
35: {
36: SizeF stringSize = g.MeasureString(i.ToString(),font);
37: x = GetCos(i*deg + 90) * FaceRadius;
38: y = GetSin(i*deg + 90) * FaceRadius;
39: if ( currentTime.Second == i * 5)
40: g.DrawString(i.ToString(), font, greenBrush, -x, -y,format);
41: else
42: g.DrawString(i.ToString(), font, brush, -x, -y,format);
43: }
44: }
45:
46: private void DrawTime(Graphics g, bool forceDraw)
47: {
48: rotation = GetSecondRotation();
49: state = g.Save();
50: g.RotateTransform(rotation);
51: g.FillEllipse(blankBrush,-25,-secondLength,50,50);
52: g.Restore(state);
53: }
54:
55: private float GetHourRotation()
56: { }
57: private float GetMinuteRotation()
58: { return( 360f * currentTime.Minute / 60f ); }
59:
60: private float GetSecondRotation()
61: { return(360f * currentTime.Second / 60f); }
62:
63: private class LtrDraw
64: { }
65:
66: private class StringDraw
67: {
68: public StringDraw(string s, ClockFaceCtrl theControl)
69: { }
70:
71: public void DrawString(Graphics g, Brush brush)
72: {
73: foreach (LtrDraw theLtr in theString)
74: {
75: theLtr.DrawString(g,brush,theControl);
76: }
77: ClockFaceCtrl.offset += 1;
78: }
79: }
80:
81: private void DrawDate(Graphics g)
82: {
83: Brush brush = new SolidBrush(ForeColor);
84: sdToday.DrawString(g,brush);
85: }
86: }
Why Is This GUI Framework Different From All Other Gui Frameworks?
Writing a templatable custom control in Silverlight is exactly the same, only totally different. That is, the architectural build/ buy/ make-do decisions are the same, and the derivation decisions are the same, but the new wrinkle is the separation of logic from visuals though a contract enforced by the Parts and States model and leveraged by the Visual State Machine.
It is to that, I will turn when I wake up, but I suddenly realized that it is 2:20 am, and I should be sleeping. Very much.
More here: Digging Into Custom Controls