Search code examples
uwpbindinguwp-xaml

How to work around UpdateSourceTrigger ignored in NumberBox


I have tried to set UpdateSourceTrigger to PropertyChanged on a data bound NumberBox in a UWP application.

My data form paradigm is to not show the Save button until there is a data change to actually save, but as the data source is not updated until focus leaves the control, the save button does not become available until after the user moves to a different input control first.

  • user cannot click on the save button because it is disabled, and so does not trigger a focus change.

This is the minimal example, if you use the spinners to change the value, the button is available, if you only type into the control you have to click the button twice, once to enable it (to commit the value change) and then once again, now that the button is available.

If UpdateSourceTrigger is not working, how can commit the change so the user can click the button the first time when it should be available, so before focus change?

<Page
    x:Class="App3.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:App3"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
    xmlns:muxc="using:Microsoft.UI.Xaml.Controls"
    mc:Ignorable="d"
    Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <Page.DataContext>
        <local:DataClass/>
    </Page.DataContext>
    <Grid>
        <StackPanel>
            <muxc:NumberBox Value="{Binding Value, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" SpinButtonPlacementMode="Inline"/>
            <Button IsEnabled="{Binding NotZero, Mode=OneWay}" Click="Button_Click">Click if Not Zero</Button>
        </StackPanel>
    </Grid>
</Page>
using System;
using System.ComponentModel;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;

// The Blank Page item template is documented at https://go.microsoft.com/fwlink/?LinkId=402352&clcid=0x409

namespace App3
{
    /// <summary>
    /// An empty page that can be used on its own or navigated to within a Frame.
    /// </summary>
    public sealed partial class MainPage : Page
    {
        public MainPage()
        {
            this.InitializeComponent();
        }

        public DataClass Model { get => this.DataContext as DataClass; }

        private async void Button_Click(object sender, RoutedEventArgs e)
        {
            var msg = new Windows.UI.Popups.MessageDialog($"Value: {Model.Value}");
            await msg.ShowAsync();
        }
    }

    public class DataClass : INotifyPropertyChanged
    {
        public double Value
        {
            get => _v;
            set
            {
                _v = value;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Value)));
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(NotZero)));
            }
        }
        private double _v;
        public bool NotZero { get => _v != 0; }
        public event PropertyChangedEventHandler PropertyChanged;
    }  
}

Solution

  • This is the default behavior of NumberBox, which is usually used to make some judgments on the content in NumberBox after input is completed.

    In the control style definition of NumberBox, the main body is TextBox. If you want to intervene in this process, you need to get this TextBox.

    1. Viusal method defined

    public static class StaticExtension
    {
        public static FrameworkElement VisualTreeFindName(this DependencyObject element, string name)
        {
            if (element == null || string.IsNullOrWhiteSpace(name))
            {
                return null;
            }
            if (name.Equals((element as FrameworkElement)?.Name, StringComparison.OrdinalIgnoreCase))
            {
                return element as FrameworkElement;
            }
            var childCount = VisualTreeHelper.GetChildrenCount(element);
            for (int i = 0; i < childCount; i++)
            {
                var result = VisualTreeHelper.GetChild(element, i).VisualTreeFindName(name);
                if (result != null)
                {
                    return result;
                }
            }
            return null;
        }
    }
    

    2. Attach TextChanged handler

    Xaml

    <muxc:NumberBox ... Loaded="NumberBox_Loaded"/>
    

    Xaml.cs

    private void NumberBox_Loaded(object sender, RoutedEventArgs e)
    {
        var box = sender as NumberBox;
        var textBox = box.VisualTreeFindName<TextBox>("InputBox");
        textBox.TextChanged += TextBox_TextChanged;
    }
    
    private void TextBox_TextChanged(object sender, TextChangedEventArgs e)
    {
        string text = (sender as TextBox).Text;
        bool isNumber = !text.Any(t => !char.IsDigit(t));
        if (isNumber)
        {
            double.TryParse(text, out double value);
            if (value != Model.Value)
                Model.Value = value;
        }
    }