Привязки к TextBox в MVVM

191

Одним из главных преимуществ отделения бизнес-логики с помощью шаблона 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 инициализируются значениями цветовых составляющих, а все необходимые преобразования данных выполняются незаметно.

Измененная программа ColorScroll с текстовыми полями 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;
}

В некоторых ситуациях проверку ввода данных лучше организовать под юрисдикцией модели представления (а не представления).

Пройди тесты
Лучший чат для C# программистов