Привязки к TextBox в MVVM
191WinRT --- Привязки к TextBox в MVVM
Одним из главных преимуществ отделения бизнес-логики с помощью шаблона MVVM является возможность полной переработки пользовательского интерфейса без изменения модели представления. Предположим, ваша программа позволяет выбирать цвета по аналогии с приложением ColorScroll, рассмотренным ранее, но цветовые составляющие вводятся в полях TextBox. Работать с такой программой будет не очень удобно, но возможно.
Следующий проект использует тот же класс RgbViewModel, что и ранее. Кроме того, файл отделенного кода содержит такой же конструктор, как в том проекте:
public MainPage()
{
this.InitializeComponent();
this.DataContext = new RgbViewModel();
// Инициализация цветом выделения
(this.DataContext as RgbViewModel).Color =
new UISettings().UIElementColor(UIElementType.Highlight);
}
Файл XAML создает экземпляры трех элементов управления TextBox и определяет привязки данных между свойствами Red, Green и Blue объекта RgbViewModel:
<Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid Grid.Column="0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Text="R"
Grid.Row="0"
Grid.Column="0" />
<TextBox Text="{Binding Red, Mode=TwoWay}"
Grid.Row="0"
Grid.Column="1" />
<TextBlock Text="G"
Grid.Row="1"
Grid.Column="0" />
<TextBox Text="{Binding Green, Mode=TwoWay}"
Grid.Row="1"
Grid.Column="1" />
<TextBlock Text="B"
Grid.Row="2"
Grid.Column="0" />
<TextBox Text="{Binding Blue, Mode=TwoWay}"
Grid.Row="2"
Grid.Column="1" />
</Grid>
<!-- Результат -->
<Rectangle Grid.Column="3"
Grid.Row="0"
Grid.RowSpan="3">
<Rectangle.Fill>
<SolidColorBrush
Color="{Binding Color}" />
</Rectangle.Fill>
</Rectangle>
</Grid>
При запуске программы элементы управления TextBox инициализируются значениями цветовых составляющих, а все необходимые преобразования данных выполняются незаметно.
Теперь попробуйте прикоснуться к элементу управления TextBox и ввести другое число. Ничего не происходит. Теперь прикоснитесь к другому элементу TextBox или нажмите клавишу Tab, чтобы передать фокус ввода. Ага! Теперь число, введенное в первом поле TextBox, принимается и используется для обновления цвета.
Экспериментируя с этой программой, вы увидите, что Windows Runtime весьма снисходительно относится к вводу букв и знаков в текстовых полях и не выдает исключения, но любое введенное значение регистрируется только при потере фокуса ввода полем TextBox.
Такое поведение реализовано намеренно. Допустим, модель представления, связанная с TextBox, использует модель для обновления базы данных через сетевое подключение. Когда пользователь вводит текст в поле TextBox, он может совершать ошибки и стирать символы; подумайте, должно ли каждое изменение передаваться по сети? По этой причине ввод пользователя в TextBox считается завершенным и готовым к обработке только тогда, когда TextBox теряет фокус ввода.
К сожалению, в настоящее время изменить это поведение невозможно. Также нет возможности включить проверку данных в привязку. Если такое поведение привязки TextBox для вас неприемлемо и вам не хочется дублировать логику TextBox в собственном элементе управления, остается только один вариант: отказаться от привязок и использовать обработчик события TextChanged.
Одно из возможных решений продемонстрировано в следующем проекте. В нем тоже используется класс RgbViewModel. Файл XAML похож на файл из предыдущего проекта, не считая того, что элементы управления TextBox теперь обладают именами и им назначены обработчики TextChanged:
<TextBox Name="redTextBox"
Grid.Row="0"
Grid.Column="1"
Text="0"
TextChanged="OnTextBoxTextChanged" />
...
<TextBox Name="greenTextBox"
Grid.Row="1"
Grid.Column="1"
Text="0"
TextChanged="OnTextBoxTextChanged" />
...
<TextBox Name="blueTextBox"
Grid.Row="2"
Grid.Column="1"
Text="0"
TextChanged="OnTextBoxTextChanged" />
Впрочем, элемент Rectangle содержит те же привязки, что и в предыдущих программах.
Так как мы заменяем двусторонние привязки, нам понадобятся не только обработчики событий для элементов TextBox, но и обработчик события PropertyChanged для объекта RgbViewModel. Организовать обновление TextBox при изменении свойства модели представления несложно, но я также решил добавить проверку текста, введенного пользователем:
using System.ComponentModel;
using Windows.UI;
using Windows.UI.ViewManagement;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media;
namespace WinRTTestApp
{
public sealed partial class MainPage : Page
{
RgbViewModel rgbViewModel;
Brush textBoxTextBrush;
Brush textBoxErrorBrush = new SolidColorBrush(Colors.Red);
public MainPage()
{
this.InitializeComponent();
// Получение кисти для поля TextBox
textBoxTextBrush = this.Resources["TextBoxForegroundThemeBrush"] as SolidColorBrush;
// Создание экземпляра RgbViewModel и сохранение его в поле
rgbViewModel = new RgbViewModel();
rgbViewModel.PropertyChanged += OnRgbViewModelPropertyChanged;
this.DataContext = rgbViewModel;
// Инициализация цветом выделения
rgbViewModel.Color = new UISettings().UIElementColor(UIElementType.Highlight);
}
private void OnRgbViewModelPropertyChanged(object sender, PropertyChangedEventArgs args)
{
switch (args.PropertyName)
{
case "Red":
redTextBox.Text = rgbViewModel.Red.ToString("F0");
break;
case "Green":
greenTextBox.Text = rgbViewModel.Green.ToString("F0");
break;
case "Blue":
blueTextBox.Text = rgbViewModel.Blue.ToString("F0");
break;
}
}
private void OnTextBoxTextChanged(object sender, TextChangedEventArgs args)
{
byte value;
if (sender == redTextBox && Validate(redTextBox, out value))
rgbViewModel.Red = value;
if (sender == greenTextBox && Validate(greenTextBox, out value))
rgbViewModel.Green = value;
if (sender == blueTextBox && Validate(blueTextBox, out value))
rgbViewModel.Blue = value;
}
private bool Validate(TextBox txtbox, out byte value)
{
bool valid = byte.TryParse(txtbox.Text, out value);
txtbox.Foreground = valid ? textBoxTextBrush : textBoxErrorBrush;
return valid;
}
}
}
Метод Validate() использует стандартный метод TryParse() для преобразования текста в значение byte. Если преобразование проходит успешно, то модель представления обновляется, а если нет - текст выводится красным шрифтом, привлекающим внимание пользователя к проблеме.
Такое решение хорошо работает и в том случае, если числа вводятся с начальными пробелами или нулями. Предположим, вы ввели 0 в первом поле TextBox. Это действительное значение типа byte, поэтому свойство Red объекта RgbViewModel обновляется введенным значением; это приводит к срабатыванию метода PropertyChanged, и в поле TextBox заносится значение «0» типа Text. Все нормально. Теперь введите цифру 5. Поле TextBox содержит строку «05». Метод TryParse считает введенное значение действительным строковым представлением для типа byte, а свойство Red обновляется значением 5.
Теперь обработчик PropertyChanged задает свойству Text поля TextBox строковое значение «5», заменяя «05». Но позиция курсора не изменяется, поэтому теперь он располагается перед цифрой 5, а не после нее.
Возможно, лучшим решением этой проблемы будет игнорирование событий PropertyChanged при задании свойства модели представления в обработчике TextChanged.
Задача решается простой установкой флага:
bool blockViewModelUpdates;
// ...
private void OnRgbViewModelPropertyChanged(object sender, PropertyChangedEventArgs args)
{
if (blockViewModelUpdates)
return;
// ...
}
private void OnTextBoxTextChanged(object sender, TextChangedEventArgs args)
{
blockViewModelUpdates = true;
// ...
blockViewModelUpdates = false;
}
В некоторых ситуациях проверку ввода данных лучше организовать под юрисдикцией модели представления (а не представления).