Fundamentos de los Custom Controls
En el ecosistema de Windows Presentation Foundation (WPF), un Custom Control representa la máxima expresión de extensibilidad. A diferencia de un UserControl, que es una composición estáticca de elementos existantes, un Custom Control separa estrictamente el comportamiento (definido en C#) de la apariencia (definida en un archivo de recursos XAML, generalmente Generic.xaml).
| Requerimiento | Solución recomendada | Justificación |
|---|---|---|
| Combinar controles para una vista rápida | UserControl | Desarrollo ágil, lógica y vista acopladas. |
| Crear una biblioteca de UI reutilizable | CustomControl | Soporta plantillas (Templating) y temas. |
| Modificar el comportamiento base de un control | CustomControl | Permite la herencia de clases y sobreescritura de metadatos. |
Escenario 1: Botón con bordes suavizados
El objetivo es extender el botón estándar para permitir la configuración directa del radio de las esquinas sin necesidad de redefinir el estilo global en cada instancia.
// BotonSuave.cs
using System.Windows;
using System.Windows.Controls;
namespace MiLibreriaUI
{
public class BotonSuave : Button
{
static BotonSuave()
{
// Vinculamos el control con su estilo definido en Themes/Generic.xaml
DefaultStyleKeyProperty.OverrideMetadata(
typeof(BotonSuave),
new FrameworkPropertyMetadata(typeof(BotonSuave)));
}
// Definición de la propiedad de dependencia para el radio de curvatura
public static readonly DependencyProperty RadioCurvaturaProperty =
DependencyProperty.Register("RadioCurvatura", typeof(CornerRadius),
typeof(BotonSuave), new PropertyMetadata(new CornerRadius(0)));
public CornerRadius RadioCurvatura
{
get => (CornerRadius)GetValue(RadioCurvaturaProperty);
set => SetValue(RadioCurvaturaProperty, value);
}
}
}
En el archivo Themes/Generic.xaml, definimos la estructura visual mediante un ControlTemplate:
<Style TargetType="{x:Type local:BotonSuave}">
<Setter Property="Background" Value="#2ecc71"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="Padding" Value="10,5"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:BotonSuave}">
<Border x:Name="CapaExterna"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding RadioCurvatura}">
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center"
Margin="{TemplateBinding Padding}"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="CapaExterna" Property="Background" Value="#27ae60"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Escenario 2: Control de acción con icono y texto
Este control permite integrar una imagen y una etiqueta descriptiva, soportando eventos de clic personalizados y orientación flexible.
// AccionVisual.cs
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace MiLibreriaUI
{
public class AccionVisual : Control
{
static AccionVisual()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(AccionVisual),
new FrameworkPropertyMetadata(typeof(AccionVisual)));
}
public static readonly DependencyProperty IconoProperty =
DependencyProperty.Register("Icono", typeof(ImageSource), typeof(AccionVisual));
public static readonly DependencyProperty TituloProperty =
DependencyProperty.Register("Titulo", typeof(string), typeof(AccionVisual), new PropertyMetadata("Click aquí"));
public ImageSource Icono
{
get => (ImageSource)GetValue(IconoProperty);
set => SetValue(IconoProperty, value);
}
public string Titulo
{
get => (string)GetValue(TituloProperty);
set => SetValue(TituloProperty, value);
}
// Evento Enrutado para el Clic
public static readonly RoutedEvent ClicEvent =
EventManager.RegisterRoutedEvent("Clic", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(AccionVisual));
public event RoutedEventHandler Clic
{
add => AddHandler(ClicEvent, value);
remove => RemoveHandler(ClicEvent, value);
}
protected override void OnMouseLeftButtonDown(System.Windows.Input.MouseButtonEventArgs e)
{
base.OnMouseLeftButtonDown(e);
RaiseEvent(new RoutedEventArgs(ClicEvent));
}
}
}
XAML para el diseño de AccionVisual:
<Style TargetType="{x:Type local:AccionVisual}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:AccionVisual}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="1" Padding="8">
<StackPanel Orientation="Horizontal">
<Image Source="{TemplateBinding Icono}" Width="16" Height="16" Margin="0,0,8,0"/>
<TextBlock Text="{TemplateBinding Titulo}" VerticalAlignment="Center"/>
</StackPanel>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Escenario 3: Selector de calificación profesional
Este componente permite al usuario seleccionar una puntuación mediante estrellas, manejando lógica de redondeo y estados visuales complejos.
// SelectorCalificacion.cs
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
namespace MiLibreriaUI
{
public class SelectorCalificacion : Control
{
static SelectorCalificacion()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(SelectorCalificacion),
new FrameworkPropertyMetadata(typeof(SelectorCalificacion)));
}
public static readonly DependencyProperty PuntajeProperty =
DependencyProperty.Register("Puntaje", typeof(int), typeof(SelectorCalificacion),
new FrameworkPropertyMetadata(0, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
public int Puntaje
{
get => (int)GetValue(PuntajeProperty);
set => SetValue(PuntajeProperty, value);
}
public static readonly DependencyProperty MaximoProperty =
DependencyProperty.Register("Maximo", typeof(int), typeof(SelectorCalificacion), new PropertyMetadata(5));
public int Maximo
{
get => (int)GetValue(MaximoProperty);
set => SetValue(MaximoProperty, value);
}
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
base.OnMouseLeftButtonDown(e);
Point pos = e.GetPosition(this);
double proporcion = pos.X / ActualWidth;
Puntaje = (int)Math.Ceiling(proporcion * Maximo);
}
}
}
Resumen de implementación
Para construir componentes robustos en WPF, es vital seguir estos principios técnicos:
- Registro de Metadatos: Siempre sobrescriba
DefaultStyleKeyen el constructor estático para asegurar que el motor de renderizado localice la plantilla correcta. - Propiedades de Dependencia: Use
DependencyPropertypara permitir que sus controles participen en el sistema de Binding, Animaciones y Estilos de WPF. - Localización de Recursos: Los estilos por defecto deben residir en
/Themes/Generic.xamldentro del ensamblado del control; de lo contrario, el sistema no los encontrará automáticamente. - TemplateBinding: Utilice esta marca de extensión dentro del XAML para conectar las propiedades de la clase C# con los elementos visuales de la plantilla de forma eficiente.