0
votes

To begin with, this is in .NET 4.0 because it has to be. I know some bugs have been fixed in later versions of .NET, so if this is an actual .NET bug, I guess I'm going to have to live with using user controls which don't seem to have this issue.

I created a custom control library in WPF to make customizable buttons that will be used in 3rd party software. I seem to have an issue, however, with multiple buttons resulting in the content for all but one of the buttons to go missing. I have confirmed the problem in SNOOP. The content just isn't there. The SNOOP tree gets as far as the content presenter and then there's nothing under it, except for the one button that does have content. I've created a very bare bones example of the problem.

My Library's Generic.xaml is as follows:

<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:CustomControlsLibrary.Controls">

<Style x:Key="CustomButtonStyle" TargetType="{x:Type controls:CustomButton}">
    <Setter Property="FontSize" Value="16" />
    <Setter Property="FontWeight" Value="Bold" />
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type controls:CustomButton}">
                <Border CornerRadius="{TemplateBinding CornerRadius}" BorderThickness="3" BorderBrush="{TemplateBinding BorderBrush}" Background="{TemplateBinding Background}">
                    <ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" ContentSource="Content" />
                </Border>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

<Style x:Key="Button1Style" TargetType="{x:Type controls:Button1}" BasedOn="{StaticResource CustomButtonStyle}" >
    <Setter Property="CornerRadius" Value="4" />
    <Setter Property="BorderBrush" Value="White" />
    <Setter Property="Height" Value="40" />
    <Setter Property="Width" Value="100" />
    <Setter Property="Foreground" Value="White" />
    <Setter Property="Content">
        <Setter.Value>
            <TextBlock Text="{Binding RelativeSource={RelativeSource AncestorType=controls:Button1}, Path=Text}" />
        </Setter.Value>
    </Setter>
</Style>

The two control classes are as follows:

CustomButton:

public class CustomButton : Button
{
    public static readonly DependencyProperty CornerRadiusProperty =
        DependencyProperty.Register("CornerRadius", typeof(CornerRadius), typeof(CustomButton), new FrameworkPropertyMetadata(new CornerRadius(0)));

    public CornerRadius CornerRadius
    {
        get { return (CornerRadius)GetValue(CornerRadiusProperty); }
        set { SetValue(CornerRadiusProperty, value); }
    }

    static CustomButton()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(CustomButton), new FrameworkPropertyMetadata(typeof(CustomButton)));
    }
}

Button1:

public class Button1 : CustomButton
{

    public static readonly DependencyProperty TextProperty =
        DependencyProperty.Register("Text", typeof(string), typeof(Button1), new FrameworkPropertyMetadata(""));

    public string Text
    {
        get { return (string)GetValue(TextProperty); }
        set { SetValue(TextProperty, value); }
    }

    static Button1()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(Button1), new FrameworkPropertyMetadata(typeof(Button1)));
    }
}

I then create a simple WPF application with just a main window with all logic in MainWindow.xaml:

<Window x:Class="CustomControlLibraryTestApp.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:controls="clr-namespace:CustomControlsLibrary.Controls;assembly=CustomControlsLibrary"
    Title="MainWindow" Height="350" Width="525" Background="DarkGray">

<Window.Resources>
    <ResourceDictionary Source="pack://application:,,,/CustomControlsLibrary;component/Themes/Generic.xaml" />
</Window.Resources>

<StackPanel>
    <controls:Button1 Style="{StaticResource Button1Style}" Background="Red" Text="Button 1" />
    <controls:Button1 Style="{StaticResource Button1Style}" Background="Blue" Text="Button 2" />
</StackPanel>

When run, the content for Button 1 goes missing while Button 2 looks just fine. Removing Button 2 from the Window causes Button 1 to look as expected.

And as mentioned earlier, SNOOP indicates that Button 1's content is just not there when both buttons are present.

Any ideas?

2

2 Answers

3
votes

I'm going to throw in a dissenting opinion here, starting with a quote from Matthew MacDonalds "Pro WPF in C#":

Custom controls are still a useful way to build custom widgets that you can share between applications, but they’re no longer a requirement when you want to enhance and customize core controls. (To understand how remarkable this change is, it helps to point out that this book’s predecessor, Pro .NET 2.0 Windows Forms and Custom Controls in C#, had nine complete chapters about custom controls and additional examples in other chapters. But in this book, you’ve made it to Chapter 18 without a single custom control sighting!)

Put simply, there is just no need to be creating extra button classes just to control properties that already exist in the templates. You can do that just as easily with data binding or attached properties etc and it will be a lot more compatible with tools like Blend.

To illustrate the point here's a helper class for the two properties you're exposing in your sample code:

public static class ButtonHelper
{
    public static double GetCornerRadius(DependencyObject obj)
    {
        return (double)obj.GetValue(CornerRadiusProperty);
    }

    public static void SetCornerRadius(DependencyObject obj, double value)
    {
        obj.SetValue(CornerRadiusProperty, value);
    }

    // Using a DependencyProperty as the backing store for CornerRadius.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty CornerRadiusProperty =
        DependencyProperty.RegisterAttached("CornerRadius", typeof(double), typeof(ButtonHelper), new PropertyMetadata(0.0));


    public static string GetButtonText(DependencyObject obj)
    {
        return (string)obj.GetValue(ButtonTextProperty);
    }

    public static void SetButtonText(DependencyObject obj, string value)
    {
        obj.SetValue(ButtonTextProperty, value);
    }

    // Using a DependencyProperty as the backing store for ButtonText.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty ButtonTextProperty =
        DependencyProperty.RegisterAttached("ButtonText", typeof(string), typeof(ButtonHelper), new PropertyMetadata(""));

}

Now we can immediately create two style, one for each of your button types, that bind to these properties internally:

    <Style x:Key="RoundedButtonStyle" TargetType="{x:Type Button}" >
        <Setter Property="Margin" Value="10" />
        <Setter Property="HorizontalAlignment" Value="Left" />
        <Setter Property="VerticalAlignment" Value="Center" />
        <Setter Property="Foreground" Value="White" />
        <Setter Property="FontSize" Value="16" />
        <Setter Property="FontWeight" Value="Bold" />
        <Setter Property="BorderBrush" Value="Red" />
        <Setter Property="Background" Value="Red" />
        <Setter Property="controls:ButtonHelper.CornerRadius" Value="4" />
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="Button">
                    <Border CornerRadius="{Binding Path=(controls:ButtonHelper.CornerRadius),
                        RelativeSource={RelativeSource TemplatedParent}}" BorderThickness="3"
                            BorderBrush="{TemplateBinding BorderBrush}"
                            Background="{TemplateBinding Background}">
                        <ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" ContentSource="Content" />
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

    <Style x:Key="TextButtonStyle" TargetType="{x:Type Button}" BasedOn="{StaticResource RoundedButtonStyle}">
        <Setter Property="BorderBrush" Value="Blue" />
        <Setter Property="Background" Value="Blue" />
        <Setter Property="controls:ButtonHelper.ButtonText" Value="TextButton" />
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="Button">
                    <Border CornerRadius="{Binding Path=(controls:ButtonHelper.CornerRadius),
                        RelativeSource={RelativeSource TemplatedParent}}" BorderThickness="3"
                            BorderBrush="{TemplateBinding BorderBrush}"
                            Background="{TemplateBinding Background}">
                        <TextBlock Text="{Binding Path=(controls:ButtonHelper.ButtonText),
                            RelativeSource={RelativeSource TemplatedParent}}" Background="Transparent" />
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

That's it! No custom control needed no need for x:Shared due to content being specified directly in a style, and it's a lot more light-weight. Here's an example of them being used:

<UniformGrid Columns="2">

    <Button Style="{StaticResource RoundedButtonStyle}" Content="RoundedButton" />
    <Button Style="{StaticResource RoundedButtonStyle}" Content="RoundedButton big radius" controls:ButtonHelper.CornerRadius="20"/>

    <Button Style="{StaticResource TextButtonStyle}" />
    <Button Style="{StaticResource TextButtonStyle}" controls:ButtonHelper.ButtonText="TextButton new text"/>

    <Button Style="{StaticResource TextButtonStyle}" BorderBrush="Green" Background="Green"
            controls:ButtonHelper.ButtonText="Both text and radius"
            controls:ButtonHelper.CornerRadius="20" />

</UniformGrid>

And here's the result:

enter image description here

I do realize of course that I've specified the border in each template, but that too can be easily removed by placing a content control inside the border and using data templating to set the content.

1
votes

What's happening is that the style actually has a single TextBlock instance. When the style is applied to the second button the TextBlock is actually re-parented to the new control. You should be able to avoid this by setting x:Shared="false" on the TextBlock element.