1

I am currently working on making my WPF application a little bit more generic. Up to this point, for each button I wanted to create, I used a different style to modify roundness (and it creates a lot of useless code).

Using the following code I've managed to create a variable I can change from the XAML file, but I cannot link it to the roundness itself.

Could anyone please tell me what I am doing wrong? I already have checked on so many forums but no one seems to have the answer other than "don't do it in a generical way".

I can precise that everything is compiling and the style is otherwise correctly applied to the button (there is no xaml linking problem).

The style I am using:

<Style x:Key="AwakeButton" TargetType="{x:Type customcontrols:AwakeButton}" BasedOn="{StaticResource {x:Type Button}}"
       xmlns:extensions="Awake.Services.Properties:Extensions">
    <Setter Property="customcontrols:AwakeButton.BorderRoundness" Value="4.0"/>
    <Style.Resources>
        <Style TargetType="Border">
            <Setter Property="CornerRadius" Value="{Binding Path=BorderRoundness}" />
            <!--<Setter Property="CornerRadius" Value="10" />-->
        </Style>
    </Style.Resources>
</Style>

The overload of the button I created to do so:

public class AwakeButton : Button
{
    public AwakeButton()
    {
        
    }

    public static DependencyProperty BorderRoundnessProperty =
         DependencyProperty.RegisterAttached("BorderRoundness", typeof(double), typeof(AwakeButton)); 
    public static void SetBorderRoundness(UIElement element, double value)
    {
        element.SetValue(BorderRoundnessProperty, value);
        
    }

    public static double GetBorderRoundness(UIElement element)
    {
        return (double)element.GetValue(BorderRoundnessProperty);
    }
}

How I am using it in the page:

<customcontrols:AwakeButton Style="{StaticResource AwakeButton}" Margin="142,115,0,0"  Width="136" Height="167" BorderRoundness="5">
thatguy
  • 21,059
  • 6
  • 30
  • 40
Matthieu
  • 21
  • 2
  • 1
    [this my answer](https://stackoverflow.com/a/40663219/1506454) shows how to use attached DP. if you create derived button class, you can use regular DP, but still need to change Template – ASh Feb 26 '21 at 08:39
  • I already saw that answer, but by that time I could not make it work (namespace issue), and it is easier for me to use a custom class for now I think (even though I don't really need one) – Matthieu Feb 26 '21 at 08:53

2 Answers2

1

You have to bind the BorderRoundness to the parent AwakeButton, otherwise it is resolved using the current DataContext, which does not contain this property. Furthermore, if you derive from Button, you do not have to make the dependency property attached, you could just register a normal one using the Register(...) method. Also make DPs static and readonly.

<Setter Property="CornerRadius" Value="{Binding BorderRoundness, RelativeSource={RelativeSource AncestorType={x:Type local:AwakeButton}}}" />

If you do not change anything special about the button, you could also create attached properties instead of a dedicated sub type just for exposing a BorderRoundness property.

public static class ButtonProperties
{
   public static readonly DependencyProperty BorderRoundnessProperty =
      DependencyProperty.RegisterAttached("BorderRoundness", typeof(double), typeof(ButtonProperties));

   public static void SetBorderRoundness(UIElement element, double value)
   {
      element.SetValue(BorderRoundnessProperty, value);

   }

   public static double GetBorderRoundness(UIElement element)
   {
      return (double)element.GetValue(BorderRoundnessProperty);
   }
}

You can refer to the BorderRoundness using attached property binding syntax (parentheses).

<Style x:Key="AwakeButton" TargetType="{x:Type Button}" BasedOn="{StaticResource {x:Type Button}}">
   <Setter Property="local:ButtonProperties.BorderRoundness" Value="4.0"/>
   <Style.Resources>
      <Style TargetType="Border">
         <Setter Property="CornerRadius" Value="{Binding (local:ButtonProperties.BorderRoundness), RelativeSource={RelativeSource AncestorType={x:Type Button}}}" />
      </Style>
   </Style.Resources>
</Style>

You use regular button now with the newly created attached border roundness property.

<Button Grid.Row="0" Style="{StaticResource AwakeButton}" Margin="142,115,0,0"  Width="136" Height="167" local:ButtonProperties.BorderRoundness="5"/>
thatguy
  • 21,059
  • 6
  • 30
  • 40
-1

The roundness is applied as CornerRadius to the Border of the Button. The Border is defined in the ControlTemplate of the Button. The ControlTemplate defines the appearance of a control.
In other words, you need to delegate the property values to the related elements in the ControlTemplate.

To delegate the values to the ControlTemplate, you have to override this template and bind the templated parents properties to the template elements:

In your AwakeButton define the BorderRoundness property as simple DependencyProperty (not attached) and the override the default style definition, so that the AwakeButton will use its own default Style. This way the Button is reusable withgout having to redefine the Style each time you weant to use it, which is especially important wjen you publish your project as library:

AwakeButton.cs

public class AwakeButton : Button
{
  public static readonly DependencyProperty BorderRoundnessProperty = DependencyProperty.Register(
    "BorderRoundness",
    typeof(Thickness),
    typeof(AwakeButton),
    new PropertyMetadata(default(Thickness)));

  public Thickness DestinationPath
  {
    get => (Thickness) GetValue(AwakeButton.BorderRoundnessProperty);
    set => SetValue(AwakeButton.BorderRoundnessProperty, value);
  }

  static AwakeButton()
  {
    DefaultStyleKeyProperty.OverrideMetadata(typeof(AwakeButton), new FrameworkPropertyMetadata(typeof(AwakeButton)));
  }
}

Generic.xaml.cs
This file is located in the Themes folder and contains all default styles. WPF will automatically check this file for a default style and apply it if no other Style override was found.

<Style TargetType="AwakeButton">
  <Setter Property="Template">
    <Setter.Value>
      <ControlTemplate TargetType="AwakeButton">
        <Border BorderBrush={TemplateBinding BorderBrush}"
                BorderThickness="{TemplateBinding BorderThickness}"
                BorderBrush="{TemplateBinding BorderBrush}"
                CornerRadius="{TemplateBinding BorderRoundness}">
          <ContentPresenter />
        </Border>
      </ControlTemplate>
    </Setter.Value>
  </Setter>
</Stayle>

Example

<Grid>
  <AwakeButton BorderRoundness="8" />
</Grid>

But if you want to make it really general, using an attached property, you have to make a attached behavior. The following code works with every DependencyObject that contains a Border as child in its visual tree:

class Element : DependencyObject
{
  #region CornerRoundness attached property

  public static readonly DependencyProperty CornerRoundnessProperty = DependencyProperty.RegisterAttached(
    "CornerRoundness",
    typeof(CornerRadius),
    typeof(Element),
    new PropertyMetadata(default(CornerRadius), Element.OnCornerRoundnessChanged));

  public static void SetCornerRoundness(DependencyObject attachingElement, CornerRadius value) =>
    attachingElement.SetValue(Element.CornerRoundnessProperty, value);

  public static CornerRadius GetCornerRoundness(DependencyObject attachingElement) =>
    (CornerRadius) attachingElement.GetValue(Element.CornerRoundnessProperty);

  #endregion CornerRoundness attached property


  private static void OnCornerRoundnessChanged(DependencyObject attachingElement, DependencyPropertyChangedEventArgs e)
  {
    if (Element.TryFindVisualChildElement(attachingElement, out Border elementBorder))
    {
      elementBorder.CornerRadius = (CornerRadius) e.NewValue;
    }
  }

  public static bool TryFindVisualChildElement<TChild>(DependencyObject parent, out TChild resultElement)
    where TChild : DependencyObject
  {
    resultElement = null;

    if (parent is Popup popup)
    {
      parent = popup.Child;
      if (parent == null)
      {
        return false;
      }
    }

    for (var childIndex = 0; childIndex < VisualTreeHelper.GetChildrenCount(parent); childIndex++)
    {
      DependencyObject childElement = VisualTreeHelper.GetChild(parent, childIndex);

      if (childElement is TChild child)
      {
        resultElement = child;
        return true;
      }

      if (Element.TryFindVisualChildElement(childElement, out resultElement))
      {
        return true;
      }
    }

    return false;
  }
}

Example

<StackPanel>
  <Button Element.CornerRoundness="8" />
  <ToggleButton Element.CornerRoundness="8" />
</StackPanel>
BionicCode
  • 1
  • 4
  • 28
  • 44