Как запретить обновления данных в WPF в TextBox?

441
04 мая 2017, 11:33

У меня есть метод во ViewModel который обновляет данные каждую секунду с базы, получается когда я хочу изменить данные в TexBox данные которые я ввожу перебиваются каждую секунду, как запретить обновлять данные когда TexBox находится в фокусе??

Answer 1

UPDATE: Более гибкое решение с использованием Behaviors:

public class TextBoxBehavior : Behavior<TextBox>
{
    private readonly DependencyProperty _targetProperty = TextBox.TextProperty;
    private BindingExpression _bindingExpression;
    private DateTime _keyDownRaisedAt;
    private bool _bindingCleared;
    protected override void OnAttached()
    {
        base.OnAttached();
        _bindingExpression = AssociatedObject.GetBindingExpression(_targetProperty);
        AssociatedObject.LostFocus += AssociatedObjectOnLostFocus;
        AssociatedObject.PreviewTextInput += AssociatedObjectOnPreviewTextInput;
        AssociatedObject.PreviewKeyDown += AssociatedObjectOnPreviewKeyDown;
    }
    private void AssociatedObjectOnPreviewKeyDown(object sender, KeyEventArgs keyEventArgs)
    {
        _keyDownRaisedAt = DateTime.Now;
    }
    private void AssociatedObjectOnLostFocus(object sender, RoutedEventArgs routedEventArgs)
    {    
        if (_bindingCleared && _bindingExpression != null)
        {
            string tmp = AssociatedObject.Text;
            AssociatedObject.SetBinding(_targetProperty, _bindingExpression.ParentBinding);
            _bindingCleared = false;
            AssociatedObject.SetCurrentValue(_targetProperty, tmp);
        }
    }
    private void AssociatedObjectOnPreviewTextInput(object sender, TextCompositionEventArgs textCompositionEventArgs)
    {
        if (!_bindingCleared && AssociatedObject.IsFocused && IsKeyDown())
        {
            BindingOperations.ClearBinding(AssociatedObject, _targetProperty);
            _bindingCleared = true;
        }
    }
    private bool IsKeyDown()
    {
        DateTime currentTime = DateTime.Now;
        return (currentTime - _keyDownRaisedAt) < TimeSpan.FromMilliseconds(10);
    }
    protected override void OnDetaching()
    {
        base.OnDetaching();
        AssociatedObject.PreviewTextInput -= AssociatedObjectOnPreviewTextInput;
        AssociatedObject.PreviewKeyDown -= AssociatedObjectOnPreviewKeyDown;
        AssociatedObject.LostFocus -= AssociatedObjectOnLostFocus;
    }
}

Для его подключения в XAML-разметке необходимо написать:

<TextBox Text="{Binding Path=Text, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
    <i:Interaction.Behaviors>
        <Behaviors:TextBoxBehavior />
    </i:Interaction.Behaviors>
</TextBox>

И не забыть так же соответствующее пространство имён:

xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"

Решение с кастомным TextBox:

public class CustomTextBox : TextBox
{
    private DateTime _keyDownRaisedAt;
    private BindingExpression _binding;
    private bool _bindingCleared;
    protected override void OnPreviewKeyDown(KeyEventArgs e)
    {
        _keyDownRaisedAt = DateTime.Now;
        base.OnPreviewKeyDown(e);
    }
    public override void EndInit()
    {
        _binding = this.GetBindingExpression(TextProperty);
        base.EndInit();
    }

    protected override void OnLostFocus(RoutedEventArgs e)
    {
        if (_bindingCleared)
        {
            var tmp = this.Text;
            SetBinding(TextProperty, _binding.ParentBinding);
            _bindingCleared = false;
            SetCurrentValue(TextProperty, tmp);
        }
        base.OnLostFocus(e);
    }
    protected override void OnPreviewTextInput(TextCompositionEventArgs e)
    {
        DateTime current = DateTime.Now;
        if (!_bindingCleared && IsFocused && (current - _keyDownRaisedAt) < TimeSpan.FromMilliseconds(10))
        {
            BindingOperations.ClearBinding(this, TextProperty);
            _bindingCleared = true;
        }
        base.OnPreviewTextInput(e);
    }
}
Answer 2

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

Доменная модель данных из БД (у вас она есть своя):

class UserModel
{
    public int Id { get; set; }
    public string Name { get; set; }
}

Набросок репозитория для получения экземпляров UserModel:

class UserRepository
{
    List<UserModel> users;
    public UserRepository()
    {
        users = new List<UserModel>
        {
            new UserModel { Id = 1, Name = "Иванов Иван" },
            new UserModel { Id = 2, Name = "Петров Петр" },
            new UserModel { Id = 3, Name = "Сидоров Сидор" },
            new UserModel { Id = 4, Name = "Антонов Антон" },
            new UserModel { Id = 5, Name = "Сергеев Сергей" }
        };
    }
    public UserModel GetUserByIndex(int index) => users[index];
    public UserModel GetUserById(int id) => users.First(u => u.Id == id);
    public int UsersCount => users.Count;
}

"Стандартные" (немного упрощенные) вспомогательные классы для MVVM, честно собранные на просторах SO/ruSO:

База для VM:

class VM : INotifyPropertyChanged
{
    protected void Set<T>(ref T field, T value, [CallerMemberName] string propertyName = "")
    {
        field = value;
        NotifyPropertyChanged(propertyName);
    }
    protected void NotifyPropertyChanged([CallerMemberName] string propertyName = "") =>
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    public event PropertyChangedEventHandler PropertyChanged;
}

Реализация ICommand:

class DelegateCommand : ICommand
{
    Action<object> execute;
    Predicate<object> canExecute = _ => true;
    public DelegateCommand(Action<object> execute)
    {
        if (execute == null) throw new NullReferenceException(nameof(execute));
        this.execute = execute;
    }
    public DelegateCommand(Action<object> execute, Predicate<object> canExecute)
        : this(execute)
    {
        if (canExecute == null) throw new NullReferenceException(nameof(canExecute));
        this.canExecute = canExecute;
    }
    public event EventHandler CanExecuteChanged;
    public bool CanExecute(object parameter) => canExecute(parameter);
    public void Execute(object parameter) => execute(parameter);
}

Основная VM:

class MainVM : VM
{
    UserRepository repo = new UserRepository();
    UserModel selectedUser;
    public UserModel SelectedUser
    {
        get { return selectedUser; }
        set { Set(ref selectedUser, value); }
    }
    DelegateCommand pauseCommand;
    public ICommand PauseCommand
    {
        get
        {
            if (pauseCommand == null)
                pauseCommand = new DelegateCommand(_ => PauseTimer());
            return pauseCommand;
        }
    }
    DelegateCommand playCommand;
    public ICommand PlayCommand
    {
        get
        {
            if (playCommand == null)
                playCommand = new DelegateCommand(_ => StartTimer());
            return playCommand;
        }
    }
    Timer timer = new Timer(1000);
    int currentIndex = -1;
    public MainVM()
    {
        timer.Elapsed += (o, e) => NextUser();
        timer.Start();
    }
    void NextUser()
    {
        currentIndex = (currentIndex + 1) % repo.UsersCount;
        SelectedUser = repo.GetUserByIndex(currentIndex);
    }
    void PauseTimer()
    {
        timer.Stop();
    }
    void StartTimer()
    {
        // Здесь надо обновить запись в БД:
        // repo.UpdateUser(SelectedUser);
        timer.Start();
    }
}

Разметка содержимого окна:

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>
        <RowDefinition/>
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="Auto"/>
        <ColumnDefinition/>
    </Grid.ColumnDefinitions>
    <TextBlock Margin="5,5,0,5" VerticalAlignment="Center"
               Text="{Binding SelectedUser.Id}"/>
    <TextBox Grid.Column="1"
             Margin="5" Padding="5"
             Text="{Binding SelectedUser.Name}">
        <i:Interaction.Triggers>
            <i:EventTrigger EventName="GotFocus">
                <i:InvokeCommandAction Command="{Binding PauseCommand}"/>
            </i:EventTrigger>
            <i:EventTrigger EventName="LostFocus">
                <i:InvokeCommandAction Command="{Binding PlayCommand}"/>
            </i:EventTrigger>
        </i:Interaction.Triggers>
    </TextBox>
    <Button Grid.ColumnSpan="2" Grid.Row="1"
            Margin="5" Padding="5"
            VerticalAlignment="Top" HorizontalAlignment="Center"
            Content="Click Me"/>
</Grid>

Здесь используется пространство имен xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity", не забудьте подключить NuGet-пакет System.Windows.Interactivity

Выглядит всё вот так:

Answer 3

Ещё одно решение по мотивам идеи с attached behaviour, которую продемонстрировал @Nikita.

public class NondisruptiveUpdateBehavior : Behavior<TextBox>
{
    readonly Binding binding;
    public NondisruptiveUpdateBehavior()
    {
        binding = new Binding(TextProperty.Name) { Source = this };
    }
    // dependency property, которое будет прикреплено к источнику
    public string Text
    {
        get => (string)GetValue(TextProperty);
        set => SetValue(TextProperty, value);
    }
    public static readonly DependencyProperty TextProperty =
        DependencyProperty.Register(
            "Text", typeof(string), typeof(NondisruptiveUpdateBehavior),
            new FrameworkPropertyMetadata(
                    null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
    protected override void OnAttached()
    {
        base.OnAttached();
        // подпишемся на изменение фокуса
        AssociatedObject.GotFocus += OnGotFocus;
        AssociatedObject.LostFocus += OnLostFocus;
        // если фокуса нет, установим привязку
        if (!AssociatedObject.IsFocused)
            Setup(false);
    }
    protected override void OnDetaching()
    {
        // если фокуса нет, уберём привязку
        if (!AssociatedObject.IsFocused)
            OnGotFocus(null, null);
        // и отпишемся от изменений фокуса
        AssociatedObject.LostFocus -= OnLostFocus;
        AssociatedObject.GotFocus -= OnGotFocus;
        base.OnDetaching();
    }
    void OnGotFocus(object sender, RoutedEventArgs e) => Clear();
    void OnLostFocus(object sender, RoutedEventArgs e) => Setup(true);
    void Clear()
    {
        string tmp = AssociatedObject.Text;
        // убрать привязку тексбокса к нашему свойству
        BindingOperations.ClearBinding(AssociatedObject, TextBox.TextProperty);
        // передать значение вверх
        AssociatedObject.Text = tmp;
    }
    void Setup(bool propagate)
    {
        string tmp = AssociatedObject.Text;
        // установить привязку тексбокса к нашему свойству
        BindingOperations.SetBinding(AssociatedObject, TextBox.TextProperty, binding);
        if (propagate)
        {
            // передать значение вниз
            SetCurrentValue(TextProperty, tmp);
            var expr = BindingOperations.GetBindingExpression(this, TextProperty);
            expr?.UpdateTarget(); // если нет привязки вниз, то и не нужно
        }
    }
}

Пользоваться так:

<TextBox xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity">
    <i:Interaction.Behaviors>
        <local:NondisruptiveUpdateBehavior Text="{Binding Value}"/>
    </i:Interaction.Behaviors>
</TextBox>

Не забудьте подключить сборку System.Windows.Interactivity.

Результат:

READ ALSO
Выбрать несколько полей в combocheckbox

Выбрать несколько полей в combocheckbox

ЗдравствуйтеОчень нужна ваша помощь

324
Как узнать, какой return сработал в unit test

Как узнать, какой return сработал в unit test

Есть метод, который добавляет нового пользователяЕсли пользователь добавлен - перенаправляет на index, иначе возвращает вьюшку с моделью

285
Unity - как написать &ldquo;после того как анимация закончится&rdquo;?

Unity - как написать “после того как анимация закончится”?

Unity, C#Есть молдель с animator, в котором уже настроены стейты-анимации и параметры для переходов между ними

595