Información general sobre la creación de controles

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
  • 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 modelos de contenido de WPF, vea Modelo de contenido de WPF.

  • 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 todos los controles TextBlock tengan la fuente Arial de color rojo con un tamaño de fuente de 14. Puede crear un estilo como un recurso y establecer las propiedades adecuadas correspondientes. 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 una 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 radio para crear el semáforo.

    NotaNota

    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 radio (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 radio 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 Extensible Application Markup Language (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 desarrollo es muy similar al utilizado para el desarrollo 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 la ControlTemplate del control para personalizar su apariencia. Aunque generar un Control personalizado no es tan sencillo como generar un UserControl, un 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. Para obtener un ejemplo, vea Usar objetos DrawingVisual. Track es un ejemplo de un control de WPF que usa 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. En las próximas secciones se describen algunos procedimientos que debe seguir, independientemente del modelo que emplee para crear el control personalizado, de modo que los usuarios de su control personalizado puedan usar estas características como lo harían para un control incluido con WPF. 

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 un 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 emplea 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 el delegado CoerceValueCallback denominado CoerceValue. CoerceValue garantiza que Value es mayor o igual que MinValue y menor o igual que MaxValue.

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

        ''' <summary>
        ''' Identifies the Value dependency property.
        ''' </summary>
        Public Shared ReadOnly ValueProperty As DependencyProperty = DependencyProperty.Register("Value", GetType(Decimal), GetType(NumericUpDown), New FrameworkPropertyMetadata(MinValue, New PropertyChangedCallback(AddressOf OnValueChanged), New CoerceValueCallback(AddressOf CoerceValue)))

        ''' <summary>
        ''' Gets or sets the value assigned to the control.
        ''' </summary>
        Public Property Value() As Decimal
            Get
                Return CDec(GetValue(ValueProperty))
            End Get
            Set(ByVal value As Decimal)
                SetValue(ValueProperty, value)
            End Set
        End Property

        Private Shared Overloads Function CoerceValue(ByVal element As DependencyObject, ByVal value As Object) As Object
            Dim newValue As Decimal = CDec(value)
            Dim control As NumericUpDown = CType(element, NumericUpDown)

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

            Return newValue
        End Function

        Private Shared Sub OnValueChanged(ByVal obj As DependencyObject, ByVal args As DependencyPropertyChangedEventArgs)
            Dim control As NumericUpDown = CType(obj, NumericUpDown)

            Dim e As New RoutedPropertyChangedEventArgs(Of Decimal)(CDec(args.OldValue), CDec(args.NewValue), ValueChangedEvent)
            control.OnValueChanged(e)
        End Sub
/// <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 deWPF, 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 un 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 genere el evento ValueChanged.

        ''' <summary>
        ''' Identifies the ValueChanged routed event.
        ''' </summary>
        Public Shared ReadOnly ValueChangedEvent As RoutedEvent = EventManager.RegisterRoutedEvent("ValueChanged", RoutingStrategy.Bubble, GetType(RoutedPropertyChangedEventHandler(Of Decimal)), GetType(NumericUpDown))

        ''' <summary>
        ''' Occurs when the Value property changes.
        ''' </summary>
        Public Custom Event ValueChanged As RoutedPropertyChangedEventHandler(Of Decimal)
            AddHandler(ByVal value As RoutedPropertyChangedEventHandler(Of Decimal))
                MyBase.AddHandler(ValueChangedEvent, value)
            End AddHandler
            RemoveHandler(ByVal value As RoutedPropertyChangedEventHandler(Of Decimal))
                MyBase.RemoveHandler(ValueChangedEvent, value)
            End RemoveHandler
            RaiseEvent(ByVal sender As System.Object, ByVal e As RoutedPropertyChangedEventArgs(Of Decimal))
            End RaiseEvent
        End Event

        ''' <summary>
        ''' Raises the ValueChanged event.
        ''' </summary>
        ''' <param name="args">Arguments associated with the ValueChanged event.</param>
        Protected Overridable Sub OnValueChanged(ByVal args As RoutedPropertyChangedEventArgs(Of Decimal))
            MyBase.RaiseEvent(args)
        End Sub
/// <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 Sub UpdateTextBlock()
            valueText.Text = Value.ToString()
        End Sub
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.

Diseñar para diseñadores

Para recibir soporte técnico para controles personalizados de WPF en 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 adjuntas

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

  • Consideremos una clase public static readonly DependencyProperty 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 SetValue del objeto de dependencia de destino, respectivamente. Los diseñadores pueden tener acceso a la propiedad adjunta 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 adjuntas, vea Información general sobre propiedades asociadas.

Definir y usar recursos compartidos

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 solo contiene controles no tiene asociado ningún objeto Application, por lo que no hay disponible ningún 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 usa 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 denominado 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, use 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 con el fin de cambiar la apariencia del control en función del tema que se emplee. Por ejemplo, la apariencia de un elemento Button en el tema de Windows clásico (el tema predeterminado de Windows 2000) es distinta de la de un elemento Button en el tema Luna de Windows (el tema predeterminado de Windows XP) porque Button usa 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 denominada 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

Apariencia clásica 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 comprueba Classic.xaml para el recurso. Si no se ha definido el recurso en el archivo correspondiente al tema actual o en Classic.xaml, el control utiliza el recurso genérico, que está en un archivo de diccionario de recursos denominado generic.xaml. El archivo generic.xaml se encuentra 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.

NumericUpDown Custom Control with Theme and UI Automation Support Sample 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.

        Shared Sub New()
            DefaultStyleKeyProperty.OverrideMetadata(GetType(NumericUpDown), New FrameworkPropertyMetadata(GetType(NumericUpDown)))
        End Sub
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.

<LinearGradientBrush 
     x:Key="{ComponentResourceKey TypeInTargetAssembly={x:Type local:Painter}, 
                                  ResourceId=MyEllipseBrush}"  
                                  StartPoint="0,0" EndPoint="1,0">
    <GradientStop Color="Blue" Offset="0" />
    <GradientStop Color="Red" Offset="0.5" />
    <GradientStop Color="Green" Offset="1"/>
</LinearGradientBrush>

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

<RepeatButton 
    Grid.Column="1" Grid.Row="0"
    Background="{StaticResource {ComponentResourceKey 
                        TypeInTargetAssembly={x:Type local:NumericUpDown}, 
                        ResourceId=ButtonBrush}}">
    Up
</RepeatButton>
<RepeatButton 
    Grid.Column="1" Grid.Row="1"
    Background="{StaticResource {ComponentResourceKey 
                    TypeInTargetAssembly={x:Type local:NumericUpDown}, 
                    ResourceId=ButtonBrush}}">
    Down
 </RepeatButton>

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)>
[assembly: ThemeInfo(ResourceDictionaryLocation.SourceAssembly, 
           ResourceDictionaryLocation.SourceAssembly)]

Vea también

Conceptos

Empaquetar URI en WPF

Otros recursos

WPF Designer

Personalización de controles