Standardizing TextChanged Event - lucyberryhub/WPF.Tutorial GitHub Wiki
In our initial approach, we repeatedly wrote similar logic for handling TextBox
input validation and updating related data. This led to messy, repetitive, and hard-to-maintain code. Letโs review the before scenario first.
private void BerryTextBox_TextChanged(object sender, TextChangedEventArgs e)
{
try
{
if (sender is TextBox textBox)
{
string inputText = textBox.Text.Trim();
// Validate the input
if (inputText == "Cherry" || inputText == "Berry")
{
// Reset background color
textBox.ClearValue(TextBox.BackgroundProperty);
// Update the berry list
var items = BerryDataGrid.ItemsSource as List<BerryModel>;
if (items != null)
{
BerryViewModel.UpdateBerryList(items);
}
}
else
{
// Invalid input: Turn the background pink!
textBox.Background = System.Windows.Media.Brushes.LightPink;
MessageBox.Show("Only 'Berry' or 'Cherry' are valid inputs!", "Error", MessageBoxButton.OK, MessageBoxImage.Warning);
}
}
}
catch (Exception ex)
{
MessageBox.Show($"Oops! An error occurred: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
- Repetition: Same logic for input validation and data updates is repeated in multiple handlers.
- Hard to Maintain: Any change to validation or update logic must be applied in multiple places.
- Error-Prone: Copy-pasting similar code increases the chance of mistakes.
By using a helper class, we can centralize the validation and update logic. This way, the code is clean, reusable, and easy to maintain. Now, letโs explain this step-by-step.
This class handles:
- Validation: Checks whether the input is valid.
-
Data Update: Dynamically invokes an action (
updateAction
) to update the data.
Hereโs the code:
public static class BerryHelper
{
/// <summary>
/// Validates the input and updates the berry list if valid.
/// </summary>
/// <typeparam name="T">The type of the model associated with the DataGrid.</typeparam>
/// <param name="textBox">The TextBox being validated.</param>
/// <param name="itemsSource">The collection bound to the DataGrid.</param>
/// <param name="updateAction">The action to perform when updating the data source.</param>
public static void HandleTextChanged<T>(TextBox textBox, IEnumerable<T> itemsSource, Action<IEnumerable<T>> updateAction)
{
if (textBox == null) return;
string inputText = textBox.Text.Trim();
// Validate input
if (inputText == "Berry" || inputText == "Cherry")
{
// Reset background color to default
textBox.ClearValue(TextBox.BackgroundProperty);
if (itemsSource != null)
{
// Update the data source
updateAction?.Invoke(itemsSource);
}
}
else
{
// Highlight invalid input
textBox.Background = System.Windows.Media.Brushes.LightPink;
}
}
}
-
Action<T>
: Represents a delegate that takes one parameter and returns no value. -
Action<IEnumerable<T>>
: A specialized delegate where the parameter is anIEnumerable<T>
(a collection of items like a list).
Using an Action<IEnumerable<T>>
allows us to decouple the logic of what happens after the data is validated. This way:
- Flexibility: You can pass any custom logic for updating the data source.
- Reusability: The same method can handle updates for different types of models or data.
Letโs dissect the code block:
if (itemsSource != null)
{
// Update the data source
updateAction?.Invoke(itemsSource);
}
-
itemsSource != null
:- This checks if the
ItemsSource
(i.e., the bound collection of theDataGrid
) exists and is not null. - Without this, you risk invoking an update on a null object, which will throw an exception.
- This checks if the
-
updateAction?.Invoke(itemsSource)
:- The
updateAction
is the delegate passed to the helper method. -
.Invoke(itemsSource)
executes the custom update logic, passing theitemsSource
as the parameter.
- The
-
Why the
?.
Operator?- This ensures that if
updateAction
is null (not provided), the method will skip this step without throwing aNullReferenceException
.
- This ensures that if
Hereโs how you might call the HandleTextChanged
method:
private void BerryTextBox_TextChanged(object sender, TextChangedEventArgs e)
{
try
{
if (sender is TextBox textBox)
{
// Retrieve the data source
var items = BerryDataGrid.ItemsSource as List<BerryModel>;
// Pass the update logic as a lambda
BerryHelper.HandleTextChanged(textBox, items, (updatedItems) =>
{
// Serialize and save the data to a JSON file
string json = Newtonsoft.Json.JsonConvert.SerializeObject(updatedItems, Newtonsoft.Json.Formatting.Indented);
File.WriteAllText("berries.json", json);
});
}
}
catch (Exception ex)
{
MessageBox.Show($"Oops! An error occurred: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
-
Flexibility:
- You can define different update logic for different scenarios or models.
- For example, updating JSON, saving to a database, or logging changes can all be passed as
updateAction
.
-
Readability:
- The main handler becomes cleaner because the core logic is encapsulated in the helper.
-
Error Handling:
- Centralized logic makes debugging easier if something goes wrong.
if (inputText == "Cherry" || inputText == "Berry")
{
textBox.ClearValue(TextBox.BackgroundProperty);
var items = BerryDataGrid.ItemsSource as List<BerryModel>;
if (items != null)
{
BerryViewModel.UpdateBerryList(items);
}
}
else
{
textBox.Background = System.Windows.Media.Brushes.LightPink;
}
BerryHelper.HandleTextChanged(textBox, items, (updatedItems) =>
{
string json = Newtonsoft.Json.JsonConvert.SerializeObject(updatedItems, Newtonsoft.Json.Formatting.Indented);
File.WriteAllText("berries.json", json);
});
By using a centralized helper with Action<IEnumerable<T>>
, weโve:
- Eliminated repetitive code.
- Made input handling flexible and reusable.
- Ensured better maintainability and scalability.
Now your TextChanged
handlers will be as juicy and fresh as your favorite berries! ๐๐