Build your sentence in MAUI

dotnet maui cross platform development

I’m creating on app for helping people to learn languages called LanguageInUse and I want to build a page where you can put together your sentence using MAUI. As a result, I like to have a component to reuse in my app.

The source code is available on GitHub.

Setup the project

First, my goal is to create an interactive component in .NET MAUI where users can tap on words to form a sentence, which is a fun and engaging task. Here’s a basic example to get you started:

  1. Set up the project: ensure you have a .NET MAUI project set up in Visual Studio.
  2. Create the UI: Define a StackLayout for the words and a Label to display the formed sentence.
  3. Handle the word taps: Use Button controls for the words and handle their Clicked events to update the sentence.

After creating a new MAUI project, I’m going to replace the MainPage with another ContentPage using C# (not XAML).

using Microsoft.Maui.Controls;

namespace MAUISentenceBuilder
{
    public class MainPage : ContentPage
    {
        private Label sentenceLabel;
        private string formedSentence = "";

        public MainPage()
        {
            sentenceLabel = new Label
            {
                Text = "Formed Sentence: ",
                FontSize = 24,
                HorizontalOptions = LayoutOptions.Center,
                VerticalOptions = LayoutOptions.Start
            };

            var words = new[] { "Hello", "world", "this", "is", "MAUI" };
            var wordButtons = new StackLayout
            {
                Orientation = StackOrientation.Horizontal,
                HorizontalOptions = LayoutOptions.Center,
                VerticalOptions = LayoutOptions.End,
                Spacing = 10
            };

            foreach (var word in words)
            {
                var button = new Button
                {
                    Text = word,
                    FontSize = 18
                };
                button.Clicked += OnWordButtonClicked;
                wordButtons.Children.Add(button);
            }

            Content = new StackLayout
            {
                Children = { sentenceLabel, wordButtons }
            };
        }

        private void OnWordButtonClicked(object sender, EventArgs e)
        {
            if (sender is Button button)
            {
                formedSentence += button.Text + " ";
                sentenceLabel.Text = "Formed Sentence: " + formedSentence;
            }
        }
    }
}
First example - Build your sentence in MAUI
The first run of the component

Explanation

  • Label: Displays the formed sentence.
  • StackLayout: Holds the word buttons at the bottom of the screen.
  • Button Click Event: Appends the clicked word to the sentence and updates the label.

This example provides a basic structure. You can expand it by adding features like clearing the sentence, rearranging words, or validating the formed sentence.

Move the selected words

Now, I like to see the selected words below the list of words. So, let’s modify the example to display the selected words below the list of words.

using Microsoft.Maui.Controls;

namespace MAUISentenceBuilder
{
    public class MainPage : ContentPage
    {
        private Label sentenceLabel;
        private string formedSentence = "";

        public MainPage()
        {
            sentenceLabel = new Label
            {
                Text = "Formed Sentence: ",
                FontSize = 24,
                HorizontalOptions = LayoutOptions.Center,
                VerticalOptions = LayoutOptions.End
            };

            var words = new[] { "Hello", "world", "this", "is", "MAUI" };
            var wordButtons = new StackLayout
            {
                Orientation = StackOrientation.Horizontal,
                HorizontalOptions = LayoutOptions.Center,
                VerticalOptions = LayoutOptions.Start,
                Spacing = 10
            };

            foreach (var word in words)
            {
                var button = new Button
                {
                    Text = word,
                    FontSize = 18
                };
                button.Clicked += OnWordButtonClicked;
                wordButtons.Children.Add(button);
            }

            Content = new StackLayout
            {
                Children = { wordButtons, sentenceLabel }
            };
        }

        private void OnWordButtonClicked(object sender, EventArgs e)
        {
            if (sender is Button button)
            {
                formedSentence += button.Text + " ";
                sentenceLabel.Text = "Formed Sentence: " + formedSentence;
            }
        }
    }
}

Explanation

  • Label: Displays the formed sentence and is positioned below the word buttons.
  • StackLayout: The wordButtons layout is placed at the top, and the sentenceLabel is placed below it.

Now, the selected words will appear below the list of words. This is making easier for users to see the sentence they are forming.

Display words as buttons

Next step, I want to see the selected words below as a button. Once a word is selected, it has to disappear from the top list and appear at the bottom. If the user clicks on a word in the bottom list, it will disappear and reappear in the list at the top

Let’s create a more interactive component where selected words move between two lists.

  1. Define the UI: We’ll have two StackLayouts, one for the available words and one for the selected words.
  2. Handle word selection and deselection: We’ll update the lists dynamically based on user interactions.
using Microsoft.Maui.Controls;
using System.Collections.Generic;

namespace MAUISentenceBuilder
{
    public class MainPage : ContentPage
    {
        private StackLayout availableWordsLayout;
        private StackLayout selectedWordsLayout;
        private List<string> availableWords;
        private List<string> selectedWords;

        public MainPage()
        {
            availableWords = new List<string> { "Hello", "world", "this", "is", "MAUI" };
            selectedWords = new List<string>();

            availableWordsLayout = new StackLayout
            {
                Orientation = StackOrientation.Horizontal,
                HorizontalOptions = LayoutOptions.Center,
                VerticalOptions = LayoutOptions.Start,
                Spacing = 10
            };

            selectedWordsLayout = new StackLayout
            {
                Orientation = StackOrientation.Horizontal,
                HorizontalOptions = LayoutOptions.Center,
                VerticalOptions = LayoutOptions.End,
                Spacing = 10
            };

            UpdateWordButtons();

            Content = new StackLayout
            {
                Children = { availableWordsLayout, selectedWordsLayout }
            };
        }

        private void UpdateWordButtons()
        {
            availableWordsLayout.Children.Clear();
            selectedWordsLayout.Children.Clear();

            foreach (var word in availableWords)
            {
                var button = new Button
                {
                    Text = word,
                    FontSize = 18
                };
                button.Clicked += OnAvailableWordClicked;
                availableWordsLayout.Children.Add(button);
            }

            foreach (var word in selectedWords)
            {
                var button = new Button
                {
                    Text = word,
                    FontSize = 18
                };
                button.Clicked += OnSelectedWordClicked;
                selectedWordsLayout.Children.Add(button);
            }
        }

        private void OnAvailableWordClicked(object sender, EventArgs e)
        {
            if (sender is Button button)
            {
                availableWords.Remove(button.Text);
                selectedWords.Add(button.Text);
                UpdateWordButtons();
            }
        }

        private void OnSelectedWordClicked(object sender, EventArgs e)
        {
            if (sender is Button button)
            {
                selectedWords.Remove(button.Text);
                availableWords.Add(button.Text);
                UpdateWordButtons();
            }
        }
    }
}

Explanation

  • Two StackLayouts: One for available words and one for selected words.
  • Dynamic Updates: The UpdateWordButtons method updates the buttons in both layouts based on the current state of the lists.
  • Event HandlersOnAvailableWordClicked moves a word from the available list to the selected list, and OnSelectedWordClicked moves it back.

This setup allows users to interactively build and modify their sentence by moving words between the two lists.

Add animation

Now, when the user taps on a button, I like to see an animation that moves the button from to top list to the bottom list or vice-versa.

Adding animations to move buttons between the lists will make the interaction more engaging. Here’s how you can implement this in .NET MAUI:

  1. Define the animations: We’ll use TranslateTo for moving the buttons.
  2. Update the event handlers: Incorporate the animations when buttons are clicked.

Here’s the updated code:

using Microsoft.Maui.Controls;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace MAUISentenceBuilder
{
    public class MainPage : ContentPage
    {
        private StackLayout availableWordsLayout;
        private StackLayout selectedWordsLayout;
        private List<string> availableWords;
        private List<string> selectedWords;

        public MainPage()
        {
            availableWords = new List<string> { "Hello", "world", "this", "is", "MAUI" };
            selectedWords = new List<string>();

            availableWordsLayout = new StackLayout
            {
                Orientation = StackOrientation.Horizontal,
                HorizontalOptions = LayoutOptions.Center,
                VerticalOptions = LayoutOptions.Start,
                Spacing = 10
            };

            selectedWordsLayout = new StackLayout
            {
                Orientation = StackOrientation.Horizontal,
                HorizontalOptions = LayoutOptions.Center,
                VerticalOptions = LayoutOptions.End,
                Spacing = 10
            };

            UpdateWordButtons();

            Content = new StackLayout
            {
                Children = { availableWordsLayout, selectedWordsLayout }
            };
        }

        private void UpdateWordButtons()
        {
            availableWordsLayout.Children.Clear();
            selectedWordsLayout.Children.Clear();

            foreach (var word in availableWords)
            {
                var button = new Button
                {
                    Text = word,
                    FontSize = 18
                };
                button.Clicked += OnAvailableWordClicked;
                availableWordsLayout.Children.Add(button);
            }

            foreach (var word in selectedWords)
            {
                var button = new Button
                {
                    Text = word,
                    FontSize = 18
                };
                button.Clicked += OnSelectedWordClicked;
                selectedWordsLayout.Children.Add(button);
            }
        }

        private async void OnAvailableWordClicked(object sender, EventArgs e)
        {
            if (sender is Button button)
            {
                await AnimateButton(button, availableWordsLayout, selectedWordsLayout);
                availableWords.Remove(button.Text);
                selectedWords.Add(button.Text);
                UpdateWordButtons();
            }
        }

        private async void OnSelectedWordClicked(object sender, EventArgs e)
        {
            if (sender is Button button)
            {
                await AnimateButton(button, selectedWordsLayout, availableWordsLayout);
                selectedWords.Remove(button.Text);
                availableWords.Add(button.Text);
                UpdateWordButtons();
            }
        }

        private async Task AnimateButton(Button button, StackLayout fromLayout, StackLayout toLayout)
        {
            var initialPosition = button.Bounds;
            fromLayout.Children.Remove(button);
            toLayout.Children.Add(button);

            var finalPosition = button.Bounds;
            button.TranslationX = initialPosition.X - finalPosition.X;
            button.TranslationY = initialPosition.Y - finalPosition.Y;

            await button.TranslateTo(0, 0, 500, Easing.CubicInOut);
        }
    }
}

Explanation

  • AnimateButton Method: This method handles the animation of moving the button from one layout to another.
    • Initial and Final Positions: Calculate the initial and final positions of the button.
    • Translation: Set the button’s translation to move it from the initial to the final position.
    • TranslateTo: Animate the button to its new position over 500 milliseconds using a cubic easing function.

This setup will animate the buttons as they move between the available and selected word lists, providing a smooth and visually appealing transition.

Add placeholder

Now, when a button is clicked and moved in the other list on the bottom, I like to see a placeholder for this button. If a button in the bottom part is clicked, the button has to return in the right placeholder on the top.

When a button is clicked and moved to the other list, a placeholder will appear in its original position. If a button in the bottom part is clicked, it will return to its original placeholder at the top.

using Microsoft.Maui.Controls;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace MAUISentenceBuilder
{
    public class MainPage : ContentPage
    {
        private StackLayout availableWordsLayout;
        private StackLayout selectedWordsLayout;
        private List<string> availableWords;
        private List<string> selectedWords;
        private Dictionary<string, BoxView> placeholders;

        public MainPage()
        {
            availableWords = new List<string> { "Hello", "world", "this", "is", "MAUI" };
            selectedWords = new List<string>();
            placeholders = new Dictionary<string, BoxView>();

            availableWordsLayout = new StackLayout
            {
                Orientation = StackOrientation.Horizontal,
                HorizontalOptions = LayoutOptions.Center,
                VerticalOptions = LayoutOptions.Start,
                Spacing = 10
            };

            selectedWordsLayout = new StackLayout
            {
                Orientation = StackOrientation.Horizontal,
                HorizontalOptions = LayoutOptions.Center,
                VerticalOptions = LayoutOptions.End,
                Spacing = 10
            };

            UpdateWordButtons();

            Content = new StackLayout
            {
                Children = { availableWordsLayout, selectedWordsLayout }
            };
        }

        private void UpdateWordButtons()
        {
            availableWordsLayout.Children.Clear();
            selectedWordsLayout.Children.Clear();

            foreach (var word in availableWords)
            {
                var button = new Button
                {
                    Text = word,
                    FontSize = 18
                };
                button.Clicked += OnAvailableWordClicked;
                availableWordsLayout.Children.Add(button);

                var placeholder = new BoxView
                {
                    Color = Colors.Gray,
                    WidthRequest = 80,
                    HeightRequest = 40,
                    HorizontalOptions = LayoutOptions.Center,
                    VerticalOptions = LayoutOptions.Center
                };
                placeholders[word] = placeholder;
            }

            foreach (var word in selectedWords)
            {
                var button = new Button
                {
                    Text = word,
                    FontSize = 18
                };
                button.Clicked += OnSelectedWordClicked;
                selectedWordsLayout.Children.Add(button);
            }

            foreach (var word in placeholders.Keys)
            {
                if (!availableWords.Contains(word))
                {
                    availableWordsLayout.Children.Add(placeholders[word]);
                }
            }
        }

        private async void OnAvailableWordClicked(object sender, EventArgs e)
        {
            if (sender is Button button)
            {
                availableWords.Remove(button.Text);
                selectedWords.Add(button.Text);
                await AnimateButton(button, availableWordsLayout, selectedWordsLayout);
                UpdateWordButtons();
            }
        }

        private async void OnSelectedWordClicked(object sender, EventArgs e)
        {
            if (sender is Button button)
            {
                selectedWords.Remove(button.Text);
                availableWords.Add(button.Text);
                await AnimateButton(button, selectedWordsLayout, availableWordsLayout);
                UpdateWordButtons();
            }
        }

        private async Task AnimateButton(Button button, Layout fromLayout, Layout toLayout)
        {
            var initialPosition = button.Bounds;
            fromLayout.Children.Remove(button);
            toLayout.Children.Add(button);

            var finalPosition = button.Bounds;
            button.TranslationX = initialPosition.X - finalPosition.X;
            button.TranslationY = initialPosition.Y - finalPosition.Y;

            await button.TranslateTo(0, 0, 500, Easing.CubicInOut);
        }
    }
}

Explanation

  • Placeholders Dictionary: A dictionary to store placeholders for each word.
  • BoxView as Placeholders: Gray BoxView elements act as placeholders.
  • UpdateWordButtons Method: Adds placeholders to the layout when a word is moved.
  • AnimateButton Method: Animates the button to its new position.

This setup will ensure that placeholders appear in the original positions of the buttons, and buttons can return to their placeholders when clicked.

Placeholder improvement

The placeholder has to be displayed in the same position as the selected button

Let’s update the code so that the placeholder appears in the same position as the selected button. When a button is clicked and moved to the other list, a placeholder will appear in its original position. If a button in the bottom part is clicked, it will return to its original placeholder at the top.

using Microsoft.Maui.Controls;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace MAUISentenceBuilder
{
    public class MainPage : ContentPage
    {
        private StackLayout availableWordsLayout;
        private StackLayout selectedWordsLayout;
        private List<string> availableWords;
        private List<string> selectedWords;
        private Dictionary<string, BoxView> placeholders;

        public MainPage()
        {
            availableWords = new List<string> { "Hello", "world", "this", "is", "MAUI" };
            selectedWords = new List<string>();
            placeholders = new Dictionary<string, BoxView>();

            availableWordsLayout = new StackLayout
            {
                Orientation = StackOrientation.Horizontal,
                HorizontalOptions = LayoutOptions.Center,
                VerticalOptions = LayoutOptions.Start,
                Spacing = 10
            };

            selectedWordsLayout = new StackLayout
            {
                Orientation = StackOrientation.Horizontal,
                HorizontalOptions = LayoutOptions.Center,
                VerticalOptions = LayoutOptions.End,
                Spacing = 10
            };

            UpdateWordButtons();

            Content = new StackLayout
            {
                Children = { availableWordsLayout, selectedWordsLayout }
            };
        }

        private void UpdateWordButtons()
        {
            availableWordsLayout.Children.Clear();
            selectedWordsLayout.Children.Clear();

            foreach (var word in availableWords)
            {
                var button = new Button
                {
                    Text = word,
                    FontSize = 18
                };
                button.Clicked += OnAvailableWordClicked;
                availableWordsLayout.Children.Add(button);

                if (!placeholders.ContainsKey(word))
                {
                    var placeholder = new BoxView
                    {
                        Color = Colors.Gray,
                        WidthRequest = 80,
                        HeightRequest = 40,
                        HorizontalOptions = LayoutOptions.Center,
                        VerticalOptions = LayoutOptions.Center
                    };
                    placeholders[word] = placeholder;
                }
            }

            foreach (var word in selectedWords)
            {
                var button = new Button
                {
                    Text = word,
                    FontSize = 18
                };
                button.Clicked += OnSelectedWordClicked;
                selectedWordsLayout.Children.Add(button);
            }

            foreach (var word in placeholders.Keys)
            {
                if (!availableWords.Contains(word))
                {
                    availableWordsLayout.Children.Add(placeholders[word]);
                }
            }
        }

        private async void OnAvailableWordClicked(object sender, EventArgs e)
        {
            if (sender is Button button)
            {
                availableWords.Remove(button.Text);
                selectedWords.Add(button.Text);
                await AnimateButton(button, availableWordsLayout, selectedWordsLayout);
                UpdateWordButtons();
            }
        }

        private async void OnSelectedWordClicked(object sender, EventArgs e)
        {
            if (sender is Button button)
            {
                selectedWords.Remove(button.Text);
                availableWords.Add(button.Text);
                await AnimateButton(button, selectedWordsLayout, availableWordsLayout);
                UpdateWordButtons();
            }
        }

        private async Task AnimateButton(Button button, Layout fromLayout, Layout toLayout)
        {
            var initialPosition = button.Bounds;
            fromLayout.Children.Remove(button);
            toLayout.Children.Add(button);

            var finalPosition = button.Bounds;
            button.TranslationX = initialPosition.X - finalPosition.X;
            button.TranslationY = initialPosition.Y - finalPosition.Y;

            await button.TranslateTo(0, 0, 500, Easing.CubicInOut);
        }
    }
}

Explanation

  • Placeholders Dictionary: A dictionary to store placeholders for each word.
  • BoxView as Placeholders: Gray BoxView elements act as placeholders.
  • UpdateWordButtons Method: Adds placeholders to the layout when a word is moved.
  • AnimateButton Method: Animates the button to its new position.

This setup will ensure that placeholders appear in the original positions of the buttons, and buttons can return to their placeholders when clicked.

Validate the sentence

After the user selects at least one of the buttons, a button has to appear to validate the sentence. If the selected words are the same of the available words, the user gives a correct answer.

Let’s add a validation button that appears once the user selects at least one word. This button will check if the selected words form the correct sentence. Here’s how you can implement this:

  1. Add a validation button: The button will be initially hidden and will appear when at least one word is selected.
  2. Validate the sentence: Check if the selected words match the original list of available words.
using Microsoft.Maui.Controls;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace MAUISentenceBuilder
{
    public class MainPage : ContentPage
    {
        private StackLayout availableWordsLayout;
        private StackLayout selectedWordsLayout;
        private List<string> availableWords;
        private List<string> selectedWords;
        private Dictionary<string, BoxView> placeholders;
        private Button validateButton;

        public MainPage()
        {
            availableWords = new List<string> { "Hello", "world", "this", "is", "MAUI" };
            selectedWords = new List<string>();
            placeholders = new Dictionary<string, BoxView>();

            availableWordsLayout = new StackLayout
            {
                Orientation = StackOrientation.Horizontal,
                HorizontalOptions = LayoutOptions.Center,
                VerticalOptions = LayoutOptions.Start,
                Spacing = 10
            };

            selectedWordsLayout = new StackLayout
            {
                Orientation = StackOrientation.Horizontal,
                HorizontalOptions = LayoutOptions.Center,
                VerticalOptions = LayoutOptions.End,
                Spacing = 10
            };

            validateButton = new Button
            {
                Text = "Validate Sentence",
                FontSize = 18,
                HorizontalOptions = LayoutOptions.Center,
                VerticalOptions = LayoutOptions.End,
                IsVisible = false
            };
            validateButton.Clicked += OnValidateButtonClicked;

            UpdateWordButtons();

            Content = new StackLayout
            {
                Children = { availableWordsLayout, selectedWordsLayout, validateButton }
            };
        }

        private void UpdateWordButtons()
        {
            availableWordsLayout.Children.Clear();
            selectedWordsLayout.Children.Clear();

            foreach (var word in availableWords)
            {
                var button = new Button
                {
                    Text = word,
                    FontSize = 18
                };
                button.Clicked += OnAvailableWordClicked;
                availableWordsLayout.Children.Add(button);

                if (!placeholders.ContainsKey(word))
                {
                    var placeholder = new BoxView
                    {
                        Color = Colors.Gray,
                        WidthRequest = 80,
                        HeightRequest = 40,
                        HorizontalOptions = LayoutOptions.Center,
                        VerticalOptions = LayoutOptions.Center
                    };
                    placeholders[word] = placeholder;
                }
            }

            foreach (var word in selectedWords)
            {
                var button = new Button
                {
                    Text = word,
                    FontSize = 18
                };
                button.Clicked += OnSelectedWordClicked;
                selectedWordsLayout.Children.Add(button);
            }

            foreach (var word in placeholders.Keys)
            {
                if (!availableWords.Contains(word))
                {
                    availableWordsLayout.Children.Add(placeholders[word]);
                }
            }

            validateButton.IsVisible = selectedWords.Any();
        }

        private async void OnAvailableWordClicked(object sender, EventArgs e)
        {
            if (sender is Button button)
            {
                availableWords.Remove(button.Text);
                selectedWords.Add(button.Text);
                await AnimateButton(button, availableWordsLayout, selectedWordsLayout);
                UpdateWordButtons();
            }
        }

        private async void OnSelectedWordClicked(object sender, EventArgs e)
        {
            if (sender is Button button)
            {
                selectedWords.Remove(button.Text);
                availableWords.Add(button.Text);
                await AnimateButton(button, selectedWordsLayout, availableWordsLayout);
                UpdateWordButtons();
            }
        }

        private async Task AnimateButton(Button button, Layout fromLayout, Layout toLayout)
        {
            var initialPosition = button.Bounds;
            fromLayout.Children.Remove(button);
            toLayout.Children.Add(button);

            var finalPosition = button.Bounds;
            button.TranslationX = initialPosition.X - finalPosition.X;
            button.TranslationY = initialPosition.Y - finalPosition.Y;

            await button.TranslateTo(0, 0, 500, Easing.CubicInOut);
        }

        private void OnValidateButtonClicked(object sender, EventArgs e)
        {
            if (selectedWords.SequenceEqual(availableWords))
            {
                DisplayAlert("Correct!", "You have formed the correct sentence.", "OK");
            }
            else
            {
                DisplayAlert("Incorrect", "The sentence is not correct. Try again.", "OK");
            }
        }
    }
}

Explanation

  • Validation Button: A button that appears when at least one word is selected.
  • Validation Logic: Checks if the selected words match the original list of available words.
  • UpdateWordButtons Method: Updates the visibility of the validation button based on the selection.

This setup will allow users to validate their sentence and receive feedback on whether it is correct.

Convert to a component

Let’s create a reusable .NET MAUI component where availableWords is a parameter. This component will encapsulate the functionality and allow you to pass different sets of words.

Here’s how you can define the component:

  1. Create a new class for the component: This class will inherit from ContentView.
  2. Define the availableWords parameter: Use a bindable property to allow setting the words from outside the component.
  3. Implement the component logic: Include the UI and interaction logic within the component.

Here’s the code for the SentenceBuilder component:

using Microsoft.Maui.Controls;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace MAUISentenceBuilder
{
    public class SentenceBuilder : ContentView
    {
        public static readonly BindableProperty AvailableWordsProperty =
            BindableProperty.Create(nameof(AvailableWords), typeof(List<string>), typeof(SentenceBuilder), new List<string>(), propertyChanged: OnAvailableWordsChanged);

        public List<string> AvailableWords
        {
            get => (List<string>)GetValue(AvailableWordsProperty);
            set => SetValue(AvailableWordsProperty, value);
        }

        private StackLayout availableWordsLayout;
        private StackLayout selectedWordsLayout;
        private List<string> selectedWords;
        private Dictionary<string, BoxView> placeholders;
        private Button validateButton;

        public SentenceBuilder()
        {
            selectedWords = new List<string>();
            placeholders = new Dictionary<string, BoxView>();

            availableWordsLayout = new StackLayout
            {
                Orientation = StackOrientation.Horizontal,
                HorizontalOptions = LayoutOptions.Center,
                VerticalOptions = LayoutOptions.Start,
                Spacing = 10
            };

            selectedWordsLayout = new StackLayout
            {
                Orientation = StackOrientation.Horizontal,
                HorizontalOptions = LayoutOptions.Center,
                VerticalOptions = LayoutOptions.End,
                Spacing = 10
            };

            validateButton = new Button
            {
                Text = "Validate Sentence",
                FontSize = 18,
                HorizontalOptions = LayoutOptions.Center,
                VerticalOptions = LayoutOptions.End,
                IsVisible = false
            };
            validateButton.Clicked += OnValidateButtonClicked;

            Content = new StackLayout
            {
                Children = { availableWordsLayout, selectedWordsLayout, validateButton }
            };

            UpdateWordButtons();
        }

        private static void OnAvailableWordsChanged(BindableObject bindable, object oldValue, object newValue)
        {
            var control = (SentenceBuilder)bindable;
            control.UpdateWordButtons();
        }

        private void UpdateWordButtons()
        {
            availableWordsLayout.Children.Clear();
            selectedWordsLayout.Children.Clear();

            foreach (var word in AvailableWords)
            {
                var button = new Button
                {
                    Text = word,
                    FontSize = 18
                };
                button.Clicked += OnAvailableWordClicked;
                availableWordsLayout.Children.Add(button);

                if (!placeholders.ContainsKey(word))
                {
                    var placeholder = new BoxView
                    {
                        Color = Colors.Gray,
                        WidthRequest = 80,
                        HeightRequest = 40,
                        HorizontalOptions = LayoutOptions.Center,
                        VerticalOptions = LayoutOptions.Center
                    };
                    placeholders[word] = placeholder;
                }
            }

            foreach (var word in selectedWords)
            {
                var button = new Button
                {
                    Text = word,
                    FontSize = 18
                };
                button.Clicked += OnSelectedWordClicked;
                selectedWordsLayout.Children.Add(button);
            }

            foreach (var word in placeholders.Keys)
            {
                if (!AvailableWords.Contains(word))
                {
                    availableWordsLayout.Children.Add(placeholders[word]);
                }
            }

            validateButton.IsVisible = selectedWords.Any();
        }

        private async void OnAvailableWordClicked(object sender, EventArgs e)
        {
            if (sender is Button button)
            {
                AvailableWords.Remove(button.Text);
                selectedWords.Add(button.Text);
                await AnimateButton(button, availableWordsLayout, selectedWordsLayout);
                UpdateWordButtons();
            }
        }

        private async void OnSelectedWordClicked(object sender, EventArgs e)
        {
            if (sender is Button button)
            {
                selectedWords.Remove(button.Text);
                AvailableWords.Add(button.Text);
                await AnimateButton(button, selectedWordsLayout, availableWordsLayout);
                UpdateWordButtons();
            }
        }

        private async Task AnimateButton(Button button, Layout fromLayout, Layout toLayout)
        {
            var initialPosition = button.Bounds;
            fromLayout.Children.Remove(button);
            toLayout.Children.Add(button);

            var finalPosition = button.Bounds;
            button.TranslationX = initialPosition.X - finalPosition.X;
            button.TranslationY = initialPosition.Y - finalPosition.Y;

            await button.TranslateTo(0, 0, 500, Easing.CubicInOut);
        }

        private void OnValidateButtonClicked(object sender, EventArgs e)
        {
            if (selectedWords.SequenceEqual(AvailableWords))
            {
                //DisplayAlert("Correct!", "You have formed the correct sentence.", "OK");
            }
            else
            {
                //DisplayAlert("Incorrect", "The sentence is not correct. Try again.", "OK");
            }
        }
    }
}

Explanation

  • Bindable PropertyAvailableWords is defined as a bindable property to allow setting it from outside the component.
  • Component Logic: The logic for handling word selection, deselection, and validation is encapsulated within the SentenceBuilder class.
  • UI Layout: The component’s UI is defined within the ContentView.

Usage Example:

To use this component in your main page, you can set the AvailableWords property like this:

using Microsoft.Maui.Controls;

namespace MAUISentenceBuilder
{
    public class MainPage : ContentPage
    {
        public MainPage()
        {
            var sentenceBuilder = new SentenceBuilder
            {
                AvailableWords = new List<string> { "Hello", "world", "this", "is", "MAUI" }
            };

            Content = new StackLayout
            {
                Children = { sentenceBuilder }
            };
        }
    }
}

This setup makes the SentenceBuilder component reusable with different sets of words.

Add events

So, from the component is not possible to use DisplayAlert. Now, I want to change the code in this way. OnValidateButtonClicked has to raise an event to return true or false and remove the DisplayAlert.

Let’s modify the SentenceBuilder component to raise an event instead of displaying an alert. This event will return true if the sentence is correct and false otherwise.

Here’s the updated SentenceBuilder component:

using Microsoft.Maui.Controls;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace MAUISentenceBuilder
{
    public class SentenceBuilder : ContentView
    {
        public static readonly BindableProperty AvailableWordsProperty =
            BindableProperty.Create(nameof(AvailableWords), typeof(List<string>), typeof(SentenceBuilder), new List<string>(), propertyChanged: OnAvailableWordsChanged);

        public List<string> AvailableWords
        {
            get => (List<string>)GetValue(AvailableWordsProperty);
            set => SetValue(AvailableWordsProperty, value);
        }

        public event EventHandler<bool> SentenceValidated;

        private StackLayout availableWordsLayout;
        private StackLayout selectedWordsLayout;
        private List<string> selectedWords;
        private Dictionary<string, BoxView> placeholders;
        private Button validateButton;

        public SentenceBuilder()
        {
            selectedWords = new List<string>();
            placeholders = new Dictionary<string, BoxView>();

            availableWordsLayout = new StackLayout
            {
                Orientation = StackOrientation.Horizontal,
                HorizontalOptions = LayoutOptions.Center,
                VerticalOptions = LayoutOptions.Start,
                Spacing = 10
            };

            selectedWordsLayout = new StackLayout
            {
                Orientation = StackOrientation.Horizontal,
                HorizontalOptions = LayoutOptions.Center,
                VerticalOptions = LayoutOptions.End,
                Spacing = 10
            };

            validateButton = new Button
            {
                Text = "Validate Sentence",
                FontSize = 18,
                HorizontalOptions = LayoutOptions.Center,
                VerticalOptions = LayoutOptions.End,
                IsVisible = false
            };
            validateButton.Clicked += OnValidateButtonClicked;

            Content = new StackLayout
            {
                Children = { availableWordsLayout, selectedWordsLayout, validateButton }
            };

            UpdateWordButtons();
        }

        private static void OnAvailableWordsChanged(BindableObject bindable, object oldValue, object newValue)
        {
            var control = (SentenceBuilder)bindable;
            control.UpdateWordButtons();
        }

        private void UpdateWordButtons()
        {
            availableWordsLayout.Children.Clear();
            selectedWordsLayout.Children.Clear();

            foreach (var word in AvailableWords)
            {
                var button = new Button
                {
                    Text = word,
                    FontSize = 18
                };
                button.Clicked += OnAvailableWordClicked;
                availableWordsLayout.Children.Add(button);

                if (!placeholders.ContainsKey(word))
                {
                    var placeholder = new BoxView
                    {
                        Color = Colors.Gray,
                        WidthRequest = 80,
                        HeightRequest = 40,
                        HorizontalOptions = LayoutOptions.Center,
                        VerticalOptions = LayoutOptions.Center
                    };
                    placeholders[word] = placeholder;
                }
            }

            foreach (var word in selectedWords)
            {
                var button = new Button
                {
                    Text = word,
                    FontSize = 18
                };
                button.Clicked += OnSelectedWordClicked;
                selectedWordsLayout.Children.Add(button);
            }

            foreach (var word in placeholders.Keys)
            {
                if (!AvailableWords.Contains(word))
                {
                    availableWordsLayout.Children.Add(placeholders[word]);
                }
            }

            validateButton.IsVisible = selectedWords.Any();
        }

        private async void OnAvailableWordClicked(object sender, EventArgs e)
        {
            if (sender is Button button)
            {
                AvailableWords.Remove(button.Text);
                selectedWords.Add(button.Text);
                await AnimateButton(button, availableWordsLayout, selectedWordsLayout);
                UpdateWordButtons();
            }
        }

        private async void OnSelectedWordClicked(object sender, EventArgs e)
        {
            if (sender is Button button)
            {
                selectedWords.Remove(button.Text);
                AvailableWords.Add(button.Text);
                await AnimateButton(button, selectedWordsLayout, availableWordsLayout);
                UpdateWordButtons();
            }
        }

        private async Task AnimateButton(Button button, Layout fromLayout, Layout toLayout)
        {
            var initialPosition = button.Bounds;
            fromLayout.Children.Remove(button);
            toLayout.Children.Add(button);

            var finalPosition = button.Bounds;
            button.TranslationX = initialPosition.X - finalPosition.X;
            button.TranslationY = initialPosition.Y - finalPosition.Y;

            await button.TranslateTo(0, 0, 500, Easing.CubicInOut);
        }

        private void OnValidateButtonClicked(object sender, EventArgs e)
        {
            bool isCorrect = selectedWords.SequenceEqual(AvailableWords);
            SentenceValidated?.Invoke(this, isCorrect);
        }
    }
}

Explanation

  • Event DeclarationSentenceValidated event is declared to notify when the sentence is validated.
  • Event Invocation: The OnValidateButtonClicked method raises the SentenceValidated event with true if the sentence is correct and false otherwise.

Usage Example

To use this component and handle the validation event on your main page:

using Microsoft.Maui.Controls;

namespace MAUISentenceBuilder
{
    public class MainPage : ContentPage
    {
        public MainPage()
        {
            var sentenceBuilder = new SentenceBuilder
            {
                AvailableWords = new List<string> { "Hello", "world", "this", "is", "MAUI" }
            };
            sentenceBuilder.SentenceValidated += OnSentenceValidated;

            Content = new StackLayout
            {
                Children = { sentenceBuilder }
            };
        }

        private void OnSentenceValidated(object sender, bool isCorrect)
        {
            if (isCorrect)
            {
                DisplayAlert("Correct!", "You have formed the correct sentence.", "OK");
            }
            else
            {
                DisplayAlert("Incorrect", "The sentence is not correct. Try again.", "OK");
            }
        }
    }
}

This setup makes the SentenceBuilder component reusable and allows you to handle the validation result on your main page.

Add more properties to the component

The next step is to add some properties to the component such as the color of the button and placeholder, the font and the text size.

using Microsoft.Maui.Controls;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace MAUISentenceBuilder
{
    public class SentenceBuilder : ContentView
    {
        public static readonly BindableProperty AvailableWordsProperty =
            BindableProperty.Create(nameof(AvailableWords), typeof(List<string>), typeof(SentenceBuilder), new List<string>(), propertyChanged: OnAvailableWordsChanged);

        public static readonly BindableProperty ButtonColorProperty =
            BindableProperty.Create(nameof(ButtonColor), typeof(Color), typeof(SentenceBuilder), Colors.Blue);

        public static readonly BindableProperty PlaceholderColorProperty =
            BindableProperty.Create(nameof(PlaceholderColor), typeof(Color), typeof(SentenceBuilder), Colors.Gray);

        public static readonly BindableProperty FontFamilyProperty =
            BindableProperty.Create(nameof(FontFamily), typeof(string), typeof(SentenceBuilder), "Arial");

        public static readonly BindableProperty TextSizeProperty =
            BindableProperty.Create(nameof(TextSize), typeof(double), typeof(SentenceBuilder), 18.0);

        public List<string> AvailableWords
        {
            get => (List<string>)GetValue(AvailableWordsProperty);
            set => SetValue(AvailableWordsProperty, value);
        }

        public Color ButtonColor
        {
            get => (Color)GetValue(ButtonColorProperty);
            set => SetValue(ButtonColorProperty, value);
        }

        public Color PlaceholderColor
        {
            get => (Color)GetValue(PlaceholderColorProperty);
            set => SetValue(PlaceholderColorProperty, value);
        }

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

        public double TextSize
        {
            get => (double)GetValue(TextSizeProperty);
            set => SetValue(TextSizeProperty, value);
        }

        public event EventHandler<bool> SentenceValidated;

        private StackLayout availableWordsLayout;
        private StackLayout selectedWordsLayout;
        private List<string> selectedWords;
        private Dictionary<string, BoxView> placeholders;
        private Button validateButton;

        public SentenceBuilder()
        {
            selectedWords = new List<string>();
            placeholders = new Dictionary<string, BoxView>();

            availableWordsLayout = new StackLayout
            {
                Orientation = StackOrientation.Horizontal,
                HorizontalOptions = LayoutOptions.Center,
                VerticalOptions = LayoutOptions.Start,
                Spacing = 10
            };

            selectedWordsLayout = new StackLayout
            {
                Orientation = StackOrientation.Horizontal,
                HorizontalOptions = LayoutOptions.Center,
                VerticalOptions = LayoutOptions.End,
                Spacing = 10
            };

            validateButton = new Button
            {
                Text = "Validate Sentence",
                FontSize = TextSize,
                FontFamily = FontFamily,
                BackgroundColor = ButtonColor,
                HorizontalOptions = LayoutOptions.Center,
                VerticalOptions = LayoutOptions.End,
                IsVisible = false
            };
            validateButton.Clicked += OnValidateButtonClicked;

            Content = new StackLayout
            {
                Children = { availableWordsLayout, selectedWordsLayout, validateButton }
            };

            UpdateWordButtons();
        }

        private static void OnAvailableWordsChanged(BindableObject bindable, object oldValue, object newValue)
        {
            var control = (SentenceBuilder)bindable;
            control.UpdateWordButtons();
        }

        private void UpdateWordButtons()
        {
            availableWordsLayout.Children.Clear();
            selectedWordsLayout.Children.Clear();

            foreach (var word in AvailableWords)
            {
                var button = new Button
                {
                    Text = word,
                    FontSize = TextSize,
                    FontFamily = FontFamily,
                    BackgroundColor = ButtonColor
                };
                button.Clicked += OnAvailableWordClicked;
                availableWordsLayout.Children.Add(button);

                if (!placeholders.ContainsKey(word))
                {
                    var placeholder = new BoxView
                    {
                        Color = PlaceholderColor,
                        WidthRequest = 80,
                        HeightRequest = 40,
                        HorizontalOptions = LayoutOptions.Center,
                        VerticalOptions = LayoutOptions.Center,
                        IsVisible = false
                    };
                    placeholders[word] = placeholder;
                }

                availableWordsLayout.Children.Add(placeholders[word]);
            }

            foreach (var word in selectedWords)
            {
                var button = new Button
                {
                    Text = word,
                    FontSize = TextSize,
                    FontFamily = FontFamily,
                    BackgroundColor = ButtonColor
                };
                button.Clicked += OnSelectedWordClicked;
                selectedWordsLayout.Children.Add(button);
            }

            validateButton.IsVisible = selectedWords.Any();
        }

        private async void OnAvailableWordClicked(object sender, EventArgs e)
        {
            if (sender is Button button)
            {
                placeholders[button.Text].IsVisible = true;
                AvailableWords.Remove(button.Text);
                selectedWords.Add(button.Text);
                await AnimateButton(button, availableWordsLayout, selectedWordsLayout);
                UpdateWordButtons();
            }
        }

        private async void OnSelectedWordClicked(object sender, EventArgs e)
        {
            if (sender is Button button)
            {
                placeholders[button.Text].IsVisible = false;
                selectedWords.Remove(button.Text);
                AvailableWords.Add(button.Text);
                await AnimateButton(button, selectedWordsLayout, availableWordsLayout);
                UpdateWordButtons();
            }
        }

        private async Task AnimateButton(Button button, Layout fromLayout, Layout toLayout)
        {
            var initialPosition = button.Bounds;
            fromLayout.Children.Remove(button);
            toLayout.Children.Add(button);

            var finalPosition = button.Bounds;
            button.TranslationX = initialPosition.X - finalPosition.X;
            button.TranslationY = initialPosition.Y - finalPosition.Y;

            await button.TranslateTo(0, 0, 500, Easing.CubicInOut);
        }

        private void OnValidateButtonClicked(object sender, EventArgs e)
        {
            bool isCorrect = selectedWords.SequenceEqual(AvailableWords);
            SentenceValidated?.Invoke(this, isCorrect);
        }
    }
}

Explanation

  • Bindable Properties: Added properties for ButtonColorPlaceholderColorFontFamily, and TextSize with default values.
  • Customization: The button and placeholder colors, font family, and text size can now be customized through these properties.

Usage Example

To use this component and customize the appearance of your main page.

using Microsoft.Maui.Controls;

namespace MAUISentenceBuilder
{
    public class MainPage : ContentPage
    {
        public MainPage()
        {
            var sentenceBuilder = new SentenceBuilder
            {
                AvailableWords = new List<string> { "Hello", "world", "this", "is", "MAUI" },
                ButtonColor = Colors.Green,
                PlaceholderColor = Colors.LightGray,
                FontFamily = "Helvetica",
                TextSize = 20
            };
            sentenceBuilder.SentenceValidated += OnSentenceValidated;

            Content = new StackLayout
            {
                Children = { sentenceBuilder }
            };
        }

        private void OnSentenceValidated(object sender, bool isCorrect)
        {
            if (isCorrect)
            {
                DisplayAlert("Correct!", "You have formed the correct sentence.", "OK");
            }
            else
            {
                DisplayAlert("Incorrect", "The sentence is not correct. Try again.", "OK");
            }
        }
    }
}

Enable the validation from the MainPage

Now, I like to have an event when it is possible to validate a sentence. The validation can be triggered from the MainPage. Let’s add an event that notifies when it is possible to validate a sentence. This event will be triggered whenever the selected words list is updated, allowing the main page to handle the validation logic.

using Microsoft.Maui.Controls;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace MAUISentenceBuilder
{
    public class SentenceBuilder : ContentView
    {
        public static readonly BindableProperty AvailableWordsProperty =
            BindableProperty.Create(nameof(AvailableWords), typeof(List<string>), typeof(SentenceBuilder), new List<string>(), propertyChanged: OnAvailableWordsChanged);

        public static readonly BindableProperty ButtonColorProperty =
            BindableProperty.Create(nameof(ButtonColor), typeof(Color), typeof(SentenceBuilder), Colors.Blue);

        public static readonly BindableProperty PlaceholderColorProperty =
            BindableProperty.Create(nameof(PlaceholderColor), typeof(Color), typeof(SentenceBuilder), Colors.Gray);

        public static readonly BindableProperty FontFamilyProperty =
            BindableProperty.Create(nameof(FontFamily), typeof(string), typeof(SentenceBuilder), "Arial");

        public static readonly BindableProperty TextSizeProperty =
            BindableProperty.Create(nameof(TextSize), typeof(double), typeof(SentenceBuilder), 18.0);

        public List<string> AvailableWords
        {
            get => (List<string>)GetValue(AvailableWordsProperty);
            set => SetValue(AvailableWordsProperty, value);
        }

        public Color ButtonColor
        {
            get => (Color)GetValue(ButtonColorProperty);
            set => SetValue(ButtonColorProperty, value);
        }

        public Color PlaceholderColor
        {
            get => (Color)GetValue(PlaceholderColorProperty);
            set => SetValue(PlaceholderColorProperty, value);
        }

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

        public double TextSize
        {
            get => (double)GetValue(TextSizeProperty);
            set => SetValue(TextSizeProperty, value);
        }

        public event EventHandler<bool> SentenceValidated;
        public event EventHandler CanValidateSentenceChanged;

        private StackLayout availableWordsLayout;
        private StackLayout selectedWordsLayout;
        private List<string> selectedWords;
        private Dictionary<string, BoxView> placeholders;
        private Button validateButton;

        public SentenceBuilder()
        {
            selectedWords = new List<string>();
            placeholders = new Dictionary<string, BoxView>();

            availableWordsLayout = new StackLayout
            {
                Orientation = StackOrientation.Horizontal,
                HorizontalOptions = LayoutOptions.Center,
                VerticalOptions = LayoutOptions.Start,
                Spacing = 10
            };

            selectedWordsLayout = new StackLayout
            {
                Orientation = StackOrientation.Horizontal,
                HorizontalOptions = LayoutOptions.Center,
                VerticalOptions = LayoutOptions.End,
                Spacing = 10
            };

            validateButton = new Button
            {
                Text = "Validate Sentence",
                FontSize = TextSize,
                FontFamily = FontFamily,
                BackgroundColor = ButtonColor,
                HorizontalOptions = LayoutOptions.Center,
                VerticalOptions = LayoutOptions.End,
                IsVisible = false
            };
            validateButton.Clicked += OnValidateButtonClicked;

            Content = new StackLayout
            {
                Children = { availableWordsLayout, selectedWordsLayout, validateButton }
            };

            UpdateWordButtons();
        }

        private static void OnAvailableWordsChanged(BindableObject bindable, object oldValue, object newValue)
        {
            var control = (SentenceBuilder)bindable;
            control.UpdateWordButtons();
        }

        private void UpdateWordButtons()
        {
            availableWordsLayout.Children.Clear();
            selectedWordsLayout.Children.Clear();

            foreach (var word in AvailableWords)
            {
                var button = new Button
                {
                    Text = word,
                    FontSize = TextSize,
                    FontFamily = FontFamily,
                    BackgroundColor = ButtonColor
                };
                button.Clicked += OnAvailableWordClicked;
                availableWordsLayout.Children.Add(button);

                if (!placeholders.ContainsKey(word))
                {
                    var placeholder = new BoxView
                    {
                        Color = PlaceholderColor,
                        WidthRequest = 80,
                        HeightRequest = 40,
                        HorizontalOptions = LayoutOptions.Center,
                        VerticalOptions = LayoutOptions.Center,
                        IsVisible = false
                    };
                    placeholders[word] = placeholder;
                }

                availableWordsLayout.Children.Add(placeholders[word]);
            }

            foreach (var word in selectedWords)
            {
                var button = new Button
                {
                    Text = word,
                    FontSize = TextSize,
                    FontFamily = FontFamily,
                    BackgroundColor = ButtonColor
                };
                button.Clicked += OnSelectedWordClicked;
                selectedWordsLayout.Children.Add(button);
            }

            validateButton.IsVisible = selectedWords.Any();
            CanValidateSentenceChanged?.Invoke(this, EventArgs.Empty);
        }

        private async void OnAvailableWordClicked(object sender, EventArgs e)
        {
            if (sender is Button button)
            {
                placeholders[button.Text].IsVisible = true;
                AvailableWords.Remove(button.Text);
                selectedWords.Add(button.Text);
                await AnimateButton(button, availableWordsLayout, selectedWordsLayout);
                UpdateWordButtons();
            }
        }

        private async void OnSelectedWordClicked(object sender, EventArgs e)
        {
            if (sender is Button button)
            {
                placeholders[button.Text].IsVisible = false;
                selectedWords.Remove(button.Text);
                AvailableWords.Add(button.Text);
                await AnimateButton(button, selectedWordsLayout, availableWordsLayout);
                UpdateWordButtons();
            }
        }

        private async Task AnimateButton(Button button, Layout fromLayout, Layout toLayout)
        {
            var initialPosition = button.Bounds;
            fromLayout.Children.Remove(button);
            toLayout.Children.Add(button);

            var finalPosition = button.Bounds;
            button.TranslationX = initialPosition.X - finalPosition.X;
            button.TranslationY = initialPosition.Y - finalPosition.Y;

            await button.TranslateTo(0, 0, 500, Easing.CubicInOut);
        }

        private void OnValidateButtonClicked(object sender, EventArgs e)
        {
            bool isCorrect = selectedWords.SequenceEqual(AvailableWords);
            SentenceValidated?.Invoke(this, isCorrect);
        }
    }
}

Explanation

  • CanValidateSentenceChanged Event: This event is triggered whenever the selected words list is updated, indicating that the sentence can be validated.
  • UpdateWordButtons Method: Invokes the CanValidateSentenceChanged event to notify the main page.

Usage Example

To use this component and handle the validation event on your main page:

using Microsoft.Maui.Controls;

namespace MAUISentenceBuilder
{
    public class MainPage : ContentPage
    {
        private SentenceBuilder sentenceBuilder;

        public MainPage()
        {
            sentenceBuilder = new SentenceBuilder
            {
                AvailableWords = new List<string> { "Hello", "world", "this", "is", "MAUI" },
                ButtonColor = Colors.Green,
                PlaceholderColor = Colors.LightGray,
                FontFamily = "Helvetica",
                TextSize = 20
            };
            sentenceBuilder.SentenceValidated += OnSentenceValidated;
            sentenceBuilder.CanValidateSentenceChanged += OnCanValidateSentenceChanged;

            var validateButton = new Button
            {
                Text = "Validate",
                FontSize = 18,
                IsVisible = false
            };
            validateButton.Clicked += (sender, e) => sentenceBuilder.OnValidateButtonClicked(sender, e);

            Content = new StackLayout
            {
                Children = { sentenceBuilder, validateButton }
            };
        }

        private void OnSentenceValidated(object sender, bool isCorrect)
        {
            if (isCorrect)
            {
                DisplayAlert("Correct!", "You have formed the correct sentence.", "OK");
            }
            else
            {
                DisplayAlert("Incorrect", "The sentence is not correct. Try again.", "OK");
            }
        }

        private void OnCanValidateSentenceChanged(object sender, EventArgs e)
        {
            var validateButton = (Button)((StackLayout)Content).Children.Last();
            validateButton.IsVisible = sentenceBuilder.SelectedWords.Any();
        }
    }
}

Add property for animation duration

Now, I can customize the animation duration for button transitions. Let’s add a bindable property to the SentenceBuilder component to allow customization of the animation duration.

using Microsoft.Maui.Controls;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace MAUISentenceBuilder
{
    public class SentenceBuilder : ContentView
    {
        public static readonly BindableProperty AvailableWordsProperty =
            BindableProperty.Create(nameof(AvailableWords), typeof(List<string>), typeof(SentenceBuilder), new List<string>(), propertyChanged: OnAvailableWordsChanged);

        public static readonly BindableProperty ButtonColorProperty =
            BindableProperty.Create(nameof(ButtonColor), typeof(Color), typeof(SentenceBuilder), Colors.Blue);

        public static readonly BindableProperty PlaceholderColorProperty =
            BindableProperty.Create(nameof(PlaceholderColor), typeof(Color), typeof(SentenceBuilder), Colors.Gray);

        public static readonly BindableProperty FontFamilyProperty =
            BindableProperty.Create(nameof(FontFamily), typeof(string), typeof(SentenceBuilder), "Arial");

        public static readonly BindableProperty TextSizeProperty =
            BindableProperty.Create(nameof(TextSize), typeof(double), typeof(SentenceBuilder), 18.0);

        public static readonly BindableProperty AnimationDurationProperty =
            BindableProperty.Create(nameof(AnimationDuration), typeof(uint), typeof(SentenceBuilder), 500u);

        public List<string> AvailableWords
        {
            get => (List<string>)GetValue(AvailableWordsProperty);
            set => SetValue(AvailableWordsProperty, value);
        }

        public Color ButtonColor
        {
            get => (Color)GetValue(ButtonColorProperty);
            set => SetValue(ButtonColorProperty, value);
        }

        public Color PlaceholderColor
        {
            get => (Color)GetValue(PlaceholderColorProperty);
            set => SetValue(PlaceholderColorProperty, value);
        }

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

        public double TextSize
        {
            get => (double)GetValue(TextSizeProperty);
            set => SetValue(TextSizeProperty, value);
        }

        public uint AnimationDuration
        {
            get => (uint)GetValue(AnimationDurationProperty);
            set => SetValue(AnimationDurationProperty, value);
        }

        public List<string> SelectedWords => selectedWords;

        public event EventHandler<bool> SentenceValidated;
        public event EventHandler CanValidateSentenceChanged;

        private StackLayout availableWordsLayout;
        private StackLayout selectedWordsLayout;
        private List<string> selectedWords;
        private Dictionary<string, BoxView> placeholders;
        private Button validateButton;

        public SentenceBuilder()
        {
            selectedWords = new List<string>();
            placeholders = new Dictionary<string, BoxView>();

            availableWordsLayout = new StackLayout
            {
                Orientation = StackOrientation.Horizontal,
                HorizontalOptions = LayoutOptions.Center,
                VerticalOptions = LayoutOptions.Start,
                Spacing = 10
            };

            selectedWordsLayout = new StackLayout
            {
                Orientation = StackOrientation.Horizontal,
                HorizontalOptions = LayoutOptions.Center,
                VerticalOptions = LayoutOptions.End,
                Spacing = 10
            };

            validateButton = new Button
            {
                Text = "Validate Sentence",
                FontSize = TextSize,
                FontFamily = FontFamily,
                BackgroundColor = ButtonColor,
                HorizontalOptions = LayoutOptions.Center,
                VerticalOptions = LayoutOptions.End,
                IsVisible = false
            };
            validateButton.Clicked += OnValidateButtonClicked;

            Content = new StackLayout
            {
                Children = { availableWordsLayout, selectedWordsLayout, validateButton }
            };

            UpdateWordButtons();
        }

        private static void OnAvailableWordsChanged(BindableObject bindable, object oldValue, object newValue)
        {
            var control = (SentenceBuilder)bindable;
            control.UpdateWordButtons();
        }

        private void UpdateWordButtons()
        {
            availableWordsLayout.Children.Clear();
            selectedWordsLayout.Children.Clear();

            foreach (var word in AvailableWords)
            {
                var button = new Button
                {
                    Text = word,
                    FontSize = TextSize,
                    FontFamily = FontFamily,
                    BackgroundColor = ButtonColor
                };
                button.Clicked += OnAvailableWordClicked;
                availableWordsLayout.Children.Add(button);

                if (!placeholders.ContainsKey(word))
                {
                    var placeholder = new BoxView
                    {
                        Color = PlaceholderColor,
                        WidthRequest = 80,
                        HeightRequest = 40,
                        HorizontalOptions = LayoutOptions.Center,
                        VerticalOptions = LayoutOptions.Center,
                        IsVisible = false
                    };
                    placeholders[word] = placeholder;
                }

                availableWordsLayout.Children.Add(placeholders[word]);
            }

            foreach (var word in selectedWords)
            {
                var button = new Button
                {
                    Text = word,
                    FontSize = TextSize,
                    FontFamily = FontFamily,
                    BackgroundColor = ButtonColor
                };
                button.Clicked += OnSelectedWordClicked;
                selectedWordsLayout.Children.Add(button);
            }

            validateButton.IsVisible = selectedWords.Any();
            CanValidateSentenceChanged?.Invoke(this, EventArgs.Empty);
        }

        private async void OnAvailableWordClicked(object sender, EventArgs e)
        {
            if (sender is Button button)
            {
                placeholders[button.Text].IsVisible = true;
                AvailableWords.Remove(button.Text);
                selectedWords.Add(button.Text);
                await AnimateButton(button, availableWordsLayout, selectedWordsLayout);
                UpdateWordButtons();
            }
        }

        private async void OnSelectedWordClicked(object sender, EventArgs e)
        {
            if (sender is Button button)
            {
                placeholders[button.Text].IsVisible = false;
                selectedWords.Remove(button.Text);
                AvailableWords.Add(button.Text);
                await AnimateButton(button, selectedWordsLayout, availableWordsLayout);
                UpdateWordButtons();
            }
        }

        private async Task AnimateButton(Button button, Layout fromLayout, Layout toLayout)
        {
            var initialPosition = button.Bounds;
            fromLayout.Children.Remove(button);
            toLayout.Children.Add(button);

            var finalPosition = button.Bounds;
            button.TranslationX = initialPosition.X - finalPosition.X;
            button.TranslationY = initialPosition.Y - finalPosition.Y;

            await button.TranslateTo(0, 0, AnimationDuration, Easing.CubicInOut);
        }

        private void OnValidateButtonClicked(object sender, EventArgs e)
        {
            bool isCorrect = selectedWords.SequenceEqual(AvailableWords);
            SentenceValidated?.Invoke(this, isCorrect);
        }
    }
}

Explanation

  • AnimationDuration Property: Added a bindable property AnimationDuration with a default value of 500 milliseconds.
  • AnimateButton Method: Uses the AnimationDuration property to control the duration of the button transition animation.

Usage Example

To use this component and customize the animation duration in your main page.

using Microsoft.Maui.Controls;

namespace MAUISentenceBuilder
{
    public class MainPage : ContentPage
    {
        private SentenceBuilder sentenceBuilder;

        public MainPage()
        {
            sentenceBuilder = new SentenceBuilder
            {
                AvailableWords = new List<string> { "Hello", "world", "this", "is", "MAUI" },
                ButtonColor = Colors.Green,
                PlaceholderColor = Colors.LightGray,
                FontFamily = "Helvetica",
                TextSize = 20,
                AnimationDuration = 1000 // Custom animation duration in milliseconds
            };
            sentenceBuilder.SentenceValidated += OnSentenceValidated;
            sentenceBuilder.CanValidateSentenceChanged += OnCanValidateSentenceChanged;

            var validateButton = new Button
            {
                Text = "Validate",
                FontSize = 18,
                IsVisible = false
            };
            validateButton.Clicked += (sender, e) => sentenceBuilder.OnValidateButtonClicked(sender, e);

            Content = new StackLayout
            {
                Children = { sentenceBuilder, validateButton }
            };
        }

        private void OnSentenceValidated(object sender, bool isCorrect)
        {
            if (isCorrect)
            {
                DisplayAlert("Correct!", "You have formed the correct sentence.", "OK");
            }
            else
            {
                DisplayAlert("Incorrect", "The sentence is not correct. Try again.", "OK");
            }
        }

        private void OnCanValidateSentenceChanged(object sender, EventArgs e)
        {
            var validateButton = (Button)((StackLayout)Content).Children.Last();
            validateButton.IsVisible = sentenceBuilder.SelectedWords.Any();
        }
    }
}

Explanation:

  • AnimationDuration Property: Set the AnimationDuration property to customize the duration of the button transition animation.

This setup allows you to customize the animation duration for button transitions while maintaining default values.

Add Accessible feature to the component

Handling accessibility features in your SentenceBuilder component is crucial to ensure that it is usable by everyone, including those with disabilities. Here are some key steps and considerations to make your component more accessible:

1. Semantic Elements

Use semantic elements and properties to ensure that assistive technologies can interpret your UI correctly.

  • Labels and Descriptions: Use the AutomationProperties class to set labels and descriptions for buttons and other interactive elements.
  • Content Descriptions: Provide meaningful content descriptions for non-text elements like images or icons.

2. Keyboard Navigation

Ensure that all interactive elements can be navigated using the keyboard.

  • Tab Order: Set the TabIndex property to control the order in which elements receive focus.
  • Focus Visuals: Ensure that focused elements are visually distinguishable.

3. Accessible Names and Roles

Assign accessible names and roles to UI elements.

  • AutomationProperties.Name: Set this property to provide a name for the element that can be read by screen readers.
  • AutomationProperties.HelpText: Use this property to provide additional context or instructions.

4. Touch Target Size

Ensure that touch targets are large enough to be easily tapped.

  • Minimum Size: Follow guidelines for minimum touch target sizes (e.g., 48×48 dp).

5. Color Contrast

Ensure sufficient color contrast between text and background.

  • Contrast Ratios: Use tools to check that your color contrast meets accessibility standards (e.g., WCAG 2.1).

6. Dynamic Updates

Notify assistive technologies of dynamic content changes.

  • Live Regions: Use AutomationProperties.LiveSetting to inform screen readers about updates to dynamic content.

7. Testing

Regularly test your component with accessibility tools.

  • Screen Readers: Test with screen readers like NVDA, JAWS, or VoiceOver.
  • Accessibility Insights: Use tools like Accessibility Insights for automated testing.

Example Implementation

Here’s an example of how you can implement some of these features in your SentenceBuilder component.

private void UpdateWordButtons()
{
    availableWordsLayout.Children.Clear();
    selectedWordsLayout.Children.Clear();

    foreach (var word in AvailableWords)
    {
        var button = new Button
        {
            Text = word,
            FontSize = TextSize,
            FontFamily = FontFamily,
            BackgroundColor = ButtonColor
        };
        button.Clicked += OnAvailableWordClicked;
        button.SetValue(SemanticProperties.DescriptionProperty, word);
        button.SetValue(SemanticProperties.HintProperty, $"Button for {word}");
        availableWordsLayout.Children.Add(button);

        if (!placeholders.ContainsKey(word))
        {
            var placeholder = new BoxView
            {
                Color = PlaceholderColor,
                WidthRequest = 80,
                HeightRequest = 40,
                HorizontalOptions = LayoutOptions.Center,
                VerticalOptions = LayoutOptions.Center,
                IsVisible = false
            };
            placeholders[word] = placeholder;
        }

        availableWordsLayout.Children.Add(placeholders[word]);
    }

    foreach (var word in selectedWords)
    {
        var button = new Button
        {
            Text = word,
            FontSize = TextSize,
            FontFamily = FontFamily,
            BackgroundColor = ButtonColor
        };
        button.Clicked += OnSelectedWordClicked;
        button.SetValue(SemanticProperties.DescriptionProperty, word);
        button.SetValue(SemanticProperties.HintProperty, $"Selected button for {word}");
        selectedWordsLayout.Children.Add(button);
    }

    validateButton.IsVisible = selectedWords.Any();
    CanValidateSentenceChanged?.Invoke(this, EventArgs.Empty);
}

Explanation

  • AutomationProperties: Added AutomationProperties.Name and AutomationProperties.HelpText to buttons for better screen reader support.
  • Focus Visuals: Ensure that buttons have clear focus visuals.
  • Touch Target Size: Ensure buttons are large enough to be easily tapped.

Wrap up

I hope this code can help you and give you an example of an MAUI component. Please keep in contact if you have any questions or improvements.

Related posts

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.