Información general sobre la creación de controles

Actualización: noviembre 2007

La extensibilidad del modelo de control Windows Presentation Foundation (WPF) reduce en gran medida la necesidad de crear un nuevo control. Sin embargo, en ciertos casos es posible que aún necesite crear un control personalizado. En este tema se explican las características que minimizan la necesidad de crear un control personalizado y los diferentes modelos de creación de controles de Windows Presentation Foundation (WPF). En este tema también se muestra cómo crear un nuevo control.

Este tema contiene las secciones siguientes.

  • Alternativas a la escritura de un nuevo control
  • Modelos para la creación de controles
  • Fundamentos de creación de controles
  • Heredar de UserControl frente a usar ControlTemplate
  • Temas relacionados

Alternativas a la escritura de un nuevo control

Históricamente, si se deseaba obtener una experiencia personalizada de un control existente, las posibilidades estaban limitadas a cambiar las propiedades estándar del control, tales como el color de fondo, el ancho del borde y el tamaño de fuente. Si se deseaba extender la apariencia o comportamiento de un control más allá de estos parámetros predefinidos, necesitaba crear un nuevo control, normalmente heredado de uno existente, e invalidar el método responsable de dibujarlo. Aunque esto sigue siendo posible, WPF permite personalizar los controles existentes gracias a su modelo de contenido enriquecido, así como estilos, plantillas y desencadenadores. La lista siguiente proporciona ejemplos de cómo utilizar estas características para crear experiencias personalizadas y coherentes sin tener que crear un nuevo control.

  • Contenido enriquecido. Muchos de los controles estándar de WPF son compatibles con contenido enriquecido. Por ejemplo, la propiedad de contenido de un control Button es del tipo Object, de modo que, en teoría, en un control Button se puede mostrar cualquier elemento. Para que un botón muestre una imagen y texto, puede agregar una imagen y un control TextBlock a StackPanel y asignar StackPanel a la propiedad Content. Debido a que estos controles permiten mostrar elementos visuales de WPF y datos arbitrarios, se reduce la necesidad de crear un nuevo control o modificar un control existente para permitir una visualización compleja. Para obtener más información sobre el modelo de contenido de Button y otros controles, vea Información general sobre el modelo de contenido de controles. Para obtener más información sobre otros modelos de contenido de WPF, vea Modelos de contenido.

  • Estilos. Un objeto Style es una colección de valores que representan propiedades para un control. Utilizando estilos, puede crear una representación reutilizable del aspecto y el comportamiento deseados para un control sin necesidad de escribir un nuevo control. Por ejemplo, suponga que desea que la fuente de todos los controles TextBlock sea Arial de color rojo con un tamaño de 14. Puede crear un estilo como un recurso y establecer las propiedades adecuadas en consecuencia. A continuación, todos los controles TextBlock que agregue a la aplicación tendrán la misma apariencia.

  • Plantillas de datos.DataTemplate permite personalizar cómo se muestran los datos en un control. Por ejemplo, DataTemplate se puede utilizar para especificar cómo se muestran los datos en ListBox. Se puede ver un ejemplo en Información general sobre plantillas de datos. Además de personalizar la apariencia de los datos, un objeto DataTemplate puede incluir elementos de interfaz de usuario, lo que aporta gran flexibilidad en las interfaces de usuario personalizadas. Por ejemplo, con DataTemplate se puede crear un control ComboBox en el que cada elemento contenga una casilla.

  • Plantillas de control. Muchos controles de WPF utilizan ControlTemplate para definir la estructura y apariencia del control, a fin de independizar su apariencia de su funcionalidad. Es posible cambiar drásticamente la apariencia de un control si se redefine su ControlTemplate. Por ejemplo, supongamos que desea un control semejante a un semáforo. La interfaz de usuario y la funcionalidad de este control son sencillas. El control está compuesto de tres círculos y sólo uno de ellos puede estar encendido en un momento dado. Después de reflexionar, puede que se dé cuenta de que RadioButton proporciona la funcionalidad de permitir la selección de una sola opción a la vez, aunque la apariencia predeterminada de RadioButton no se parece nada a un semáforo. Dado que RadioButton utiliza una plantilla de control para definir su apariencia, resulta fácil redefinir ControlTemplate para adaptarlo a los requisitos del control y utilizar los botones de opción para crear el semáforo.

    Nota

    Aunque RadioButton puede utilizar un objeto DataTemplate, en este ejemplo no basta con DataTemplate. DataTemplate define la apariencia del contenido de un control. En el caso de un botón de opción (RadioButton), su contenido es aquello que aparece a la derecha del círculo, que indica si RadioButton está seleccionado. En el ejemplo del semáforo, el botón de opción tiene que ser sólo un círculo capaz de "encenderse". Dado que el requisito de apariencia del semáforo es tan distinto de la apariencia predeterminada de RadioButton, es necesario redefinir ControlTemplate. En general, DataTemplate se utiliza para definir el contenido (o los datos) de un control y ControlTemplate se utiliza para definir la estructura del control.

  • Desencadenadores.Trigger permite cambiar dinámicamente la apariencia y el comportamiento de un control sin crear uno nuevo. Por ejemplo, suponga que tiene varios controles ListBox en una aplicación y desea que todos los elementos de ListBox se muestren en negrita y en rojo cuando están seleccionados. Su primer impulso podría ser crear una clase que herede de ListBox e invalidar el método OnSelectionChanged para cambiar la apariencia del elemento seleccionado; sin embargo, sería más apropiado agregar a un estilo de ListBoxItem un desencadenador que cambie la apariencia del elemento seleccionado. Un desencadenador permite cambiar los valores de las propiedades o realizar acciones basadas en el valor de una propiedad. Un objeto EventTrigger permite realizar acciones cuando se produce un evento.

Para obtener más información sobre los estilos, plantillas y desencadenadores, vea Aplicar estilos y plantillas.

En general, si el control tiene la misma funcionalidad que un control existente, pero desea modificar su apariencia, antes de nada debe estudiar si puede utilizar cualquiera de los métodos descritos en esta sección a fin de cambiar la apariencia del control existente.

Modelos para la creación de controles

El modelo de contenido enriquecido, los estilos, las plantillas y los desencadenadores minimizan la necesidad de crear controles nuevos. Sin embargo, si se ve obligado a crear uno nuevo, es importante comprender los distintos modelos de creación de controles de WPF. WPF proporciona tres modelos generales para crear controles, cada uno de los cuales ofrece un conjunto diferente de características y un nivel de flexibilidad distinto. Las clases base de los tres modelos son UserControl, Control y FrameworkElement.

Derivar de UserControl

La manera más sencilla de crear un control en WPF es derivar de UserControl. Cuando se genera un control que hereda de UserControl, se agregan los componentes existentes a UserControl, se les da un nombre y se hace referencia los controladores de eventos en Lenguaje de marcado de aplicaciones extensible (XAML). A continuación, puede hacer referencia a los elementos con nombre y definir los controladores de eventos en el código. Este modelo de programación es muy similar al utilizado para la programación de aplicaciones en WPF.

Si está generado correctamente, un control UserControl puede aprovechar las ventajas del contenido enriquecido, los estilos y los desencadenadores. Sin embargo, si el control hereda de UserControl, las personas que lo utilicen no podrán usar DataTemplate o ControlTemplate para personalizar su apariencia. Es necesario derivar de la clase Control o de una de sus clases derivadas (excepto UserControl) para crear un control personalizado que admita plantillas.

Ventajas de derivar de UserControl

Considere la posibilidad de derivar de UserControl si se cumplen todas las condiciones siguientes:

  • Desea generar un control de forma similar a como se genera una aplicación.

  • El control está compuesto solamente de componentes existentes.

  • No necesita permitir una personalización compleja.

Derivar de Control

Derivar de la clase Control es el modelo utilizado por la mayoría de los controles WPF existentes. Cuando se crea un control que hereda de la clase Control, su apariencia se define mediante plantillas. Al hacerlo, se independiza la lógica de funcionamiento de la representación visual. También se puede garantizar la independencia entre la interfaz de usuario y la lógica si se usan comandos y enlaces en lugar de eventos, y se evita en lo posible hacer referencia a los elementos de ControlTemplate. Si la interfaz de usuario y la lógica del control están debidamente desconectadas, un usuario del control podrá redefinir el objeto ControlTemplate del control para personalizar su apariencia. Aunque generar un objeto Control personalizado no es tan sencillo como generar un objeto UserControl, un objeto Control personalizado proporciona mayor flexibilidad.

Ventajas de derivar de Control

Considere la posibilidad de derivar de Control en lugar de utilizar la clase UserControl si se cumple cualquiera de las condiciones siguientes:

  • Desea que la apariencia del control sea personalizable a través del objeto ControlTemplate.

  • Desea que el control admita temas diferentes.

Derivar de FrameworkElement

Los controles derivados de UserControl o Control se basan en la composición de elementos existentes. Para muchos escenarios, ésta es una solución aceptable, porque cualquier objeto que hereda de FrameworkElement puede estar en un objeto ControlTemplate. Sin embargo, en ocasiones la apariencia de un control requiere una funcionalidad que va más allá de la simple composición de elementos. Para estos escenarios, basar un componente en FrameworkElement es la opción correcta.

Hay dos métodos estándar para generar componentes basados en FrameworkElement: la representación directa y la composición de elementos personalizada. La representación directa implica invalidar el método OnRender de FrameworkElement y proporcionar operaciones DrawingContext que definan explícitamente el aspecto visual del componente. Éste es el método utilizado por Image y Border. La composición de elementos personalizada implica utilizar objetos de tipo Visual para crear la apariencia del componente. Por ejemplo, vea Usar objetos DrawingVisual. Track es un ejemplo de un control de WPF que utiliza la composición de elementos personalizada. También es posible mezclar la representación directa y la composición de elementos personalizada en el mismo control.

Ventajas de derivar de FrameworkElement

Considere la posibilidad de derivar de FrameworkElement si se cumple cualquiera de las condiciones siguientes:

  • Desea tener un control preciso sobre la apariencia del control más allá de lo que proporciona la simple composición de elementos.

  • Desea definir el aspecto del control definiendo una lógica de representación propia.

  • Desea componer elementos existentes de maneras nuevas que excedan lo posible con UserControl y Control.

Fundamentos de creación de controles

Como se comentó anteriormente, una de las características más eficaces de WPF es la posibilidad de no tener que limitarse a establecer las propiedades básicas de un control para modificar su apariencia y comportamiento, sin estar obligado a crear un control personalizado. Las características de estilo, enlace de datos y desencadenadores son posibles gracias al sistema de propiedades de WPF y al sistema de eventos de WPF. Si implementa propiedades de dependencia y eventos enrutados en un control personalizado, los usuarios podrán utilizar estas características exactamente igual que para un control distribuido con WPF, independientemente del modelo utilizado para crearlo. 

Utilizar propiedades de dependencia

Cuando una propiedad es de dependencia, es posible realizar las acciones siguientes:

  • Establecer la propiedad en un estilo.

  • Enlazar la propiedad a un origen de datos.

  • Utilizar un recurso dinámico como valor de la propiedad.

  • Animar la propiedad.

Si desea que una propiedad del control admita cualquiera de estas funcionalidades, debe implementarla como propiedad de dependencia. En el ejemplo siguiente se define una propiedad de dependencia denominada Value mediante este procedimiento:

  • Defina un identificador de DependencyProperty denominado ValueProperty como campo public static readonly.

  • Registre el nombre de la propiedad en el sistema de propiedades, mediante una llamada a DependencyProperty.Register, para especificar lo siguiente:

    • El nombre de la propiedad.

    • El tipo de la propiedad.

    • El tipo que posee la propiedad.

    • Los metadatos de la propiedad. Los metadatos contienen el valor predeterminado de la propiedad, CoerceValueCallback y PropertyChangedCallback.

  • Defina una propiedad de "contenedor" de CLR denominada Value (que es el mismo nombre que se utiliza para registrar la propiedad de dependencia) mediante la implementación de los descriptores de acceso get y set de la propiedad. Observe que los descriptores de acceso get y set llaman únicamente a GetValue y SetValue, respectivamente. Se recomienda que los descriptores de acceso de las propiedades de dependencia no contengan lógica adicional, porque los clientes y WPF pueden omitir dichos descriptores y llamar a GetValue y SetValue directamente. Por ejemplo, cuando una propiedad está enlazada a un origen de datos, no se llama al descriptor de acceso set de la propiedad. En lugar de agregar lógica adicional a los descriptores de acceso get y set, utilice los delegados ValidateValueCallback, PropertyChangedCallback y CoerceValueCallback para responder a los cambios del valor o comprobar si el valor ha cambiado. Para obtener más información acerca de estas devoluciones de llamada, vea Devoluciones de llamada y validación de las propiedades de dependencia.

  • Defina un método para CoerceValueCallback denominado CoerceValue. CoerceValue garantiza que Value sea mayor o igual que MinValue y menor o igual que MaxValue.

  • Defina un método para PropertyChangedCallback, denominado OnValueChanged. OnValueChanged crea un objeto RoutedPropertyChangedEventArgs<T> y se prepara para provocar el evento enrutado ValueChanged. Los eventos enrutados se abordan en la sección siguiente.

/// <summary>
/// Identifies the Value dependency property.
/// </summary>
public static readonly DependencyProperty ValueProperty =
    DependencyProperty.Register(
        "Value", typeof(decimal), typeof(NumericUpDown),
        new FrameworkPropertyMetadata(MinValue, new PropertyChangedCallback(OnValueChanged),
                                      new CoerceValueCallback(CoerceValue)));

/// <summary>
/// Gets or sets the value assigned to the control.
/// </summary>
public decimal Value
{          
    get { return (decimal)GetValue(ValueProperty); }
    set { SetValue(ValueProperty, value); }
}

private static object CoerceValue(DependencyObject element, object value)
{
    decimal newValue = (decimal)value;
    NumericUpDown control = (NumericUpDown)element;

    newValue = Math.Max(MinValue, Math.Min(MaxValue, newValue));

    return newValue;
}

private static void OnValueChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
    NumericUpDown control = (NumericUpDown)obj;         

    RoutedPropertyChangedEventArgs<decimal> e = new RoutedPropertyChangedEventArgs<decimal>(
        (decimal)args.OldValue, (decimal)args.NewValue, ValueChangedEvent);
    control.OnValueChanged(e);
}

Para obtener más información, vea Propiedades de dependencia personalizadas.

Utilizar eventos enrutados

Al igual que las propiedades de dependencia extienden la noción de propiedades CLR con funciones adicionales, los eventos enrutados extienden la noción de eventos CLR estándar. Al crear un nuevo control WPF, también es conveniente implementar el evento como enrutado, porque un evento enrutado admite el comportamiento siguiente:

  • Los eventos se pueden controlar en un elemento primario de varios controles. Si un evento es de propagación, puede suscribirse a él un elemento primario único del árbol de elementos. A continuación, los autores de la aplicación pueden utilizar un mismo controlador para responder al evento de varios controles. Por ejemplo, si el control forma parte de cada uno de los elementos de un control ListBox (por estar incluido en DataTemplate), el programador de la aplicación puede definir el controlador del evento del control en ListBox. Cada vez que se produzca el evento en cualquiera de los controles, se llamará al controlador.

  • Los eventos enrutados se pueden utilizar en EventSetter, lo que permite a los programadores de aplicaciones especificar el controlador de un evento en un estilo.

  • Los eventos enrutados se pueden utilizar en EventTrigger, lo que resulta útil para animar propiedades mediante XAML. Para obtener más información, vea Información general sobre animaciones.

En el ejemplo siguiente se define un evento enrutado mediante este procedimiento:

  • Defina un identificador de RoutedEvent denominado ValueChangedEvent como campo public static readonly.

  • Registre el evento enrutado mediante una llamada al método EventManager.RegisterRoutedEvent. En el ejemplo se especifica la información siguiente al llamar a RegisterRoutedEvent:

    • El nombre del evento es ValueChanged.

    • La estrategia del enrutamiento es Bubble. Esto significa que primero se llama a un controlador de eventos en el origen (el objeto que provoca el evento) y, a continuación, se llama sucesivamente a los controladores de eventos en los elementos primarios del origen, empezando por el controlador de eventos del elemento primario más cercano.

    • El tipo del controlador de eventos es RoutedPropertyChangedEventHandler<T>, construido con un tipo Decimal.

    • El tipo propietario del evento es NumericUpDown.

  • Declare un evento público denominado ValueChanged e incluya declaraciones de descriptores de acceso del evento. En el ejemplo se llama a AddHandler en la declaración del descriptor de acceso add y a RemoveHandler en la declaración del descriptor de acceso remove a fin de utilizar los servicios de eventos de WPF.

  • Cree un método virtual protegido denominado OnValueChanged que provoque el evento ValueChanged.

/// <summary>
/// Identifies the ValueChanged routed event.
/// </summary>
public static readonly RoutedEvent ValueChangedEvent = EventManager.RegisterRoutedEvent(
    "ValueChanged", RoutingStrategy.Bubble, 
    typeof(RoutedPropertyChangedEventHandler<decimal>), typeof(NumericUpDown));

/// <summary>
/// Occurs when the Value property changes.
/// </summary>
public event RoutedPropertyChangedEventHandler<decimal> ValueChanged
{
    add { AddHandler(ValueChangedEvent, value); }
    remove { RemoveHandler(ValueChangedEvent, value); }
}

/// <summary>
/// Raises the ValueChanged event.
/// </summary>
/// <param name="args">Arguments associated with the ValueChanged event.</param>
protected virtual void OnValueChanged(RoutedPropertyChangedEventArgs<decimal> args)
{
    RaiseEvent(args);
}

Para obtener más información, vea Información general sobre eventos enrutados y Cómo: Crear un evento enrutado personalizado.

Utilizar el enlace

Para desacoplar la interfaz de usuario del control de su lógica, puede ser conveniente utilizar el enlace de datos. Esto resulta particularmente importante si la apariencia del control se define mediante ControlTemplate. Al utilizar el enlace de datos, puede que consiga eliminar la necesidad de hacer referencia a partes concretas de la interfaz de usuario desde el código. Es conveniente evitar hacer referencia a elementos incluidos en ControlTemplate, ya que cuando el código hace referencia a elementos incluidos en ControlTemplate y se modifica ControlTemplate, el elemento al que se hace referencia debe incluirse en el nuevo objeto ControlTemplate.

En el ejemplo siguiente se actualiza el control TextBlock del control NumericUpDown, para ello se le asigna un nombre y se hace referencia al cuadro de texto por su nombre en el código.

<Border BorderThickness="1" BorderBrush="Gray" Margin="2" 
        Grid.RowSpan="2" VerticalAlignment="Center" HorizontalAlignment="Stretch">
  <TextBlock Name="valueText" Width="60" TextAlignment="Right" Padding="5"/>
</Border>
private void UpdateTextBlock()
{
    valueText.Text = Value.ToString();
}

En el ejemplo siguiente se utiliza el enlace para lograr lo mismo.

<Border BorderThickness="1" BorderBrush="Gray" Margin="2" 
        Grid.RowSpan="2" VerticalAlignment="Center" HorizontalAlignment="Stretch">

    <!--Bind the TextBlock to the Value property-->
    <TextBlock 
        Width="60" TextAlignment="Right" Padding="5"
        Text="{Binding RelativeSource={RelativeSource FindAncestor, 
                       AncestorType={x:Type local:NumericUpDown}}, 
                       Path=Value}"/>

</Border>

Para obtener más información sobre el enlace de datos, vea Información general sobre el enlace de datos.

Definir y utilizar comandos

Considere la posibilidad de definir y utilizar comandos para proporcionar funcionalidad en lugar de controlar eventos. Cuando utiliza controladores de eventos en el control, las aplicaciones no pueden obtener acceso a la acción realizada dentro del controlador de eventos. Si implementa comandos en el control, las aplicaciones pueden tener acceso a la funcionalidad que, de otro modo, no estaría disponible.

El ejemplo siguiente muestra la parte de un control que controla el evento Click para que dos botones cambien el valor del control NumericUpDown. Independientemente de que el control sea UserControl o Control con ControlTemplate, la interfaz de usuario y la lógica están estrechamente unidas, ya que el control utiliza controladores de eventos.

   <RepeatButton Name="upButton" Click="upButton_Click" 
                  Grid.Column="1" Grid.Row="0">Up</RepeatButton>
                  
    <RepeatButton Name="downButton" Click="downButton_Click" 
                  Grid.Column="1" Grid.Row="1">Down</RepeatButton>
private void upButton_Click(object sender, EventArgs e)
{
        Value++;
}

private void downButton_Click(object sender, EventArgs e)
{
        Value--;
}

En el ejemplo siguiente se definen dos comandos que cambian el valor del control NumericUpDown.

public static RoutedCommand IncreaseCommand
{
    get
    {
        return _increaseCommand;
    }
}
public static RoutedCommand DecreaseCommand
{
    get
    {
        return _decreaseCommand;
    }
}

private static void InitializeCommands()
{
    _increaseCommand = new RoutedCommand("IncreaseCommand", typeof(NumericUpDown));
    CommandManager.RegisterClassCommandBinding(typeof(NumericUpDown), 
                            new CommandBinding(_increaseCommand, OnIncreaseCommand));
    CommandManager.RegisterClassInputBinding(typeof(NumericUpDown), 
                            new InputBinding(_increaseCommand, new KeyGesture(Key.Up)));

    _decreaseCommand = new RoutedCommand("DecreaseCommand", typeof(NumericUpDown));
    CommandManager.RegisterClassCommandBinding(typeof(NumericUpDown), 
                            new CommandBinding(_decreaseCommand, OnDecreaseCommand));
    CommandManager.RegisterClassInputBinding(typeof(NumericUpDown), 
                            new InputBinding(_decreaseCommand, new KeyGesture(Key.Down)));
}

private static void OnIncreaseCommand(object sender, ExecutedRoutedEventArgs e)
{
    NumericUpDown control = sender as NumericUpDown;
    if (control != null)
    {
        control.OnIncrease();
    }
}
private static void OnDecreaseCommand(object sender, ExecutedRoutedEventArgs e)
{
    NumericUpDown control = sender as NumericUpDown;
    if (control != null)
    {
        control.OnDecrease();
    }
}

protected virtual void OnIncrease()
{
    Value++;
}
protected virtual void OnDecrease()
{
    Value--;
}

private static RoutedCommand _increaseCommand;
private static RoutedCommand _decreaseCommand;

Los elementos de la plantilla pueden hacer referencia, entonces, a los comandos, como se muestra en el ejemplo siguiente.

<RepeatButton 
    Command="{x:Static local:NumericUpDown.IncreaseCommand}"  
    Grid.Column="1" Grid.Row="0">Up</RepeatButton>
<RepeatButton 
    Command="{x:Static local:NumericUpDown.DecreaseCommand}"  
    Grid.Column="1" Grid.Row="1">Down</RepeatButton>

Ahora las aplicaciones pueden hacer referencia a los enlaces para tener acceso a la funcionalidad que no era accesible cuando el control utilizaba controladores de eventos. Para obtener más información sobre comandos, vea Información general sobre comandos.

Especificar que un elemento es necesario en ControlTemplate

En las secciones anteriores se explica cómo utilizar el enlace de datos y los comandos para que un control no haga referencia a elementos de su ControlTemplate desde el código. Sin embargo, puede haber ocasiones en las que hacer referencia a un elemento sea inevitable. Si se produce esta situación, debe aplicar el atributo TemplatePartAttribute al control. Este atributo informa a los autores de la plantilla de los tipos y los nombres de los elementos incluidos en ControlTemplate. No es necesario nombrar cada uno de los elementos de ControlTemplate en TemplatePartAttribute. De hecho, cuantos menos elementos se nombren, mejor. Sin embargo, si hace referencia al elemento en el código, debe utilizar TemplatePartAttribute.

Para obtener más información sobre cómo diseñar un control que utiliza ControlTemplate, vea Instrucciones para el diseño de controles con estilos.

Diseñar para diseñadores

Para recibir soporte técnico para controles personalizados de WPF en Windows Presentation Foundation (WPF) Designer for Visual Studio (por ejemplo, la edición de propiedades con la ventana Propiedades), siga estas instrucciones. Para obtener más información sobre la programación para WPF Designer, vea WPF Designer.

Propiedades de dependencia

Es importante implementar los descriptores de acceso CLR get y set como se describió anteriormente en "Utilizar propiedades de dependencia". Los diseñadores pueden utilizar el contenedor para detectar la presencia de una propiedad de dependencia, pero no se les exige, al igual que a WPF y a los clientes del control, llamar a los descriptores de acceso al obtener o establecer la propiedad.

Propiedades asociadas

Para implementar propiedades asociadas en controles personalizados, es recomendable que utilice las siguientes instrucciones:

  • Consideremos una clase public static readonlyDependencyProperty con el formato NombreDePropiedadProperty creada mediante el método RegisterAttached. El nombre de propiedad que se pasa a RegisterAttached debe coincidir con NombreDePropiedad.

  • Implemente un par de métodos publicstatic CLR denominados SetNombreDePropiedad y GetNombreDePropiedad. Ambos métodos deben aceptar una clase derivada de DependencyProperty como su primer argumento. El método SetNombreDePropiedad también acepta un argumento cuyo tipo coincida con el tipo de datos registrado para la propiedad. El método Getmétodo NombreDePropiedad debe devolver un valor del mismo tipo. Si falta el método SetNombreDePropiedad, la propiedad se marca como de sólo lectura.

  • SetNombreDePropiedad y GetNombreDePropiedad deben enrutar directamente a los métodos GetValue y los métodos SetValue del objeto de dependencia de destino, respectivamente. Los diseñadores pueden tener acceso a la propiedad asociada mediante una llamada a través del contenedor de método o una llamada directa al objeto de dependencia de destino.

Para obtener más información sobre las propiedades asociadas, vea Información general sobre propiedades asociadas.

Definir y utilizar recursos compartidos para el control

Puede incluir el control en el mismo ensamblado que la aplicación o bien empaquetarlo en un ensamblado independiente que se pueda utilizar en varias aplicaciones. En general, la información analizada en este tema es aplicable independientemente del método que se utilice. Sin embargo, es necesario destacar una diferencia. Al incluir un control en el mismo ensamblado que una aplicación, puede agregar recursos globales al archivo app.xaml libremente. Sin embargo, un ensamblado que sólo contiene controles no tiene asociado un objeto Application, por lo que no hay disponible un archivo app.xaml.

Cuando una aplicación busca un recurso, la búsqueda se realiza en tres niveles en el orden que se indica a continuación:

  1. Nivel de elemento: el sistema empieza por el elemento que hace referencia al recurso y, a continuación, busca en los recursos del elemento primario lógico, y así sucesivamente, hasta que se alcanza el elemento raíz.

  2. Nivel de aplicación: recursos definidos por el objeto Application.

  3. Nivel de tema: los diccionarios del nivel de tema se almacenan en una subcarpeta denominada Temas. Los archivos de la carpeta Temas corresponden a los temas. Por ejemplo, puede tener Aero.NormalColor.xaml, Luna.NormalColor.xaml, Royale.NormalColor.xaml, etc. También puede tener un archivo denominado generic.xaml. Cuando el sistema busca un recurso en el nivel de temas, primero busca en el archivo específico del tema y, después, en generic.xaml.

Cuando el control se encuentra en un ensamblado independiente de la aplicación, debe colocar los recursos globales en el nivel de elemento o el nivel de tema. Ambos métodos tienen sus ventajas.

Definir recursos en el nivel de elemento

Para definir recursos compartidos en el nivel de elemento, cree un diccionario de recursos personalizado y combínelo con el diccionario de recursos del control. Cuando utilice este método, puede asignar al archivo de recursos el nombre que desee e incluir dicho archivo en la misma carpeta que los controles. Los recursos del nivel de elemento también pueden utilizar cadenas simples como claves. En el ejemplo siguiente se crea un archivo de recursos de LinearGradientBrush llamado Dictionary1.XAML.

<ResourceDictionary 
    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml">
  <LinearGradientBrush 
    x:Key="myBrush"  
    StartPoint="0,0" EndPoint="1,1">
    <GradientStop Color="Red" Offset="0.25" />
    <GradientStop Color="Blue" Offset="0.75" />
  </LinearGradientBrush>

</ResourceDictionary>

Una vez definido el diccionario, deberá combinarlo con el diccionario de recursos del control. Para ello, utilice XAML o el código.

En el ejemplo siguiente se combina un diccionario de recursos mediante XAML.

<UserControl.Resources>
    <ResourceDictionary>
        <ResourceDictionary.MergedDictionaries>
            <ResourceDictionary Source="Dictionary1.xaml"/>
        </ResourceDictionary.MergedDictionaries>
    </ResourceDictionary>
</UserControl.Resources>

El inconveniente de este enfoque es que se crea un objeto ResourceDictionary cada vez que se hace referencia a él. Por ejemplo, si tiene 10 controles personalizados en la biblioteca y se combinan los diccionarios de recursos compartidos de cada control mediante XAML, se crean 10 objetos ResourceDictionary idénticos. Para evitarlo, cree una clase estática que combine los recursos del código y devuelva el elemento ResourceDictionary resultante.

En el ejemplo siguiente se crea una clase que devuelve un elemento ResourceDictionary compartido.

internal static class SharedDictionaryManager
{
    internal static ResourceDictionary SharedDictionary
    {
        get
        {
            if (_sharedDictionary == null)
            {
                System.Uri resourceLocater =
                    new System.Uri("/ElementResourcesCustomControlLibrary;component/Dictionary1.xaml", 
                                    System.UriKind.Relative);

                _sharedDictionary = 
                    (ResourceDictionary)Application.LoadComponent(resourceLocater);
            }

            return _sharedDictionary;
        }
    }

    private static ResourceDictionary _sharedDictionary;
}

En el ejemplo siguiente se combina el recurso compartido con los recursos de un control personalizado en el constructor del control antes de llamar a InitilizeComponent. Dado que SharedDictionaryManager.SharedDictionary es una propiedad estática, ResourceDictionary sólo se crea una vez. Dado que el diccionario de recursos se combinó antes de llamar a InitializeComponent, los recursos están disponibles para el control en su archivo de XAML.

public NumericUpDown()
{
    this.Resources.MergedDictionaries.Add(SharedDictionaryManager.SharedDictionary);
    InitializeComponent();

}

Definir recursos en el nivel de tema

WPF permite crear recursos para distintos temas de Windows.  Como autor del control, puede definir un recurso para un tema concreto a fin de cambiar la apariencia del control en función del tema que se utilice. Por ejemplo, la apariencia de un elemento Button en el tema de Windows clásico (tema predeterminado de Windows 2000) es distinta de la de un elemento Button en el tema Luna de Windows (tema predeterminado de Windows XP) porque Button utiliza un elemento ControlTemplate distinto para cada tema.  

Los recursos específicos de un tema se mantienen en un diccionario de recursos con un nombre de archivo concreto. Estos archivos deben estar en una carpeta llamada Temas, que es una subcarpeta de la carpeta que contiene el control. En la tabla siguiente se enumeran los archivos de diccionario de recursos y el tema asociado a cada archivo:

Nombre del archivo de diccionario de recursos

Tema de Windows

Classic.xaml

Aspecto “clásico” de Windows 9x/2000 en Windows XP

Luna.NormalColor.xaml

Tema azul predeterminado en Windows XP

Luna.Homestead.xaml

Tema verde olivo en Windows XP

Luna.Metallic.xaml

Tema plateado en Windows XP

Royale.NormalColor.xaml

Tema predeterminado en Windows XP Media Center Edition

Aero.NormalColor.xaml

Tema predeterminado en Windows Vista

No es necesario definir un recurso para cada tema. Si no se ha definido un recurso para un tema concreto, el control utiliza el recurso genérico, que se encuentra en un archivo de diccionario de recursos llamado generic.xaml en la misma carpeta que los archivos de diccionario de recursos específicos de un tema. Aunque generic.xaml no corresponde a un tema concreto de Windows, no deja de ser un diccionario del nivel de tema.

Ejemplo NumericUpDown Custom Control with Theme and UI Automation Support contiene dos diccionarios de recursos para el control NumericUpDown: uno está en generic.xaml y el otro en Luna.NormalColor.xaml. Puede ejecutar la aplicación y cambiar entre el tema plateado de Windows XP y otro tema a fin de ver la diferencia entre las dos plantillas de control. (Si está ejecutandoWindows Vista, puede cambiar el nombre de Luna.NormalColor.xaml a Aero.NormalColor.xaml y cambiar entre dos temas, como el de Windows clásico y el tema predeterminado de Windows Vista.)

Al colocar un elemento ControlTemplate en cualquiera de los archivos de diccionario de recursos específicos de un tema, debe crear un constructor estático para el control y llamar al método OverrideMetadata(Type, PropertyMetadata) en DefaultStyleKey, como se muestra en el ejemplo siguiente.

static NumericUpDown()
{
    DefaultStyleKeyProperty.OverrideMetadata(typeof(NumericUpDown),
               new FrameworkPropertyMetadata(typeof(NumericUpDown)));
}

Definir y hacer referencia a claves para los recursos de tema

Al definir un recurso en el nivel de elemento, puede asignar una cadena como clave de éste y obtener acceso al recurso a través de la cadena. Al definir un recurso en el nivel de tema, debe utilizar un elemento ComponentResourceKey como clave. En el ejemplo siguiente se define un recurso en generic.xaml.

En el ejemplo siguiente se hace referencia al recurso mediante la especificación de ComponentResourceKey como clave.

Especificar la ubicación de los recursos de tema

Para buscar los recursos de un control, la aplicación host debe saber que el ensamblado contiene recursos específicos del control. Para ello, agregue ThemeInfoAttribute al ensamblado que contiene el control. ThemeInfoAttribute tiene una propiedad GenericDictionaryLocation que especifica la ubicación de los recursos genéricos y una propiedad ThemeDictionaryLocation que especifica la ubicación de los recursos específicos de tema.

En el ejemplo siguiente se establecen las propiedades ThemeDictionaryLocation y GenericDictionaryLocation en SourceAssembly, para especificar que los recursos genéricos y específicos de tema están en el mismo ensamblado que el control.

[assembly: ThemeInfo(ResourceDictionaryLocation.SourceAssembly, 
           ResourceDictionaryLocation.SourceAssembly)]

Heredar de UserControl frente a usar ControlTemplate

Varios ejemplos muestran distintos métodos para escribir y empaquetar el control NumericUpDown. En Ejemplo NumericUpDown UserControl with DependencyProperty and RoutedEvent, NumericUpDown hereda de UserControl; en Ejemplo NumericUpDown Custom Control with Theme and UI Automation Support, NumericUpDown hereda de Control y utiliza un elemento ControlTemplate. En esta sección se describen brevemente algunas de las diferencias entre ambos y se explica por qué el control que utiliza ControlTemplate es más extensible.

La diferencia más importante es que el control NumericUpDown que hereda de UserControl no utiliza un elemento ControlTemplate y el control que hereda directamente de Control sí lo utiliza. En el ejemplo siguiente se muestra XAML del control que hereda de UserControl. Como puede observar, XAML es muy similar al resultado que podría obtener al crear una aplicación y comenzar con Window o Page.

<!--XAML for NumericUpDown that inherits from UserControl.-->
<UserControl x:Class="MyUserControl.NumericUpDown"
    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:MyUserControl">
    <Grid Margin="3">
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>

        <Border BorderThickness="1" BorderBrush="Gray" Margin="2" 
                Grid.RowSpan="2" VerticalAlignment="Center" HorizontalAlignment="Stretch">

            <!--Bind the TextBlock to the Value property-->
            <TextBlock 
                Width="60" TextAlignment="Right" Padding="5"
                Text="{Binding RelativeSource={RelativeSource FindAncestor, 
                               AncestorType={x:Type local:NumericUpDown}}, 
                               Path=Value}"/>

        </Border>

        <RepeatButton Name="upButton" Click="upButton_Click" 
                      Grid.Column="1" Grid.Row="0">Up</RepeatButton>

        <RepeatButton Name="downButton" Click="downButton_Click" 
                      Grid.Column="1" Grid.Row="1">Down</RepeatButton>

    </Grid>
</UserControl>

En el ejemplo siguiente se muestra ControlTemplate del control que hereda de Control. Observe que ControlTemplate es similar a XAML en UserControl, con tan sólo algunas diferencias de sintaxis.

<!--ControlTemplate for NumericUpDown that inherits from
    Control.-->
<Style TargetType="{x:Type local:NumericUpDown}">
  <Setter Property="HorizontalAlignment" Value="Center"/>
  <Setter Property="VerticalAlignment" Value="Center"/>
  <Setter Property="Template">
    <Setter.Value>
      <ControlTemplate TargetType="{x:Type local:NumericUpDown}">
        <Grid Margin="3">
          <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition/>
          </Grid.RowDefinitions>
          <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition/>
          </Grid.ColumnDefinitions>

          <Border BorderThickness="1" BorderBrush="Gray" 
                  Margin="2" Grid.RowSpan="2" 
                  VerticalAlignment="Center" HorizontalAlignment="Stretch">

            <TextBlock Text="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Value}" 
                       Width="60" TextAlignment="Right" Padding="5"/>
          </Border>

          <RepeatButton Command="{x:Static local:NumericUpDown.IncreaseCommand}"  
                        Grid.Column="1" Grid.Row="0">Up</RepeatButton>

          <RepeatButton Command="{x:Static local:NumericUpDown.DecreaseCommand}"
                        Grid.Column="1" Grid.Row="1">Down</RepeatButton>

        </Grid>

      </ControlTemplate>
    </Setter.Value>
  </Setter>
</Style>

La mayor diferencia entre los dos ejemplos anteriores es que aquel en el que se utiliza ControlTemplate tiene una apariencia personalizable y no así el que hereda de UserControl. En el caso en el que NumericUpDown hereda de UserControl, los desarrolladores de aplicaciones no pueden hacer nada para cambiar la apariencia del control. De hecho, aunque NumericUPDown tiene una propiedad ControlTemplate (dado que UserControl hereda de Control), si alguien intenta establecerla, se producirá una excepción en tiempo de ejecución. Por otra parte, un desarrollador de aplicaciones que utiliza el control NumericUpDown que hereda de Control puede crear libremente un nuevo elemento ControlTemplate para el control. Por ejemplo, se podría crear un elemento ControlTemplate que colocara los botones a izquierda y derecha de TextBlock en lugar de encima y debajo.

La diferencia entre los dos enfoques es evidente en la diferencia sintáctica de los ejemplos anteriores. El control que utiliza ControlTemplate establece la propiedad Template en un elemento Style para NumericUpDown. Éste es un método habitual de crear plantillas de control. Al establecer la propiedad Template en un estilo, se especifica que todas las instancias del control utilizarán ese elemento ControlTemplate. Los desarrolladores de aplicaciones pueden cambiar libremente la propiedad Template de un control NumericcUpDown para personalizar su apariencia. En cambio, el elemento XAML del control que hereda de UserControl rellena la propiedad Content de NumericUpDown (<UserControl.Content> está implícito en XAML). Si un desarrollador de aplicaciones no puede cambiar la propiedad Content, no se puede utilizar NumericUpDown.

Otra diferencia entre los ejemplos es la forma en que los controles responden a los botones Arriba y Abajo. El control que hereda de UserControl controla el evento Click y el control que utiliza ControlTemplate implementa comandos y se enlaza a éstos en el elemento ControlTemplate. Como resultado, un desarrollador de aplicaciones que crea un nuevo elemento ControlTemplate para NumericUpDown puede establecer también un enlace a los comandos y conservar la funcionalidad del control. Si ControlTemplate controla el evento Click en lugar de enlazarse a los comandos, el desarrollador de aplicaciones debería implementar controladores de eventos al crear un nuevo elemento ControlTemplate e interrumpir de esa forma la encapsulación de NumericUpDown.

Otra diferencia es la sintaxis del enlace entre la propiedad Text de TextBlock y la propiedad Value. En el caso de UserControl, el enlace especifica que RelativeSource es el control NumericUpDown primario y se enlaza a la propiedad Value. En el caso de ControlTemplate, RelativeSource es el control al que pertenece la plantilla. El resultado obtenido es el mismo, pero vale la pena mencionar que la sintaxis de enlace difiere entre ambos ejemplos.

En Ejemplo NumericUpDown Custom Control with Theme and UI Automation Support, el control NumericUpDown se encuentra en un ensamblado independiente de la aplicación y define y utiliza recursos de nivel de tema, pero en Ejemplo NumericUpDown UserControl with DependencyProperty and RoutedEvent, el control NumericUpDown está en el mismo ensamblado que la aplicación y no define ni utiliza recursos de nivel de tema.

Por último, Ejemplo NumericUpDown Custom Control with Theme and UI Automation Support muestra cómo crear un elemento AutomationPeer para el control NumericUpDown. Para obtener más información sobre la compatibilidad de Automatización de la interfaz de usuario con los controles personalizados, vea Automatización de la interfaz de usuario de un control personalizado de WPF.

Vea también

Conceptos

URIs de paquete en Windows Presentation Foundation

Otros recursos

WPF Designer

Personalización de controles

Ejemplos de personalización de controles