Как не потерять обновления источника привязки?

Предположим, у меня есть модальное диалоговое окно с текстовым полем и кнопками ОК/Отмена. И он построен на MVVM - т.е. Имеет объект ViewModel со строковым свойством, к которому привязано текстовое поле.

Скажем, я ввожу текст в текстовое поле, а затем возьмусь за мышь и нажмите "ОК". Все работает нормально: в момент щелчка текстовое поле теряет фокус, что заставляет механизм привязки обновлять свойство ViewModel. Я получаю свои данные, все счастливы.

Теперь предположим, что я не использую мышь. Вместо этого я просто нажал Enter на клавиатуре. Это также приводит к тому, что кнопка "ОК" соответствует "click", так как она отмечена как IsDefault="True". Но угадайте, что? В этом случае текстовое поле не теряет фокус, и поэтому механизм привязки остается невинно невежественным, и я не получаю свои данные. Dang!

Другой вариант одного сценария: предположим, что у меня есть форма ввода данных прямо в главном окне, введите в него некоторые данные, а затем нажмите Ctrl+S для "Сохранить". Угадай, что? Моя последняя запись не сохраняется!

Это может быть несколько устранено с помощью UpdateSourceTrigger=PropertyChanged, но это не всегда возможно.

Одним очевидным случаем может быть использование StringFormat со связыванием - текст продолжает прыгать обратно в "форматированное" состояние, когда я пытаюсь ввести его.

И еще один случай, с которым я столкнулся сам, - это когда у меня есть некоторая трудоемкая обработка в средстве определения свойств viewmodel, и я только хочу выполнить его, когда пользователь "выполнил" ввод текста.

Это похоже на вечную проблему: я помню, как пытался систематически ее решать давным-давно, с тех пор как я начал работать с интерактивными интерфейсами, но я никогда не преуспевал. Раньше я всегда использовал какие-то хаки - например, добавляя метод "EnsureDataSaved" каждому "ведущему" (как в "MVP" ) и вызывал его в "критических" точках или что-то в этом роде...

Но со всеми классными технологиями, а также пустой шумихой WPF, я ожидал, что они придумают хорошее решение.

Ответ 1

В критических точках вы можете принудительно привязать привязку к вашей модели представления:

var textBox = Keyboard.FocusedElement as TextBox;
BindingOperations.GetBindingExpression(textBox, TextBox.TextProperty).UpdateSource();

Edit:

Хорошо, так как вам не нужны хаки, нам приходится сталкиваться с уродливой правдой:

  • Чтобы реализовать чистое представление, свойства, выставленные вашей моделью просмотра, должны быть дружественными к частым обновлениям привязки.

Аналогией, которую мы можем использовать, является текстовый редактор. Если приложение было гигантским текстовым полем, привязанным к файлу на диске, каждое нажатие клавиши привело бы к записи всего файла. Даже концепция экономии не нужна. Это извратно, но ужасно неэффективно. Мы все сразу видим, что модели представления необходимо выставить буфер для привязки к представлению, и это повторно вводит концепцию сохранения и принудительного управления состоянием в нашей модели представления.

Тем не менее, мы видим, что это все еще недостаточно эффективно. Для файлов среднего размера накладные расходы на обновление буфера всего файла при каждом нажатии клавиши становятся невыносимыми. Затем мы показываем команды в нашей модели представления для эффективного управления буфером, никогда не обменивая весь буфер с представлением.

Итак, мы заключаем, что для достижения эффективности и оперативности с использованием чистого MVVM нам необходимо выставить эффективную модель представления. Это означает, что все текстовые поля могут привязываться к свойствам без каких-либо вредоносных эффектов. Но, это также означает, что вам нужно нажать состояние вниз в модель представления, чтобы справиться с этим. И это нормально, потому что модель представления не является моделью; это задача - обрабатывать потребности представления.

Верно, что мы можем быстро прототипировать пользовательские интерфейсы, воспользовавшись ярлыками, такими как привязка к изменениям фокусировки. Но привязка к изменениям фокусировки может иметь негативные последствия в реальных приложениях, и если это так, мы просто не должны ее использовать.

Какая альтернатива? Выставляйте свойство, удобное для частых обновлений. Назовите это так же, как было вызвано старое неэффективное свойство. Реализуйте свойство fast, используя свойство slow с логикой, которая зависит от состояния модели представления. Модель просмотра получает команду save. Он знает, было ли быстрое свойство перенесено в свойство slow. Он может решить, когда и где медленное свойство будет синхронизировано с моделью.

Но вы говорите, разве мы не перетащили хак из вида в модель представления? Нет, мы потеряли некоторую элегантность и простоту, но вернемся к аналогии с текстовым редактором. У нас есть, чтобы решить проблему, и это задача модели представления для ее решения.

Если мы хотим использовать чистый MVVM, и мы хотим эффективности и отзывчивости, то хромые эвристики, например, позволяют избежать обновления источника привязки, пока элемент не потеряет фокус, не поможет. Они вводят столько проблем, сколько они решают. В этом случае мы должны позволить модели представления выполнить свою работу, даже если это означает сложность добавления.

Предполагая, что мы его принимаем, как мы можем справиться со сложностью? Мы можем реализовать общий класс утилиты оболочки для буферизации свойства slow и позволить модели представления подключать методы get и set. Наш класс утилиты может автоматически регистрироваться для сохранения командных событий, чтобы уменьшить количество шаблонов кода в нашей модели просмотра.

Если мы сделаем это правильно, все части модели представления, которые были бы достаточно быстры, чтобы использоваться с измененной привязкой свойства, все равно будут одинаковыми, а другие, которые были бы достойны задавать вопрос "Является ли это тоже свойство медленный?" будет содержать небольшой код для решения проблемы, а представление не будет более разумным.

Ответ 2

Это сложный вопрос, и я согласен с тем, что решение, не требующее взлома и более или менее свободного кода, должно быть найдено. Вот мои мысли:

  • Ответ отвечает за то, что он задает значение IsDefault true и разрешает эту "проблему"
  • ViewModel не должен быть ответственным, чтобы исправить это, он может вводить зависимости от VM к V и тем самым нарушать шаблон.
  • Без добавления (С#) кода в View все, что вы можете сделать, это либо сменить привязки (например, UpdateSourceTrigger = PropertyChanged), либо добавить код в базовый класс Button. В базовом классе кнопки вы можете переключить фокус на кнопку перед выполнением команды. Все еще взломанный, но более чистый, чем добавление кода в виртуальную машину.

Итак, в настоящий момент единственные "хорошие" решения, которые я вижу, требуют от разработчиков представления придерживаться правила; установите привязку определенным образом или используйте специальную кнопку.

Ответ 3

Проблема заключается в том, что текст TextBox имеет триггер источника по умолчанию для LostFocus вместо PropertyChanged. IMHO это был неправильный выбор по умолчанию, так как он довольно неожиданен и может вызвать всевозможные проблемы (например, те, которые вы описываете).

  • Простейшим решением было бы всегда явно использовать UpdateSourceTrigger = PropertyChanged (как предложено другим).
  • Если это невозможно (по какой-либо причине), я бы обработал события Unloaded, Closing или Closed и вручную обновил привязку (как показано Риком).

К сожалению, кажется, что некоторые сценарии по-прежнему немного проблематичны с помощью TextBox, поэтому необходимы некоторые обходные пути. Например, см. мой вопрос. Возможно, вы захотите открыть ошибку Connect (или два) с вашими конкретными проблемами.

EDIT: Нажав Ctrl + S с фокусом на TextBox, я бы сказал, что поведение верное. В конце концов, вы выполняете команду. Это не имеет никакого отношения к текущему (клавиатурному) фокусу. Команда может даже зависеть от сфокусированного элемента! Вы не нажимаете на кнопку или подобное, что приведет к изменению фокуса (однако, в зависимости от кнопки, она может запускать ту же команду, что и раньше).

Итак, если вы хотите только обновить связанный текст, когда теряете фокус из TextBox, но в то же время вы хотите запустить команду с новейшим содержимым TextBox (т.е. изменения без потери фокуса), это не соответствует. Поэтому либо вы должны изменить привязку к PropertyChanged, либо вручную обновить привязку.

ИЗМЕНИТЬ № 2: Что касается ваших двух случаев, почему вы не всегда можете использовать PropertyChanged:

  • Что именно вы делаете с StringFormat? Во всем моем пользовательском интерфейсе я до сих пор использую StringFormat для переформатирования данных, которые я получаю из ViewModel. Тем не менее, я не уверен, что использование StringFormat с данными, которые затем снова редактируются пользователем, должно работать. Я предполагаю, что вы хотите отформатировать текст для отображения, а затем "неформатизировать" текст, который пользователь вводит для дальнейшей обработки в вашей ViewModel. Из вашего описания кажется, что он не "неформатирован" правильно все время.
    • Откройте ошибку подключения, если она не работает должным образом.
    • Напишите свой собственный ValueConverter, который вы используете в привязке.
    • Имейте отдельное свойство с последним "действительным" значением и используйте это значение в своей ViewModel; обновите его только после получения другого "действительного" значения из свойства, которое вы используете в привязке данных.
  • Если у вас есть давний набор свойств (например, "проверка" ), я бы использовал длинную часть в отдельном методе (геттеры и сеттеры обычно должны быть относительно "быстрыми" ). Затем запустите этот метод в рабочем потоке /threadpool/BackgroundWorker (сделайте его прерывистым, чтобы вы могли перезапустить его с новым значением, когда пользователь вводит больше данных) или тому подобное.

Ответ 4

Я бы добавил обработчик события Click для кнопки по умолчанию. Обработчик события кнопки выполняется до вызова команды, поэтому привязки данных можно обновить, изменив фокус в обработчике событий.

private void Button_Click(object sender, RoutedEventArgs e) {
    ((Control)sender).Focus();
}

Однако, я не знаю, можно ли использовать аналогичный подход с другими ключами shorcut.

Ответ 5

Да, у меня довольно много опыта. У WPF и Silverlight все еще есть свои области боли. MVVM не решает все это; это не волшебная пуля, и поддержка в рамках системы становится все лучше, но все еще не хватает. Например, я все еще нахожу редактирование глубоких детских коллекций проблемой.

В настоящий момент я обрабатываю эти ситуации в каждом конкретном случае, потому что многое зависит от того, как должен работать индивидуальный вид. Это то, что я провожу большую часть своего времени, потому что Я генерирую много сантехники, используя T4, поэтому у меня есть время для этих причуд.

Ответ 6

Что вы думаете о прокси-команде и KeyBinding для клавиши ENTER?

Редакция: Там у нас есть одна команда утилиты (например, конвертер), которая требует знания конкретного вида. Эта команда может быть повторно использована для любого диалога с той же ошибкой. И вы добавляете эту функциональность/взломать только в поле зрения, где эта ошибка существует, и VM будет ясной.

VM создает, чтобы адаптировать бизнес для просмотра и должна предоставлять определенные функции, такие как преобразование данных, команды пользовательского интерфейса, дополнительные/вспомогательные поля, уведомления и хаки/обходные пути. И если у нас есть утечки между уровнями в MVVM, у нас есть проблемы с: высокой связностью, повторным использованием кода, модульным тестированием для VM, болевым кодом.

Использование в xaml (без IsDefault на кнопке):

<Window.Resources>
    <model:ButtonProxyCommand x:Key="proxyCommand"/>
</Window.Resources>

<Window.InputBindings>
    <KeyBinding Key="Enter"
          Command="{Binding Source={StaticResource proxyCommand}, Path=Instance}" 
          CommandParameter="{Binding ElementName=_okBtn}"/>
</Window.InputBindings>
<StackPanel>
    <TextBox>
        <TextBox.Text>
            <Binding Path="Text"></Binding>
        </TextBox.Text>
    </TextBox>
    <Button Name="_okBtn" Command="{Binding Command}">Ok</Button>
</StackPanel>

Здесь используется специальная команда proxy, которая получает элемент (CommandParameter) для перемещения фокуса и выполнения. Но для этого класса требуется ButtonBase для CommandParameter:

public class ButtonProxyCommand : ICommand
{
    public bool CanExecute(object parameter)
    {
        var btn = parameter as ButtonBase;

        if (btn == null || btn.Command == null)
            return false;

        return btn.Command.CanExecute(btn.CommandParameter);
    }

    public event EventHandler CanExecuteChanged;

    public void Execute(object parameter)
    {
        if (parameter == null)
            return;

        var btn = parameter as ButtonBase;

        if (btn == null || btn.Command == null)
            return;

        Action a = () => btn.Focus();
        var op = Dispatcher.CurrentDispatcher.BeginInvoke(a);

        op.Wait();
        btn.Command.Execute(btn.CommandParameter);
    }

    private static ButtonProxyCommand _instance = null;
    public static ButtonProxyCommand Instance
    {
        get
        {
            if (_instance == null)
                _instance = new ButtonProxyCommand();

            return _instance;
        }
    }
}

Это просто идея, а не полное решение.