Información general sobre la creación de controles

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

Alternativas a la escritura de un nuevo control

Históricamente, si quería obtener una experiencia personalizada con un control existente, estaba limitado a cambiar las propiedades estándar del control, como el color de fondo, el ancho del borde y el tamaño de la fuente. Si se deseara extender la apariencia o el comportamiento de un control más allá de estos parámetros predefinidos, necesitaría crear un nuevo control; para ello, lo que haría es heredarlo de un control existente e invalidar el método responsable de dibujar el control. Aunque esto sigue siendo una opción, WPF permite personalizar los controles existentes mediante su modelo de contenido enriquecido, sus estilos, plantillas y desencadenadores. En la lista siguiente se proporcionan ejemplos de cómo se pueden usar estas características para crear experiencias personalizadas y coherentes sin tener que crear un nuevo control.

  • Contenido enriquecido. Muchos de los controles WPF estándar admiten contenido enriquecido. Por ejemplo, la propiedad de contenido de un elemento Button es de tipo Object, así que, en teoría, nada se puede mostrar en un control Button. Para que un botón muestre una imagen y texto, puede agregar una imagen y un elemento TextBlock a un elemento StackPanel y asignar el elemento StackPanel a la propiedad Content. Dado que los controles pueden mostrar elementos visuales WPF y datos arbitrarios, no es tan necesario crear un nuevo control o modificar un control existente para admitir una visualización compleja. Para más información sobre el modelo de contenido de Button y otros modelos de contenido de WPF, consulte WPF Content Model (Modelo de contenido de WPF).

  • Estilos. Un elemento Style es una colección de valores que representan propiedades de un control. Mediante el uso de estilos, puede crear una representación reutilizable de la apariencia y el comportamiento deseados de un control sin necesidad de escribir un nuevo control. Por ejemplo, supongamos que desea que todos los controles TextBlock tengan fuente Arial roja con un tamaño de fuente 14. Puede crear un estilo como un recurso y establecer las propiedades adecuadas según corresponda. Luego, cada elemento TextBlock que agrega a la aplicación tendrá la misma apariencia.

  • Plantillas de datos. Un elemento DataTemplate le permite personalizar cómo se muestran los datos en un control. Por ejemplo, un elemento DataTemplate se puede usar para especificar cómo se muestran los datos en un control ListBox. Para ver un ejemplo de esto, consulte Data Templating Overview (Introducción a las 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 usan un elemento ControlTemplate para definir la estructura y la apariencia del control, que separa su apariencia de su funcionalidad. Puede cambiar completamente la apariencia de un control si redefine su ControlTemplate. Por ejemplo, supongamos que desea un control que se parezca a un semáforo. Este control tiene una interfaz de usuario y una funcionalidad sencillas. El control son tres círculos, y solo uno se puede iluminar cada vez. Después de reflexionar, habrá observado que un elemento RadioButton ofrece la funcionalidad de que solo uno se selecciona cada vez, pero la apariencia predeterminada del elemento RadioButton no se parece en nada a las luces de un semáforo. Como el elemento RadioButton usa una plantilla de control para definir su apariencia, resulta fácil redefinir el elemento ControlTemplate para ajustarse a los requisitos del control y usar botones de selección para crear el semáforo.

    Nota:

    Aunque un elemento RadioButton puede usar un elemento DataTemplate, un elemento DataTemplate no es suficiente en este ejemplo. El elemento DataTemplate define la apariencia del contenido de un control. En el caso de un control RadioButton, el contenido es lo que aparece a la derecha del círculo que indica si se selecciona el elemento RadioButton. En el ejemplo del semáforo, el botón de selección debe ser un círculo que pueda "encenderse". Dado que el requisito de apariencia del semáforo es tan distinto de la apariencia predeterminada del control RadioButton, es necesario redefinir el elemento ControlTemplate. En general, un elemento DataTemplate se usa para definir el contenido (o los datos) de un control y un elemento ControlTemplate se usa para definir cómo se estructura un control.

  • Desencadenadores. Un elemento 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 su aplicación y quiere que los elementos de cada ListBox estén en negrita y rojo al seleccionarlos. Su primer impulso podría ser crear una clase que hereda de ListBox e invalidar el método OnSelectionChanged para cambiar la apariencia del elemento seleccionado. Sin embargo, un mejor enfoque es agregar un desencadenador a un estilo de un control ListBoxItem que cambia la apariencia del elemento seleccionado. Un desencadenador permite cambiar los valores de propiedad o realizar acciones según el valor de una propiedad. Un elemento EventTrigger le permite realizar acciones cuando se produce un evento.

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

En general, si el control refleja la funcionalidad de un control existente, pero quiere que el control tenga un aspecto diferente, primero debe considerar si puede usar cualquiera de los métodos descritos en esta sección para cambiar la apariencia del control existente.

Modelos para crear controles

El modelo de contenido enriquecido, los estilos, las plantillas y los desencadenadores reducen la necesidad de crear un nuevo control. Sin embargo, si necesita crear un nuevo control, es importante comprender los diferentes modelos de creación de controles de WPF. WPF proporciona tres modelos generales para la creación de un control, cada uno de los cuales proporciona un conjunto de características y un nivel de flexibilidad diferentes. Las clases base de los tres modelos son UserControl, Control y FrameworkElement.

Derivación de UserControl

La manera más sencilla de crear un control en WPF es derivar de UserControl. Cuando creas un control que hereda de UserControl, agregas componentes existentes al control UserControl, nombras los componentes y haces referencia a los controladores de eventos en XAML. Luego 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 modelo utilizado para el desarrollo de aplicaciones en WPF.

Si se crea correctamente, un control UserControl puede aprovechar las ventajas de contenido enriquecido, estilos y desencadenadores. Sin embargo, si el control hereda de UserControl, la gente que use su control no podrá utilizar un elemento DataTemplate o ControlTemplate para personalizar su apariencia. Es necesario derivar de la clase Control o de una de sus clases derivadas (que no sea UserControl) para crear un control personalizado que admita plantillas.

Ventajas de derivar de UserControl

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

  • Desea crear el control de forma similar a cómo crea una aplicación.

  • El control solo consta de componentes existentes.

  • No se necesita personalización compleja.

Derivación de Control

Derivar de la clase Control es el modelo que usan 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 separa 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 puede redefinir el elemento ControlTemplate del control para personalizar su apariencia. Aunque crear un control Control personalizado no es tan sencillo como crear un control UserControl, un control Control personalizado proporciona la máxima flexibilidad.

Ventajas de derivar de Control

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

  • Desea que la apariencia del control se pueda personalizar mediante el elemento ControlTemplate.

  • Desea que el control admita distintos temas.

Derivación de FrameworkElement

Los controle derivados de UserControl o Control se basan en la composición de elementos existentes. Para muchos escenarios, esta 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. Este 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 ver un ejemplo, consulte Using DrawingVisual Objects (Uso de 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.

Conceptos básicos 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 el sistema de eventos 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.

Uso de 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 esta funcionalidad, debe implementarla como una 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:

    • Nombre de la propiedad.

    • Tipo de la propiedad.

    • El tipo al que pertenece 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 llamada 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. Observe que los descriptores de acceso get y set solo llaman 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. En lugar de agregar lógica adicional a los descriptores de acceso, use los delegados ValidateValueCallback, CoerceValueCallback y PropertyChangedCallback para responder a los cambios del valor o comprobar si el valor ha cambiado. Para más información sobre estas devoluciones de llamada, consulte 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 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);
}
''' <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

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

Uso de eventos enrutados

Al igual que las propiedades de dependencia extienden la noción de propiedades CLR con funcionalidad adicional, los eventos enrutados extienden la noción de eventos CLR estándar. Cuando se crea 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 desarrollador 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 de eventos.

  • Los eventos enrutados se pueden utilizar en EventSetter, lo que permite a los desarrolladores 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, consulte 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 de enrutamiento es Bubble, lo que 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 de propiedad del evento es NumericUpDown.

  • Declare un evento público denominado ValueChanged e incluya declaraciones de descriptores de acceso del evento. El ejemplo llama a AddHandler en la declaración del descriptor de acceso add y RemoveHandler en la declaración del descriptor de acceso remove para usar 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 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);
}
''' <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

Para más información, consulte Routed Events Overview (Introducción a los eventos enrutados) y Create a Custom Routed Event (Creación de un evento enrutado personalizado).

Uso del 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();
}
Private Sub UpdateTextBlock()
    valueText.Text = Value.ToString()
End Sub

En el ejemplo siguiente se usa 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 más información sobre el enlace de datos, consulte Información general sobre el enlace de datos.

Diseño para diseñadores

Para recibir soporte técnico para controles WPF personalizados de WPF Designer para Visual Studio (por ejemplo, la edición de propiedades con la ventana Propiedades), siga estas instrucciones. Para obtener más información sobre el desarrollo de WPF Designer, consulte Diseño de XAML en Visual Studio.

Propiedades de dependencia

Es importante implementar los descriptores de acceso CLR get y set como se describió anteriormente en "Uso de 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 PropertyNameProperty que se creó mediante el método RegisterAttached. El nombre de propiedad que se pasa a RegisterAttached debe coincidir con NombreDePropiedad.

  • Implemente un par de métodos CLR public static denominados SetPropertyName y GetPropertyName. 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 GetNombreDePropiedad método debe devolver un valor del mismo tipo. Si falta el método SetNombreDePropiedad, la propiedad se marca como de solo lectura.

  • Set PropertyName y GetPropertyName 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 más información sobre las propiedades adjuntas, consulte Attached Properties Overview (Introducción a las propiedades adjuntas).

Definición y uso de 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, hay una diferencia que vale la pena tener en cuenta. Al incluir un control en el mismo ensamblado que una aplicación, puede agregar recursos globales al archivo App.xaml. 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. El 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. El nivel de aplicación.

    Los recursos definidos por el objeto Application.

  3. El 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, podría 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 lo busca en el archivo específico del tema y, a continuación, lo busca en generic.xaml.

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

Definición de los recursos en el nivel de elemento

Puede definir los recursos compartidos en el nivel de elemento mediante la creación de un diccionario de recursos personalizado y combinarlo con el diccionario de recursos del control. Cuando utiliza este método, puede nombrar el archivo de recursos que desee y este puede estar en la misma carpeta que los controles. Los recursos en el nivel de elemento también pueden utilizar cadenas simples como claves. En el ejemplo siguiente se crea un archivos de recursos LinearGradientBrush llamado Dictionary1.xaml.

<ResourceDictionary 
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://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, debe combinarlo con el diccionario de recursos del control. Para ello, utilice XAML o 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 InitializeComponent. Dado que SharedDictionaryManager.SharedDictionary es una propiedad estática, el objeto ResourceDictionary se crea una sola vez. Como el diccionario de recursos se combinó antes de llamar a InitializeComponent, los recursos están disponibles para el control en su archivo XAML.

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

Definición de recursos en el nivel de tema

WPF le 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 del 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 Themes 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 que está asociado a cada archivo:

Nombre de 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 del tema. Aunque generic.xaml no corresponde a un tema específico de Windows, sigue siendo un diccionario de nivel de tema.

El ejemplo de C# o Visual Basic de control personalizado NumericUpDown y compatibilidad para automatización de la interfaz de usuario contiene dos diccionarios de recursos para el control NumericUpDown: uno es genérico y el otro se encuentra en Luna.NormalColor.xaml.

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)));
}
Shared Sub New()
    DefaultStyleKeyProperty.OverrideMetadata(GetType(NumericUpDown), New FrameworkPropertyMetadata(GetType(NumericUpDown)))
End Sub
Definición de los recursos de tema y referencia a ellos

Al definir un recurso en el nivel de elemento, puede asignar una cadena como su clave 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 la 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>
Especificación de 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 GenericDictionaryLocation y ThemeDictionaryLocation 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