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>