Two way binding of a textbox to a slider in WPF

15,735

Solution 1

UPDATE

OK, now I see why you were trying to do it like that. I have a couple of suggestions that may help.

My first one is a bit more opinionated, but I offer it nonetheless. If the problem you are trying to solve is throttling requests to a back-end database, I would contend that your ViewModel need not concern itself with that. I would push that down a layer into an object that is making the call to the back-end based on the updated value passed down from the ViewModel.

You could create a poor-man's throttling attempt by recording DateTimeOffset.Now each time a call is made to the method to query the back-end DB. Compare that value to the last value recorded. If the TimeSpan between falls beneath your minimum threshold, update the value to which it was compared, and ignore the request.

You could do a similar thing with a timer and resetting the timer each time a request is made, but that is messier.

When the call returns from the back-end, this layer raises an event which the ViewModel handles and does whatever it needs to do with the data returned.

As another suggestion, I would also check out what the ReactiveExtensions give you. It takes a bit to kind of wrap your brain around how they work, but you could create an Observable from a stream of events, and then use the Throttle() method to return another Observable. You subscribe to that Observable and perform your call there. It would take more re-thinking the design and architecture of your software, but it is intriguing.

Paul Betts created an entire MVVM framework based around Rx called ReactiveUI. I first learned about throttling Observables in one of his blog posts here.

Good luck!

ORIGINAL POST

If I understand your problem correctly, it sounds like you would like both the Slider and the TextBox to reflect the same property of the DataContext (normally, a ViewModel). It looks like you are trying to duplicate what the binding mechanism of WPF gives you. I was able to get a quick prototype of this working. Here's the code I used.

For the view, I just created a new window with this as the content of the Window.

<StackPanel>
  <Slider Value="{Binding TheValue}" Margin="16" />
  <TextBox Text="{Binding TheValue}" Margin="16" />
</StackPanel>

Notice that both the Slider and the TextBox are bound to the same (cleverly-named) value of the DataContext. When the user enters a new value into the TextBox, the value will change, and the property change notification (in the ViewModel) will cause the slider to update its value automatically.

Here is the code for the ViewModel (i.e., the DataContext of the View).

class TextySlideyViewModel : ViewModelBase
{
  private double _theValue;

  public double TheValue
  {
    get { return _theValue; }
    set
    {
      if(_theValue == value)
        return;

      _theValue = value;
      OnPropertyChanged("TheValue");
    }
  }
}

My ViewModel is derived from a ViewModelBase class which implements the INotifyPropertyChanged interface. The OnPropertyChanged() method is defined in the base class which merely raises the event for the property whose name is passed as the parameter.

Lastly, I created the View and assigned a new instance of the ViewModel as its DataContext (I did this directly in the App's OnStartup() method for this example).

I hope this helps get you going in the right direction.

Solution 2

UPDATE:

Along the lines with Eric, but as a separate suggestion of operation.

  1. Bind both controls to Count as two way as I suggested below.
  2. Create a timer which fires off every second that checks two variables.
  3. (Timer Check #1) Checks to see if a database request is ongoing (such as a Boolean flag). If it is true, it does nothing. If there is no operation (false), it goes to step 4.
  4. (Timer Check #2) It checks to see if count is changed. If count has changed it sets the data request ongoing flag (as found/used in step 3) and initiates an async database call and exits.
  5. (Database Action Call) Gets the database data and updates the VM accordingly. It sets the data request ongoing flag to false which allows the timer check to start a new request if count is changed.

That way you can manage the updates even if a user goes crazy with the slider.


I believe you may have over thought this. Remove all the events off of the slider and the textbox. If the first value (set programmatically) should not call your DoWhatever method, then put in a check in that code to skip the first initialization....

I recommend that you make the slider bind to Count as a TwoWay mode and have the Count Property do the other process you need (as shown on your entity class). No need to check for clicks or any other event. If the user changes the value in the textbox it changes the slider and visa versa.

<Slider Name="slider"
        VerticalAlignment="Top"
        Value="{Binding Count, Mode=TwoWay}"
        Width="200"
        Minimum="0"
        Maximum="100" />
<TextBox VerticalAlignment="Top"
         HorizontalAlignment="Left"
         Grid.Column="1"
         Width="100"
         Text="{Binding Count,Mode=TwoWay,UpdateSourceTrigger=LostFocus}"
         Height="25" />
Share:
15,735
ak3nat0n
Author by

ak3nat0n

I am a software engineer at Google. I love everything software engineering, distributed systems. Mainly involved in distributed system, API development, and large scale data processing.

Updated on June 04, 2022

Comments

  • ak3nat0n
    ak3nat0n almost 2 years

    I am having the hardest time figuring out a way to solve a problem I am having with databinding on a slider and a textbox.

    The setup: the current value of the slider is displayed inside of the textbox. When the user drags the slider the value is reflected inside the textbox. The user can choose to drag the slider and release to the value he chooses, click anywhere on the slider track to set the value or enter the value manually in the texbox. In the last case, the value entered in the textbox should update the slider position.

    The texbox is two way bound to a datacontext property while the slider is one way bound to the same property. When the user slides or click on the slider tracker, I use the dragcompleted event of the slider to notify the datacontext of the modification. When the user clicks on the tracker on the other hand I use the OnValueChanged event of the slider to notify the datacontext (and use a flag to ensure the OnValueChanged was not triggered by a slider move)

    The problem: The OnValueChanged event fires even when initializing the slider value with the binding value so I cannot figure out whether the value is actually coming from the user or the binding.

    Could you please suggest maybe and alternative way to do the binding to ensure we can distinguish between user update and binding udpates for the slider? Thnak you!

    UPDATE Sorry I forgot to mention why I am not binding directly both slider and textbox two ways like the below answers suggest. The update to the data context value is supposed to trigger a call to a backend server and retrieve data from a database. The problem is that when the user drags the slider it constantly fires updates. I go around the problem by only relying to the actual onValueChanged event to call the DoWhatever method. I hope that's a bit clearer. Sorry for omitting this...

    I quickly put together the sample below for you to give it a try.

    The xaml:

    <Window x:Class="SliderIssue.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid HorizontalAlignment="Center"
          VerticalAlignment="Center">
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
    
        <Slider Name="slider" VerticalAlignment="Top"  
                ValueChanged="slider_ValueChanged"
                Thumb.DragStarted="slider_DragStarted"
                Thumb.DragCompleted="slider_DragCompleted"
                Value="{Binding Count}"
                Width="200"
                Minimum="0"
                Maximum="100"/>
        <TextBox VerticalAlignment="Top" 
                 HorizontalAlignment="Left"
                 Grid.Column="1" 
                 Width="100" 
                 Text="{Binding Count,Mode=TwoWay,UpdateSourceTrigger=LostFocus}" 
                 Height="25"/>    
    </Grid>
    

    The code behind:

    using System.Windows;
    
    namespace SliderIssue
    {
        /// <summary>
        /// Interaction logic for MainWindow.xaml
        /// </summary>
        public partial class MainWindow : Window
        {
            private bool _dragStarted;
    
            public MainWindow()
            {
                InitializeComponent();
    
                var item = new Item();
                DataContext = item;
            }
    
            private void slider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
            {
                if (!_dragStarted)
                {
                    var item = (Item)DataContext;
                    item.DoWhatever(e.NewValue);
                }
            }
    
            private void slider_DragStarted(object sender, System.Windows.Controls.Primitives.DragStartedEventArgs e)
            {
                _dragStarted = true;
            }
    
            private void slider_DragCompleted(object sender, System.Windows.Controls.Primitives.DragCompletedEventArgs e)
            {
                _dragStarted = false;
    
                var item = (Item) DataContext;
                item.DoWhatever(slider.Value);
            }
        }
    }
    

    A simple data class:

    using System.ComponentModel;
    
    namespace SliderIssue
    {
        public class Item : INotifyPropertyChanged
        {
            private int _count = 50;
            public int Count
            {
                get { return _count; }
                set
                {
                    if (_count != value)
                    {
                        _count = value;
                        DoWhatever(_count);
                        OnPropertyChanged("Count");
                    }
                }
            }
    
            public void DoWhatever(double value)
            {
                //do something with value
                //and blablabla
            }
    
            public event PropertyChangedEventHandler PropertyChanged;
    
            protected void OnPropertyChanged(string property)
            {
                if (PropertyChanged != null)
                {
                    PropertyChanged(this, new PropertyChangedEventArgs(property));
                }
            }
        }
    }