Skip to main content

.NET Custom Controls

Introduction

Custom controls are one of the most powerful features in .NET desktop development, allowing you to extend the standard control library with specialized functionality tailored to your application's needs. Whether you're building Windows Forms or WPF applications, custom controls enable you to create reusable UI components that encapsulate both visual appearance and behavior.

In this tutorial, you'll learn:

  • What custom controls are and why they're useful
  • Different types of custom controls in .NET
  • How to create basic custom controls in Windows Forms and WPF
  • Best practices for designing reusable controls
  • Real-world applications of custom controls

What Are Custom Controls?

Custom controls are user interface components that you build yourself to provide functionality not available in the standard control library. They allow you to:

  1. Bundle repeated functionality into a single, reusable component
  2. Customize the appearance of existing controls
  3. Combine multiple controls into a cohesive unit
  4. Encapsulate complex behavior behind a simple interface

Types of Custom Controls in .NET

In .NET desktop development, you can create several types of custom controls:

For Windows Forms:

  1. User Controls - Composed of existing controls
  2. Inherited Controls - Extend functionality of existing controls
  3. Custom Controls - Built from scratch using GDI+ drawing

For WPF:

  1. User Controls - Composite controls built from existing elements
  2. Custom Controls - Controls with completely customized templates and behaviors

Creating a Custom Control in Windows Forms

Let's start with a simple example: creating a labeled text box control that combines a Label and a TextBox into a single reusable component.

Step 1: Create a User Control

  1. In Visual Studio, right-click your project
  2. Select Add > User Control
  3. Name it "LabeledTextBox.cs"

Step 2: Design the Control

csharp
using System;
using System.Windows.Forms;

namespace MyControls
{
public partial class LabeledTextBox : UserControl
{
private Label label1;
private TextBox textBox1;

public LabeledTextBox()
{
InitializeComponent();

// Configure the control's default size
this.Size = new System.Drawing.Size(250, 50);

// Setup the label
label1 = new Label();
label1.Location = new System.Drawing.Point(3, 3);
label1.Size = new System.Drawing.Size(100, 23);
label1.Text = "Label:";

// Setup the textbox
textBox1 = new TextBox();
textBox1.Location = new System.Drawing.Point(110, 3);
textBox1.Size = new System.Drawing.Size(137, 20);

// Add controls to the container
this.Controls.Add(label1);
this.Controls.Add(textBox1);
}

// Property to get/set the label text
public string LabelText
{
get { return label1.Text; }
set { label1.Text = value; }
}

// Property to get/set the textbox text
public string TextValue
{
get { return textBox1.Text; }
set { textBox1.Text = value; }
}
}
}

Step 3: Use Your Custom Control

Once you've built the control, you can use it in any Windows Forms application:

csharp
// In your form's constructor or initialization method
LabeledTextBox nameField = new LabeledTextBox();
nameField.LabelText = "Name:";
nameField.Location = new System.Drawing.Point(12, 12);
this.Controls.Add(nameField);

// Later, retrieve the value
string name = nameField.TextValue;

Creating a Custom Control in WPF

WPF offers even more flexibility with custom controls. Let's create a simple rating control that displays stars for ratings.

Step 1: Create a Custom Control

  1. Add a new WPF Custom Control to your project
  2. Name it "StarRating.cs"

Step 2: Define the Control

csharp
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Shapes;

namespace MyControls.WPF
{
public class StarRating : Control
{
static StarRating()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(StarRating),
new FrameworkPropertyMetadata(typeof(StarRating)));
}

public static readonly DependencyProperty RatingProperty =
DependencyProperty.Register("Rating", typeof(int), typeof(StarRating),
new PropertyMetadata(0, OnRatingChanged));

public static readonly DependencyProperty MaxRatingProperty =
DependencyProperty.Register("MaxRating", typeof(int), typeof(StarRating),
new PropertyMetadata(5, OnMaxRatingChanged));

public int Rating
{
get { return (int)GetValue(RatingProperty); }
set { SetValue(RatingProperty, value); }
}

public int MaxRating
{
get { return (int)GetValue(MaxRatingProperty); }
set { SetValue(MaxRatingProperty, value); }
}

private static void OnRatingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
StarRating control = d as StarRating;
control.UpdateStars();
}

private static void OnMaxRatingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
StarRating control = d as StarRating;
control.UpdateStars();
}

private StackPanel starPanel;

public override void OnApplyTemplate()
{
base.OnApplyTemplate();
starPanel = new StackPanel { Orientation = Orientation.Horizontal };
this.AddChild(starPanel);
UpdateStars();
}

private void UpdateStars()
{
if (starPanel == null)
return;

starPanel.Children.Clear();

for (int i = 1; i <= MaxRating; i++)
{
Polygon star = new Polygon();
star.Points = CreateStarPoints();
star.Fill = i <= Rating ? Brushes.Gold : Brushes.LightGray;
star.Stroke = Brushes.DarkGray;
star.StrokeThickness = 1;
star.Width = 20;
star.Height = 20;
star.Stretch = Stretch.Uniform;
star.Margin = new Thickness(2);

int rating = i;
star.MouseDown += (s, e) => {
Rating = rating;
};

starPanel.Children.Add(star);
}
}

private PointCollection CreateStarPoints()
{
PointCollection points = new PointCollection();
points.Add(new Point(50, 0));
points.Add(new Point(61, 35));
points.Add(new Point(98, 35));
points.Add(new Point(68, 57));
points.Add(new Point(79, 91));
points.Add(new Point(50, 70));
points.Add(new Point(21, 91));
points.Add(new Point(32, 57));
points.Add(new Point(2, 35));
points.Add(new Point(39, 35));
return points;
}
}
}

Step 3: Use the Control in XAML

Once you've implemented the control, you can use it in your XAML:

xml
<Window x:Class="MyApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:MyControls.WPF"
Title="Custom Controls Demo" Height="350" Width="525">
<StackPanel Margin="20">
<TextBlock Text="Rate this application:" Margin="0,0,0,5"/>
<controls:StarRating Rating="3" MaxRating="5"/>
</StackPanel>
</Window>

Creating a Control from an Existing Control

Sometimes you want to extend an existing control rather than create a composite control. Here's how to create a text box that only accepts numeric input:

csharp
using System;
using System.Windows.Forms;

namespace MyControls
{
public class NumericTextBox : TextBox
{
public NumericTextBox()
{
this.KeyPress += NumericTextBox_KeyPress;
}

private void NumericTextBox_KeyPress(object sender, KeyPressEventArgs e)
{
// Allow digits, control characters, and decimal point
if (!char.IsControl(e.KeyChar) && !char.IsDigit(e.KeyChar) && e.KeyChar != '.')
{
e.Handled = true; // Suppress the key press
}

// Only allow one decimal point
if (e.KeyChar == '.' && this.Text.Contains("."))
{
e.Handled = true;
}
}

// Add a property to get the value as a decimal
public decimal Value
{
get
{
decimal result;
if (decimal.TryParse(this.Text, out result))
return result;
return 0;
}
set
{
this.Text = value.ToString();
}
}
}
}

To use this control:

csharp
NumericTextBox priceInput = new NumericTextBox();
priceInput.Location = new System.Drawing.Point(100, 50);
this.Controls.Add(priceInput);

// Later, get the value as a decimal
decimal price = priceInput.Value;

Creating Advanced Custom Controls

For more advanced scenarios, you might want to create controls that:

  1. Support data binding
  2. Include animation
  3. Implement custom rendering
  4. Support accessibility features

Example: A Progress Button in WPF

Let's create a button that shows progress:

csharp
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;

namespace MyControls.WPF
{
public class ProgressButton : Button
{
static ProgressButton()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(ProgressButton),
new FrameworkPropertyMetadata(typeof(ProgressButton)));
}

public static readonly DependencyProperty ProgressProperty =
DependencyProperty.Register("Progress", typeof(double), typeof(ProgressButton),
new PropertyMetadata(0.0, OnProgressChanged));

public double Progress
{
get { return (double)GetValue(ProgressProperty); }
set { SetValue(ProgressProperty, value); }
}

private static void OnProgressChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
ProgressButton button = d as ProgressButton;
button.UpdateProgressVisual();
}

private Border progressIndicator;

public override void OnApplyTemplate()
{
base.OnApplyTemplate();

// The template must contain a Border named PART_ProgressIndicator
progressIndicator = GetTemplateChild("PART_ProgressIndicator") as Border;
UpdateProgressVisual();
}

private void UpdateProgressVisual()
{
if (progressIndicator != null)
{
// Ensure progress is between 0 and 1
double clampedProgress = Math.Max(0, Math.Min(1, Progress));
progressIndicator.Width = ActualWidth * clampedProgress;
}
}
}
}

You'd also need to define a template in Generic.xaml:

xml
<Style TargetType="{x:Type local:ProgressButton}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:ProgressButton}">
<Grid>
<Border x:Name="PART_ProgressIndicator"
Background="#3399FF"
HorizontalAlignment="Left"
Height="{TemplateBinding Height}"/>
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center"/>
<Border BorderBrush="#CCCCCC"
BorderThickness="1"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

Real-World Applications

Custom controls are extremely valuable in real-world applications. Here are some common use cases:

1. Specialized Input Controls

  • Date range picker
  • Color picker with transparency
  • Enhanced rich text editor
  • Numeric input with validation

2. Business-Specific Controls

  • Financial chart control
  • Appointment scheduler
  • Product configurator
  • Interactive dashboard widgets

3. Enhanced Standard Controls

  • Data grid with filtering and grouping
  • TreeView with checkboxes
  • Multi-select combo box
  • Auto-complete text box

Example: Creating a Credit Card Input Control

Here's a practical example of a control that formats credit card numbers as they're entered:

csharp
using System;
using System.Windows.Forms;
using System.Text.RegularExpressions;

namespace MyControls
{
public class CreditCardTextBox : TextBox
{
private bool isProcessing = false;

public CreditCardTextBox()
{
this.MaxLength = 19; // 16 digits + 3 spaces
this.TextChanged += CreditCardTextBox_TextChanged;
}

private void CreditCardTextBox_TextChanged(object sender, EventArgs e)
{
if (isProcessing)
return;

isProcessing = true;

// Remove any non-digit characters
string digitsOnly = Regex.Replace(this.Text, @"\D", "");

// Format with spaces every 4 digits
string formatted = "";
for (int i = 0; i < digitsOnly.Length; i++)
{
if (i > 0 && i % 4 == 0)
formatted += " ";
formatted += digitsOnly[i];
}

// Update the text and maintain cursor position
int selectionStart = this.SelectionStart;
int lengthDiff = formatted.Length - this.Text.Length;

this.Text = formatted;

// Adjust cursor position if needed
this.SelectionStart = Math.Min(selectionStart + lengthDiff, this.Text.Length);

isProcessing = false;
}

// Property to get the card number without spaces
public string CardNumber
{
get
{
return Regex.Replace(this.Text, @"\s", "");
}
}

// Validate the card using Luhn algorithm
public bool IsValid()
{
string number = this.CardNumber;

if (string.IsNullOrWhiteSpace(number) || number.Length < 13)
return false;

int sum = 0;
bool alternate = false;

for (int i = number.Length - 1; i >= 0; i--)
{
int n = int.Parse(number[i].ToString());

if (alternate)
{
n *= 2;
if (n > 9)
n = n - 9;
}

sum += n;
alternate = !alternate;
}

return (sum % 10 == 0);
}
}
}

Best Practices for Custom Controls

When building custom controls, keep these best practices in mind:

  1. Focus on reusability - Design controls to be used in multiple contexts
  2. Provide clear documentation - Document properties, events, and usage examples
  3. Follow naming conventions - Use standard naming patterns for properties and events
  4. Support designer integration - Add design-time attributes for better tooling
  5. Implement proper event handling - Avoid event leaks and ensure proper cleanup
  6. Consider accessibility - Make controls usable with screen readers and keyboard navigation
  7. Test thoroughly - Test in various scenarios and screen sizes

Summary

Custom controls are a powerful way to extend .NET desktop applications with reusable components. Whether you're creating simple composite controls or complex custom renderings, the ability to create your own controls allows you to build more maintainable and consistent user interfaces.

In this tutorial, we've covered:

  • The fundamentals of custom controls in .NET
  • Creating Windows Forms user controls and custom controls
  • Building WPF custom controls with templates
  • Extending existing controls with new functionality
  • Real-world applications and best practices

By mastering custom controls, you can significantly improve your development productivity and create more polished, professional applications.

Additional Resources

Here are some resources to further explore .NET custom controls:

Exercises

  1. Create a password strength meter control that visually indicates password strength as the user types.
  2. Build a custom file browser control that lets users navigate and select files.
  3. Develop a custom "toast notification" control that can display temporary messages.
  4. Create a color picker control with RGB and HSL editing capabilities.
  5. Extend the DataGridView control to add built-in filtering and sorting features.


If you spot any mistakes on this website, please let me know at feedback@compilenrun.com. I’d greatly appreciate your feedback! :)