xamlwinui-3windows-app-sdkwinui-xaml

Positioning TextBlock relative to another Textblock Inside RelativePanel in a WinUI 3 UserControl


I have a UserControl in WinUI 3 where I have defined a RelativePanel containing 2 TextBlock. One of these Textblocks serves as a Label, and I would like to position it relative to the other Textblock. To achieve this, I’ve created a DependencyProperty called LabelPosition, that I can set to Above, Below, Right or Left. I also have a callback where I set all the parameters of the RelativePanel so that the Label is positioned according to what is specified in the LabelPosition property.

This is the XAML code of the UserControl:

<?xml version="1.0" encoding="utf-8"?>
<UserControl
    x:Class="ClientMan.Controls.FormFieldUserControl"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:ClientMan.Controls"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    x:Name="FormField"
   >
    <RelativePanel>
        <TextBlock x:Name="FieldLabel"
            Text="{x:Bind Label}" />

        <TextBlock x:Name="FieldTextBlock"
           Text="{x:Bind Text}"
           IsTextSelectionEnabled="True" />
    </RelativePanel>
</UserControl>

In code-behind I have this:

//other code
  public static readonly DependencyProperty LabelPositionProperty = DependencyProperty.Register(nameof(LabelPosition), typeof(Position), typeof(FormFieldUserControl), new PropertyMetadata(Position.Above, OnPositionLabelChanged));
        public Position LabelPosition
        {
            get => (Position)GetValue(LabelPositionProperty);
            set => SetValue(LabelPositionProperty, value);
        }

        private static void OnPositionLabelChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs eventArgs)
        {
            var control = dependencyObject as FormFieldUserControl;
            var newPosition = (Position)eventArgs.NewValue;

            switch (newPosition)
            {
                case Position.Left:
                    ResetPositionRelativePanel([control.FieldLabel, control.FieldTextBlock]);
                    RelativePanel.SetAlignRightWithPanel(control.FieldTextBlock, true);
                    RelativePanel.SetAlignTopWithPanel(control.FieldTextBlock, true);
                    RelativePanel.SetAlignLeftWithPanel(control.FieldLabel, true);
                    RelativePanel.SetAlignVerticalCenterWith(control.FieldLabel, control.FieldTextBlock);
                    RelativePanel.SetRightOf(control.FieldTextBlock, control.FieldLabel);
                    break;
                case Position.Right:
                   //other code
                default:
                    break;
            }
        }

The problem is with RelativePanel.SetAlignVerticalCenterWith and RelativePanel.SetRightOf, with this I put the Label vertically centered relative to the other TextBlock and then I position the FieldTextBlock to the right of the Label, but this create a circular dependency and the app crash.

I’ve considered a potential solution where I take the width of the Label, remove the call to the RelativePanel.SetRightOf method, add a call to RelativePanel.SetAlignLeftWithPanel(control.FieldTextBlockBorder, true) and finally add a margin, equal to the width of the Label, to the left of FieldTextBlock.

However, I’m not sure if this is the best approach or if there are better ways to achieve the desired layout.

Edit:

This is what I try to achive

Above label

Below label

Right label

Left label

When I try to use Left or Right the app crash at startup (I don't get any error or warning in the error list).

Taking the code posted above as example, if I comment out one between RelativePanel.SetRightOf(control.FieldTextBlock, control.FieldLabel); or RelativePanel.SetAlignVerticalCenterWith(control.FieldLabel, control.FieldTextBlock); the app runs as expected.

I also tried to set the same properties in XAML and I get the following error

xaml error


Solution

  • IMHO, it's easier to implement this feature by just using a Grid and its ColumnDefinitions:

    FormFieldUserControl.xaml

    <UserControl
        x:Class="RelativePanelDemo.FormFieldUserControl"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:local="using:RelativePanelDemo"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d">
    
        <Grid
            x:Name="RootGrid"
            RowDefinitions="Auto,Auto"
            ColumnDefinitions="Auto,Auto">
            <TextBlock
                x:Name="FieldLabel"
                Text="{x:Bind Label}" />
            <TextBlock
                x:Name="FieldTextBlock"
                IsTextSelectionEnabled="True"
                Text="{x:Bind Text}" />
        </Grid>
    </UserControl>
    

    FormFieldUserControl.xaml.cs

    using Microsoft.UI.Xaml;
    using Microsoft.UI.Xaml.Controls;
    
    namespace RelativePanelDemo;
    
    public enum LabelPosition
    {
        Top,
        Bottom,
        Left,
        Right,
    }
    
    public sealed partial class FormFieldUserControl : UserControl
    {
    
        public static readonly DependencyProperty LabelProperty =
            DependencyProperty.Register(
                nameof(Label),
                typeof(string),
                typeof(FormFieldUserControl),
                new PropertyMetadata(default));
    
        public static readonly DependencyProperty TextProperty =
            DependencyProperty.Register(
                nameof(Text),
                typeof(string),
                typeof(FormFieldUserControl),
                new PropertyMetadata(default));
    
        public static readonly DependencyProperty LabelPositionProperty =
            DependencyProperty.Register(
                nameof(LabelPosition),
                typeof(LabelPosition),
                typeof(FormFieldUserControl),
                new PropertyMetadata(LabelPosition.Left, OnLabelPositionPropertyChanged));
    
        public FormFieldUserControl()
        {
            InitializeComponent();
            RefreshPosition();
        }
    
        public string Label
        {
            get => (string)GetValue(LabelProperty);
            set => SetValue(LabelProperty, value);
        }
        public string Text
        {
            get => (string)GetValue(TextProperty);
            set => SetValue(TextProperty, value);
        }
    
        public LabelPosition LabelPosition
        {
            get => (LabelPosition)GetValue(LabelPositionProperty);
            set => SetValue(LabelPositionProperty, value);
        }
    
        private static void OnLabelPositionPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            if (d is FormFieldUserControl control)
            {
                control.RefreshPosition();
            }
        }
    
        private void RefreshPosition()
        {
            switch (LabelPosition)
            {
                case LabelPosition.Top:
                    Grid.SetRow(FieldLabel, 0);
                    Grid.SetColumn(FieldLabel, 0);
                    Grid.SetRow(FieldTextBlock, 1);
                    Grid.SetColumn(FieldTextBlock, 0);
                    break;
                case LabelPosition.Bottom:
                    Grid.SetRow(FieldLabel, 1);
                    Grid.SetColumn(FieldLabel, 0);
                    Grid.SetRow(FieldTextBlock, 0);
                    Grid.SetColumn(FieldTextBlock, 0);
                    break;
                case LabelPosition.Left:
                    Grid.SetRow(FieldLabel, 0);
                    Grid.SetColumn(FieldLabel, 0);
                    Grid.SetRow(FieldTextBlock, 0);
                    Grid.SetColumn(FieldTextBlock, 1);
                    break;
                case LabelPosition.Right:
                    Grid.SetRow(FieldLabel, 0);
                    Grid.SetColumn(FieldLabel, 1);
                    Grid.SetRow(FieldTextBlock, 0);
                    Grid.SetColumn(FieldTextBlock, 0);
                    break;
                default:
                    break;
            }
        }
    }