35
votes

is there any "best practice" way to replace a part of the default template. The current use case is a treeview. As default, the treeview has this small triangle shapes to expand and collapse.

I know how to replace these if I replace the whole control template, as shown in the code below. I am not sure if there is a way to "keep all default, just change XY". Its not a style, I basically need to replace a part of an existing control template.

To illustrate, take a look at the following XAML. The first smaller block is the relevant XAML I want to be able to adapt.

The bigger second and third part are basically a copy of the default templates, only to administer the "changed" part from the beginning.

Is there a better way to do this, saving the long and confusing XAML in the second half?

        <ResourceDictionary 
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" >





  <Style x:Key="ExpandCollapseToggleStyle" TargetType="ToggleButton">
    <Setter Property="Focusable" Value="False"/>
     <Setter Property="Template">
      <Setter.Value>
        <ControlTemplate TargetType="ToggleButton">
          <Grid
            Width="15"
            Height="13"
            Background="Transparent">
            <Path x:Name="ExpandPath"
              HorizontalAlignment="Left" 
              VerticalAlignment="Center" 
              Margin="1,1,1,1"
              Fill="Black"
              Data="M 4 0 L 8 4 L 4 8 Z"/>
          </Grid>
          <ControlTemplate.Triggers>
            <Trigger Property="IsChecked"
                 Value="True">
              <Setter Property="Data"
                  TargetName="ExpandPath"
                  Value="M 0 4 L 8 4 L 4 8 Z"/>
            </Trigger>
          </ControlTemplate.Triggers>
        </ControlTemplate>
      </Setter.Value>
    </Setter>
  </Style>

<Setter Property="Template">
  <Setter.Value>
    <ControlTemplate TargetType="{x:Type TreeViewItem}">
      <Grid>
        <Grid.ColumnDefinitions>
          <ColumnDefinition MinWidth="19"
                    Width="Auto"/>
          <ColumnDefinition Width="Auto"/>
          <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
          <RowDefinition Height="Auto"/>
          <RowDefinition/>
        </Grid.RowDefinitions>
        <ToggleButton x:Name="Expander"
                Style="{StaticResource ExpandCollapseToggleStyle}"
                IsChecked="{Binding Path=IsExpanded,
                            RelativeSource={RelativeSource TemplatedParent}}"
                ClickMode="Press"/>
        <Border Name="Bd"
            Grid.Column="1"
            Background="{TemplateBinding Background}"
            BorderBrush="{TemplateBinding BorderBrush}"
            BorderThickness="{TemplateBinding BorderThickness}"
            Padding="{TemplateBinding Padding}">
          <ContentPresenter x:Name="PART_Header"
                    ContentSource="Header"
                    HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"/>
        </Border>
        <ItemsPresenter x:Name="ItemsHost"
                Grid.Row="1"
                Grid.Column="1"
                Grid.ColumnSpan="2"/>
      </Grid>
      <ControlTemplate.Triggers>
        <Trigger Property="IsExpanded"
             Value="false">
          <Setter TargetName="ItemsHost"
              Property="Visibility"
              Value="Collapsed"/>
        </Trigger>
        <Trigger Property="HasItems"
             Value="false">
          <Setter TargetName="Expander"
              Property="Visibility"
              Value="Hidden"/>
        </Trigger>
        <MultiTrigger>
          <MultiTrigger.Conditions>
            <Condition Property="HasHeader"
                   Value="false"/>
            <Condition Property="Width"
                   Value="Auto"/>
          </MultiTrigger.Conditions>
          <Setter TargetName="PART_Header"
              Property="MinWidth"
              Value="75"/>
        </MultiTrigger>
        <MultiTrigger>
          <MultiTrigger.Conditions>
            <Condition Property="HasHeader"
                   Value="false"/>
            <Condition Property="Height"
                   Value="Auto"/>
          </MultiTrigger.Conditions>
          <Setter TargetName="PART_Header"
              Property="MinHeight"
              Value="19"/>
        </MultiTrigger>
        <Trigger Property="IsSelected"
             Value="true">
          <Setter TargetName="Bd"
              Property="Background"
              Value="{DynamicResource {x:Static SystemColors.HighlightBrushKey}}"/>
          <Setter Property="Foreground"
              Value="{DynamicResource {x:Static SystemColors.HighlightTextBrushKey}}"/>
        </Trigger>
        <MultiTrigger>
          <MultiTrigger.Conditions>
            <Condition Property="IsSelected"
                   Value="true"/>
            <Condition Property="IsSelectionActive"
                   Value="false"/>
          </MultiTrigger.Conditions>
          <Setter TargetName="Bd"
              Property="Background"
              Value="{DynamicResource {x:Static SystemColors.ControlBrushKey}}"/>
          <Setter Property="Foreground"
              Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/>
        </MultiTrigger>
        <Trigger Property="IsEnabled"
             Value="false">
          <Setter Property="Foreground"
              Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
        </Trigger>
      </ControlTemplate.Triggers>
    </ControlTemplate>
  </Setter.Value>
</Setter>

4

4 Answers

26
votes

Unfortunately, I think you have to replace the entire template:

From MSDN: http://msdn.microsoft.com/en-us/library/aa970773.aspx

Controls in Windows Presentation Foundation (WPF) have a ControlTemplate that contains the visual tree of that control. You can change the structure and appearance of a control by modifying the ControlTemplate of that control. There is no way to replace only part of the visual tree of a control; to change the visual tree of a control you must set the Template property of the control to its new and complete ControlTemplate.

10
votes

Actually there is a way (sort of). You can create your own custom control and override the OnApplyTemplate function to change the style dynamically.

For example, create a custom control like so (I am doing this in silverlight but it's all the same I presume):

namespace SilverlightClassLibrary1
{
    public class MyButton: Button
    {
        public string BackgroundColor { get; set; }

        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();

            if (BackgroundColor != null)
            {
                Rectangle r = this.GetTemplateChild("BackgroundGradient") as Rectangle;

                if (r != null)
                {
                    r.Fill = new SolidColorBrush(Color.FromArgb(255, 
                        Convert.ToByte(BackgroundColor.Substring(1,2),16),
                        Convert.ToByte(BackgroundColor.Substring(3,2),16),
                        Convert.ToByte(BackgroundColor.Substring(5,2),16)));
                }
            }
        }
    }
}

The interesting part is the GetTemplateChild method, that's looking for a Rectangle control named "BackgroundGradient". (BTW, it's easier if you define custom controls in a separate project, so create a new "Silverlight class library" project if you haven't already done so and put it into that project.)

Then add a new resource dictionary file and override the control template and make sure you have a rectangle named "BackgroundGradient". In this case we're using the standard button control template that I've cut down a bit:

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:custom="clr-namespace:SilverlightClassLibrary1;assembly=SilverlightClassLibrary1">

    <Style TargetType="custom:MyButton">
        <Setter Property="Background" Value="#FF1F3B53"/>
        <Setter Property="Foreground" Value="#FF000000"/>
        <Setter Property="Padding" Value="3"/>
        <Setter Property="BorderThickness" Value="1"/>
        <Setter Property="BorderBrush">
            <Setter.Value>
                <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
                    <GradientStop Color="#FFA3AEB9" Offset="0"/>
                    <GradientStop Color="#FF8399A9" Offset="0.375"/>
                    <GradientStop Color="#FF718597" Offset="0.375"/>
                    <GradientStop Color="#FF617584" Offset="1"/>
                </LinearGradientBrush>
            </Setter.Value>
        </Setter>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="Button">
                    <Grid>
                        <Border x:Name="Background" CornerRadius="3" Background="White" BorderThickness="{TemplateBinding BorderThickness}" BorderBrush="{TemplateBinding BorderBrush}">
                            <Grid Background="{TemplateBinding Background}"  Margin="1">
                                <Border Opacity="0"  x:Name="BackgroundAnimation" Background="#FF448DCA" />
                                <Rectangle x:Name="BackgroundGradient" Fill="White" >
                                </Rectangle>
                            </Grid>
                        </Border>
                        <ContentPresenter
                              x:Name="contentPresenter"
                              Content="{TemplateBinding Content}"
                              ContentTemplate="{TemplateBinding ContentTemplate}"
                              VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
                              HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                              Margin="{TemplateBinding Padding}"/>
                        <Rectangle x:Name="DisabledVisualElement" RadiusX="3" RadiusY="3" Fill="#FFFFFFFF" Opacity="0" IsHitTestVisible="false" />
                        <Rectangle x:Name="FocusVisualElement" RadiusX="2" RadiusY="2" Margin="1" Stroke="#FF6DBDD1" StrokeThickness="1" Opacity="0" IsHitTestVisible="false" />
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

So you can now declare a button control and override a style if you'd like:

<UserControl x:Class="SilverlightApplication1.MainPage"
            ...
            xmlns:custom="clr-namespace:SilverlightClassLibrary1;assembly=SilverlightClassLibrary1">

        <custom:MyButton>Normal Button 1</custom:MyButton>
        <custom:MyButton>Normal Button 2</custom:MyButton>

        <custom:MyButton BackgroundColor="#8888cc">Customized Background</custom:MyButton>

I presume you could get get even more clever and pass through a resource name or a style name and load it dynamically.

You then need to include your resource file as part of your application:

<Application xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
             x:Class="SilverlightApplication1.App"
             >
    <Application.Resources>
        <ResourceDictionary >
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="Dictionary1.xaml" />
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Application.Resources>
</Application>

and you'll see your custom property changes in your XAML designer.

4
votes

I would redesign your template to be a Custom Control, using ContentPresenters as placeholders for the elements that will change.

To use these placeholders you must link them to dependency properties.

Check this post to see how this is done

http://www.codeproject.com/Articles/82464/How-to-Embed-Arbitrary-Content-in-a-WPF-Control

"Using Custom Control" explains my approach

2
votes

2020 Update

I came across this question on my search for an answer and this is how I did it:

To override the default control templates: use Microsoft Blend > Via the Designer right click on the control you wish to override > Edit Template > Edit Current or Edit a Copy (If you're using the same control multiple times like in my case a Menu Item) save the template in a Resource Dictionary and use it as a Dynamic Resource on the controls you wish to override. and of course, edit the template the way you want.