Как не потерять обязательные обновления исходного кода? - PullRequest
11 голосов
/ 13 января 2011

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

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

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

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

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

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

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

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

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

Ответы [ 6 ]

3 голосов
/ 13 января 2011

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

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

Редактировать:

ОК, поскольку вы этого не сделаетеЧтобы взломать, мы должны столкнуться с ужасной правдой:

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

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

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

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

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

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

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

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

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

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

0 голосов
/ 17 января 2011

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

РЕДАКТИРОВАНИЕ: Там у нас есть одна служебная команда (например, конвертер), которая требует знания о конкретном представлении.Эта команда может быть повторно использована для любого диалога с той же ошибкой.И вы добавите эту функциональность / хак только в том случае, если эта ошибка существует, и ВМ будет понятна.

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

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

<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>

Там используется специальная прокси-команда, которая получает элемент (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;
        }
    }
}

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

0 голосов
/ 16 января 2011

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

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

0 голосов
/ 15 января 2011

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

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

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

0 голосов
/ 15 января 2011

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

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

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

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

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

РЕДАКТИРОВАТЬ # 2: Что касается ваших двух случаев, почему вы не всегда можете использовать PropertyChanged:

  1. Что именно вы делаете с StringFormat? Во всей своей работе с пользовательским интерфейсом до сих пор я использую StringFormat для переформатирования данных, которые я получаю из ViewModel. Однако я не уверен, как должно работать использование StringFormat с данными, которые затем снова редактируются пользователем. Я предполагаю, что вы хотите отформатировать текст для отображения, а затем «отформатировать» текст, который пользователь вводит для дальнейшей обработки в вашей ViewModel. Из вашего описания кажется, что он не всегда «не отформатирован» правильно.
    1. Откройте ошибку Connect, если она не работает должным образом.
    2. Напишите ваш собственный ValueConverter, который вы используете в привязке.
    3. Имейте отдельное свойство с последним «действительным» значением и используйте это значение в вашей ViewModel; обновляйте его только после получения другого «действительного» значения из свойства, которое вы используете в привязке данных.
  2. Если у вас есть длительный установщик свойств (например, шаг «проверки»), я бы выполнял длительную часть в отдельном методе (геттеры и сеттеры обычно должны быть относительно «быстрыми»). Затем запустите этот метод в рабочем потоке / threadpool / BackgroundWorker (сделайте его прерываемым, чтобы вы могли перезапустить его с новым значением, как только пользователь введет больше данных) или аналогичным.
0 голосов
/ 15 января 2011

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

  1. Представление является ответственным, потому что оно устанавливает IsDefault в true и учитывает эту «проблему»
  2. ViewModel не должен никоим образом нести ответственность за исправление этого, он может вводить зависимости от VM до V и, следовательно, нарушать шаблон.
  3. Без добавления кода (C #) в View все, что вы можете сделать, это либо изменить привязки (например, UpdateSourceTrigger = PropertyChanged), либо добавить код в базовый класс Button. В базовом классе кнопки вы можете сместить фокус на кнопку перед выполнением команды. Все еще хакерский, но более чистый, чем добавление кода в виртуальную машину.

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...