Редактиране

Споделяне чрез


Control authoring overview

The extensibility of the Windows Presentation Foundation (WPF) control model greatly reduces the need to create a new control. However, in certain cases you may still need to create a custom control. This topic discusses the features that minimize your need to create a custom control and the different control authoring models in Windows Presentation Foundation (WPF). This topic also demonstrates how to create a new control.

Alternatives to Writing a New Control

Historically, if you wanted to get a customized experience from an existing control, you were limited to changing the standard properties of the control, such as background color, border width, and font size. If you wished to extend the appearance or behavior of a control beyond these predefined parameters, you would need to create a new control, usually by inheriting from an existing control and overriding the method responsible for drawing the control. Although that is still an option, WPF enables to you customize existing controls by using its rich content model, styles, templates, and triggers. The following list gives examples of how these features can be used to create custom and consistent experiences without having to create a new control.

  • Rich Content. Many of the standard WPF controls support rich content. For example, the content property of a Button is of type Object, so theoretically anything can be displayed on a Button. To have a button display an image and text, you can add an image and a TextBlock to a StackPanel and assign the StackPanel to the Content property. Because the controls can display WPF visual elements and arbitrary data, there is less need to create a new control or to modify an existing control to support a complex visualization. For more information about the content model for Button and other content models in WPF, see WPF Content Model.

  • Styles. A Style is a collection of values that represent properties for a control. By using styles, you can create a reusable representation of a desired control appearance and behavior without writing a new control. For example, assume that you want all of your TextBlock controls to have red, Arial font with a font size of 14. You can create a style as a resource and set the appropriate properties accordingly. Then every TextBlock that you add to your application will have the same appearance.

  • Data Templates. A DataTemplate enables you to customize how data is displayed on a control. For example, a DataTemplate can be used to specify how data is displayed in a ListBox. For an example of this, see Data Templating Overview. In addition to customizing the appearance of data, a DataTemplate can include UI elements, which gives you a lot of flexibility in custom UIs. For example, by using a DataTemplate, you can create a ComboBox in which each item contains a check box.

  • Control Templates. Many controls in WPF use a ControlTemplate to define the control's structure and appearance, which separates the appearance of a control from the functionality of the control. You can drastically change the appearance of a control by redefining its ControlTemplate. For example, suppose you want a control that looks like a stoplight. This control has a simple user interface and functionality. The control is three circles, only one of which can be lit up at a time. After some reflection, you might realize that a RadioButton offers the functionality of only one being selected at a time, but the default appearance of the RadioButton looks nothing like the lights on a stoplight. Because the RadioButton uses a control template to define its appearance, it is easy to redefine the ControlTemplate to fit the requirements of the control, and use radio buttons to make your stoplight.

    Note

    Although a RadioButton can use a DataTemplate, a DataTemplate is not sufficient in this example. The DataTemplate defines the appearance of the content of a control. In the case of a RadioButton, the content is whatever appears to the right of the circle that indicates whether the RadioButton is selected. In the example of the stoplight, the radio button needs just be a circle that can "light up." Because the appearance requirement for the stoplight is so different than the default appearance of the RadioButton, it is necessary to redefine the ControlTemplate. In general a DataTemplate is used for defining the content (or data) of a control, and a ControlTemplate is used for defining how a control is structured.

  • Triggers. A Trigger allows you to dynamically change the appearance and behavior of a control without creating a new control. For example, suppose you have multiple ListBox controls in your application and want the items in each ListBox to be bold and red when they are selected. Your first instinct might be to create a class that inherits from ListBox and override the OnSelectionChanged method to change the appearance of the selected item, but a better approach is to add a trigger to a style of a ListBoxItem that changes the appearance of the selected item. A trigger enables you to change property values or take actions based on the value of a property. An EventTrigger enables you to take actions when an event occurs.

For more information about styles, templates, and triggers, see Styling and Templating.

In general, if your control mirrors the functionality of an existing control, but you want the control to look different, you should first consider whether you can use any of the methods discussed in this section to change the existing control's appearance.

Models for Control Authoring

The rich content model, styles, templates, and triggers minimize the need for you to create a new control. However, if you do need to create a new control, it is important to understand the different control authoring models in WPF. WPF provides three general models for creating a control, each of which provides a different set of features and level of flexibility. The base classes for the three models are UserControl, Control, and FrameworkElement.

Deriving from UserControl

The simplest way to create a control in WPF is to derive from UserControl. When you build a control that inherits from UserControl, you add existing components to the UserControl, name the components, and reference event handlers in XAML. You can then reference the named elements and define the event handlers in code. This development model is very similar to the model used for application development in WPF.

If built correctly, a UserControl can take advantage of the benefits of rich content, styles, and triggers. However, if your control inherits from UserControl, people who use your control will not be able to use a DataTemplate or ControlTemplate to customize its appearance. It is necessary to derive from the Control class or one of its derived classes (other than UserControl) to create a custom control that supports templates.

Benefits of Deriving from UserControl

Consider deriving from UserControl if all of the following apply:

  • You want to build your control similarly to how you build an application.

  • Your control consists only of existing components.

  • You don't need to support complex customization.

Deriving from Control

Deriving from the Control class is the model used by most of the existing WPF controls. When you create a control that inherits from the Control class, you define its appearance by using templates. By doing so, you separate the operational logic from the visual representation. You can also ensure the decoupling of the UI and logic by using commands and bindings instead of events and avoiding referencing elements in the ControlTemplate whenever possible. If the UI and logic of your control are properly decoupled, a user of your control can redefine the control's ControlTemplate to customize its appearance. Although building a custom Control is not as simple as building a UserControl, a custom Control provides the most flexibility.

Benefits of Deriving from Control

Consider deriving from Control instead of using the UserControl class if any of the following apply:

  • You want the appearance of your control to be customizable via the ControlTemplate.

  • You want your control to support different themes.

Deriving from FrameworkElement

Controls that derive from UserControl or Control rely upon composing existing elements. For many scenarios, this is an acceptable solution, because any object that inherits from FrameworkElement can be in a ControlTemplate. However, there are times when a control's appearance requires more than the functionality of simple element composition. For these scenarios, basing a component on FrameworkElement is the right choice.

There are two standard methods for building FrameworkElement-based components: direct rendering and custom element composition. Direct rendering involves overriding the OnRender method of FrameworkElement and providing DrawingContext operations that explicitly define the component visuals. This is the method used by Image and Border. Custom element composition involves using objects of type Visual to compose the appearance of your component. For an example, see Using DrawingVisual Objects. Track is an example of a control in WPF that uses custom element composition. It is also possible to mix direct rendering and custom element composition in the same control.

Benefits of Deriving from FrameworkElement

Consider deriving from FrameworkElement if any of the following apply:

  • You want to have precise control over the appearance of your control beyond what is provided by simple element composition.

  • You want to define the appearance of your control by defining your own render logic.

  • You want to compose existing elements in novel ways that go beyond what is possible with UserControl and Control.

Control Authoring Basics

As discussed earlier, one of the most powerful features of WPF is the ability to go beyond setting basic properties of a control to change its appearance and behavior, yet still not needing to create a custom control. The styling, data binding, and trigger features are made possible by the WPF property system and the WPF event system. The following sections describe some practices that you should follow, regardless of the model you use to create the custom control, so that users of your custom control can use these features just as they would for a control that is included with WPF.

Use Dependency Properties

When a property is a dependency property, it is possible to do the following:

  • Set the property in a style.

  • Bind the property to a data source.

  • Use a dynamic resource as the property's value.

  • Animate the property.

If you want a property of your control to support any of this functionality, you should implement it as a dependency property. The following example defines a dependency property named Value by doing the following:

  • Define a DependencyProperty identifier named ValueProperty as a public static readonly field.

  • Register the property name with the property system, by calling DependencyProperty.Register, to specify the following:

    • The name of the property.

    • The type of the property.

    • The type that owns the property.

    • The metadata for the property. The metadata contains the property's default value, a CoerceValueCallback and a PropertyChangedCallback.

  • Define a CLR wrapper property named Value, which is the same name that is used to register the dependency property, by implementing the property's get and set accessors. Note that the get and set accessors only call GetValue and SetValue respectively. It is recommended that the accessors of dependency properties not contain additional logic because clients and WPF can bypass the accessors and call GetValue and SetValue directly. For example, when a property is bound to a data source, the property's set accessor is not called. Instead of adding additional logic to the get and set accessors, use the ValidateValueCallback, CoerceValueCallback, and PropertyChangedCallback delegates to respond to or check the value when it changes. For more information on these callbacks, see Dependency Property Callbacks and Validation.

  • Define a method for the CoerceValueCallback named CoerceValue. CoerceValue ensures that Value is greater or equal to MinValue and less than or equal to MaxValue.

  • Define a method for the PropertyChangedCallback, named OnValueChanged. OnValueChanged creates a RoutedPropertyChangedEventArgs<T> object and prepares to raise the ValueChanged routed event. Routed events are discussed in the next section.

/// <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

For more information, see Custom Dependency Properties.

Use Routed Events

Just as dependency properties extend the notion of CLR properties with additional functionality, routed events extend the notion of standard CLR events. When you create a new WPF control, it is also good practice to implement your event as a routed event because a routed event supports the following behavior:

  • Events can be handled on a parent of multiple controls. If an event is a bubbling event, a single parent in the element tree can subscribe to the event. Then application authors can use one handler to respond to the event of multiple controls. For example, if your control is a part of each item in a ListBox (because it is included in a DataTemplate), the application developer can define the event handler for your control's event on the ListBox. Whenever the event occurs on any of the controls, the event handler is called.

  • Routed events can be used in an EventSetter, which enables application developers to specify the handler of an event within a style.

  • Routed events can be used in an EventTrigger, which is useful for animating properties by using XAML. For more information, see Animation Overview.

The following example defines a routed event by doing the following:

  • Define a RoutedEvent identifier named ValueChangedEvent as a public static readonly field.

  • Register the routed event by calling the EventManager.RegisterRoutedEvent method. The example specifies the following information when it calls RegisterRoutedEvent:

    • The name of the event is ValueChanged.

    • The routing strategy is Bubble, which means that an event handler on the source (the object that raises the event) is called first, and then event handlers on the source's parent elements are called in succession, starting with the event handler on the closest parent element.

    • The type of the event handler is RoutedPropertyChangedEventHandler<T>, constructed with a Decimal type.

    • The owning type of the event is NumericUpDown.

  • Declare a public event named ValueChanged and includes event-accessor declarations. The example calls AddHandler in the add accessor declaration and RemoveHandler in the remove accessor declaration to use the WPF event services.

  • Create a protected, virtual method named OnValueChanged that raises the ValueChanged event.

/// <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

For more information, see Routed Events Overview and Create a Custom Routed Event.

Use Binding

To decouple the UI of your control from its logic, consider using data binding. This is particularly important if you define the appearance of your control by using a ControlTemplate. When you use data binding, you might be able to eliminate the need to reference specific parts of the UI from the code. It's a good idea to avoid referencing elements that are in the ControlTemplate because when the code references elements that are in the ControlTemplate and the ControlTemplate is changed, the referenced element needs to be included in the new ControlTemplate.

The following example updates the TextBlock of the NumericUpDown control, assigning a name to it and referencing the textbox by name in code.

<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

The following example uses binding to accomplish the same thing.

<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>

For more information about data binding, see Data Binding Overview.

Design for Designers

To receive support for custom WPF controls in the WPF Designer for Visual Studio (for example, property editing with the Properties window), follow these guidelines. For more information on developing for the WPF Designer, see Design XAML in Visual Studio.

Dependency Properties

Be sure to implement CLR get and set accessors as described earlier, in "Use Dependency Properties." Designers may use the wrapper to detect the presence of a dependency property, but they, like WPF and clients of the control, are not required to call the accessors when getting or setting the property.

Attached Properties

You should implement attached properties on custom controls using the following guidelines:

  • Have a public static readonly DependencyProperty of the form PropertyNameProperty that was creating using the RegisterAttached method. The property name that is passed to RegisterAttached must match PropertyName.

  • Implement a pair of public static CLR methods named SetPropertyName and GetPropertyName. Both methods should accept a class derived from DependencyProperty as their first argument. The SetPropertyName method also accepts an argument whose type matches the registered data type for the property. The GetPropertyName method should return a value of the same type. If the SetPropertyName method is missing, the property is marked read-only.

  • Set PropertyName and GetPropertyName must route directly to the GetValue and SetValue methods on the target dependency object, respectively. Designers may access the attached property by calling through the method wrapper or making a direct call to the target dependency object.

For more information on attached properties, see Attached Properties Overview.

Define and Use Shared Resources

You can include your control in the same assembly as your application, or you can package your control in a separate assembly that can be used in multiple applications. For the most part, the information discussed in this topic applies regardless of the method you use. There is one difference worth noting, however. When you put a control in the same assembly as an application, you are free to add global resources to the App.xaml file. But an assembly that contains only controls does not have an Application object associated with it, so an App.xaml file is not available.

When an application looks for a resource, it looks at three levels in the following order:

  1. The element level.

    The system starts with the element that references the resource and then searches resources of the logical parent and so forth until the root element is reached.

  2. The application level.

    Resources defined by the Application object.

  3. The theme level.

    Theme-level dictionaries are stored in a subfolder named Themes. The files in the Themes folder correspond to themes. For example, you might have Aero.NormalColor.xaml, Luna.NormalColor.xaml, Royale.NormalColor.xaml, and so on. You can also have a file named generic.xaml. When the system looks for a resource at the themes level, it first looks for it in the theme-specific file and then looks for it in generic.xaml.

When your control is in an assembly that is separate from the application, you must put your global resources at the element level or at the theme level. Both methods have their advantages.

Defining Resources at the Element Level

You can define shared resources at the element level by creating a custom resource dictionary and merging it with your control's resource dictionary. When you use this method, you can name your resource file anything you want, and it can be in the same folder as your controls. Resources at the element level can also use simple strings as keys. The following example creates a LinearGradientBrush resource file named 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>

Once you have defined your dictionary, you need to merge it with your control's resource dictionary. You can do this by using XAML or code.

The following example merges a resource dictionary by using XAML.

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

The disadvantage to this approach is that a ResourceDictionary object is created each time you reference it. For example, if you have 10 custom controls in your library and merge the shared resource dictionaries for each control by using XAML, you create 10 identical ResourceDictionary objects. You can avoid this by creating a static class that merges the resources in code and returns the resulting ResourceDictionary.

The following example creates a class that returns a shared ResourceDictionary.

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;
}

The following example merges the shared resource with the resources of a custom control in the control's constructor before it calls InitializeComponent. Because the SharedDictionaryManager.SharedDictionary is a static property, the ResourceDictionary is created only once. Because the resource dictionary was merged before InitializeComponent was called, the resources are available to the control in its XAML file.

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

Defining Resources at the Theme Level

WPF enables you to create resources for different Windows themes. As a control author, you can define a resource for a specific theme to change your control's appearance depending on what theme is in use. For example, the appearance of a Button in the Windows Classic theme (the default theme for Windows 2000) differs from a Button in the Windows Luna theme (the default theme for Windows XP) because the Button uses a different ControlTemplate for each theme.

Resources that are specific to a theme are kept in a resource dictionary with a specific file name. These files must be in a folder named Themes that is a subfolder of the folder that contains the control. The following table lists the resource dictionary files and the theme that is associated with each file:

Resource dictionary file name Windows theme
Classic.xaml Classic Windows 9x/2000 look on Windows XP
Luna.NormalColor.xaml Default blue theme on Windows XP
Luna.Homestead.xaml Olive theme on Windows XP
Luna.Metallic.xaml Silver theme on Windows XP
Royale.NormalColor.xaml Default theme on Windows XP Media Center Edition
Aero.NormalColor.xaml Default theme on Windows Vista

You do not need to define a resource for every theme. If a resource is not defined for a specific theme, then the control checks Classic.xaml for the resource. If the resource is not defined in the file that corresponds to the current theme or in Classic.xaml, the control uses the generic resource, which is in a resource dictionary file named generic.xaml. The generic.xaml file is located in the same folder as the theme-specific resource dictionary files. Although generic.xaml does not correspond to a specific Windows theme, it is still a theme-level dictionary.

The C# or Visual Basic NumericUpDown custom control with theme and UI automation support sample contains two resource dictionaries for the NumericUpDown control: one is in generic.xaml, and the other is in Luna.NormalColor.xaml.

When you put a ControlTemplate in any of the theme-specific resource dictionary files, you must create a static constructor for your control and call the OverrideMetadata(Type, PropertyMetadata) method on the DefaultStyleKey, as shown in the following example.

static NumericUpDown()
{
    DefaultStyleKeyProperty.OverrideMetadata(typeof(NumericUpDown),
               new FrameworkPropertyMetadata(typeof(NumericUpDown)));
}
Shared Sub New()
    DefaultStyleKeyProperty.OverrideMetadata(GetType(NumericUpDown), New FrameworkPropertyMetadata(GetType(NumericUpDown)))
End Sub
Defining and Referencing Keys for Theme Resources

When you define a resource at the element level, you can assign a string as its key and access the resource via the string. When you define a resource at the theme level, you must use a ComponentResourceKey as the key. The following example defines a resource in 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>

The following example references the resource by specifying the ComponentResourceKey as the key.

<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>
Specifying the Location of Theme Resources

To find the resources for a control, the hosting application needs to know that the assembly contains control-specific resources. You can accomplish that by adding the ThemeInfoAttribute to the assembly that contains the control. The ThemeInfoAttribute has a GenericDictionaryLocation property that specifies the location of generic resources, and a ThemeDictionaryLocation property that specifies the location of the theme-specific resources.

The following example sets the GenericDictionaryLocation and ThemeDictionaryLocation properties to SourceAssembly, to specify that the generic and theme-specific resources are in the same assembly as the control.

[assembly: ThemeInfo(ResourceDictionaryLocation.SourceAssembly,
           ResourceDictionaryLocation.SourceAssembly)]
<Assembly: ThemeInfo(ResourceDictionaryLocation.SourceAssembly, ResourceDictionaryLocation.SourceAssembly)>

See also