WPF Custom GroupBox Header OpacityMask issues

573 views Asked by At

I've created a custom GroupBox control that provides a button at the top right. I noticed that the border does not provide a gap for the button like it does for the header, so I wrote a custom BorderGapConverter. Everything works until the content of the header changes. The border, and I believe the opacitymask doesn't change until the window that contains the control is resized.

ButtonedGroupBox.cs

namespace My.Controls
{
    /// <summary>
    /// Interaction logic for ButtonedGroupBox.xaml
    /// </summary>
    public partial class ButtonedGroupBox : GroupBox
    {
        public static readonly DependencyProperty CommandProperty = DependencyProperty.Register("Command", typeof(ICommand), typeof(ButtonedGroupBox), new FrameworkPropertyMetadata(null));
        public static readonly DependencyProperty CommandParameterProperty = DependencyProperty.Register("CommandParameter", typeof(object), typeof(ButtonedGroupBox), new FrameworkPropertyMetadata(null));

        protected override void OnHeaderChanged(object oldHeader, object newHeader)
        {
            base.OnHeaderChanged(oldHeader, newHeader);
            try
            {
                GetBindingExpression(OpacityMaskProperty).UpdateTarget();
            }
            catch (NullReferenceException) { }
        }

        public ICommand Command { get { return (ICommand)GetValue(CommandProperty); } set { SetValue(CommandProperty, value); } }
        public object CommandParameter { get { return GetValue(CommandParameterProperty); } set { SetValue(CommandParameterProperty, value); } }

        public ButtonedGroupBox()
        {
            InitializeComponent();
        }
    }
}

ButtonedGroupBox.xaml

<GroupBox x:Class="My.Controls.ButtonedGroupBox" x:Name="ctrlThis"
          xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
          xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
          xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
          xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
          xmlns:cv="clr-namespace:My.ValueConverters"
          mc:Ignorable="d"
          d:DesignHeight="300" d:DesignWidth="300">
    <GroupBox.Resources>
        <BorderGapMaskConverter x:Key="BorderGapMaskConverter"/>
        <cv:BorderGapConverter x:Key="MyBorderGapMaskConverter"/>
    </GroupBox.Resources>
    <GroupBox.Style>
        <Style TargetType="{x:Type GroupBox}">
            <Setter Property="BorderBrush" Value="#D5DFE5"/>
            <Setter Property="BorderThickness" Value="1"/>
        </Style>
    </GroupBox.Style>
    <GroupBox.Template>
        <ControlTemplate TargetType="{x:Type GroupBox}">
            <Grid SnapsToDevicePixels="true">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="6"/>
                    <ColumnDefinition Width="Auto"/>
                    <ColumnDefinition Width="*"/>
                    <ColumnDefinition Width="Auto"/>
                    <ColumnDefinition Width="6"/>
                </Grid.ColumnDefinitions>
                <Grid.RowDefinitions>
                    <RowDefinition Height="Auto"/>
                    <RowDefinition Height="Auto"/>
                    <RowDefinition Height="*"/>
                    <RowDefinition Height="6"/>
                </Grid.RowDefinitions>
                <Border BorderBrush="Transparent" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" Grid.ColumnSpan="5" Grid.Column="0" CornerRadius="4" Grid.Row="1" Grid.RowSpan="3"/>
                <Border BorderBrush="White" BorderThickness="{TemplateBinding BorderThickness}" Grid.ColumnSpan="5" CornerRadius="4" Grid.Row="1" Grid.RowSpan="3">
                    <Border.OpacityMask>
                        <MultiBinding ConverterParameter="6" Converter="{StaticResource MyBorderGapMaskConverter}">
                            <Binding RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType=Grid}"/>
                            <Binding Path="ActualWidth" RelativeSource="{RelativeSource Self}"/>
                            <Binding Path="ActualHeight" RelativeSource="{RelativeSource Self}"/>
                        </MultiBinding>
                    </Border.OpacityMask>
                    <Border BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="3">
                        <Border BorderBrush="White" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="2"/>
                    </Border>
                </Border>
                <Border x:Name="Header" Grid.Column="1" Padding="3,1,3,1" Grid.Row="0" Grid.RowSpan="2">
                    <ContentPresenter ContentSource="Header" RecognizesAccessKey="True" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
                </Border>
                <Border Grid.Column="3" Padding="3,1,3,1" Grid.Row="0" Grid.RowSpan="2">
                    <Button x:Name="CopyButton" Content="Copy" Grid.Column="3" Grid.Row="0" Grid.RowSpan="2" HorizontalAlignment="Stretch" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"
                        HorizontalContentAlignment="Center" VerticalContentAlignment="Center"
                        Command="{Binding Command, ElementName=ctrlThis}" CommandParameter="{Binding CommandParameter, ElementName=ctrlThis}"/>
                </Border>
                <ContentPresenter Grid.ColumnSpan="3" Grid.Column="1" Margin="{TemplateBinding Padding}" Grid.Row="2" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
            </Grid>
        </ControlTemplate>
    </GroupBox.Template>
</GroupBox>

BorderGapConverter

public class BorderGapConverter : IMultiValueConverter
    {
        public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            Type doubleType = typeof(double);

            // Data Validation
            if (values == null) return DependencyProperty.UnsetValue;

            foreach (var value in values)
            {
                Type valueType = value.GetType();
                if (value == null || (!typeof(FrameworkElement).IsAssignableFrom(valueType) && !doubleType.IsAssignableFrom(valueType))) 
                    return DependencyProperty.UnsetValue;
            }

            Type paramType = parameter.GetType();;
            if (parameter == null || (!doubleType.IsAssignableFrom(paramType) && !typeof(string).IsAssignableFrom(paramType))) return DependencyProperty.UnsetValue;

            double offset = 0d;
            if (parameter is string)
                offset = double.Parse((string)parameter, NumberFormatInfo.InvariantInfo);
            else
                offset = (double)parameter;

            Grid hostGrid = (Grid)values[0];
            double borderWidth = (double)values[1];
            double borderHeight = (double)values[2];

            if (borderWidth == 0d || borderHeight == 0d) return null;

            // Create a 5x2 Grid
            Grid grid = new Grid() { Width = borderWidth, Height = borderHeight };
            foreach(ColumnDefinition col in hostGrid.ColumnDefinitions)
                grid.ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(col.ActualWidth) });

            // Define rows
            grid.RowDefinitions.Add(new RowDefinition() { Height = new GridLength(borderHeight / 2d) });
            grid.RowDefinitions.Add(new RowDefinition() { Height = new GridLength(1d, GridUnitType.Star) });

            /**
             * Cartesian Coordinates
             * - 0 1 2
             * 0|-|-|-|
             * 1|-|-|-|
             */
            // Set rectangle 1 to 0, 0 to 0, 1
            Rectangle rectangle = new Rectangle() { Fill = Brushes.Black };
            Grid.SetRowSpan(rectangle, 2);
            Grid.SetRow(rectangle, 0);
            Grid.SetColumn(rectangle, 0);

            // Set rectangle2 to 1, 1
            Rectangle rectangle2 = new Rectangle() { Fill = Brushes.Black };
            Grid.SetRow(rectangle2, 1);
            Grid.SetColumn(rectangle2, 1);

            // Set rectangle3 to 0, 2 to 1, 2
            Rectangle rectangle3 = new Rectangle() { Fill = Brushes.Black };
            Grid.SetRowSpan(rectangle3, 2);
            Grid.SetRow(rectangle3, 0);
            Grid.SetColumn(rectangle3, 2);

            // Set rectangle4 to 0, 2 to 1, 2
            Rectangle rectangle4 = new Rectangle() { Fill = Brushes.Black };
            Grid.SetRow(rectangle4, 1);
            Grid.SetColumn(rectangle4, 3);

            // Set rectangle3 to 0, 2 to 1, 2
            Rectangle rectangle5 = new Rectangle() { Fill = Brushes.Black };
            Grid.SetRowSpan(rectangle5, 2);
            Grid.SetRow(rectangle5, 0);
            Grid.SetColumn(rectangle5, 4);

            grid.Children.Add(rectangle);
            grid.Children.Add(rectangle2);
            grid.Children.Add(rectangle3);
            grid.Children.Add(rectangle4);
            grid.Children.Add(rectangle5);
            return new VisualBrush(grid);
        }

        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
        {
            return new object[] { Binding.DoNothing };
        }
    }

I've tried to override the metadata on HeaderProperty, override OnHeaderChanged, changed to using a HeaderTemplate, and bound Header and set a HeaderStringFormat to no avail.

Here is how I'm using the control:

        <ctrl:ButtonedGroupBox Command="{Binding CopyContentsCommand}" Header="{Binding DynamicTitle}" HeaderStringFormat="Static - {0}">
            <TextBlock>
                Lorem ipsum sit dolorem
            </TextBlock>
        </ctrl:ButtonedGroupBox>

How do I get the opacitymask to update when the header's underlying textblock changes?

EDIT

So I added the Header element's ActualWidth to the MultiBinding, which works but it seems so unnecessary.

<MultiBinding ConverterParameter="6" Converter="{StaticResource MyBorderGapMaskConverter}">
                                        <Binding RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType=Grid}"/>
                                        <Binding Path="ActualWidth" RelativeSource="{RelativeSource Self}"/>
                                        <Binding Path="ActualHeight" RelativeSource="{RelativeSource Self}"/>
                                        <Binding ElementName="Header" Path="ActualWidth"/>
                                    </MultiBinding>
0

There are 0 answers