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:
- Set up the project: ensure you have a .NET MAUI project set up in Visual Studio.
- Create the UI: Define a
StackLayout
for the words and aLabel
to display the formed sentence. - Handle the word taps: Use
Button
controls for the words and handle theirClicked
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;
}
}
}
}

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 thesentenceLabel
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.
- Define the UI: We’ll have two
StackLayout
s, one for the available words and one for the selected words. - 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
StackLayout
s: 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 Handlers:
OnAvailableWordClicked
moves a word from the available list to the selected list, andOnSelectedWordClicked
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:
- Define the animations: We’ll use
TranslateTo
for moving the buttons. - 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:
- Add a validation button: The button will be initially hidden and will appear when at least one word is selected.
- 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:
- Create a new class for the component: This class will inherit from
ContentView
. - Define the
availableWords
parameter: Use a bindable property to allow setting the words from outside the component. - 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 Property:
AvailableWords
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 Declaration:
SentenceValidated
event is declared to notify when the sentence is validated. - Event Invocation: The
OnValidateButtonClicked
method raises theSentenceValidated
event withtrue
if the sentence is correct andfalse
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
ButtonColor
,PlaceholderColor
,FontFamily
, andTextSize
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
andAutomationProperties.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.