Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

WPF themeing best practices

This is a best practices question regarding wpf themeing and more specifically skinning. This is more of an opinion based question since I don't have a problem making this work but more of a general wondering if my conclusions cover all the scenarios, and if any one else came across the same thoughts on the issue and what was their approach .

Some background, Our team is required to define a way to give our system the ability to be themeable.

We broke this ability down to 2 categories :

1) The styles of our controls which we simply call 'Theme'.

2) The resources they use to customize their appearance called 'Skin' this includes Brushes , and all sorts of sizing structs like CornerRadius , BorderThickness etc.

The way which a Skin is set for the system is a simple case of merging the skin dictionary last into our app's resources.

  <Application.Resources>
    <ResourceDictionary>
        <ResourceDictionary.MergedDictionaries>
            <ResourceDictionary Source="Default.skin.xaml" />
            <ResourceDictionary Source="Theme.xaml" />
        </ResourceDictionary.MergedDictionaries>
    </ResourceDictionary>
</Application.Resources>

A different skin being merged last into our app.

  protected override void OnStartup(StartupEventArgs e)
  {
       base.OnStartup(e);

       string skin = e.Args[0];
       if (skin == "Blue")
       {         .
            ResourceDictionary blueSkin = new ResourceDictionary();
            blueSkin.Source = new Uri("Blue.skin.xaml", UriKind.Relative);

            Application.Current.Resources.MergedDictionaries.Add(blueSkin);
       }
  }

Inside Theme.xaml :

   <!-- Region TextBox ControlTemplate -->

<ControlTemplate TargetType="{x:Type TextBox}" x:Key="TextBoxTemplate">
    <Border  Background="{TemplateBinding Background}"  
         BorderBrush="{TemplateBinding BorderBrush}" 
         BorderThickness="{TemplateBinding BorderThickness}"
         CornerRadius="{StaticResource TextBoxCornerRadius}" >
      <Border x:Name="shadowBorder" BorderBrush="{StaticResource TextBoxShadowBrush}"                                   
        CornerRadius="{StaticResource TextBoxInnerShadowCornerRadius}" 
        BorderThickness="{StaticResource TextBoxInnerShadowBorderThickness}" 
        Margin="{StaticResource TextBoxInnerShadowNegativeMarginForShadowOverlap}" >
            <ScrollViewer x:Name="PART_ContentHost"  Padding="{TemplateBinding Padding}" 
                    VerticalAlignment="{TemplateBinding VerticalContentAlignment}" 
                    HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" />
        </Border>
    </Border>

    <ControlTemplate.Triggers>
        <Trigger Property="BorderThickness" Value="0">
            <Setter TargetName="shadowBorder" Property="BorderThickness" Value="0" />
        </Trigger>
    </ControlTemplate.Triggers>
</ControlTemplate>

<!-- EndRegion -->

<!-- Region TextBox Style -->

<Style x:Key="{x:Type TextBox}" TargetType="{x:Type TextBox}">     
    <Setter Property="BorderBrush" Value="{StaticResource TextBoxBorderBrush}" />
    <Setter Property="Background" Value="{StaticResource TextBoxBackgroundBrush}" />
    <Setter Property="BorderThickness" Value="{StaticResource TextBoxBorderThickness}" />

    <Setter Property="Padding" Value="{StaticResource TextBoxPadding}" />
    <Setter Property="Template" Value="{StaticResource TextBoxTemplate}"/>
  <Style.Triggers>

        <Trigger Property="IsMouseOver" Value="True">
            <Setter Property="Background" Value="{StaticResource TextBoxIsMouseOverBackgroundBrush}" />
            <Setter Property="BorderBrush" Value="{StaticResource TextBoxIsMouseOverBorderBrush}" />
        </Trigger>
        <Trigger Property="IsFocused" Value="True">
            <Setter Property="Background" Value="{StaticResource TextBoxIsMouseWithinBackgroundBrush}" />
            <Setter Property="BorderBrush" Value="{StaticResource TextBoxIsMouseWithinBorderBrush}" />
        </Trigger>

    </Style.Triggers>
</Style>

<!-- EndRegion -->

In the TextBox ControlTemplate there are elements bound to DependencyProperties using TemplateBinding and some like the CornerRadius and InnerCornerRadius, InnerBorderThickness and InnerBorderBrush which are given their value from resources.

What would be the best approach ?

creating a derived control with the relevant Dependency properties which would reference the relevant resources and then have the elements in the control template bind to them.

Or

have the elements inside the template reference these resources themselves.

Using the Dependency Property approach :

Advantages :

1) Clarity, we have a clearer API for our control and better understanding of how our control looks and behaves the way it does.

2) The template does not have to change in order to be customizable. Everything is controlled via style.

3) Triggers as well change the look and feel of the control without the need to override the control template, no need for ControlTemplate triggers.

4) "Blendabilty" using blend i can much easily customize my control.

5) Styles themselves are inheritable. so if i want to change just one aspect of the control all i need to do is inherit from the default style.

Disadvantages :

1) Implementing yet another custom control.

2) Implementing numerous dependency properties, some of which do not have much to do with the control and are only there to satisfy something we have in our template.

  • Just to clarify this means inheriting from TextBox something like InnerShadowTextBox and implementing dependency properties with in it for all the above.

This will intensify if I have numerous elements inside my template which have to be customizable.

Something like this monstrosity:

  <Style x:Key="{x:Type cc:ComplexControl}" TargetType="{x:Type cc:ComplexControl}">
    <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type cc:ComplexControl}">
                    <Grid>
                        <Ellipse Fill="Red" Margin="0" Stroke="Black" StrokeThickness="1"/>
                        <Ellipse Fill="Green"  Margin="6" Stroke="Red" StrokeThickness="1"/>
                        <Ellipse Fill="Blue" Margin="12"/>
                        <Ellipse Fill="Aqua" Margin="24" />
                        <Ellipse Fill="Beige" Margin="32"/>
                        <StackPanel Orientation="Horizontal" Width="25" Height="25"
                                    VerticalAlignment="Center" HorizontalAlignment="Center">
                            <Rectangle Fill="Black" Width="2" />
                            <Rectangle Fill="Black" Width="2" Margin="2,0,0,0"/>
                            <Rectangle Fill="Black" Width="2" Margin="2,0,0,0"/>
                            <Rectangle Fill="Black" Width="2" Margin="2,0,0,0"/>
                        </StackPanel>

                    </Grid>
                </ControlTemplate>
            </Setter.Value>
    </Setter>
</Style>

Which would require numerous resources :

 <SolidColorBrush x:Key="Ellipse1Fill">Red</SolidColorBrush>
 <SolidColorBrush x:Key="Ellipse2Fill">Green</SolidColorBrush>
 <SolidColorBrush x:Key="Ellipse3Fill">Blue</SolidColorBrush>
 <SolidColorBrush x:Key="Ellipse4Fill">Aqua</SolidColorBrush>
 <SolidColorBrush x:Key="Ellipse5Fill">Beige</SolidColorBrush>
 <SolidColorBrush x:Key="Ellipse1Stroke">Beige</SolidColorBrush>
 <sys:Double x:Key="Ellipse1StrokeThickness>1</sys:Double>
      ......... and many more 

I would have a large list of resources either way. But with dependency properties. I would also need to assign need to find meaning in every little part,Which sometimes isn't much more then "it looks good" and does not have much to do with the control or What if tomorrow I would want to change the template.

Using the approach where the resources are referenced from within the control template.

Advantages :

1) Easy to use, side steps the ugliness describes in the disadvantages described above in the Dp approach while providing a "hack" that enables a theme.

Disadvantages :

1) If I would want to further customize my control like add a trigger that influences the inner border of my TextBox I would simply have to create a new control template.

2) Not a clear API, Lets say I would like to change the BorderBrush of the inner border in a specific view.

         <TextBox>
             <TextBox.Resources>
                  <SolidColorBrush x:Key="InnerBorderBrush" Color="Red" />
             </TextBox.Resources>
          </TextBox> 

Which isn't that bad come to think about it… we sometimes do this with Selector implementations which internally use the specific resources when getting rid of the inactive selection and hightlight colors like so :

   <ListBox>
       <ListBox.Resources>
          <SolidColorBrush x:Key="{x:Static SystemColors.HighlightBrushKey}" Color="Transparent"/>
          <SolidColorBrush x:Key="{x:Static SystemColors.HighlightTextBrushKey}" Color="Transparent"/>
          <SolidColorBrush x:Key="{x:Static SystemColors.InactiveSelectionHighlightBrushKey}" Color="Transparent"/>
          <SolidColorBrush x:Key="{x:Static SystemColors.InactiveSelectionHighlightTextBrushKey}" Color="Transparent"/>
       </ListBox.Resources>
 </ListBox>

Conclusions :

The hybrid described in the TextBox Style above is the way to go.

1) Dependency properties will be introduced only for aspects of the control which relate to the control's logic including specific template part's .

2) The resource names would be comprised by a clear naming convention and separated in files based on the control they relate to and common usages in views,Like Common brushes used in views in our app.

3) Control templates should aspire to be minimalistic and to use existing Dependency properties. Like Background, Foreground, BorderBrush etc.

I would greatly appreciate your input and thoughts on the matter , thanks in advance.

like image 888
eran otzap Avatar asked Aug 14 '15 21:08

eran otzap


1 Answers

As Xavier said, this might be a better question for Code Review. But I will convey some key thoughts on your question, even though a lot of it will come to personal (or team) style and requirements.

After creating several dozen themes, I would recommend against custom controls whenever possible. Over time, the maintainability goes down quite a bit.

If you require minor modifications to a style, it is better to use DataTemplates and Data Triggers if the situation allows. This way you are changing the style in a clean way.

Additionally, you can leverage the BasedOn property. Create your "base" style and have multiple styles that have the attribute BasedOn="{myBaseStyle}. This will allow you lots of options without cluttering your code.

As a rule of thumb, I always recommend having more brushes/colors/resources as opposed to more styles or templates. We usually have our hierarchy set for colors->brushes->styles->templates. This helps reuse the colors while still maintaining separation via brushes.

Using DynamicResource as opposed to StaticResource is also useful in some situations where you load resources dynamically.

Hope this helps. Would love to write more, but some of the parameters for writing a solid theme is very context specific. If you have further examples, I'd be glad to add more information.

like image 117
Dax Pandhi Avatar answered Oct 28 '22 22:10

Dax Pandhi