Pages

Sunday, January 21, 2018

Creating RadioButtonsGroup control in Xamarin.Forms




The RadioButton control is one of the most used controls in almost any application, however Xamarin Forms does not ship with one, so what do you do?
Yes, create it; it’s a good opportunity to have some fun creating a missing control.
Let’s address the problem in detail.

What do you need?
I need a RadioButtonsGroup control not just a single control; that can render nth of radio buttons equal to the items bound to it from its ItemsSource property. We also need to know which item from the ItemsSource collection has been selected by data binding, for that, the control will have SelectedItem Property for the whole selected object, but you may be interested in only one property from that object like ID, so we’ll implement SelectedValue as well, and also SelectedIndex. And Orientation property for the Horizontal & Vertical orientation.
When you bind the control to a collection of items, the control need to know what property is used for the displayed text and what property used for returning the SelectedValue, so it will contain DisplayMemberPath and SelectedValuePath properties, that we will use reflection to manipulate.

So far so good, we did not touch on how we would draw these little circles of the control itself. In a previous version, I used two Unicode characters: and . However, in this version I will draw them using Frame  (look at the gif above to see the differences).

Let’s write the control!

Add a new class, give it the name RadioButtonsGroup, add two fields; parentStack of type StackLayout that will host the radio buttons , and the other field is lbRadios of type List<SelectionFrame> that we’ll create it later.
In the constructor:

   public RadioButtonsGroup()
        {
            parentStack = new StackLayout();
            Content = parentStack;
        }

The ItemsSource is a bindable property that encapsulate a CLR property of type IEnumerable<object>, in the propertyChanged delegate parameter we are going to make most of the work our control need:

public static readonly BindableProperty ItemsSourceProperty = BindableProperty.Create(nameof(ItemsSource), typeof(IEnumerable<object>), typeof(RadioButtonsGroup), defaultBindingMode: BindingMode.TwoWay, propertyChanged: OnItemsSourceChanged);

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

The rest of the properties:

public static readonly BindableProperty SelectedIndexProperty = BindableProperty.Create(nameof(SelectedIndex), typeof(int), typeof(RadioButtonsGroup), defaultValue: -1, defaultBindingMode: BindingMode.TwoWay);
        public static readonly BindableProperty SelectedItemProperty = BindableProperty.Create(nameof(SelectedItem), typeof(object), typeof(RadioButtonsGroup), defaultBindingMode: BindingMode.TwoWay);
        public static readonly BindableProperty OrientationProperty = BindableProperty.Create(nameof(Orientation), typeof(StackOrientation), typeof(RadioButtonsGroup), defaultValue: StackOrientation.Vertical);
public static readonly BindableProperty DisplayMemberPathProperty = BindableProperty.Create(nameof(DisplayMemberPath), typeof(string), typeof(RadioButtonsGroup));
        public static readonly BindableProperty SelectedValuePathProperty = BindableProperty.Create(nameof(SelectedValuePath), typeof(string), typeof(RadioButtonsGroup));
        public static readonly BindableProperty SelectedValueProperty = BindableProperty.Create(nameof(SelectedValue), typeof(object), typeof(RadioButtonsGroup));



        public int SelectedIndex
        {
            get { return (int)GetValue(SelectedIndexProperty); }
            set { SetValue(SelectedIndexProperty, value); }
        }

        public object SelectedItem
        {
            get { return GetValue(SelectedItemProperty); }
            set { SetValue(SelectedItemProperty, value); }
        }

        public StackOrientation Orientation
        {
            get { return (StackOrientation)GetValue(OrientationProperty); }
            set { SetValue(OrientationProperty, value); }
        }

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

        public string SelectedValuePath
        {
            get { return (string)GetValue(SelectedValuePathProperty); }
            set { SetValue(SelectedValuePathProperty, value); }
        }

        public object SelectedValue
        {
            get { return GetValue(SelectedValueProperty); }
            set { SetValue(SelectedValueProperty, value); }
        }

Now let’s implement OnItemsSourceChanged method which is called once the ItemsSource collection has changed, that is when it’s bound to the collection after first constructed:

        private static void OnItemsSourceChanged(BindableObject bindable, object oldValue, object newValue)
        {
            if (newValue == null)
                return;
            var @this = bindable as RadioButtonsGroup; //catch the current instance
set the parentStack’s Orientation to Orientation property of the RadioButtonsGroup:
parentStack.Orientation = Orientation;

look at this picture to get an insight of how we are building the control:



Now let’s add the radio buttons, remember: for every item in the ItemsSource collection there’s a corresponding radio button:

foreach (var item in items)
            {
                StackLayout radioStack = new StackLayout
                {
                    Orientation = StackOrientation.Horizontal,
                    BindingContext = item,
                    HorizontalOptions = Orientation == StackOrientation.Horizontal ? LayoutOptions.CenterAndExpand : LayoutOptions.Fill,
                };
                TapGestureRecognizer tap = new TapGestureRecognizer();
                tap.Tapped += RadioChecked;
                radioStack.GestureRecognizers.Add(tap);

                var displayText = DisplayMemberPath == null ? item.ToString() : item.GetType().GetProperty(DisplayMemberPath).GetValue(item, null).ToString();

                Label radioText = new Label { Text = displayText, VerticalOptions = LayoutOptions.Center };
                SelectionFrame circle = new SelectionFrame { ClassId = "r", BorderColor = FrontColor, VerticalOptions = LayoutOptions.Center };
                circle.HorizontalOptions = LayoutOptions.EndAndExpand;
                radioStack.Children.Add(radioText);
                radioStack.Children.Add(circle);
            parentStack.Children.Add(radioStack);

            }

The radioStack will host a single radio button, setting the BindingContext is important, as the selectedItem will live in it. The checked event of the radio button is no more than the tapped event of the radioStack of current item, so whenever the radioStack is tapped, we set the selected value and selected index and draw the inner filled circle to mark it as selected. The only way we get the displayed text of the radio button from DisplayMemberPath is by reflection. We first check if the DisplayMemberPath is null, then the displayed text will be just the ToString of the item, it’s useful if the ItemsSource is just of type IEnumerable<string>. Otherwise we get the corresponding value of the property having the name specified by DisplayMemberPath.
After we get the text to display, we create the label radioText.
The first half of the control is complete; now create the other half, the circle:
SelectionFrame circle = new SelectionFrame { ClassId = "r", VerticalOptions = LayoutOptions.Center }; we will come to implementing SelectionFrame shortly.
Now let’s combine the circle with the text inside the radioStack to make a single radio button. After we’ve added all radio buttons to the parentStack, let’s mark one of them as selected if the user provided a valid SelectedIdex. We apparently need a list of all SelectionFrames to change the style of the selected one, so let’s get them:

            lbRadios = parentStack.Children.Where(x => x is StackLayout).SelectMany(x => ((StackLayout)x).Children.Where(l => l.ClassId == "r").Cast<SelectionFrame>()).ToList();

and then we decide if we mark one of them as selected or not:

if (SelectedIndex >= 0)
            {
                try
                {
                    lbRadios[SelectedIndex].IsSelected = true;
                }
                catch (ArgumentOutOfRangeException) //if the user provided invalid value we set it to 0 as default
                {
                    SetValue(SelectedIndexProperty, 0);
                    lbRadios[SelectedIndex].IsSelected = true;
                }
            }

Now let’ implement the RadioChecked method, that’s called when a radio button is tapped:

private void RadioChecked(object sender, EventArgs e)
        {
            StackLayout stRadio = (StackLayout)sender;
            var lb = stRadio.Children.First(x => x.ClassId == "r") as SelectionFrame;
            if (lb is null)
                return;
            if (!lb.IsSelected)
            {
                if (SelectedIndex >= 0)
                    lbRadios.Single(x => x.IsSelected).IsSelected = false;
                lb.IsSelected = true;
                SelectedItem = stRadio.BindingContext;
                SelectedValue = SelectedValuePath == null ? null : SelectedItem.GetType().GetProperty(SelectedValuePath).GetValue(SelectedItem, null);
                SelectedIndex = ItemsSource.ToList().IndexOf(SelectedItem);
                OnSelectionChanged?.Invoke(this, new SelectionChangedEventArgs(SelectedItem, SelectedValue, SelectedIndex));
            }
        }

The sender object is radioStack, we need to change the style of the SelectionFrame from unselected to selected and clear the selected one, so let’s get the selection frame:
            var lb = stRadio.Children.First(x => x.ClassId == "r") as SelectionFrame;

Then we see if the item is already selected, if so, we do nothing, otherwise we check it, and if there’s already a checked item (SelectedIndex  > 0) then we unselect it,
after that we set the SelectedItem, SelectedValue and SelectedIndex, and fire the OnSelectionChanged event, you can easily guess what it does by parameters passed to its delegate

   if (!lb.IsSelected)
            {
                if (SelectedIndex >= 0)
                    lbRadios.Single(x => x.IsSelected).IsSelected = false;
                lb.IsSelected = true;
                SelectedItem = stRadio.BindingContext;
                SelectedValue = SelectedValuePath == null ? null : SelectedItem.GetType().GetProperty(SelectedValuePath).GetValue(SelectedItem, null);
                SelectedIndex = ItemsSource.ToList().IndexOf(SelectedItem);
                OnSelectionChanged?.Invoke(this, new SelectionChangedEventArgs(SelectedItem, SelectedValue, SelectedIndex));
            }

Now, let’s create the SelectionFrame.
Drawing these little circle is not simple as you might expect. So let’s discuss how frames work in Xamarin:
The frame is a plain full-colored area, so you cannot just draw circle, frame is a nice workaround. To draw an unselected radio button (empty circle) you have two frames; the outer with greater radius and with solid color, and the inner circle with smaller radius and white color, this way we’ll have a circular frame (off course after setting the corner radius of the frames)
To draw a checked circle we will center a smaller circular frame inside these two frames with the same color of the outer frame, actually it’s always there, but in the unselected mode it has the same color as the middle circle. Now let’s create them:

        Frame f1;
        Frame f2;
        Frame f3;
        readonly double f3r;

public SelectionFrame()
        {
            f3r = 4;
            const double f2r = 6;
            const double f1r = 7;
            f3 = new Frame
            {
                VerticalOptions = LayoutOptions.Center,
                HorizontalOptions = LayoutOptions.Center,
                BackgroundColor = Color.White,
                Padding = 0,
                HasShadow = false,
                OutlineColor = Color.Transparent,
                Margin = 0,
                CornerRadius = (float)f3r,
                HeightRequest = f3r * 2,
                WidthRequest = f3r * 2
            };

            f2 = new Frame
            {
                VerticalOptions = LayoutOptions.Center,
                HorizontalOptions = LayoutOptions.Center,
                BackgroundColor = Color.White,
                HasShadow = false,
                Padding = 0,
                Margin = 0,
                CornerRadius = (float)f2r,
                HeightRequest = f2r * 2,
                WidthRequest = f2r * 2,
                Content = f3
            };

            f1 = new Frame
            {
                VerticalOptions = LayoutOptions.Center,
                HorizontalOptions = LayoutOptions.Center,
                BackgroundColor = BorderColor,
                HasShadow = false,
                Padding = new Thickness(0),
                CornerRadius = (float)f1r,
                HeightRequest = f1r * 2,
                WidthRequest = f1r * 2,
                Content = f2
            };
            Frame f0 = new Frame
            {
                BackgroundColor = Color.Transparent,
                HasShadow = false,
                Content = f1
            };
            Content = f1;
        }

This is the IsSelected property you’ve seen in RadioButtonsGroup class.
        bool isSelected;
        public bool IsSelected
        {
            get { return isSelected; }
            set
            {
                isSelected = value;
                SelectionChanged();
            }
        }

                                                        
We need to change the color of the inner frame (smallest frame) to black when selected, or white when unselected:

        private void SelectionChanged()
        {
            if (IsSelected)
          
                f3.BackgroundColor = BorderColor;         
            else
                f3.BackgroundColor = Color.White;
        }

That is it; you’ve created the RadioButtonsGroup control.

In the GitHub repo you will find some additional work like animating the inner circle of the frame and some other properties like FrontColor and other properties for the font.

GitHub repo:
https://github.com/mshwf/CustomRadioButtonsGroup

How to do code reviews correctly

Introduction Code review is a special event in the life cycle of software development, it's where ownership is transferred from the deve...