Search code examples
c#custom-controlswinui-3

C# winui 3 route event from component control to parent custom control


Using C# winui 3 (.NET 6), I have written a custom control CommonButtonControl made up of a Button containing an Image and a TextBlock. The idea is that I can keep a consistent look and feel for all buttons in my app and simplify my xaml so I only need to add the CommonButtonControl with values for ImageSource, Text, and Command.

All working fine so far, but I would also like to add a Click event which would get triggered on CommonButtonControl when the component Button Click event gets triggered. This would be to add consistency with the standard Button control and allow me to catch the Click event from CommonButtonControl to do such things as showing a confirmation Dialog before doing anything else.

I have added a Click event to my CommonButtonControl class. However I have the xaml for CommonButtonControl defined in a DataDictionary (as per this documentation), and when I try to catch the Click event for the component Button, it will only call code that is in the code behind the DataDictionary - not code that is in the CommonButtonControl class. There does not appear to be any way to call the CommonButtonControl object to tell it that the component Button event has fired so that I can raise the CommonButtonControl Click event. Any suggestions? It seems conceptually incorrect to add a RoutedEventHandler as a DependencyProperty.

Resource Dictionary

<ResourceDictionary
    x:Class="MyApp.Views.DataTemplatesDictionary"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:MyApp.Views">

    <Style TargetType="local:CommonButtonControl" >
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="local:CommonButtonControl">
                    <Button
                        Command="{x:Bind Command, Mode=OneWay}"
                        Click="Button_Click">
                        <Grid>
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="32"/>
                                <ColumnDefinition Width="auto"/>
                            </Grid.ColumnDefinitions>

                            <Image
                                Grid.Column="0"
                                Width="32"
                                Height="32"
                                Source="{x:Bind ImageSource, Mode=OneWay}"
                                />

                            <TextBlock
                                Margin="8,0,0,0"
                                Grid.Column="1"
                                VerticalAlignment="Center"
                                Text="{x:Bind Text, Mode=OneWay}"
                                Style="{ThemeResource BodyTextBlockStyle}"
                            />
                        </Grid>
                    </Button>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>


</ResourceDictionary>

Code behind resource dictionary

partial class DataTemplatesDictionary
    {
        public DataTemplatesDictionary()
        {
            InitializeComponent();
        }

        private void Button_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
        {
            //how to route this to the OnButtonClick method in CommonButtonControl?
        }


    }

CommonButtonControl class

public sealed class CommonButtonControl : Control
    {
        #region members

        private DependencyProperty TextProperty = DependencyProperty.Register(
            nameof(Text),
            typeof(string),
            typeof(CommonButtonControl),
            new PropertyMetadata(default(string)));

        private DependencyProperty ImageSourceProperty = DependencyProperty.Register(
            nameof(ImageSource),
            typeof(ImageSource),
            typeof(CommonButtonControl),
            new PropertyMetadata(default(ImageSource)));

        private DependencyProperty CommandProperty = DependencyProperty.Register(
            nameof(Command),
            typeof(ICommand),
            typeof(CommonButtonControl),
            new PropertyMetadata(null));

        public event RoutedEventHandler Click;

        #endregion

        #region properties

        private void OnButtonClick(object sender, RoutedEventArgs e)
        {
            //how do I call this method when the component button click event is raised?
            Click?.Invoke(this, new());
        }


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

        public ImageSource ImageSource
        {
            get => (ImageSource)GetValue(ImageSourceProperty);
            set => SetValue(ImageSourceProperty, value);
        }

        public ICommand Command
        {
            get => (ICommand)GetValue(CommandProperty);
            set => SetValue(CommandProperty, value);
        }

        #endregion



        public CommonButtonControl()
        {
            this.DefaultStyleKey = typeof(CommonButtonControl);
        }
    }

Usage 1 - via Command (works)

           <local:CommonButtonControl 
                Command="{x:Bind _app.DeleteItemCommand}"
                Text="Delete Item"
                ImageSource="{x:Bind _app.DeleteIcon}"
                />

Usage 2 - via Click Event (how to get this to work?)

            <local:CommonButtonControl 
                Click="CommonButtonControl_Click"
                Text="Delete Item"
                ImageSource="{x:Bind _app.DeleteIcon}"
                />

Solution

  • If you name your Button, let's say "ButtonControl",

    <Style TargetType="local:CustomButton">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="local:CustomButton">
                    <Button x:Name="ButtonControl" />
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
    

    then you can get it in your custom control class and subscribe to ButtonControl's Click event:

    CustomButton.cs

    public sealed class CustomButton : Control
    {
        public CustomButton()
        {
            this.DefaultStyleKey = typeof(CustomButton);
        }
    
        public event RoutedEventHandler? Click;
    
        private Button? ButtonControl { get; set; }
    
        protected override void OnApplyTemplate()
        {
            base.OnApplyTemplate();
    
            if (ButtonControl is not null)
            {
                ButtonControl.Click -= ButtonControl_Click;
            }
    
            if (GetTemplateChild(nameof(ButtonControl)) is Button button)
            {
                ButtonControl = button;
                ButtonControl.Click += ButtonControl_Click;
            }
        }
    
        private void ButtonControl_Click(object sender, RoutedEventArgs e)
        {
            Click?.Invoke(this, e);
        }
    }