Search code examples
c#wpfdata-binding

In WPF, how can I have binding on LostFocus but validation on PropertyChanged?


I want to be able to bind a TextBox with the UpdateSourceTrigger set to LostFocus (the default) but to perform validation as the user types in the text. The best I can come up with is to handle the TextChanged event and call a validation method on the view model. I'm wondering if there's a better solution.

My view model listens to property changes in the model in order to update itself (including formatting). I don't want to bind with the UpdateSourceTrigger set to PropertyChanged because that causes the text to be formatted as soon as the user types (for example, the user might want to type "1.2" yet as soon as he/she types "1" the text changes to "1.0" because of the automatic formatting by the view model).


Solution

  • Elaborating on the comment I left, here's an example of how it can be done.

    FYI, I used the nuget package MvvmLight for the plumbing.

    MainWindow.xaml

    <StackPanel>
        <TextBox x:Name="myTextBox" Text="{Binding SomeNumberViewModel.IntermediateText, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" Width="100" Margin="5"/>
        <Button Content="Hi" HorizontalAlignment="Center" Padding="5,15" Margin="5"/>
    </StackPanel>
    

    MainWindow.xaml.cs

    public MainViewModel ViewModel
    {
        get
        {
            return this.DataContext as MainViewModel;
        }
    }
    public MainWindow()
    {
        InitializeComponent();
        this.myTextBox.LostFocus += MyTextBox_LostFocus;
    }
    
    private void MyTextBox_LostFocus(object sender, RoutedEventArgs e)
    {
        // If the IntermediateText has no validation errors, then update your model.
        if (string.IsNullOrEmpty(this.ViewModel.SomeNumberViewModel[nameof(this.ViewModel.SomeNumberViewModel.IntermediateText)]))
        {
            // Update your model and it gets formatted result
            this.ViewModel.SomeNumberViewModel.ModelValue = this.ViewModel.SomeNumberViewModel.IntermediateText;
    
            // Then, update your IntermediateText to update the UI.
            this.ViewModel.SomeNumberViewModel.IntermediateText = this.ViewModel.SomeNumberViewModel.ModelValue;
        }
    }
    

    MainViewModel.cs

    public class MainViewModel : ViewModelBase
    {
        private SomeNumberViewModel someNumberViewModel;
    
        public string MyTitle { get => "Stack Overflow Question 65279367"; }
    
        public SomeNumberViewModel SomeNumberViewModel
        {
            get
            {
                if (this.someNumberViewModel == null)
                    this.someNumberViewModel = new SomeNumberViewModel(new MyModel());
                return this.someNumberViewModel;
            }
        }
    }
    

    SomeNumberViewModel.cs

    public class SomeNumberViewModel : ViewModelBase, IDataErrorInfo
    {
        public SomeNumberViewModel(MyModel model)
        {
            this.Model = model;
        }
    
        private string intermediateText;
        public string IntermediateText { get => this.intermediateText; set { this.intermediateText = value; RaisePropertyChanged(); } }
        public string ModelValue 
        { 
            get => this.Model.SomeNumber.ToString("0.00"); 
            
            set 
            {
                try
                {
                    this.Model.SomeNumber = Convert.ToDouble(value);
                    RaisePropertyChanged();
                }
                catch
                {
                }
            } 
        }
    
        public MyModel Model { get; private set; }
        
        public string Error { get => null; }
    
        public string this[string columnName]
        {
            get
            {
                switch (columnName)
                {
                    case "IntermediateText":
                        if (!string.IsNullOrEmpty(this.IntermediateText) && FormatErrors(this.IntermediateText))
                            return "Format errors";
                        break;
                }
    
                return string.Empty;
            }
        }
    
        /// <summary>
        /// Only allow numbers to be \d+, or \d+\.\d+
        /// For Example: 1, 1.0, 11.23, etc.
        /// Anything else is a format violation.
        /// </summary>
        /// <param name="numberText"></param>
        /// <returns></returns>
        private bool FormatErrors(string numberText)
        {
            var valid = (Regex.IsMatch(numberText, @"^(\d+|\d+\.\d+)$"));
            return !valid;
        }
    }
    

    MyModel.cs

    public class MyModel
    {
        public double SomeNumber { get; set; }
    }