0

I really tried to find the solution but I failed. So I have ResourceDictionary in separate xaml file and Control class in another cs file. Here is xaml code:

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:local="clr-namespace:WpfApp1" x:Class="Control1">
    <Style TargetType="{x:Type local:Control1}">
        <Setter Property="GridColor" Value="Red"/>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type local:Control1}">
                    <Grid>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition/>
                            <ColumnDefinition/>
                        </Grid.ColumnDefinitions>
                        <Grid x:Name="PART_Grid" Background="{TemplateBinding GridColor}" 
                              Height="{TemplateBinding Height}" Width="{TemplateBinding Width}"/>
                        <Button Grid.Column="1" x:Name ="PART_Button" Width="50" Height="50"/>
                    </Grid>
                    <ControlTemplate.Triggers>
                        <Trigger Property="IsChecked" Value="True">
                            <Setter Property="GridColor" Value="Black"/>
                        </Trigger>
                    </ControlTemplate.Triggers>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

In cs file I change IsChecked by OnMouseEnter and OnMouseLeave event handlers. It works fine. The problem is that when I change GridColor in OnButtonClick event handler for example, it changes, but after that the trigger setter doesn't work (but another setters still work fine). No exceptions, no messages in output. Did I miss something?

Here is cs code if somebody needs it

  public class Control1: Control
    {
        public static readonly DependencyProperty GridColorProperty = 
            DependencyProperty.Register("GridColor", typeof(Brush), typeof(Control1), new PropertyMetadata(new SolidColorBrush(Colors.Red)));

        public static readonly DependencyProperty IsCheckedProperty = 
            DependencyProperty.Register("IsChecked", typeof(bool), typeof(Control1), new PropertyMetadata(false));

        public Brush GridColor
        {
            get { return (Brush)GetValue(GridColorProperty); }
            set { SetValue(GridColorProperty, value); }
        }

        public bool IsChecked
        {
            get { return (bool)GetValue(IsCheckedProperty); }
            set { SetValue(IsCheckedProperty, value); }
        }

        public override void OnApplyTemplate()
        {
            ((Grid)GetTemplateChild("PART_Grid")).MouseEnter += Grid_MouseEnter;
            ((Grid)GetTemplateChild("PART_Grid")).MouseLeave += Grid_MouseLeave;
            ((Button)GetTemplateChild("PART_Button")).Click += Button_Click;
            base.OnApplyTemplate();
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            GridColor = new SolidColorBrush(Colors.DarkBlue);
        }

        private void Grid_MouseLeave(object sender, System.Windows.Input.MouseEventArgs e)
        {
            IsChecked = false;
        }

        private void Grid_MouseEnter(object sender, System.Windows.Input.MouseEventArgs e)
        {
            IsChecked = true;
        }
    }
  • That's the expected behavior. If you set a property explicitly then it will override your style. You need to move the color-changing logic into another trigger somehow if you don't want it to interfere. – RogerN Jun 27 '19 at 17:04

1 Answers1

0

You seem to have run into a problem with dependency property value precedence: It looks to me like you're setting a local value (#3 in the linked precedence list) in the click handler, and that overrides the value set by the control template trigger (#4a in the precedence list).

The only time you want to mix precedences is when you explicitly want to replace something done in a default style or a base class or something of that nature.

But your approach isn't the best idea anyway: Ideally, you write WPF controls as if a stranger with no access to the source will have to write the default style and default template. And then you write your default template as if you were a stranger. Anything the template may need to know about should be cleanly exposed as a property.

That's not always possible, but I'll demonstrate how you can do it in this case.

The blue brush indicates a state: The button has been clicked. That state isn't explicitly stored or exposed anywhere, it's just implicit in the value of the GridColor property (which you really should have named GridBrush or GridBackground, since it's not a Color).

So let's add a read-only dependency property for that state, and push the decision about brush color off to the template, where it belongs. This is really powerful, because if you want some sibling control to change its state depending on our HasBeenClicked state, you can just add a binding in the parent view.

You could also make HasBeenClicked a regular read/write dependency property, if you wanted a consumer of the control to be able to change that state programmatically or via a binding.

//  This is not a good name, but I don't know what your semantics are. 
public bool HasBeenClicked
{
    get { return (bool)GetValue(HasBeenClickedProperty); }
    protected set { SetValue(HasBeenClickedPropertyKey, value); }
}

internal static readonly DependencyPropertyKey HasBeenClickedPropertyKey =
    DependencyProperty.RegisterReadOnly(nameof(HasBeenClicked), typeof(bool), typeof(Control1),
        new PropertyMetadata(false));

public static readonly DependencyProperty HasBeenClickedProperty = HasBeenClickedPropertyKey.DependencyProperty;

private void Button_Click(object sender, RoutedEventArgs e)
{
    HasBeenClicked = true;
}

And in the control template:

<ControlTemplate.Triggers>
    <!-- 
    The last applicable trigger wins: If both are true, GridColor will be Black 
    -->
    <Trigger Property="HasBeenClicked" Value="True">
        <Setter Property="GridColor" Value="DarkBlue"/>
    </Trigger>
    <Trigger Property="IsChecked" Value="True">
        <Setter Property="GridColor" Value="Black"/>
    </Trigger>
</ControlTemplate.Triggers>
  • Thank you, that's what I need, but what if I need to change brush property not to one value? For example, imagine that here is some kind of Paint or whatever and I need to set many values to this brush during this program work and also I need a triggers that will make this brush bigger, smaller or something else? – Hirano Kohta Jun 28 '19 at 08:15
  • @HiranoKohta You could make the state property an enum. Then you could either add more triggers, or write a value converter (IValueConverter — lots of examples around). – 15ee8f99-57ff-4f92-890c-b56153 Jun 28 '19 at 11:10