WinRT XAML Calendar

Today, I want to share with you my last developed control : a WinRT Calendar Control

  

WinRTXamlCalendar

This control is very simple. It’s based on 3 ListBox. There is no infinite scroll because a simple ListBox doesn’t support such Behavior.

You can grab the code on codeplex : https://xamlwinrtcalendar.codeplex.com/

This is a short video on how this calendar works in a simple Windows 8 application :

<source ="<source"="="&lt;source"">

Some particular points of interests :

VisualStates

By design the Visual States and Transition from the Listbox control didn’t work correctly with the control, I had to make my own transitions and disable standards transitions.

There is three States : Selected, UnSelected and Transparent:

image

 

 [TemplateVisualState(Name = "PickerItemSelected", GroupName = "Picker")]
[TemplateVisualState(Name = "PickerItemUnSelected", GroupName = "Picker")]
[TemplateVisualState(Name = "PickerItemTransparent", GroupName = "Picker")]
public sealed class PickerSelectorItem : ListBoxItem
{
..

And the Template of course :

 <VisualStateManager.VisualStateGroups>
     <VisualStateGroup x:Name="Picker">
         <VisualState x:Name="PickerItemTransparent">
             <Storyboard>
                 <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Background" Storyboard.TargetName="InnerGrid">
                     <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource TransparentColor}"/>
                 </ObjectAnimationUsingKeyFrames>
                 <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Foreground" Storyboard.TargetName="ContentPresenter">
                     <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource TransparentColor}"/>
                 </ObjectAnimationUsingKeyFrames>
             </Storyboard>
         </VisualState>
         <VisualState x:Name="PickerItemSelected">
             <Storyboard>
                 <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Background" Storyboard.TargetName="InnerGrid">
                     <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PurpleColor}"/>
                 </ObjectAnimationUsingKeyFrames>
                 <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Foreground" Storyboard.TargetName="ContentPresenter">
                     <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource WhiteColor}"/>
                 </ObjectAnimationUsingKeyFrames>
             </Storyboard>
         </VisualState>
         <VisualState x:Name="PickerItemUnSelected">
             <Storyboard>
                 <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Background" Storyboard.TargetName="InnerGrid">
                     <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource DarkGrayColor}"/>
                 </ObjectAnimationUsingKeyFrames>
                 <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Foreground" Storyboard.TargetName="ContentPresenter">
                     <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource WhiteColor}"/>
                 </ObjectAnimationUsingKeyFrames>
             </Storyboard>
         </VisualState>
     </VisualStateGroup>
     <VisualStateGroup x:Name="CommonStates">
         <VisualState x:Name="Normal">
         </VisualState>
         <VisualState x:Name="PointerOver" >
         </VisualState>
         <VisualState x:Name="Disabled" >
         </VisualState>
         <VisualState x:Name="Pressed" >
         </VisualState>
     </VisualStateGroup>
     <VisualStateGroup x:Name="SelectionStates">
         <VisualState x:Name="Unselected">
         </VisualState>
         <VisualState x:Name="Selected">
         </VisualState>
         <VisualState x:Name="SelectedUnfocused" >
         </VisualState>
         <VisualState x:Name="SelectedDisabled" >
         </VisualState>
         <VisualState x:Name="SelectedPointerOver">
         </VisualState>
         <VisualState x:Name="SelectedPressed">
         </VisualState>
     </VisualStateGroup>
     <VisualStateGroup x:Name="FocusStates">
         <VisualState x:Name="Focused" >
         </VisualState>
         <VisualState x:Name="Unfocused">
         </VisualState>
         <VisualState x:Name="PointerFocused">
         </VisualState>
     </VisualStateGroup>
 </VisualStateManager.VisualStateGroups>

 

Center the selected ListBoxItem

When you select an item in one of the three ListBox, you can’t center the ListBoxItem (especialy when it’s the first one)

To achieve this goal, there is a simple method : apply some margin on the ItemsPresenter like this:

image

 /// <summary>
/// Applies the margin on items presenter to allow first and last item to be centered
/// </summary>
public void ApplyMarginOnItemsPresenter()
{
    var itemsPresenter = this.ScrollViewer.GetVisualDescendent<ItemsPresenter>();
    Thickness t = new Thickness(0, this.ScrollViewer.ViewportHeight / 2, 0, this.ScrollViewer.ViewportHeight / 2);
    itemsPresenter.Margin = t;

}

 

ScrollViewer animation

We can’t override default animation of the ScrollViewer. Like WPF or Silverlight this action is not accessible.

I need to animate the ScrollViewer to correctly place the selected item in the center of the control. To achieve this goal, I used an internal Scrollbar which will be animated. 

On each value change, we will make a scroll to vertical offset on the ScrollViewer.

The slider used :

 sliderVertical = new Slider
{
    SmallChange = 0.0000000001,
    Minimum = double.MinValue,
    Maximum = double.MaxValue,
    StepFrequency = 0.0000000001
};
sliderVertical.ValueChanged += OnVerticalOffsetChanged;

the Vertical offset changed Handler:

 /// <summary>
/// Hook to animate the scrollviewer
/// </summary>
private void OnVerticalOffsetChanged(object sender, RangeBaseValueChangedEventArgs e)
{
    if (this.ScrollViewer == null) return;

    this.viewer.ViewChanged -= ViewerViewChanged;
    this.ScrollViewer.ScrollToVerticalOffset(e.NewValue);
}

And the animation used to center the item :

 /// <summary>
 /// Select the current item and snap it on the good placement (middle) with animation
 /// </summary>
 internal void SelectAndSnapTo(DateTimeWrapper wrapper)
 {
     double viewerVerticalOffestTarget = GetViewerVerticalOffestTarget(wrapper);

     if (viewerVerticalOffestTarget <= 0d)
         return;

     storyBoardSnap = new Storyboard();
     var animationSnap = new DoubleAnimation
     {
         EnableDependentAnimation = true,
         From = viewer.VerticalOffset,
         To = viewerVerticalOffestTarget,
         Duration = animationDuration,
         EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut }
     };

     storyBoardSnap.Children.Add(animationSnap);

     Storyboard.SetTarget(storyBoardSnap, sliderVertical);
     Storyboard.SetTargetProperty(animationSnap, "Value");

     storyBoardSnap.Completed += (sender, o) =>
     {
         // Reactivate Scrollviewer
         viewer.VerticalScrollMode = ScrollMode.Enabled;
         viewer.ViewChanged -= ViewerViewChanged;
         viewer.ViewChanged += ViewerViewChanged;
         this.GoToUnFocused();
     };

     this.GoToFocusState();
 
     // Prevent viewer to scroll beacause the animation will force scrollviewer to scroll 
     viewer.VerticalScrollMode = ScrollMode.Disabled;
     storyBoardSnap.Begin();

Get the correct item and the correct animation

When we select an item, we need to know:

  • Wich item (UIElement) ?
  • What is its position?
  • What is the delta beetween this item and the center of listbox ?

image

To get the correct item, we can use the ItemContainerGenerator. To get its bound, we can use a GeneralTransform :

 rowElement = this.ItemContainerGenerator.ContainerFromItem(dtw) as PickerSelectorItem;
 if (rowElement == null)
     return 0d;

 Point origin, bottom;

 GeneralTransform transform = rowElement.TransformToVisual(this.ScrollViewer);

 if (transform == null || !transform.TryTransform(new Point(), out origin) ||
     !transform.TryTransform(new Point(rowElement.ActualWidth, rowElement.ActualHeight), out bottom))
     return 0d;

For each item, selected or not, tapped or manipulated, I always need to know what is the center item or what item I tapped and the Delta of vertical offset to animate

Thanks to VerticalOffset and ViewportHeight, we have all what we need to calculate this delta:

 // Get delta ScrollViewer
double viewerVerticalOffset = this.ScrollViewer.VerticalOffset;

// Vertical size of the Viewer
double viewerPortHeight = this.ScrollViewer.ViewportHeight;

// get the middle
var middle = viewerPortHeight / 2;

// Get top element
var topElementTarget = (middle - (rowElement.ActualHeight / 2));

// Set Delta
var delta = rectItem.Top - topElementTarget;

// Déduction du vertical offset cible :
return viewerVerticalOffset + delta;

Globalization

This control supports globalization. You have to specify the correct language “before” the first invoke of the control

In the sample, you can see this code line in the OnLaunched Event :

 // Change current application language
 Windows.Globalization.ApplicationLanguages.PrimaryLanguageOverride = "fr-FR";

image

Support ?

This control is provider “as is”. I know there is some limitations and ( maybe ? ) some bugs. Feel free to post comments here or on Codeplex !

Happy Calendar time Sourire