Dan
Dan

Reputation: 7724

Update text boxes when a property's value changes - WPF

The context of this example is there are four text boxes that hold a total amount of time. 1 for hours, 1 for minutes, 1 for seconds, and 1 for milliseconds.

There is a fifth text box that holds the total time in just milliseconds. This can be seen in the image below.


I have made an implementation of IMultiValueConverter that should convert 4 TextBox components and the converted value in a property. It should also be able to update the 4 boxes when the property's value changes.

When the user types in the text box that holds the converted output and then that box loses focus, the other 4 text boxes are updated. However, when the property's value is programmatically changed, in this case by a button click, the values in the 4 text boxes are not updated.

How can I make these 4 text boxes update through the converter?

The ultimate goal, in this example, is to store the total time (in milliseconds) in a property and have 5 text boxes updating through bindings when that property is updated.

This is the code for the converter.

using System;
using System.Globalization;
using System.Windows.Data;

namespace MultiBinding_Example
{
    public class MultiDoubleToStringConverter : IMultiValueConverter
    {
        private const double HOURS_TO_MILLISECONDS = 3600000;
        private const double MINUTES_TO_MILLISECONDS = 60000;
        private const double SECONDS_TO_MILLISECONDS = 1000;
        private const string ZERO_STRING = "0";
        private object valBuffer = null;

        /*
         * values[0] is the variable from the view model
         * values[1] is hours
         * values[2] is the remaining whole minutes
         * values[3] is the remaining whole seconds
         * values[4] is the remaining whole milliseconds rounded to the nearest millisecond
         */

        public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
        {
            object returnVal = ZERO_STRING;
            try
            {
                if (values != null)
                {
                    double hoursToMilliseconds = (values[1] == null || values[1].ToString() == string.Empty) ? 0 : System.Convert.ToDouble(values[1]) * HOURS_TO_MILLISECONDS;
                    double minutesToMilliseconds = (values[2] == null || values[2].ToString() == string.Empty) ? 0 : System.Convert.ToDouble(values[2]) * MINUTES_TO_MILLISECONDS;
                    double secondsToMilliseconds = (values[3] == null || values[3].ToString() == string.Empty) ? 0 : System.Convert.ToDouble(values[3]) * SECONDS_TO_MILLISECONDS;
                    double totalTime = ((values[4] == null || values[4].ToString() == string.Empty) ? 0 : System.Convert.ToDouble(values[4])) + secondsToMilliseconds + minutesToMilliseconds + hoursToMilliseconds;
                    returnVal = totalTime.ToString();

                    if (values[0] == valBuffer)
                    {
                        values[0] = returnVal;
                    }
                    else
                    {
                        valBuffer = returnVal = values[0];
                        ConvertBack(returnVal, new Type[] { typeof(string), typeof(string), typeof(string), typeof(string), typeof(string) }, parameter, culture);
                    }
                }
            }
            catch (FormatException) { }

            return returnVal;
        }

        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
        {
            try
            {
                if (value != null && value.ToString() != string.Empty)
                {
                    double timeInMilliseconds = System.Convert.ToDouble(value);

                    object[] timeValues = new object[5];
                    timeValues[0] = value;
                    timeValues[1] = Math.Floor(timeInMilliseconds / HOURS_TO_MILLISECONDS).ToString();
                    timeValues[2] = Math.Floor((timeInMilliseconds % HOURS_TO_MILLISECONDS) / MINUTES_TO_MILLISECONDS).ToString();
                    timeValues[3] = Math.Floor((timeInMilliseconds % MINUTES_TO_MILLISECONDS) / SECONDS_TO_MILLISECONDS).ToString();
                    timeValues[4] = Math.Round(timeInMilliseconds % SECONDS_TO_MILLISECONDS, MidpointRounding.AwayFromZero).ToString();
                    return timeValues;
                }
            }
            catch (FormatException) { }

            return new object[] { ZERO_STRING, ZERO_STRING, ZERO_STRING, ZERO_STRING, ZERO_STRING };
        }
    }
}

To test this, I have a quite a simple layout that consists of a few Label components, a few TextBox components and a Button.

It looks like this.

enter image description here

The XAML for it is the following.

<Window x:Class="MultiBinding_Example.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:MultiBinding_Example"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Window.Resources>
        <local:MultiDoubleToStringConverter x:Key="multiDoubleToStringConverter"/>
    </Window.Resources>
    <StackPanel>
        <Label Content="Multi Value Converter" HorizontalAlignment="Center" FontSize="35" FontWeight="Bold" Margin="0, 25, 0, 0"/>
        <Label Content="Formatted Total Time" FontWeight="Bold" FontSize="24" Margin="20, 10"/>
        <Grid Margin="80, 10">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="auto"/>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="auto"/>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="auto"/>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="auto"/>
            </Grid.ColumnDefinitions>
            <TextBox Name="Hours" HorizontalContentAlignment="Right" VerticalContentAlignment="Center" Text="0"  Grid.Column="0"/>
            <Label Content="Hours" Grid.Column="1" Margin="0, 0, 15, 0"/>

            <TextBox Name="Minutes" HorizontalContentAlignment="Right" VerticalContentAlignment="Center" Text="0" Grid.Column="2"/>
            <Label Content="Minutes" Grid.Column="3" Margin="0, 0, 15, 0"/>

            <TextBox Name="Seconds" HorizontalContentAlignment="Right" VerticalContentAlignment="Center" Text="0" Grid.Column="4"/>
            <Label Content="Seconds" Grid.Column="5" Margin="0, 0, 15, 0"/>

            <TextBox Name="Milliseconds" HorizontalContentAlignment="Right" VerticalContentAlignment="Center" Text="0" Grid.Column="6"/>
            <Label Content="Milliseconds" Grid.Column="7"/>
        </Grid>

        <Label Content="Unformatted Total Time" FontWeight="Bold" FontSize="24" Margin="20, 10"/>
        <Grid Margin="80, 10">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="auto"/>
            </Grid.ColumnDefinitions>
            <TextBox HorizontalContentAlignment="Right" VerticalContentAlignment="Center" Grid.Column="0">
                <TextBox.Text>
                    <MultiBinding Converter="{StaticResource multiDoubleToStringConverter}" Mode="TwoWay">
                        <Binding Path="TotalTime"/>
                        <Binding ElementName="Hours" Path="Text"/>
                        <Binding ElementName="Minutes" Path="Text"/>
                        <Binding ElementName="Seconds" Path="Text"/>
                        <Binding ElementName="Milliseconds" Path="Text"/>
                    </MultiBinding>
                </TextBox.Text>
            </TextBox>
            <Label Content="Milliseconds" Grid.Column="1"/>
        </Grid>
        <Button Grid.Column="1" Margin="250, 20" Height="50" Content="Random Total Milliseconds" Click="RandomTime_Click"/>
    </StackPanel>
</Window>

The code behind is the following.

using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows;

namespace MultiBinding_Example
{
    public partial class MainWindow : Window, INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        private Random random = new Random();

        private string totalTime;
        public string TotalTime {
            get => totalTime;
            set {
                totalTime = value;
                RaisePropertyChanged();
            }
        }

        public MainWindow()
        {
            InitializeComponent();
            DataContext = this;
            UpdateTotalTime();
        }

        private void RaisePropertyChanged([CallerMemberName] string propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }

        private void RandomTime_Click(object sender, RoutedEventArgs e)
        {
            UpdateTotalTime();
        }

        private void UpdateTotalTime()
        {
            double percent = random.NextDouble();
            double time = Math.Floor(percent * random.Next(1000, 100000000));
            TotalTime = time.ToString();
        }
    }
}

Upvotes: 2

Views: 436

Answers (1)

BradleyDotNET
BradleyDotNET

Reputation: 61339

This isn't really what a converter is for.

Converters take a set of view model values and convert them to view values for display. Then if the view values change it can convert them back to view model values.

In your case, the view model value is updated through code (not through a change to the view) and so the converter has no reason to run the ConvertBack method (the value is already a view model value!). This is one of several reasons why converters should not have side-effects.

The correct way to do this would be to have TotalTime as a property on the VM (probably as a number or TimeSpan and not a string as you have it) and then do individual converters for each of the pieces. For example:

 <TextBox Text="{Binding TotalTime, Converter={StaticResource TimeSecondsConverter}"/>

The main text box would then just be bound to TotalTime. TimeSecondsConverter would probably need to be a multi-value converter in order for ConvertBack to work.

Upvotes: 1

Related Questions