Pages

Showing posts with label Custom Controls. Show all posts
Showing posts with label Custom Controls. Show all posts

Saturday, October 19, 2019

Create a Custom Control with Xamarin.Forms

  The process of building UI elements requires, at some point, some customizations to give the unique feel and look to the application and to extend the functionality of existing controls. Whether the customization is just overriding the default TextColor of the Entry control, or creating a brand new control with new look and behavior, building custom controls can provide the simple solution to achieve that, without the need of custom renderers.

Create a Custom ToggleBar Control

The ToggleBar control can be used to show some options that the user can choose from, for example a filtering mechanism (similar to a group of radio buttons), or a light-weight tabbed control..etc, see the screenshot below (should look the same on iOS):

The behavior of the control is as follows:
  1. Every button in the control has a selected state and an unselected state determined by the IsSelected bindable property.
  2. The states are visually distinguished through the SelectedColor and the UnselectedColor bindable properties.
  3. The selected items can be obtained through the bindable property SelectedItems.
  4. The control supports multi-selection (that’s why it’s SelectedItems not SelectedItem) by setting the IsMultiSelect bindable property to true (defaults to false).
Bindable properties are the foundation of custom controls (For more information about Xamarin.Forms bindable properties, see Xamarin.Forms Bindable Properties).
Every button inside the ToggleBar control is a custom control by itself. This article will guide you through creating the ToggleButton control and the same concepts can be leveraged in the ToggleBar control (see the complete sample).
The process for creating custom controls is as follows:
  1. Create a subclass of the view you want to extend or modify.
  2. Alter the functionality of the subclass by overriding the default values of the base class’s bindable properties and/or create new bindable properties that will interact with user actions.
  3. Initialize the custom control.
  4. Process inputs in run-time through the propertyChanged delegate of the newly added bindable properties.

Create a Subclass of the View you Want to Extend

Most of custom controls are hosted in a ContentView as it is the simplest container and doesn't expose special properties (like Orientation of the StackLayout that can interfer with the the control behavior). Create a subclass from ContentView in the .NET Standard library project, name it ToggleButton, it holds the StackLayout that has two children: Label and BoxView, the following diagram illustrates the control outline:

When the StackLayout is tapped, the selection state is mutated. The visual state is defined by the TextColor property of the Label and the Color property of the BoxView.

Alter the Functionality of the Subclass:

Create the bindable properties: IsSelected, SelectedColor, UnselectedColor, Text, FontFamily and FontSize. This is the SelectedColor property along with the BindableProperty backing field:

public static readonly BindableProperty SelectedColorProperty = BindableProperty.Create(nameof(SelectedColor), typeof(Color), typeof(ToggleButton),
defaultValue: Color.Default);

 public Color SelectedColor
 {
     get { return (Color)GetValue(SelectedColorProperty); }
     set { SetValue(SelectedColorProperty, value); }
 }


Gemoji image for :bulb A bindable property is a special type of property, where the property's value is tracked by the Xamarin.Forms property system. The purpose of bindable properties is to provide a property system that supports data binding, styles, templates, and values set through parent-child relationships. The process of creating a bindable property is as follows:
  1. Create a BindableProperty instance with one of the BindableProperty.Create method overloads.
  2. Define property accessors for the BindableProperty instance.
For more information about Xamarin.Forms bindable properties, see Xamarin.Forms Bindable Properties
Gemoji image for :bulb Custom bindable properties in custom controls can be categorized into two types:
  1. Bindable properties that are passed down to the built-in bindable properties of child elements, like Text bindable property of the ToggleButton custom control, that is passed down to the Text bindable property of the Label control.
  2. Bindable properties that are specific to the custom control itself and not owned exclusively by any of the child elements, like the IsSelected bindable property. The more behavioral customization required to the custom control, the more of these bindable properties are needed.

Initializing the custom control

In the constructor of the ToggleButton class, initialize the inner controls and create the bindings between the new properties of the custom control and the properties of the inner controls (i.e., the StackLayout, the Label and the BoxView):
public ToggleButton()
{
    verticalStack = new StackLayout
    {
        Spacing = 0,
        HorizontalOptions = LayoutOptions.FillAndExpand
    };
    label = new Label
    {
        HorizontalTextAlignment = TextAlignment.Center,
        VerticalTextAlignment = TextAlignment.Center,
        VerticalOptions = LayoutOptions.CenterAndExpand,
        Margin = new Thickness(5)
    };
    boxView = new BoxView
    {
        VerticalOptions = LayoutOptions.End,
        HeightRequest = HeightRequest > 0 ? HeightRequest / 10d : 2
    };

    label.SetBinding(Label.TextColorProperty, new Binding(nameof(UnselectedColor), source: this));
    label.SetBinding(Label.TextProperty, new Binding(nameof(Text), source: this));
    label.SetBinding(Label.BackgroundColorProperty, new Binding(nameof(BackgroundColor), source: verticalStack));
    label.SetBinding(Label.WidthRequestProperty, new Binding(nameof(WidthRequest), source: verticalStack));
    label.SetBinding(Label.HeightRequestProperty, new Binding(nameof(HeightRequest), source: verticalStack));
    label.SetBinding(Label.FontFamilyProperty, new Binding(nameof(FontFamily), source: this));
    label.SetBinding(Label.FontSizeProperty, new Binding(nameof(FontSize), source: this));
    boxView.SetBinding(BoxView.BackgroundColorProperty, new Binding(nameof(BackgroundColor), source: verticalStack));
    verticalStack.GestureRecognizers.Add(new TapGestureRecognizer()
    {
        Command = new Command(() =>
        {
            IsSelected = !IsSelected;
            SelectionChanged?.Invoke(this, IsSelected);
        })
    });

    verticalStack.Children.Add(label);
    verticalStack.Children.Add(boxView);
    Content = verticalStack;
}

The constructor initializes the control properties, for example the TextColor property of the Label is bound to the UnselectedColor property of the custom control beacause the control is rendered in unselected state if IsSelected is not set, similarly, the BoxView's Color property is bound to the value of the BackgroundColor of the StackLayout to hide it, it only gets highlited with SelectedColor value when the control is selected. Setting the WidthRequest and HeightRequest for the Label and HeightRequest for the BoxView ensures they scale with the StackLayout size.
A TapGestureRecognizer is added to the StackLayout’s GestureRecognizers collection to mutate the selection state of the ToggleButton when the StackLayout is tapped. The TapGestureRecognizer provides two approaches for handling the tap action: by the Tapped event, or by the Command property. For more information about the tap gesture recognizer, see Adding a tap gesture recognizer. When the value of IsSelected propery changes, the propertyChanged delegate handles the visual state of the control (see next section).
Create SelectionChanged event that gets invoked when the StackLayout is tapped, to notify consumers of the ToggleButton (e.g. the ToggleBar control) when selection changes:
public event EventHandler<bool> SelectionChanged;

Process Inputs in run-time Through the propertyChanged Delegate

Add the propertyChanged parameter to the BindableProperty.Create method used to create the IsSelectedProperty, and handle it to mutate the visual state of the control based on the IsSelected value:
public static readonly BindableProperty IsSelectedProperty =
    BindableProperty.Create(nameof(IsSelected), typeof(bool), typeof(ToggleButton), false,
        propertyChanged: (bindable, oldValue, newValue) =>
        {
            ((ToggleButton)bindable).MutateSelect();
        }
    );
        
void MutateSelect()
{
    if (IsSelected)
    {
        button.TextColor = SelectedColor;
        underLine.Color = SelectedColor;
    }
    else
    {
        button.TextColor = UnselectedColor;
        underLine.Color = BackgroundColor;
    }
}

Consuming the Custom Control

The ToggleButton control can be referenced in XAML in the .NET Standard library project by declaring a namespace for its location and using the namespace prefix on the control element. The following code example shows how the ToggleButton control can be consumed by a XAML page:
<ContentPage ...
    xmlns:controls="clr-namespace:CustomControlsSample.CustomControls"
    ...>
    ...
    <controls:ToggleButton x:Name="toggleButton" Text="On" BackgroundColor="Black" UnselectedColor="Gray" SelectedColor="White" SelectionChanged="ToggleButton_SelectionChanged"/>
    ...
</ContentPage>
The following code example shows how the ToggleButton control can be consumed by a C# page:
public class MainPage : ContentPage
{
  public MainPage ()
  {
    var toggleButton = new ToggleButton
    {
       Text = "On",
       BackgroundColor = Color.Black,
       UnselectedColor = Color.Gray,
       SelectedColor = Color.White,
    };
   toggleButton.SelectionChanged += ToggleButton_SelectionChanged;
   Content = toggleButton;
  }
}
Attach a handler to the SelectionChanged event to handle the selection change in the code-behind file:
private async void ToggleButton_SelectionChanged(object sender, EventArgs e)
{
    string message = toggleButton.IsSelected ? "ToggleButton is selected" : "ToggleButton is unselected";
    await DisplayAlert("ToggleButton", message, "OK");
}

Summary

This article has demonstrated how to create a custom control in Xamarin.Forms, enabling developers to create new controls with new look and behavior, in the .NET Standard library project, without creating custom renderers. With the rich set of the layouts and the Animation API in Xamarin.Forms, you can even create more sophisticated and appealing controls.

Related Links

Friday, March 8, 2019

Toggles row control, building from scratch (label and box view)


 A good starting point to solve any problem is to decompose it to its simplest bits.

if we need such control (the three bars with the select-able items):


that's not a vailable in Xamarin.Forms, and we don't want to go deep with the renderers, while the solution might be doable with XAML code (and I mean by XAML any View class written in the shared project).

let's decompose the control:

It has a list (ItemsSource) of buttons:
   The behavior of the button is:
         Its selection state gets toggled when tapped.
         It has two colors; one in the selection state, and one in the unselection state
         There's a line under every selected item.

so first we need to write this singular control:

I named it ToggleButton:
it's a StackLayout with vertical orientation that has two children; Label and BoxView:
we extend it with these bindable properties:
TextProperty,SelectedColorProperty, 
UnselectedColorProperty, FontFamilyProperty 
and IsSelectedProperty, 
and has one event: SelectionChanged.
Typically I follow a convention when building reusable controls: all the propertyChanged delegates of the bindable properties share the same method and it builds the control when any property is changed especially the initial properties not the interactive ones like IsSelected.

In the Render method (the method of the propertyChanged delegate) we assign the TapGestureRecognizer to the label that mutate the IsSelected property of the control and fire the SelectionChanged event, the setter of the IsSelected property is responsible for redrawing the control to give it the un/selection appearance.

here's the code of the ToggleButton class:


    public class ToggleButton : StackLayout
    {
        Label button;
        BoxView box;
        public event EventHandler SelectionChanged;

        public ToggleButton()
        {
            Spacing = 0;
            HorizontalOptions = LayoutOptions.FillAndExpand;
        }

        #region Bindable Properties
        public static readonly BindableProperty TextProperty = BindableProperty.Create(nameof(Text), typeof(string), typeof(ToggleButton),
defaultValue: default(string), propertyChanged: CustomPropertyChanging);

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

        public static readonly BindableProperty SelectedColorProperty = BindableProperty.Create(nameof(SelectedColor), typeof(Color), typeof(ToggleButton),
defaultValue: default(Color), propertyChanged: CustomPropertyChanging);

        public Color SelectedColor
        {
            get { return (Color)GetValue(SelectedColorProperty); }
            set { SetValue(SelectedColorProperty, value); }
        }

        public static readonly BindableProperty UnselectedColorProperty = BindableProperty.Create(nameof(UnselectedColor), typeof(Color), typeof(ToggleButton),
defaultValue: default(Color), propertyChanged: CustomPropertyChanging);

        public Color UnselectedColor
        {
            get { return (Color)GetValue(UnselectedColorProperty); }
            set { SetValue(UnselectedColorProperty, value); }
        }

        public static readonly BindableProperty FontFamilyProperty = BindableProperty.Create(nameof(FontFamily), typeof(string), typeof(ToggleButton),
defaultValue: Button.FontFamilyProperty.DefaultValue, propertyChanged: CustomPropertyChanging);

        public string FontFamily
        {
            get { return (string)GetValue(FontFamilyProperty); }
            set { SetValue(FontFamilyProperty, value); }
        }

        public bool IsSelected
        {
            get { return (bool)GetValue(IsSelectedProperty); }
            set
            {
                SetValue(IsSelectedProperty, value);
                MutateSelect();
            }
        }

        public static readonly BindableProperty IsSelectedProperty =
            BindableProperty.Create(nameof(IsSelected), typeof(bool), typeof(ToggleButton), false);

        #endregion

        private static void CustomPropertyChanging(BindableObject bindable, object oldValue, object newValue)
        {
            if (newValue == null) return;
            ((ToggleButton)bindable).Render();
        }

        private void Render()
        {
            button = new Label
            {
                TextColor = UnselectedColor,
                Text = Text,
                BackgroundColor = BackgroundColor,
                FontFamily = FontFamily,
                HorizontalTextAlignment = TextAlignment.Center,
                VerticalTextAlignment = TextAlignment.Center,
                Margin = new Thickness(5)

            };

            box = new BoxView { HeightRequest = 2, Color = BackgroundColor };

            Children.Clear();
            Children.Add(button);
            Children.Add(box);
            button.GestureRecognizers.Add(new TapGestureRecognizer() { Command = new Command(() => TapCommand()) });

        }

        void TapCommand()
        {
            IsSelected = !IsSelected;
            SelectionChanged?.Invoke(this, new EventArgs());
        }

        void MutateSelect()
        {
            if (IsSelected)
            {
                button.TextColor = SelectedColor;
                box.Color = SelectedColor;
            }
            else
            {
                button.TextColor = UnselectedColor;
                box.Color = BackgroundColor;
            }
        }
    }

Next: building a suitable interactive container for a group of ToggleButtons (The TogglesRow):


We need these binndable properties:
ItemsSourceProperty, SelectedItemsProperty,
DisplayMemberPathProperty, DisplayMemberPath,
ItemsSpacingProperty, FontFamilyProperty,
IsMultiSelectProperty, SelectedColorProperty,
UnselectedColorProperty, InitialValuePathProperty,
InitialValueProperty, and one CLR property: InitialIndex
and one event: SelectedItemsChanged.

based on the IsMultiSelect property, the appearance and behavior of the control will be determined in the propertyChanged method, the foreach will loop through the ItemsSource and set the text of the toggle buttons based on the DisplayMemberPath property value, then in the SelectionChanged event of every ToggleButton we will redraw its un/selection state, add/ remove items to the SelectedItems property and SelectedIndices property, here's the full class:


    public class TogglesRow : ContentView
    {
        ScrollView scrollContainer;
        StackLayout stackContainer;
        public event EventHandler<TogglesRowSelectionChangedEventArgs> SelectedItemsChanged;

        public TogglesRow()
        {
            scrollContainer = new ScrollView
            {
                Orientation = ScrollOrientation.Horizontal,
                HorizontalScrollBarVisibility = ScrollBarVisibility.Never
            };

            stackContainer = new StackLayout
            {
                Orientation = StackOrientation.Horizontal,
                Spacing = 0,
                Margin = 0,
                Padding = 0
            };
            scrollContainer.Content = stackContainer;
            Content = scrollContainer;
        }

        public int InitialIndex { get; set; } = -1;

        #region Bindable Properties
        public static readonly BindableProperty ItemsSourceProperty = BindableProperty.Create(nameof(ItemsSource), typeof(IEnumerable<object>), typeof(TogglesRow),
          defaultBindingMode: BindingMode.TwoWay, propertyChanged: CustomPropertyChanging);

        public IEnumerable<object> ItemsSource
        {
            get { return (IEnumerable<object>)GetValue(ItemsSourceProperty); }
            set { SetValue(ItemsSourceProperty, value); }
        }

        public static readonly BindableProperty SelectedItemsProperty = BindableProperty.Create(nameof(SelectedItems), typeof(object), typeof(TogglesRow),
  defaultBindingMode: BindingMode.TwoWay);
        public object SelectedItems
        {
            get { return GetValue(SelectedItemsProperty); }
            set { SetValue(SelectedItemsProperty, value); }
        }

        public static readonly BindableProperty DisplayMemberPathProperty = BindableProperty.Create(nameof(DisplayMemberPath), typeof(string), typeof(TogglesRow),
            defaultBindingMode: BindingMode.OneTime,
            defaultValue: default(string),
            propertyChanged: CustomPropertyChanging);

        public string DisplayMemberPath
        {
            get { return (string)GetValue(DisplayMemberPathProperty); }
            set { SetValue(DisplayMemberPathProperty, value); }
        }

        public static readonly BindableProperty ItemsSpacingProperty =
        BindableProperty.Create(nameof(ItemsSpacing), typeof(double), typeof(TogglesRow), 0d);

        public double ItemsSpacing
        {
            get { return (double)GetValue(ItemsSpacingProperty); }
            set { SetValue(ItemsSpacingProperty, value); }
        }

        public static readonly BindableProperty FontFamilyProperty =
            BindableProperty.Create(nameof(FontFamily), typeof(string), typeof(TogglesRow), Button.FontFamilyProperty.DefaultValue);

        public string FontFamily
        {
            get { return (string)GetValue(FontFamilyProperty); }
            set { SetValue(FontFamilyProperty, value); }
        }

        public static readonly BindableProperty IsMultiSelectProperty =
            BindableProperty.Create(nameof(IsMultiSelect), typeof(bool), typeof(TogglesRow), false);

        public bool IsMultiSelect
        {
            get { return (bool)GetValue(IsMultiSelectProperty); }
            set { SetValue(IsMultiSelectProperty, value); }
        }

        public static readonly BindableProperty SelectedColorProperty = BindableProperty.Create(nameof(SelectedColor), typeof(Color), typeof(TogglesRow),
            defaultValue: Color.Black, propertyChanged: CustomPropertyChanging);

        public Color SelectedColor
        {
            get { return (Color)GetValue(SelectedColorProperty); }
            set { SetValue(SelectedColorProperty, value); }
        }

        public static readonly BindableProperty UnselectedColorProperty = BindableProperty.Create(nameof(UnselectedColor), typeof(Color), typeof(TogglesRow),
            defaultValue: Color.Gray, propertyChanged: CustomPropertyChanging);


        public Color UnselectedColor
        {
            get { return (Color)GetValue(UnselectedColorProperty); }
            set { SetValue(UnselectedColorProperty, value); }
        }

        public static readonly BindableProperty InitialValuePathProperty =
           BindableProperty.Create(nameof(InitialValuePath), typeof(string), typeof(TogglesRow), null,
               propertyChanged: CustomPropertyChanging);

        public string InitialValuePath
        {
            get { return (string)GetValue(InitialValuePathProperty); }
            set { SetValue(InitialValuePathProperty, value); }
        }

        public static readonly BindableProperty InitialValueProperty =
            BindableProperty.Create(nameof(InitialValue), typeof(object), typeof(TogglesRow), null,
                propertyChanged: CustomPropertyChanging);

        public object InitialValue
        {
            get { return GetValue(InitialValueProperty); }
            set { SetValue(InitialValueProperty, value); }
        } 
        #endregion

        private static void CustomPropertyChanging(BindableObject bindable, object oldValue, object newValue)
        {
            if (newValue != null)
                ((TogglesRow)bindable).Render();
        }

        private void Render()
        {
            try
            {
                if (ItemsSource == null || ItemsSource.Count() == 0)
                    return;

                stackContainer.Children.Clear();
                if (IsMultiSelect)
                    SelectedItems = new ObservableCollection<object>();
                foreach (var item in ItemsSource)
                {
                    var displayText = DisplayMemberPath == null ? item.ToString() : item.GetType().GetProperty(DisplayMemberPath).GetValue(item, null).ToString();
                    var btn = new ToggleButton
                    {
                        Text = displayText,
                        FontFamily = FontFamily,
                        BackgroundColor = BackgroundColor,
                        SelectedColor = SelectedColor,
                        UnselectedColor = UnselectedColor
                    };
                    if (!string.IsNullOrEmpty(InitialValuePath))
                    {
                        var value = item.GetType().GetProperty(InitialValuePath).GetValue(item, null);
                        if (value.ToString().Equals(InitialValue))
                            btn.IsSelected = true;

                    }
                    else if (InitialIndex >= 0)
                    {
                        if (ItemsSource.IndexOf(item) == InitialIndex)
                            btn.IsSelected = true;
                    }
                    btn.SelectionChanged += (s, e) =>
                    {
                        if (IsMultiSelect)
                        {
                            if (btn.IsSelected)
                                (SelectedItems as ObservableCollection<object>).Add(item);
                            else
                                (SelectedItems as ObservableCollection<object>).Remove(item);
                        }
                        else
                        {
                            var allToggleButtons = stackContainer.Children.Where(x => x is ToggleButton);
                            allToggleButtons?.ForEach(x => ((ToggleButton)x).IsSelected = false);
                            btn.IsSelected = true;
                            SelectedItems = item;
                        }
                        if (!IsMultiSelect && btn.IsSelected)
                            SelectedItemsChanged?.Invoke(this, new TogglesRowSelectionChangedEventArgs
                            {
                                SelectedItems = SelectedItems,
                                SelectedIndices = ItemsSource.IndexOf(item)
                            });
                        else if (IsMultiSelect)
                        {
                            SelectedItemsChanged?.Invoke(this, new TogglesRowSelectionChangedEventArgs
                            {
                                SelectedItems = SelectedItems,
                                SelectedIndices = (SelectedItems as ObservableCollection<object>).Select(x => ItemsSource.IndexOf(x))
                            });
                        }
                    };
                    stackContainer.Spacing = ItemsSpacing;
                    stackContainer.Children.Add(btn);
                }
            }
            catch (Exception ex)
            {
                throw ex;
            }
        }
    }
That's it, you have a complete native look-alike control, that you can use in many different scenarios:
and you can use it like that and it's MVVM-friendly:

<controls:TogglesRow ItemsSource="{Binding Orders}" InitialIndex="0"
SelectedItemsChanged="TogMultiSelected" 
BackgroundColor="#424242" SelectedColor="#ffd92e" 
UnselectedColor="Gray" 
ItemsSpacing="{OnPLatform iOS=12}"
DisplayMemberPath="Name" IsMultiSelect="True"/>


I'll post the full code along with other controls in GitHub.

Dynamically load jndi.properties when JMeter is running

An approach to having all test configurations in one place is using the jndi.properties file of JMeter. When maintaining many JMeter test su...