styling generic WPF controls
I am looking into creating type-safe generic controls. This is targeting the (reduced) generics support in WPF 4 and future Silverlight, and will include a hierarchy of generic controls.
I have two questions:
- Can you use style setters and template bindings for non-generic properties defined on a generic control?
- In Silverlight, is there a value I can use for the default style key in the base class that will allow using the same style in the (temporary) specific-type derived classes? (
ComponentResourceKey
does not exist in Silverlight, so the setup described below does not work.)
The test generic control below defines two test properties: a non-generic Description
property, and a generic Data
property. The control sets DefaultStyleKey
to a ComponentResourceKey
for the control.
Here i开发者_如何学Pythons how the test control is defined:
public class GenericControl<T> : Control {
static GenericControl( ) {
DefaultStyleKeyProperty.OverrideMetadata(
typeof(GenericControl<T>), new FrameworkPropertyMetadata(
new ComponentResourceKey( typeof(Proxy), "GenericControl`1" )
)
);
}
public static readonly DependencyProperty DescriptionProperty =
DependencyProperty.Register(
"Description", typeof(string), typeof(GenericControl<T>),
new PropertyMetadata( "Default Description" )
);
public static readonly DependencyProperty DataProperty =
DependencyProperty.Register(
"Data", typeof(T), typeof(GenericControl<T>),
new PropertyMetadata( default(T) )
);
public string Description { get { ... } set { ... } }
public T Data { get { ... } set { ... } }
}
Here is the style for the test control in generic.xaml
:
<Style x:Key="{ComponentResourceKey {x:Type local:Proxy}, GenericControl`1}">
<Setter Property="Control.Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Control}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Description,
RelativeSource={RelativeSource TemplatedParent}}" />
<TextBlock Text="{Binding Data,
RelativeSource={RelativeSource TemplatedParent}}" />
</StackPanel>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Here are some examples of how this test control would be declared in xaml:
<ListBox Name="list" ... />
<GenericControl x:TypeArguments="sys:Int32" Description="Count: "
Data="{Binding Items.Count, ElementName=list}" />
<Slider Name="slider" ... />
<GenericControl x:TypeArguments="sys:Double" Description="Slider Value: "
Data="{Binding Value, ElementName=slider}" />
With the current generics support in WPF 4, you cannot use an open generic type as the TargetType
of a style or control template (doing so results in a "'GenericControl`1' TargetType does not match type of element 'GenericControl`1'." exception). This has two main consequences, as mentioned in question 1 above:
- You must use a normal binding with
RelativeSource={RelativeSource TemplatedParent}
instead of aTemplateBinding
in the control template to reference properties defined by the generic control. - You cannot create a style setter for the
Description
property, even though it does not depend on the generic type of the control.
For the latter, there is a workaround in WPF: just define the non-generic properties as attached dependency properties on a proxy type. Then you can use AddOwner
to "declare" the properties on the generic control, and you can use "ProxyType.Property" syntax in a style setter. Of course, Silverlight does not support AddOwner
, and turning what is supposed to be an instance property into an attached property is not ideal in any case, so this is not really a long-term solution.
Aside: It looks like there is a regression in the xaml parsing behavior for types. Using VS2008, I can use {x:Type local:GenericControl`1}
to get the open type of the control, which I used as the example type in the ComponentResourceKey
. In VS2010 though, this results in the following error: "Character '`' was unexpected in string 'local:GenericControl`1'. Invalid XAML type name.", so I changed it to use the proxy type instead.
I posted this same question to the WPF and the Silverlight forums. There was no response on Silverlight, but here is a summary of the answer for WPF:
- To quote Shreedhar, "XAML doesn't currently support specifying open generic types".
- Using a closed generic type, such as
TargetType="{x:Type local:GenericControl(x:Int32)}"
, will work for an individual style, but will require copy-and-paste to target the same control for other type parameters. - Can create a default template on the fly for any given type argument using
XamlReader
and some string replacement, but this leaves something to be desired when creating a new style or template outside the control.
- Yes, you can. But your xaml should be based on non-generic type only. For example we made slider control like this...
SliderInt32 -> BaseRange<T> -> BaseSliderControl
We could only define style either on BaseSliderControl or only SliderInt32, but not on BaseRange. We could specify generic as well as non generic properties in BaseRange class, and they work well in SliderInt32.
- Even in silverlight, you can introduce a parent class of generic class which can serve as your type key, like as shown in above example "BaseSliderControl" control style always works as long as child does not override it.
GenericControl`1 etc name is not proper fully qualified name, so it will never work.
精彩评论