WPF Date Range Double Slider Control (Part 3–Dependency Properties)

This is part 3 in a series I’m writing about a WPF User Control I’ve created and posted up on CodePlex.  In this installment, I’m going to be talking about creating dependency properties, in the DateRangeSlider, I used them to enable/disable the upper and lower sliders independently for dealing with the maximum, minimum, upper and lower values.

First of all, a little bit about dependency properties.  A dependency property is a type of property introduced with WPF that solves many problems in WPF around styling, data binding, animation, and more.  As I’ve been working with WPF, I’ve noticed that one of the primary features of a dependency property used is its built-in ability to provide change notification.  If you start diving into MVVM, you might start asking yourself why do I need a dependency property instead of a property that utilizes the INotifyPropertyChanged implementation?  That is when the styling and animating come into play, because you can only style and animate dependency properties.

If you look through the code at CodePlex, you’ll see I have dependency properties for the slider’s minimum, maximum, upper value, lower value, enabling the lower slider, enabling the upper slider, and finally locking the lower and upper values to the minimum and maximum respectively.   We’ll take a look at the Minimum property. 

I want to start with Line 8 below, because this is where the real definition of the MinimumProperty is located.  It is a public static field and by convention, has a Property suffix.  Depedency properties are created using the DependencyProperty.Register method which at the minimum takes needs the name of the dependency property to register (“Minimum”) and the type of the property (typeof(DateTime)) and the type of the owner (typeof(DateRangeSlider)) that is registering the dependency property.  The overload of Register I used also includes a PropertyMetadata argument where I defined a default value and a callback method.

Line 2 starts the basic property definition. with your get and set methods that call the methods GetValue and SetValue which are inherited by System.Windows.DependencyObject.  Each dependency property you make will look almost identical to this because there is no guarantee that your getter or setter will be called directly, WPF will usually call the GetValue and SetValue methods directly, bypassing any custom logic you might feel compelled to add.

The last thing you’ll find in the snippet below is the callback method.  If you need custom logic to be run when your property is changed, you’ll want to use an overload of the DependencyProperty.Register method that lets to define a callback method in the PropertyMetadata argument.  In my example, I’m using the callback to check if the LowerValue should be locked to the Minimum or if the LowerValue is less than the new Minimum and adjusting it accordingly.  One last thing to note here, is that our callback is static, which means it isn’t connected to a particular instance.  The instance we want to work with is inside the argument DependencyObject d and we must first cast it out.

Dependency Property – Minimum
  1. #region Dependency Property – Minimum
  2. public DateTime Minimum
  3. {
  4.     get { return (DateTime)GetValue(MinimumProperty); }
  5.     set { SetValue(MinimumProperty, value); }
  6. }
  7.  
  8. public static readonly DependencyProperty MinimumProperty =
  9.     DependencyProperty.Register("Minimum", typeof(DateTime), typeof(DateRangeSlider), new UIPropertyMetadata(DateTime.Now.AddDays(-15), new PropertyChangedCallback(OnMinimumChanged)));
  10.  
  11. public static void OnMinimumChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
  12. {
  13.     DateRangeSlider slider = (DateRangeSlider)d;
  14.  
  15.     if (slider.IsLowerValueLockedToMin || slider.LowerValue < (DateTime)e.NewValue)
  16.     {
  17.         slider.LowerValue = (DateTime)e.NewValue;
  18.     }
  19. }
  20.  
  21. #endregion

Hopefully, this helps explain dependency properties to anyone looking for it, I’ve never claimed to be a great writer (or even a mediocre one).  If I missed anything or if you have questions, let me know.

WPF Date Range Double Slider Control (Part 2)

As I mentioned in my introduction to this series, I’m going over various aspects of how I implemented a control that allows a user to choose a DateTime range using a double slider control.  In Part One, I covered the basics of what I did to the control templates, put the slider together and showed the IValueConverters I used to handle the conversions from doubles to DateTime and doubles to TimeSpans.  In this article, I’ll be talking about what I did to keep the upper value and the lower value sliders from crossing each other.  The source for the control and a sample project can be found at CodePlex

My first thought doing this was to play it simple and bind the lower slider’s Maximum to the upper slider’s current value and the and the upper sliders Minimum to the lower’s current value.   While this seemed logical to me, when I implemented it, it put the two sliders on two totally separate scales and it failed miserably.  Not only were the sliders able to cross, since the scales were constantly changed, while I would move the lower slider, the upper slider would be moving (and not changing value).

My second attempt was much better, but still didn’t work as expected.  In this attempt, I subscribed to the ValueChanged event for each of the sliders.  In these event handlers, I had a piece of code as shown in “Attempt 2:  Fail” that was setting the slider controls values so they wouldn’t cross each other (for long) and then setting dependency properties (UpperValue and LowerValue) to those upper and lower values.  This failed because I wasn’t setting the upper slider’s value to something greater than the lower slider’s value until after this code ran.  To a user, this would be acceptable, you can’t actually see the values cross.  However, I was using this to help zoom in and out of a charting control  and once they crossed, even for a few milliseconds, the charting control would blow up on me.  It was quite messy…line fragments, points, and data everywhere.

Attempt 2: Fail
  1. private void LowerSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
  2. {
  3.     UpperSlider.Value = Math.Max(UpperSlider.Value, LowerSlider.Value + oneSecond);
  4.     LowerValue = new DateTime((long)LowerSlider.Value);
  5.     UpperValue = new DateTime((long)UpperSlider.Value);
  6. }

 

So, finally, my 3rd attempt I hit gold.  Why was I setting LowerValue and UpperValue be set by the sliders when I was also setting them in the ValueChanged events?  So I changed my bindings in the sliders to OneWay bindings so I have complete control of the range values and the sliders can suggest was my new UpperValue and LowerValue should be, but I get the final decision in my event handlers.  In my event handlers, I check if the slider values have passed each other and set the dependency properties to appropriate values that make sure that the UpperValue is greater than the LowerValue.

Slider Control Template
  1. private void UpperSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
  2. {
  3.     LowerSlider.Value = Math.Max(Math.Min(UpperSlider.Value oneSecond, LowerSlider.Value), Minimum.Ticks);
  4.  
  5.     var _upperValue = new DateTime((long)UpperSlider.Value);
  6.     var _lowerValue = new DateTime((long)LowerSlider.Value);
  7.  
  8.     if (UpperValue > _lowerValue)
  9.     {
  10.         LowerValue = _lowerValue;
  11.         UpperValue = _upperValue;
  12.     }
  13.     else
  14.     {
  15.         UpperValue = _upperValue;
  16.         LowerValue = _lowerValue;
  17.     }
  18. }