У меня есть метод во ViewModel который обновляет данные каждую секунду с базы, получается когда я хочу изменить данные в TexBox данные которые я ввожу перебиваются каждую секунду, как запретить обновлять данные когда TexBox находится в фокусе??
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);
}
}
Я пойду по пути постановки таймера на паузу, другой вариант мне кажется алогичным. У меня работает такой простой пример:
Доменная модель данных из БД (у вас она есть своя):
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
Выглядит всё вот так:
Ещё одно решение по мотивам идеи с 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.
Результат:
Сборка персонального компьютера от Artline: умный выбор для современных пользователей