开发者

WPF: Styling a Generic Base Control

开发者 https://www.devze.com 2023-02-19 02:55 出处:网络
Is it possible to provide a default style for a generic base control in WPF? Assume I have the following base classes:

Is it possible to provide a default style for a generic base control in WPF?

Assume I have the following base classes:

public abstract class View<T> : ContentControl
    where T : ViewModel
{
    static View()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(View<T>), 
            new FrameworkPropertyMetadata(typeof(View<T>)));
    }

    // Other properties, methods, etc in here
}

public abstract class ViewModel
{
    // Other properties, methods, etc in here
}

Then assume I have a two classes which inherit from these base classes:

public partial class TestView : View<TestViewModel>
{
    public TestView()
    {
        InitializeComponent();
    }

    // TestView specific methods, properties, etc
}

public class TestViewModel : ViewModel
{ /* TestViewModel specific methods, properties, etc */ }

Now I want to provide a default style for the base control that all my derived controls use:

<Style TargetType="{x:Type local:View`1}">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type local:View`1}">
                <Border Background="Magenta"
                        BorderBrush="{TemplateBinding BorderBrush}"
                        BorderThickness="{TemplateBinding BorderThickness}">
                    <StackPanel>
                        <Button>Test</Button>
                        <ContentPresenter ContentSource="Content" />
                    </StackPanel>
                </Border>
            </ControlTemplate>
        </Setter.Value>
    </Setter开发者_JAVA百科>
</Style>

However, when I use my TestView control, I don't have the template markup applied (and thus any content i might define in the XAML of my TestView control is not in the visual/logical tree).

I am basically trying to take my base view/viewmodel classes and apply a consistent look and feel. This of course works in the non-generic base view cases. However, I require the type-safe hook up between view and viewmodel so I can call methods on the viewmodel from anything that has reference to the view (which I know may not "sit well" with the way some people have implemented MVVM).


I found fairly simple solution involving a custom TypeExtension.

1 - Set the DefaultStyleKey to the default generic type as mentioned in CodeNaked's answer:

    DefaultStyleKeyProperty.OverrideMetadata(typeof(View<T>), 
        new FrameworkPropertyMetadata(typeof(View<>)));

2 - Create the following class than inherits from System.Windows.Markup.TypeExtension


    [System.Windows.Markup.ContentProperty("Type")]
    public class TypeExtension : System.Windows.Markup.TypeExtension
    {
        public TypeExtension()
            : base()
        { }

        public TypeExtension(Type type)
        : base(type)
        { }

        public override object ProvideValue(IServiceProvider serviceProvider)
        {
            if (Type == null)
                throw new InvalidOperationException("Must specify the Type");

            return Type;
        }
    }

3 - Update the style's TargetType to point to the new local:Type extension instead of the usual x:Type extension


    <Style>
        <Style.TargetType>
            <local:Type Type="{x:Type local:View`1}" />
        </Style.TargetType>
        <Setter Property="Control.Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type Control}">

        . . .

Thats it.. There is a caveat though, VS throws a compile error when you attempt to bind/set any of the dependency properties defined on the View<T> class. So you cannot use simple syntax like {TemplateBinding ViewTProperty} ...


Short answer: no

Long answer:

In your code behind you are specifying a DefaultStyleKey of typeof(View<T>), where T is resolved to an actual type. In the XAML, you are effectively doing typeof(Value<>), where T is still "undefined".

You can set your DefaultStyleKey to:

DefaultStyleKeyProperty.OverrideMetadata(typeof(View<T>), 
        new FrameworkPropertyMetadata(typeof(View<>)));

This will correctly find the Style, but will result in a exception (as TestView cannot be case to View<>).

Your best bet is to define your base Style like you do, but give it an x:Key like "ViewBaseStyle". Then create a Style for each derive type that is BasedOn ViewBaseStyle.


The way I did it is I made a base class without generics and templated that. Then I inherit the base class with a generic class (not templated), which can be used to make your class variations (also not templated). In effect, everything inheriting the base class (without generics) would have the same template.

For instance,

//You can define a template for this!
public class ViewBase : UserControl
{
    public ViewBase() : base()
    {
        DefaultStyleKey = typeof(ViewBase);
    }
}

//Use for class variations
public class View<T> : ViewBase
{
    public View() : base()
    {
        //Do whatever
    }
}

//Example class variation
public class DecimalView : View<decimal>
{
    public DecimalView() : base()
    {
        //Do whatever
    }
}

ViewBase, View<T>, and DecimalView now all share the same default style. Additionally, you can also specify an individual style for each class variation based on the original style (ViewBase), just not for the generic class.

It's worth noting that the best way, then, to bind to the top-level class' properties would be using the syntax {Binding Path, RelativeSource={RelativeSource AncestorType={x:Type ViewBase}}} versus {TemplateBinding Path}. The latter, as well as {Binding Path, RelativeSource={RelativeSource TemplatedParent}}, will only be applicable to properties owned by ViewBase.

0

精彩评论

暂无评论...
验证码 换一张
取 消

关注公众号