Microsoft-ui-xaml: Can't set the border properties of the NumberBox.

Created on 3 Feb 2020  路  15Comments  路  Source: microsoft/microsoft-ui-xaml

Describe the bug

Border properties like BorderBrush or BorderThickness can't change from xaml.

Steps to reproduce the bug

Steps to reproduce the behavior:
<ComboBox Header="ComboxBorder" BorderThickness="0.5" BorderBrush="DarkGray" />
<controls:NumberBox Header="ComboxBorder" BorderThickness="0.5" BorderBrush="DarkGray"/>

Expected behavior

Border properties should work like other controls (ComboBox, TextBox,...)

Screenshots

image

Version Info

NuGet package version:


| Windows 10 version | Saw the problem? |
| :--------------------------------- | :-------------------- |
| Insider Build (xxxxx) | |
| November 2019 Update (18363) | Yes |
| May 2019 Update (18362) | |
| October 2018 Update (17763) | |
| April 2018 Update (17134) | |
| Fall Creators Update (16299) | |
| Creators Update (15063) | |


| Device form factor | Saw the problem? |
| :-------------------- | :------------------- |
| Desktop | Yes |
| Mobile | |
| Xbox | |
| Surface Hub | |
| IoT | |

Additional context

area-NumberBox help wanted needs-assignee-attention team-Controls

Most helpful comment

To clarify: In SpinButtonPlacementMode.Inline, if we apply properties like BorderThickness and BorderBrush, is the desired look the following?

<controls:NumberBox
    BorderBrush="Orange"
    BorderThickness="4"/>

image

All 15 comments

@SavoySchuler @teaP Should we expose these TextBox properties onto NumberBox through a template bind ? Are there any other TextBox properties that would fall under the same category ?

@ranjeshj - thanks for the chat, that's exactly what we should do. @teaP is there anything you need from my end to expose these TextBox properties onto NumberBox through a template bind?

@teaP Are you currently working on this (or soon) or is this issue up for grabs for the community?

@Felix-Dev, it is up for grabs now :)

In that case, I would like to tackle this one.

To clarify: In SpinButtonPlacementMode.Inline, if we apply properties like BorderThickness and BorderBrush, is the desired look the following?

<controls:NumberBox
    BorderBrush="Orange"
    BorderThickness="4"/>

image

To clarify: In SpinButtonPlacementMode.Inline, if we apply properties like BorderThickness and BorderBrush, is the desired look the following?

image

I would say so yes

To clarify: In SpinButtonPlacementMode.Inline, if we apply properties like BorderThickness and BorderBrush, is the desired look the following?

<controls:NumberBox
    BorderBrush="Orange"
    BorderThickness="4"/>

image

I can't image if the BorderThickness=0, what will spin button looks like?

@hipo760
Setting the BorderThickness to 0 for the NumberBox would result in the following look:
image

and mouse hovering over the increase button:
image

@hipo760
Setting the BorderThickness to 0 for the NumberBox would result in the following look:
image

and mouse hovering over the increase button:
image

Looks so great, thank you!!

@ranjeshj Currently, we expose the border thickness of the NumberBox Inline SpinButtons as a resource:

<Thickness x:Key="NumberBoxSpinButtonBorderThickness">0,1,1,1</Thickness>

Note that we don't set the left border side here. This way, the border between the two spin buttons has the same thickness as the other border sides (e.g. top, bottom) as can be seen in the images above.

In order to make the SpinButtons border settable via a BorderThickness TemplateBinding (so that NumberBox.BorderThickness affects the whole NumberBox and not just its internal TextBox) I could add a new API similar to the CornerRadiusFilter API. The API would enable us to specify a mask to apply to a Thickness value in order to ignore the thickness value for specific sides. With the CornerRadiusFilter API we specify which corner radius values to keep using the Filter property. The Thickness masking API would operate the other way: Specifying the border side we want to ignore (set to 0).

An example use case for the NumberBox could look like this:

<!-- The Thickness masking resource to use -->
<primitives:ThicknessMaskConverter x:Key="LeftThicknessMaskConverter" Mask="Left"/>
<!-- Use the above Thickness masking resource for NumberBox' SpinButtons in Inline mode -->
<RepeatButton x:Name="DownSpinButton"
    BorderThickness="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=BorderThickness, Converter={StaticResource LeftThicknessMaskConverter}}"
    Style="{StaticResource NumberBoxSpinButtonStyle}"/>

Thoughts?

@Felix-Dev That sounds like a reasonable approach. This would be new API though, so adding @SavoySchuler and @MikeHillberg.

Just assigned myself so that I can take a look at this.

@SavoySchuler I have finalized my API proposal now (including a test implementation in a local branch of mine).

To recap, the NumberBox uses three borders to achieve its visual look in all the different visual states: A border around its input field (the TextBox and its border) and around each of its SpinButtons when in SpinButtonPlacementMode::Inline. See the following image of a NumberBox in rest mode:
image
To achieve the uniform border thickness used here, the border thickness values of the spin buttons are of the form 0,X,X,X (so the border's left side thickness is set to 0).

Currently, setting the NumberBox.BorderBrush API has no effect on the NumberBox which @hipo760 lists in their opening post. If one wishes to easily customize the border thickness of aNumberBox, lightweight styling has to be used instead for now. To add support for the NumberBox.BorderThickness API and make it work as intended, we need to filter the template bound BorderThickness for the spin buttons accordingly. In other words, we need to set the Thickness.Left field of a given Thickness value to 0 (zero). See the following GIF which shows what will happen to the NumberBox's border if we template bind to the NumberBox.BorderThickness without filtering the thickness value as described above:

<RepeatButton x:Name="UpSpinButton"
    BorderThickness="{TemplateBinding BorderThickness}"/>

numberbox-borderthickness-incorrect

As a comparison, here is how the NumberBox.BorderThickness API will be applied with correct filtering of the template bound BorderThickness:
numberbox-borderthickness-correct

To achieve this, I propose adding a new API similar to the existing CornerRadiusFilterConverter API. This new API will be used to create a new Thickness struct from an existing one, while using a configurable mask to set some of the Thickness fields to 0 while keeping the rest. See below for an API proposal and an API example.

API proposal

[flags]
enum ThicknessMaskKinds
{
    None = 0,
    Top = 1,
    Right = 2,
    Bottom = 4,
    Left = 8
};

runtimeclass ThicknessMaskConverter : Windows.UI.Xaml.DependencyObject, Windows.UI.Xaml.Data.IValueConverter
{
    ThicknessMaskConverter();

    [MUX_DEFAULT_VALUE("winrt::ThicknessMaskKinds::None")]
    ThicknessMaskKinds Mask{ get; set; };

    winrt::IInspectable ThicknessMaskConverter::Convert(
        winrt::IInspectable const& value, 
        winrt::TypeName const& targetType, 
        winrt::IInspectable const& parameter, 
        winrt::hstring const& language);

    winrt::IInspectable ThicknessMaskConverter::ConvertBack(
        winrt::IInspectable const& value,
        winrt::TypeName const& targetType,
        winrt::IInspectable const& parameter,
        winrt::hstring const& language);

    static Windows.UI.Xaml.DependencyProperty MaskProperty{ get; };
};

API Notes

| Property | Description |
|:----------|:--------------|
| Mask | Gets or sets the mask applied to the ThicknessMaskConverter specifying which values of a Thickness struct will be set to 0 (zero) and which to keep. Can be a combination of multiple ThicknessMaskKinds members. |
| MaskProperty | Identifies the Mask dependency property. |

| Method| Description |
|:----------|:--------------|
| Convert(winrt::IInspectable, winrt::TypeName, winrt::IInspectable, winrt::hstring) | Converts the source Thickness to a new Thickness by setting some fields to 0 specified by Mask and leave the rest intact. |
| ConvertBack(winrt::IInspectable, winrt::TypeName, winrt::IInspectable, winrt::hstring) | Not implemented. |

API Example

Consuming the proposed API is similar to how you would use the CornerRadiusFilterConverter API. See, for example, the following XAML below. Here, we first create a new instance of the ThicknessMaskConverter class with our desired Mask and then use our converter in a binding. For demo purposes, I created an example Thickness resource called ExampleThickness in order to use a binding. In the NumberBox ControlTemplate case, we would instead work with a commonly used TemplatedParent relative binding.

<Page
    xmlns:primitives="using:Microsoft.UI.Xaml.Controls.Primitives">
    <Page.Resources>
        <primitives:ThicknessMaskConverter x:Key="LeftTopRightThicknessMaskConverter" Mask="Left,Top,Right"/>

        <Thickness x:Key="ExampleThickness">4</Thickness>
    </Page.Resources>

    <Grid x:Name="RootGrid">
        <TextBox
            Text="I am a TextBox with just a bottom border"
            BorderBrush="GreenYellow"
            BorderThickness="{Binding Source={StaticResource ExampleThickness}, Converter={StaticResource LeftTopRightThicknessMaskConverter}}" />
    </Grid>
</Page>

The XAML above gives us the following TextBox look:
image

As already mentioned above we would use the ThicknessMaskConverter API in the NumberBox ControlTemplate like this:

<RepeatButton x:Name="UpSpinButton"
    BorderThickness="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=BorderThickness, Converter={StaticResource LeftThicknessMaskConverter}}"
    Style="{StaticResource NumberBoxSpinButtonStyle}"/>

where LeftThicknessMaskConverter is defined as:

<primitives:ThicknessMaskConverter x:Key="LeftThicknessMaskConverter" Mask="Left"/>

Additional Notes

  • First and foremost, I'd like to give a shoutout to @stevenbrix who is always a great help when I have specific implementation questions. Thank you, Steve!

  • Secondly, while we can view this API as filtering out specific Thickness fields and throwing away others (by setting them to 0) I went with the "Mask" terminology for now. The reason being that I can imagine we might be more interested interested in ignoring the value of only 1-2 fields of a Thickness instead of 3-4 fields. Take our NumberBox case here, for example. We filter out the Top, Right and Bottom values of a Thickness value and ignore only the Left value. If I would have gone with the filter terminology, here's how my XAML would have looked like instead:

<primitives:ThicknessFilterConverter x:Key="TopRightBottomThicknessFilterConverter" Filter="Top,Right,Bottom"/>

<RepeatButton x:Name="UpSpinButton"
    BorderThickness="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=BorderThickness, Converter={StaticResource TopRightBottomThicknessFilterConverter}}"
    Style="{StaticResource NumberBoxSpinButtonStyle}"/>

Compare that to the XAML using the Mask terminology:

<primitives:ThicknessMaskConverter x:Key="LeftThicknessMaskConverter" Mask="Left"/>

Obviously, I am open to renaming and restructuring the proposed API if the team prefers the name ThicknessFilterConverter (or any other name) over my proposed naming.

Summary

The proposed ThicknessMaskConverter API will be a great helper API to achieve proper NumberBox.BorderThickness support. More broadly speaking it is to be used in a (Template) Binding to create a new Thickness struct from an existing one, setting only some of the fields to 0 while keeping the rest.

@SavoySchuler Any update here?

Was this page helpful?
0 / 5 - 0 ratings