5
\$\begingroup\$

Suppose you just want to set border radius for different controls:

  • Button
  • TextBox
  • ComboBox

The most frequent approach I usually find is totally override default template like in answers

I find overriding default styles for every CornerRadius value of Button, TextBox, ComboBox very bad idea for the following reasons:

  • You'll have to carry a lot of styles in your resources and they will be polluted with a lot of default properties.

  • You create duplicate code for every property value if you need different values

  • You don't have clean, elegant code for setting border radius itself

So I've implemented Attached property for this. The most changing part of implementing was to implement this for ComboBox

public class CornerRadiusSetter
{
    public static CornerRadius GetCornerRadius(DependencyObject obj) => (CornerRadius)obj.GetValue(CornerRadiusProperty);

    public static void SetCornerRadius(DependencyObject obj, CornerRadius value) => obj.SetValue(CornerRadiusProperty, value);

    public static readonly DependencyProperty CornerRadiusProperty =
        DependencyProperty.RegisterAttached(nameof(Border.CornerRadius), typeof(CornerRadius),
            typeof(CornerRadiusSetter), new UIPropertyMetadata(new CornerRadius(), CornerRadiusChangedCallback));

    public static void CornerRadiusChangedCallback(object sender, DependencyPropertyChangedEventArgs e)
    {
        Control control = sender as Control;

        if (control == null) return;

        control.Loaded += Control_Loaded;
    }

    private static void Control_Loaded(object sender, EventArgs e)
    {
        Control control = sender as Control;

        if (control == null || control.Template == null) return;

        control.ApplyTemplate();

        CornerRadius cornerRadius = GetCornerRadius(control);

        Control toggleButton = control.Template.FindName("toggleButton", control) as Control;

        if (control is ComboBox && toggleButton != null)
        {
            toggleButton.ApplyTemplate();

            // Set border radius for border radius border
            Border toggleButtonBorder = toggleButton.Template.FindName("templateRoot", toggleButton) as Border;
            toggleButtonBorder.CornerRadius = cornerRadius;

            // Expand padding for combobox to avoid text clipping by border radius
            control.Padding = new Thickness(
                    control.Padding.Left + cornerRadius.BottomLeft,
                    control.Padding.Top,
                    control.Padding.Right + cornerRadius.BottomRight,
                    control.Padding.Bottom);

            // Decrease width of dropdown and center it to avoid showing "sticking" dropdown corners
            Popup popup = control.Template.FindName("PART_Popup", control) as Popup;

            Popup popup = control.Template.FindName("PART_Popup", control) as Popup;

            if (popup != null)
            {
                double offset = cornerRadius.BottomLeft - 1;
                if (offset > 0)
                    popup.HorizontalOffset = offset;
            }

            SystemDropShadowChrome shadowChrome = control.Template.FindName("shadow", control) as SystemDropShadowChrome;

            if (shadowChrome != null)
            {
                double minWidth = control.ActualWidth - cornerRadius.BottomLeft - cornerRadius.BottomRight;
                if (minWidth > 0)
                    shadowChrome.MinWidth = minWidth;
            }
        }

        // setting borders for non-combobox controls
        Border border = control.Template.FindName("border", control) as Border;

        if (border == null) return;

        border.CornerRadius = cornerRadius;
    }
}

So now you can set borders either in styles of in code via single property

<Button local:CornerRadiusSetter.CornerRadius="10">Button</Button>
<Button local:CornerRadiusSetter.CornerRadius="5,7,10,12">Button</Button>
<TextBox local:CornerRadiusSetter.CornerRadius="3,0,0,3" />
<TextBox local:CornerRadiusSetter.CornerRadius="7,8,2,1" />
<ComboBox local:CornerRadiusSetter.CornerRadius="5" />
<ComboBox local:CornerRadiusSetter.CornerRadius="7" />

Or for multiple controls inside Resources:

<Style TargetType="Button">
    <Setter Property="local:CornerRadiusSetter.CornerRadius" Value="10" />
</Style>

Results of styling

Styling demo

\$\endgroup\$
  • \$\begingroup\$ Do you mind adding some screenshots? :-) \$\endgroup\$ – t3chb0t Jun 22 '18 at 10:39
  • \$\begingroup\$ @t3chb0t Not at all. You mean I should add screenshot or you will? Also I want to know what kind of screenshot do you want. \$\endgroup\$ – Vadim Ovchinnikov Jun 22 '18 at 10:43
  • 1
    \$\begingroup\$ Oh, sorry, I meat if you have any? I'd be nice to see how the result of this utility looks like. \$\endgroup\$ – t3chb0t Jun 22 '18 at 10:44
  • 1
    \$\begingroup\$ @t3chb0t Added screenshot. There you can see also that dropdown width is decreased to avoid dropdown corner sticking. \$\endgroup\$ – Vadim Ovchinnikov Jun 22 '18 at 11:19
  • 1
    \$\begingroup\$ @VadimOvchinnikov you don't have to copy-paste. You can still bind CornerRadius property inside a template to some attached property (such as Border.CornerRadius). And then set that property on actual controls. \$\endgroup\$ – Nikita B Jun 22 '18 at 12:15
6
\$\begingroup\$

This is a nice feature, and all in all it works well. A couple of remarks though:


Split up the main method in two methods - one for ComboBoxes and one for other controls. It is more readable and easier to maintain:

private static void Control_Loaded(object sender, EventArgs e)
{
  Control control = sender as Control;

  if (control == null || control.Template == null) return;

  control.ApplyTemplate();

  if (control is ComboBox)
    SetComboBox(control as ComboBox);
  else
    SetOtherControl(control);
}

Be aware that you set the Loaded event handler every time you change the CornerRadius. You probably don't change the corner radius after app load, but if you do, you attach a new handler for the controls Loaded event. It's no big deal because it is only called once (at load time) - but anyway it looks bad. You could probably handle it like this:

public static void CornerRadiusChangedCallback(object sender, DependencyPropertyChangedEventArgs e)
{
  Control control = sender as Control;

  if (control == null) return;

  control.Loaded -= Control_Loaded;
  control.Loaded += Control_Loaded;
}

You should be aware that SystemDropShadowChrome is defined in more than one assembly (Aero and Aero2 for instance) so you'll have to link to the right assembly. Which one that is in use I think is system dependent(?). You could maybe use Reflection to overcome this(?)


Setting the properties of the Popup/Dropdown of the ComboBox at load time is a bad idea, because if the ComboBox changes size (width), the settings you made at load time are not adjusted to the new size. Instead you should make the settings for the ComboBox in both the Loaded and the SizeChanged events or maybe just in the DropDownOpened event.

\$\endgroup\$
  • \$\begingroup\$ Regarding SystemDropShadowChrome type: you are right. I can replace this line with Decorator shadowChrome = control.Template.FindName("shadow", control) as Decorator; and avoid extra DLLs dependency. \$\endgroup\$ – Vadim Ovchinnikov Jun 22 '18 at 10:45
  • \$\begingroup\$ Regarding last point I don't know how practical it could be. I don't mind this feature, but dynamically changing size for ComboBox seems to be either bad UI experience or rare need. Usual ComboBoxes have fixed sizes. Am I missing something? \$\endgroup\$ – Vadim Ovchinnikov Jun 22 '18 at 10:47
  • \$\begingroup\$ @VadimOvchinnikov: About SystemDropShadowChrome - Yes that's a way to go or simply just FrameworkElement :-) \$\endgroup\$ – Henrik Hansen Jun 22 '18 at 10:49
  • \$\begingroup\$ @VadimOvchinnikov: Changing the with of a ComboBox in a dialog for instance or in a sizable docking window is very common and a very good UI experience - IMO :-) \$\endgroup\$ – Henrik Hansen Jun 22 '18 at 10:52
4
\$\begingroup\$

Holy crap. All that just to get Rounded corners?

Yes, I wholeheartedly agree with that comment. It is astonishing, how some things that sound trivial - are extremely complicated in WPF. But there is only so much that you can do. You can either fight it or embrace it (you can also choose different framework, but that's another story). And when choosing between those two options, you have to consider this:

What are the chances, that you are not going to change default templates anyway in the future? In my experience those chances are often quite slim. If you care enough about your UI that corner radius bothers you, then that's probably not the only thing you will need to change. What about that highlight color? What about other controls? Will you change corner radius of scrollbar buttons also? Progressbar?

Correct me if I'm wrong. For this you should 1) Store nearly a hundred lines of code for default button styles, 2) Create new class with attached property (you can't write Border.CornerRadius="10" for button, can you?)

Yes, you can't, but you can set it in style. This code works:

<Page
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
  <Button>  
      <Button.Style>
          <Style TargetType="Button">
              <Setter Property="Border.CornerRadius" Value="15"/>
              <Setter Property="Width" Value="100"/>
              <Setter Property="Height" Value="100"/>
              <Setter Property="Template">
                  <Setter.Value>
                      <ControlTemplate>
                          <Border CornerRadius="{TemplateBinding Border.CornerRadius}" 
                                  BorderThickness="1" Background="Green" BorderBrush="Black"/>
                      </ControlTemplate>
                  </Setter.Value>
              </Setter>
          </Style>
      </Button.Style>
  </Button>
</Page>

But you can, of course, use your own stand-alone attached property (there is no need to subclass anything). You can even make it inherited, and set it for you entire MainWindow, so that all controls are affected.


Don't get me wrong, I often write similar behaviors myself. I just think that whenever I want to modify control template via behavior, it is a good idea to carefully consider the implications. Yes, when you override template, there is some initial copy-pasting. But then it just works. Want to databind Padding property? Fine. Want to change corner radius property via animation? Sure. Want to use those buttons inside a docked window, that can reload its content multiple times? No problem.

You solution on the other hand is very much tied to your current use case:

1) It overrides padding property, potentially breaking triggers and databindings.

2) It does not allow modifications to CornerRadius after control is loaded.

3) It will not work, if Loaded event triggers more than once. Most notably, your padding will just keep growing.

4) It relies heavily on the fact, that default button template has an element named border. But will it in the future versions of the framework? I mean realistically, it probably will, but this assumption still feels shaky.

There might be other edge cases, where you behavior will fail but the template will not. So is it all worth it? I am not so sure.

\$\endgroup\$
  • \$\begingroup\$ By the way, in this button template, are you missing default trigger implementation and if you don't include them, they won't work, am I right? \$\endgroup\$ – Vadim Ovchinnikov Jun 22 '18 at 14:07
  • \$\begingroup\$ Regarding point #3 I suppose I would just set Padding to Border element and that will work even in case of multiple Loaded triggering. I don't understand why this won't work as desired. \$\endgroup\$ – Vadim Ovchinnikov Jun 22 '18 at 14:10
  • \$\begingroup\$ @VadimOvchinnikov yes, it is missing pretty much everything, except for border. :) This was just an example of how you can replace hardcoded radius with TemplateBinding in your actual template. \$\endgroup\$ – Nikita B Jun 22 '18 at 14:12
  • \$\begingroup\$ Regarding point #2 I agree but I assume this can be solved but I haven't tried because I won't be using this case. \$\endgroup\$ – Vadim Ovchinnikov Jun 22 '18 at 14:13
  • \$\begingroup\$ Maybe it's offtopic, but I'm very curious about idea to "choose different framework". Are there any other powerful enterprise-level frameworks except WPF? \$\endgroup\$ – Vadim Ovchinnikov Jun 22 '18 at 14:16

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.