How to create a custom avalonia control

382 views Asked by At

I'm learning Avalonia and I couldn't figure this one out by reading the documentation.

Let's suppose I want to create a custom user control made for facilitating numeric input on a touchscreen. I only have the screen, with no keyboard, so I want my control to consist of two buttons and a textbox or label, like this:

|+|   Button for adding one
|5|   Actual value label
|-|   Button for substracting one

I want the control to be stackable on a higher level array of controls like itself, each one representing a single digit of the final number, or things like that.

I need the next characteristics:

  • being able to bind to the label value to read and write from the outside
  • being somehow aware of the changes of the value
  • being able to set an initial value

Thanks for any clue or hint, I've been stuck with this for at least a week and nothing seems to work. I know that Avalonia has a steep learning curve, but I don't know where to begin :)

-What I've tried:

-Using templated controls:

I couldnt figure out how to modify the label value when the button is touched, as there are no viewmodel, and I don't know how to can make the button do anything using the code behind (the documentation on this type of controls is not much helpful)

-Using user controls:

A) with viewmodel:

I create a styled property to contain the numeric value of the label in the code behind (as I know it's the way for it to be read by higher level controls) and bind the buttons to methods on the viewmodel. The problem is that I cannot modify the styled property from the VM and it doesn't update when I click the buttons. Many I don't know how to call it, but something seems wrong about accessing the code behind from VM.

B) Only code behind:

This was the most promising.

I created the styled property containing the value, and pointed the buttons to methods on the code behind that added or substracted one to the value, and then updated the label (label.content=value). It almost works, the label updates with the buttons and I can bind the styled property to be read from the outside. The problem is that I cannot write to it. If I want the control starting value to be assignable by its parent.

I feel that the answer is somehow binding the text of the label to the styled property, but the code behind derives from UserControl and I don't know how to implement also ObservableObject (I'm using community toolkit mvvm), and I cannot create any [observable propertie] to bind.

1

There are 1 answers

0
Asier Vicente Sánchez On

I figured it out. I followed the approach of the UserControl with only code behind. To update the view when the styledProperty is updated, i just needed to override the OnPropertyChanged() method:

protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
        {
            if(change.Property == MyOwnStyledProperty)
                MyOwnUpdateView();
            base.OnPropertyChanged(change);
        }

Now, when the StyledProperty is binded on higher level controls, i can read and write being sure that the front will always display the property.

Full code:

AXAML:

<UserControl
x:Class="Programa_control.ControlesCustomizados.CeldaTactil"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:i="clr-namespace:Avalonia.Xaml.Interactivity;assembly=Avalonia.Xaml.Interactivity"
xmlns:iac="clr-namespace:Avalonia.Xaml.Interactions.Custom"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Width="40"
Height="90"
d:DesignHeight="100"
d:DesignWidth="100"
mc:Ignorable="d">
<Border>
    <Border>
        <Grid RowDefinitions="* auto *">
            <Button
                Grid.Row="0"
                Click="Mas"
                Content="+"/>
            <Button
                Grid.Row="2"
                Click="Menos"
                Content="-"/>
            <Label
                Name="Etiqueta"
                Grid.Row="1"
                >
            </Label>
        </Grid>
    </Border>
</Border>

CODE BEHIND:

using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using System;

namespace Programa_control.ControlesCustomizados
{
    public partial class CeldaTactil: UserControl
    {

        #region Propiedades

        public static readonly StyledProperty<int> ValorMaximoProperty =
            AvaloniaProperty.Register<CeldaTactil, int>(nameof(ValorMaximo), 9);

        public int ValorMaximo
        {
            get => this.GetValue(ValorMaximoProperty);
            set => SetValue(ValorMaximoProperty, value);
        }


        public static readonly StyledProperty<int> ValorProperty =
            AvaloniaProperty.Register<CeldaTactil, int>(nameof(Valor), 0);

        public int Valor
        {
            get => this.GetValue(ValorProperty);
            set => SetValue(ValorProperty, value);
        }

        public static readonly RoutedEvent<RoutedEventArgs> ValorActualizadoEvent =
            RoutedEvent.Register<CeldaTactil, RoutedEventArgs>(nameof(ValorActualizado), RoutingStrategies.Bubble);

        public event EventHandler<RoutedEventArgs> ValorActualizado
        {
            add => AddHandler(ValorActualizadoEvent, value);
            remove => RemoveHandler(ValorActualizadoEvent, value);
        }
        #endregion

        protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
        {
            if(change.Property == ValorProperty)
                Actualizar();
            base.OnPropertyChanged(change);
        }

        public CeldaTactil()
        {
            InitializeComponent();
            Actualizar();
        }


        public void Mas(object sender, RoutedEventArgs args)
        {
            Valor = ( Valor + 1 ) % ( ValorMaximo + 1 );

            Actualizar();
            Notificar(sender);

        }
        public void Menos(object sender, RoutedEventArgs args)
        {
            Valor = ( Valor + ( ValorMaximo ) ) % ( ValorMaximo + 1 );

            Actualizar();
            Notificar(sender);
        }

        private void Notificar(object sender)
        {
            (sender as Control).RaiseEvent(new RoutedEventArgs(ValorActualizadoEvent));
        }

        public void Actualizar()
        {
            Etiqueta.Content = Valor;
        }
    }
}

Here is an example of an implementation of this control used as the building block of a time selector. When the user clicks on the + and - buttons, the parent Tiempo (time, as i coded it in spanish) styled propperty should update to show the changes. This was accomplished by firing a custom event (ValorActualizado) from the cell, and binding it to the Actualizar (update) method of the parent.

AXAML:

<UserControl
x:Class="Programa_control.ControlesCustomizados.SelectorTiempo"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:Programa_control.ControlesCustomizados;assembly=Programa control"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
d:DesignHeight="100"
d:DesignWidth="266"
mc:Ignorable="d">
<StackPanel Orientation="Horizontal">
    <controls:CeldaTactil Name="UnidadesHora" ValorActualizado="Actualizar"/>
    <Label Classes="Extra">h</Label>
    <controls:CeldaTactil Name="DecenasMinuto" ValorActualizado="Actualizar" ValorMaximo="5" />
    <controls:CeldaTactil Name="UnidadesMinuto" ValorActualizado="Actualizar" />
    <Label Classes="Extra">'</Label>
    <controls:CeldaTactil Name="DecenasSegundo" ValorActualizado="Actualizar" ValorMaximo="5" />
    <controls:CeldaTactil Name="UnidadesSegundo" ValorActualizado="Actualizar" />
    <Label Classes="Extra">''</Label>
</StackPanel>

CODE BEHIND:

using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using System;
using System.ComponentModel;
using System.Diagnostics;

namespace Programa_control.ControlesCustomizados
{
    public partial class SelectorTiempo: UserControl
    {
        /// <summary>
        /// Tiempo StyledProperty definition
        /// </summary>
        public static readonly StyledProperty<TimeSpan> TiempoProperty =
            AvaloniaProperty.Register<SelectorTiempo, TimeSpan>(nameof(Tiempo));

        /// <summary>
        /// Gets or sets the Tiempo property. This StyledProperty
        /// indicates ....
        /// </summary>
        public TimeSpan Tiempo
        {
            get => this.GetValue(TiempoProperty);
            set => SetValue(TiempoProperty, value);
        }

        protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
        {
            if(change.Property == TiempoProperty)
            {
                EstablecerDefault();
                Actualizar(null, null);
            }
            base.OnPropertyChanged(change);
        }


        public SelectorTiempo()
        {
            InitializeComponent();

        }

        public void Actualizar(object sender, RoutedEventArgs e)
        {
            Tiempo = new TimeSpan(UnidadesHora.Valor, DecenasMinuto.Valor * 10 + UnidadesMinuto.Valor, DecenasSegundo.Valor * 10 + UnidadesSegundo.Valor);
        }


        /// <summary>
        /// Se llama a esta funcion cada vez que cambia el valor de Tiempo
        /// </summary>
        public void EstablecerDefault()
        {
            // Parse
            UnidadesHora.Valor = Tiempo.Hours % 10;
            DecenasMinuto.Valor = Tiempo.Minutes / 10;
            UnidadesMinuto.Valor = Tiempo.Minutes % 10;
            DecenasSegundo.Valor = Tiempo.Seconds / 10;
            UnidadesSegundo.Valor = Tiempo.Seconds % 10;
        }
    }
}