1

I've searched the Internet and StackOverflow a little. Why I need this: I have two UserControls called PlusButton and MinusButton, and they have both common XAML and common code-behind. I want to use a common base class which ideally would also have XAML and code-behind.

I have seen the answer posted here but I am not experienced in XAML enough to apply that solution. I would be happy with a link to an official documentation page that learns me what I need to do this. I am used to code-behind since I also use WinForms so I do not mind if I have to do code-behind.

I understand code-behind more easily than XAML so I have much code-behind that probably would have been done more nicely in XAML.

If I use the solution posted here I get compiler errors:

  • cs_wpf_test_1.ArrowButton cannot be the root of a XAML file because it was defined using XAML. Line 1 Position 20.
  • Warning CS0108 PlusButton.InitializeComponent() hides inherited member ArrowButton.InitializeComponent(). Use the new keyword if hiding was intended.

I get the two compiler errors listed above and I do not know what is the most correct walkaround to these errors. I would like it to be easily maintainable.

The code below is a version of my code before I had those compiler errors.

PlusButton.xaml

<UserControl x:Class="cs_wpf_test_1.PlusButton"
             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:local="clr-namespace:cs_wpf_test_1"
             mc:Ignorable="d" 
             d:DesignHeight="120" d:DesignWidth="175"
             Loaded="UserControl_Loaded">
    <Button Name="MyButton" Focusable="False" Padding="0,0,0,0">
        <Button.Template>
            <ControlTemplate TargetType="{x:Type Button}">
                <Border Background="{TemplateBinding Background}"
                         BorderBrush="{TemplateBinding BorderBrush}"
                         BorderThickness="{TemplateBinding BorderThickness}">
                    <VisualStateManager.VisualStateGroups>
                        <VisualStateGroup x:Name="CommonStates" CurrentStateChanged="CommonStates_CurrentStateChanged">
                            <VisualState x:Name="Normal" />
                            <VisualState x:Name="MouseOver" />
                            <VisualState x:Name="Pressed" />
                            <VisualState x:Name="Disabled" />
                        </VisualStateGroup>
                    </VisualStateManager.VisualStateGroups>
                    <Grid>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="*"/>
                        </Grid.ColumnDefinitions>
                        <Grid.RowDefinitions>
                            <RowDefinition Height="*"/>
                        </Grid.RowDefinitions>
                        <Canvas HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
                Name="MyCanvas">
                            <Polyline Stroke="Blue" Name="MyPolyline">
                                <Polyline.Points>
                                    <PointCollection>
                                        <Point X="5" Y="95" />
                                        <Point X="95" Y="95" />
                                        <Point X="50" Y="5" />
                                        <Point X="5" Y="95" />
                                    </PointCollection>
                                </Polyline.Points>
                            </Polyline>
                        </Canvas>
                        <Canvas HorizontalAlignment="Center" VerticalAlignment="Center"
                                Width="100" Height="100" Name="MySecondCanvas">
                            <Line Stroke="Black" X1="50" Y1="10"
                                  X2="50" Y2="90"
                  Name="MySignLine1" StrokeThickness="4"/>
                            <Line Stroke="Black" X1="10" Y1="50"
                                  X2="90" Y2="50"
                  Name="MySignLine2" StrokeThickness="4"/>
                        </Canvas>
                    </Grid>
                </Border>
            </ControlTemplate>
        </Button.Template>
    </Button>
</UserControl>

PlusButton.xaml.cs

public partial class PlusButton : UserControl
{
    internal Polyline MyTemplatePolyline;
    internal Line MyTemplateSignLine1, MyTemplateSignLine2;
    internal Canvas MyTemplateCanvas,
        MyTemplateSecondCanvas;

    public PlusButton()
    {
        InitializeComponent();

        MyButton.Margin = new Thickness(
            -MyButton.BorderThickness.Left,
            -MyButton.BorderThickness.Top,
            -MyButton.BorderThickness.Right,
            -MyButton.BorderThickness.Bottom);
    }

    internal void SetPseudofocused(bool p)
    {
        if (p)
        {
            BorderBrush = Brushes.Blue;
        }
        else
        {
            BorderBrush = Brushes.Transparent;
        }
    }

    private void UserControl_Loaded(object sender, RoutedEventArgs e)
    {
        MyButton.ApplyTemplate();

        MyTemplatePolyline = (Polyline)MyButton.Template.FindName("MyPolyline", MyButton);
        MyTemplateSignLine1 = (Line)MyButton.Template.FindName("MySignLine1", MyButton);
        MyTemplateSignLine2 = (Line)MyButton.Template.FindName("MySignLine2", MyButton);
        MyTemplateCanvas = (Canvas)MyButton.Template.FindName("MyCanvas", MyButton);
        MyTemplateSecondCanvas = (Canvas)MyButton.Template.FindName("MySecondCanvas", MyButton);

        UpdateMyLayout();
    }

    private void CommonStates_CurrentStateChanged(object sender, VisualStateChangedEventArgs e)
    {
        var btn = e.Control as Button;

        if (e.NewState.Name == "MouseOver")
        {
            btn.Background = Brushes.White;
        }
        else if (e.NewState.Name == "Pressed")
        {
            btn.Background = Brushes.LightBlue;
        }
        else if (e.NewState.Name == "Disabled")
        {
            btn.Background = Brushes.Gray;
        }
        else
        {
            btn.Background = (Brush)MyButton.FindResource(SystemColors.ControlBrushKey);
        }
    }

    protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
    {
        base.OnPropertyChanged(e);

        if (e.Property == WidthProperty || e.Property == HeightProperty || e.Property == BorderThicknessProperty)
        {
            UpdateMyLayout();
        }
    }

    internal void UpdateMyLayout()
    {
        if (MyTemplateCanvas == null)
        {
            return;
        }

        MyTemplateCanvas.Height = Height;
        MyTemplateCanvas.Width = Width;

        double h = ActualHeight - BorderThickness.Top -
            BorderThickness.Bottom;
        double w = ActualWidth - BorderThickness.Left -
            BorderThickness.Right;

        MyTemplatePolyline.Points.Clear();
        MyTemplatePolyline.Points.Add(new Point(5, h - 5));
        MyTemplatePolyline.Points.Add(new Point(w - 5, h - 5));
        MyTemplatePolyline.Points.Add(new Point(w / 2, 5));
        MyTemplatePolyline.Points.Add(new Point(5, h - 5));

        h = MyTemplateSecondCanvas.ActualHeight;
        w = MyTemplateSecondCanvas.ActualWidth;

        double ltrbPadding = h / 3;

        double l1 = h - 2 * ltrbPadding;
        double l2 = w - 2 * ltrbPadding;

        double l = Math.Min(l1, l2);

        // draw a cross with two lines:
        MyTemplateSignLine1.X1 = l / 2d + ltrbPadding;
        MyTemplateSignLine1.X2 = l / 2d + ltrbPadding;
        MyTemplateSignLine1.Y1 = ltrbPadding;
        MyTemplateSignLine1.Y2 = l + ltrbPadding;

        MyTemplateSignLine2.X1 = ltrbPadding;
        MyTemplateSignLine2.X2 = l + ltrbPadding;
        MyTemplateSignLine2.Y1 = ltrbPadding + l / 2d;
        MyTemplateSignLine2.Y2 = ltrbPadding + l / 2d;

        // update focus border size:
        double v = ActualHeight / 25d;
        BorderThickness = new Thickness(v, v, v, v);
    }
}

MinusButton.xaml

The same as the first XAML file, but MySecondCanvas's XAML contains just one Line.

MinusButton.xaml.cs

The same as the first .cs file, but:

  1. It does not create or use the MyTemplateSignLine1 and MyTemplateSignLine2, it just uses MyTemplateSignLine.
  2. MyTemplatePolyline contains other points that look like a down arrow (the first arrow, turned upside-down).
  3. MyTemplateSignLine is just like MyTemplateSignLine2 from the other XAML file and code-behind.
silviubogan
  • 3,343
  • 3
  • 31
  • 57
  • 2
    After a quick look at what you have there, it seems to me you could just have a xaml template for a button and switch out a geometry defining the shape of the symbol. This would be used as the data for a Path rather than all that polyline stuff you have there. You could use an attached dependency property or the tag of the button so you can set a value for the geometry via a style. A path has a stretch property you can set to make it fill the button or fill to the proportion of the geometry. This approach would have no code other than the attached dependency property. – Andy Apr 02 '19 at 07:53

2 Answers2

1

Inheritance in WPF is a tricky thing, if you only wish to modify code you can inherit a control in a CS file, and override methods as you please, but it can get complicated if you would like to modify a xaml.

However I believe the WPF way is more towards styling / templating.

Adding a resource dictionary is fairly easy (here is how). And if you wish to change a buttons background you could add a style that changes it on trigger. And you probably wish to also remove the default behavior of a button, you can do that by overriding the template.

Heres an example of a button style (responsible for the background color changes) and template (without the default button behaviors) that you can reuse, and also change the content via the ContentPresenter:

<UserControl.Resources>
    <ResourceDictionary>
        <ResourceDictionary.MergedDictionaries>
            <ResourceDictionary>
                <Style x:Key="ColorfulButtonStyle" TargetType="Button">
                    <Style.Triggers>
                        <Trigger Property="IsPressed" Value="True">
                            <Setter Property="Background" Value="Pink"/>
                        </Trigger>
                        <Trigger Property="IsPressed" Value="False">
                            <Setter Property="Background" Value="Blue"/>
                        </Trigger>
                        <Trigger Property="IsMouseOver" Value="True">
                            <Setter Property="Background" Value="Purple"/>
                        </Trigger>
                        <Trigger Property="IsMouseOver" Value="False">
                            <Setter Property="Background" Value="green"/>
                        </Trigger>
                    </Style.Triggers>
                </Style>
                <ControlTemplate x:Key="EmptyButtonTemplate" TargetType="Button">
                    <Border Background="{TemplateBinding Background}" BorderThickness="0" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
                        <ContentPresenter/>
                    </Border>
                </ControlTemplate>
            </ResourceDictionary>
        </ResourceDictionary.MergedDictionaries>
    </ResourceDictionary>
</UserControl.Resources>

    <Button Template="{StaticResource EmptyButtonTemplate}" Style="{StaticResource ColorfulButtonStyle}" Width="30" Height="30" >
        <Ellipse HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Fill="Black"/>
        <!--Replace the ellipse with the shape you like, defined as a control in code or in xaml-->
    </Button>
Yotam S.
  • 141
  • 1
  • 11
1

The code below is after unifying the two UserControls.

My XAML:

<UserControl x:Class="cs_wpf_test_1.ArrowButton"
             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:local="clr-namespace:cs_wpf_test_1"
             mc:Ignorable="d" 
             d:DesignHeight="120" d:DesignWidth="175"
             Loaded="UserControl_Loaded">
    <Button Name="MyButton" Focusable="False" Padding="0,0,0,0">
        <Button.Resources>
            <Style x:Key="styleWithPlusSign">
                <Style.Triggers>
                    <Trigger Property="Grid.Row" Value="0">
                        <Setter Property="Path.Data" Value="M 5,95 L 95,95 50,5 5,95"></Setter>
                    </Trigger>
                    <Trigger Property="Grid.Row" Value="1">
                        <Setter Property="Path.Data" Value="M 50,10 L 50,10 L 50,90 M 10,50 L 90,50"></Setter>
                    </Trigger>
                </Style.Triggers>
            </Style>
            <Style x:Key="styleWithMinusSign">
                <Style.Triggers>
                    <Trigger Property="Grid.Row" Value="0">
                        <Setter  Property="Path.Data" Value="M 5,5 L 50,50 95,5 5,5"></Setter>
                    </Trigger>
                    <Trigger Property="Grid.Row" Value="1">
                        <Setter Property="Path.Data" Value="M 10,50 L 90,50"></Setter>
                    </Trigger>
                </Style.Triggers>
            </Style>
        </Button.Resources>
        <Button.Template>
            <ControlTemplate TargetType="{x:Type Button}">
                <Border Background="{TemplateBinding Background}"
                         BorderBrush="{TemplateBinding BorderBrush}"
                         BorderThickness="{TemplateBinding BorderThickness}">
                    <VisualStateManager.VisualStateGroups>
                        <VisualStateGroup x:Name="CommonStates" CurrentStateChanged="CommonStates_CurrentStateChanged">
                            <VisualState x:Name="Normal" />
                            <VisualState x:Name="MouseOver" />
                            <VisualState x:Name="Pressed" />
                            <VisualState x:Name="Disabled" />
                        </VisualStateGroup>
                    </VisualStateManager.VisualStateGroups>
                    <Grid>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="*"/>
                        </Grid.ColumnDefinitions>
                        <Grid.RowDefinitions>
                            <RowDefinition Height="*"/>
                        </Grid.RowDefinitions>
                        <Path Stroke="Blue"
                              Stretch="Fill"
                              x:Name="MyFirstPath"
                              Style="{StaticResource styleWithPlusSign}"
                              Grid.Row="0"/>
                        <Path Stroke="Black"
                              StrokeThickness="1"
                              Stretch="Uniform"
                              x:Name="MySecondPath"
                              Style="{Binding ElementName=MyFirstPath, Path=Style}"
                              Grid.Row="1"/>
                    </Grid>
                </Border>
            </ControlTemplate>
        </Button.Template>
    </Button>
</UserControl>

My code-behind:

/// <summary>
/// Interaction logic for ArrowButton.xaml
/// </summary>
public partial class ArrowButton : UserControl
{
    internal Path MyTemplateSecondPath,
        MyTemplateFirstPath;

    public ArrowButton()
    {
        InitializeComponent();

        // TODO: use smth like MyButton.PropertyChanged to set this:
        MyButton.Margin = new Thickness(
            -MyButton.BorderThickness.Left,
            -MyButton.BorderThickness.Top,
            -MyButton.BorderThickness.Right,
            -MyButton.BorderThickness.Bottom);
    }

    public bool State
    {
        get
        {
            return (bool)GetValue(StateProperty);
        }
        set
        {
            SetValue(StateProperty, value);
        }
    }
    public static readonly DependencyProperty StateProperty =
        DependencyProperty.Register("State", typeof(bool),
            typeof(ArrowButton), new PropertyMetadata(true, new PropertyChangedCallback(OnStateChanged)));

    private static void OnStateChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var b = d as ArrowButton;

        b.MyButton.ApplyTemplate();

        b.MyTemplateFirstPath = (Path)b.MyButton.Template.FindName("MyFirstPath", b.MyButton);

        if (b.State)
        {
            // plus
            b.MyTemplateFirstPath.Style = b.MyButton.FindResource("styleWithPlusSign") as Style;
        }
        else
        {
            // minus
            b.MyTemplateFirstPath.Style = b.MyButton.FindResource("styleWithMinusSign") as Style;
        }
    }

    internal void SetPseudofocused(bool p)
    {
        if (p)
        {
            BorderBrush = Brushes.Blue;
        }
        else
        {
            BorderBrush = Brushes.Transparent;
        }
    }

    private void UserControl_Loaded(object sender, RoutedEventArgs e)
    {
        ApplyTemplate();

        MyTemplateSecondPath = (Path)MyButton.Template.FindName("MySecondPath", MyButton);

        UpdateMyLayout();
    }

    private void CommonStates_CurrentStateChanged(object sender, VisualStateChangedEventArgs e)
    {
        var btn = e.Control as Button;

        if (e.NewState.Name == "MouseOver")
        {
            btn.Background = Brushes.White;
        }
        else if (e.NewState.Name == "Pressed")
        {
            btn.Background = Brushes.LightBlue;
        }
        else if (e.NewState.Name == "Disabled")
        {
            btn.Background = Brushes.Gray;
        }
        else
        {
            btn.Background = (Brush)MyButton.FindResource(SystemColors.ControlBrushKey);
        }
    }

    protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
    {
        base.OnPropertyChanged(e);

        if (e.Property == WidthProperty ||
            e.Property == HeightProperty ||
            e.Property == BorderThicknessProperty)
        {
            UpdateMyLayout();
        }
    }

    internal void UpdateMyLayout()
    {
        if (MyTemplateSecondPath == null)
        {
            return;
        }

        // update focus border size:
        double v = ActualHeight / 25d;
        BorderThickness = new Thickness(v, v, v, v);

        double min = Math.Min(ActualWidth, ActualHeight);

        if (State)
        {
            MyTemplateSecondPath.Margin = new Thickness(
                min / 5,
                min / 5,
                min / 5,
                min / 5);
        }
        else
        {
            MyTemplateSecondPath.Margin = new Thickness(
                min / 2.2,
                min / 2.2,
                min / 2.2,
                min / 2.2);
        }
    }
}
silviubogan
  • 3,343
  • 3
  • 31
  • 57