Search code examples
c#wpfxamlinotifypropertychanged

Calculated field not updating until edited in UI


I'm trying to test out data binding with XAML and C# as a novice programmer. I have two sliders that are bound to properties and I want to update a TextBox with the sum of the two values of the properties set by the sliders.

I'm using INotifyPropertyChanged and tried changing every property I could find but I can't get the textbox to update until I edit the textbox, at which point, the textbox updates to the correct value. Using UpdateSourceTrigger=PropertyChanged only updates the textbox as soon as I edit the textbox instead of when I select another element. I've tried writing a separate event handler that doesn't use [CallerNameMember] and uses a specified property but it didn't seem to change anything.

<Grid>
    <Grid.RowDefinitions>

    </Grid.RowDefinitions>

    <TextBox Grid.Row="0"
             Text="{Binding BoundNumber, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
             FontSize="20"
             FontWeight="Bold"
             AllowDrop="False" />

    <Slider Grid.Row="1"
            Value="{Binding BoundNumber, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
            Maximum="100"
            Minimum="10"
            IsSnapToTickEnabled="True"
            TickFrequency="10" />
    <TextBox Grid.Row="2"
             Text="{Binding BoundNumber2, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
             AllowDrop="False" />
    <Slider Grid.Row="3"

            Value="{Binding BoundNumber2, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
            Maximum="100"
            Minimum="10"
            IsSnapToTickEnabled="True"
            TickFrequency="10" />


    <TextBox Grid.Row="4"
            Name="MathBox"
             Text="{Binding QuickMath, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, NotifyOnSourceUpdated=True}">
           </TextBox>

</Grid>

public partial class OrderScreen : INotifyPropertyChanged
{
    public OrderScreen()
    {
        DataContext = this;

        InitializeComponent();
     }

    private int quickMath;
    public int QuickMath
    {
        get { return _boundNumber + _boundNumber2; }
        set
        {

            if (value != quickMath)
            {
                quickMath = value;
                OnPropertyChanged();

            }
        }
    }
    private int _boundNumber;
    public int BoundNumber
    {
        get { return _boundNumber; }
        set
        {
            if (_boundNumber != value)
            {
                _boundNumber = value;
               // MathBox.Text = quickMath.ToString();
                OnPropertyChanged();

            }
        }
    }

    private int _boundNumber2;
    public int BoundNumber2
    {
        get { return _boundNumber2; }
        set
        {
            if (_boundNumber2 != value)
            {
                _boundNumber2 = value;
                MathBox.Text = quickMath.ToString();
                OnPropertyChanged();

            }
        }
    }

 public event PropertyChangedEventHandler PropertyChanged;
    private void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {

        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

I can get it to work with the commented out MathBox.Text = quickMath.ToString(); but I was hoping there was a better way to do this with data binding. Thanks in anticipation!


Solution

  • Binding mechanism subscribes to the PropertyChanged event of DataSource object, so there is no need to "initialize" the event along with the INPC implementation, but as you might have noticed, PropertyChanged event for the QuickMath property is indeed never triggered when BoundNumber or BoundNumber2 are changed.

    You can fix it in different ways, e.g. explicitly call OnPropertyChanged for all affected properties:

    private int _boundNumber;
    public int BoundNumber
    {
        get { return _boundNumber; }
        set
        {
            if (_boundNumber != value)
            {
                _boundNumber = value;
                OnPropertyChanged();
                OnPropertyChanged(nameof(QuickMath));
            }
        }
    }
    

    Note that this way you can keep QuickMath property a read-only. This approach works nicely in other situations, like with time-related properties, say if your data source property formats a string like "Edited 2 minutes ago" based on a recorded timestamp and current time and you call PropertyChanged as a timed task.

    public int QuickMath => _boundNumber + _boundNumber2;
    

    Alternatively, you can update QuickMath along with modifying BoundNumber and BoundNumber2 to trigger OnPropertyChanged() call inside QuickMath setter:

    private int _boundNumber2;
    public int BoundNumber2
    {
        get { return _boundNumber2; }
        set
        {
            if (_boundNumber2 != value)
            {
                _boundNumber2 = value;
                OnPropertyChanged();
                QuickMath = BoundNumber + BoundNumber2;
            }
        }
    }
    

    This makes sense if the logic in QuickMath wouldn't allow making it a read-only property. In this case you have to adjust the getter accordingly and use private or protected setter there to avoid data inconsistency and unexpected behavior.

    private int _quickMath;
    public int QuickMath
    {
        get { return _quickMath; }
        private set
        {
            if (value != _quickMath)
            {
                _quickMath = value;
                OnPropertyChanged();
            }
        }
    }
    

    In both cases there is no need for two-way binding to QuickMath:

    <TextBlock Grid.Row="4" Text="{Binding QuickMath, Mode=OneWay}"/>
    

    On a side-note and looking at the rest of the code, it really worth mentioning that binding mechanism is expected to segregate UI from the data, where XAML knows about data source object properties (names and types) but not about it's internal implementation, while data source object can have no knowledge about XAML at all. So

    1. there should be no calls from data object to FrameworkElements like MathBox.Text
    2. it's considered a good design to have data object class completely separate from the page or control class.

    Hope this helps.