Using LoopingSelector and ILoopingSelectorDataSource

Custom Time EntryThis week I finished and submitted my second Windows Phone app. On one of the pages I wanted to allow users to enter a custom TimeSpan in a manner similar to entering a date or time. Of course, the SDK doesn’t directly provide the controls to replicate that experience so I turned to the LoopingSelector in the Silverlight Toolkit for Windows Phone for help. Unfortunately, documentation is pretty sparse so I’m hoping this can help someone else out.

In many ways the LoopingSelector control is similar to other list controls but one key way that it differs is that it doesn’t expose a SelectedItem property. Instead, that value is driven by its data source. If we want to provide anything more than a simple list we need to define our own implementation of ILoopingSelectorDataSource.

ILoopingSelectorDataSource is a pretty straight-forward interface exposing one property, an event, and two methods:

public interface ILoopingSelectorDataSource
{
  object SelectedItem { get; set; }
  event EventHandler SelectionChanged;
  object GetNext(object relativeTo);
  object GetPrevious(object relativeTo);
}

The names are pretty self-explanatory but for reference here’s a rundown:

  • SelectedItem gets or sets the currently selected item and should never be null.
  • SelectionChanged should be raised when the selected item is changed.
  • SelectionChangedEventArgs is the standard EventArgs implementation found in System.Windows.Controls.  It seems a bit overkill to me for this single selection data source but it works.
  • GetNext gets the element in the sequence after the provided element.
  • GetPrevious get the element in the sequence before the provided element.

The sample application that ships with the toolkit has a pretty nice example implementation that replicates the date and time selection controls that we’re familiar with on the platform. Since the example was so similar to what I needed I used it as a starting point but quickly realized that it wasn’t sufficient mainly because it didn’t provide any binding support.

I really wanted to bind SelectedItem to a value from my ViewModel so that immediately meant I needed the data source to be a DependencyObject and expose the property as a DependencyProperty. There really isn’t anything unexpected in this piece of the implementation. The only real item of note here is that TimeSpanDataSource is abstract. We’ll see why shortly.

public abstract class TimeSpanDataSource : DependencyObject, ILoopingSelectorDataSource
{
    public event EventHandler SelectionChanged;

    private void RaiseSelectionChangedEvent(SelectionChangedEventArgs e)
    {
      if (SelectionChanged != null) SelectionChanged(this, e);
    }

    public static DependencyProperty SelectedItemProperty =
      DependencyProperty.Register(
        "SelectedItem",
        typeof(object),
        typeof(TimeSpanDataSource),
        new PropertyMetadata(TimeSpan.Zero)
      );

    public object SelectedItem
    {
      get { return GetValue(SelectedItemProperty); }
      set
      {
        var oldValue = (TimeSpan)SelectedItem;
        var newValue = (TimeSpan)value;

        if (oldValue == newValue) return;

        SetValue(SelectedItemProperty, newValue);
        RaiseSelectionChangedEvent(new SelectionChangedEventArgs(new[] { oldValue }, new[] { newValue }));
      }
    }

    // rest of class snipped
}

As you can see from the screen capture above, there are actually three LoopingSelectors so users can independently select hours, minutes, or seconds. This means that I need three different data sources to control determination of the next and previous items in the sequence. Luckily determining the next and previous items follows a nice pattern so I was able to implement abstractions of the core logic in the TimeSpanDataSource class and provide a single abstract method for the variant part.

protected abstract object Adjust(TimeSpan relativeTo, int delta);

public object GetNext(object relativeTo)
{
  return Adjust((TimeSpan)relativeTo, 1);
}

public object GetPrevious(object relativeTo)
{
  return Adjust((TimeSpan)relativeTo, -1);
}

protected TimeSpan GetRelativeTo(
  TimeSpan relativeSpan,
  int maximum,
  int delta,
  Func<TimeSpan, int> getCurrentUnit,
  Func<double, TimeSpan> getNextUnit)
{
  var currentUnit = getCurrentUnit(relativeSpan);
  var nextDelta = GetNextDelta(currentUnit, delta, maximum);

  return relativeSpan.Add(getNextUnit(nextDelta));
}

private int GetNextDelta(int currentUnit, int delta, int maximum)
{
  var nextDeltaPreview = currentUnit + delta;

  if (nextDeltaPreview == -1) return maximum + delta;
  if (nextDeltaPreview == maximum) return -maximum + delta;

  return delta;
}

The GetNext and GetPrevious methods just wrap calls to the abstract Adjust method while the bulk of the computation logic is contained within the GetRelativeTo and GetNextDelta methods. The key thing to notice here is that the last two parameters of the GetRelativeTo method are delegates. This strategy allows each Adjust method implementation to be a thin wrapper around GetRelativeTo.

public class HoursDataSource : TimeSpanDataSource
{
  protected override object Adjust(TimeSpan relativeTo, int delta)
  {
    return GetRelativeTo(relativeTo, 13, delta, r => r.Hours, TimeSpan.FromHours);
  }
}

public class MinutesDataSource : TimeSpanDataSource
{
  protected override object Adjust(TimeSpan relativeTo, int delta)
  {
    return GetRelativeTo(relativeTo, 60, delta, r => r.Minutes, TimeSpan.FromMinutes);
  }
}

public class SecondsDataSource : TimeSpanDataSource
{
  protected override object Adjust(TimeSpan relativeTo, int delta)
  {
    return GetRelativeTo(relativeTo, 60, delta, r => r.Seconds, TimeSpan.FromSeconds);
  }
}

Each data source provides delegates to GetRelativeTo so it can calculate the appropriate value with the correct units and properly handle overflowing (looping) the value when the maximum or minimum is exceeded.

With the data sources in place consuming them is just a matter of building out the XAML.

<toolkitPrimitives:LoopingSelector x:Name="HourSelector" Grid.Row="0" Grid.Column="0" ItemSize="120,120" ItemMargin="6">
  <toolkitPrimitives:LoopingSelector.ItemTemplate>
    <DataTemplate>
      <StackPanel VerticalAlignment="Bottom" Margin="10 5">
        <TextBlock Text="{Binding Hours, StringFormat=\{0:00\}}" Style="{StaticResource BoxValue}" />
        <TextBlock Text="{Binding LocalizedResources.HoursLabel, Source={StaticResource LocalizedStrings}}" Style="{StaticResource BoxCaption}" />
      </StackPanel>
    </DataTemplate>
  </toolkitPrimitives:LoopingSelector.ItemTemplate>
  <toolkitPrimitives:LoopingSelector.DataSource>
    <localControls:HoursDataSource SelectedItem="{Binding CustomTime, Mode=TwoWay}" SelectionChanged="DataSourceSelectionChanged" />
  </toolkitPrimitives:LoopingSelector.DataSource>
</toolkitPrimitives:LoopingSelector>

The markup above only defines the hour selector the markup for the other two is virtually identical (excepting bindings and data source, of course) but there’s one important difference – the SelectedItem property for other two data sources aren’t actually bound to the view model. As such we need a way to keep the three synchronized. I followed the toolkit example code for the synchronization and handled the SelectionChanged event in the code-behind. I wasn’t concerned about blending concerns because I feel that the synchronization of the three data sources is purely a UI consideration in that the ViewModel only has the concept of CustomTime and doesn’t have any knowledge of the LoopingSelectors. For completeness, the synchronization code is as follows:

private void DataSourceSelectionChanged(object sender, SelectionChangedEventArgs e)
{
  var ds = (TimeSpanDataSource)sender;
  HourSelector.DataSource.SelectedItem = ds.SelectedItem;
  MinuteSelector.DataSource.SelectedItem = ds.SelectedItem;
  SecondSelector.DataSource.SelectedItem = ds.SelectedItem;
}

With all of these pieces in place selecting a TimeSpan by unit should be pretty intuitive.

Advertisements