Building a Calendar Control for WinRT in XAML

I have had a pet project for calendar control for Windows Phone 7 on CodePlex for quite sometime.  I also have had this project on NuGet as well.  I have been working for a while on porting this project to WinRT / Windows Store Applications.  I am finally done with first revision and it is now available on both CodePlex and NuGet.

In this post I will describe some of the techniques I used as well as usage of the calendar control.

Here is how it looks with the default blue template included in NuGet package.

image

I know, I never claim to be a designer, but it works for me.  It even supports snapped view.

image

If you ever authored a XAML based control in Silverlight or WPF or WP 7, you are already familiar with basic concepts.  If not, use predefined custom control template that ships with VS 2012.  Just start up new Windows Store Class Library project.

image

Now use templated control template to get you started.

image

Once you click Add two things will happen.  Your class file will be added and Generix.xaml is added under Themes folder with a default (almost blank) control template.  It will look something like this

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:Win8">

    <Style TargetType="local:Calendar">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="local:Calendar">
                    <Border
                        Background="{TemplateBinding Background}"
                        BorderBrush="{TemplateBinding BorderBrush}"
                        BorderThickness="{TemplateBinding BorderThickness}">
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

Not particularly interested, but if you have never created a templated control, this is a good start for you.  Next step is to add properties and methods to your control and of course update the template to bind to those new properties.  For example. my calendar control consists of calendar items (days) and I define another control and template for it.  I added DayNumber dependency property and bind it to a text block as following

        /// <summary>
        /// Day number for this calendar cell.
        /// This changes depending on the month shown
        /// </summary>
        public int DayNumber
        {
            get { return (int)GetValue(DayNumberProperty); }
            internal set { SetValue(DayNumberProperty, value); }
        }

        /// <summary>
        /// Day number for this calendar cell.
        /// This changes depending on the month shown
        /// </summary>
        public static readonly DependencyProperty DayNumberProperty =
            DependencyProperty.Register("DayNumber", typeof(int), typeof(CalendarItem), new PropertyMetadata(0, OnDayNumberChanged));
<TextBlock 
    x:Name="DayNumberBlock"
    Text="{Binding DayNumber, RelativeSource={RelativeSource Mode=TemplatedParent}}" 
    Foreground="White" 
    FontWeight="ExtraBold"
    FontSize="40"
    HorizontalAlignment="Right" 
    VerticalAlignment="Bottom" 
    Margin="0,0,10,10"
    Grid.Row="1"/>

 

This is just a small example, but you get the idea behind the data binding.  Now, something more interesting, working with snapped views.  Snapped views are a feature of Windows 8, where you can snap a running program to the left, taking about 300 pixels, with another program running in the remaining space.  Since I would really hate for someone to have to write a new control for snapped mode, I am using some tricks to adjust the control itself and any controls within it (like calendar items or days).  First of all, I have to listen to size changed event.

        public Calendar()
        {
            DefaultStyleKey = typeof(Calendar);
            SizeChanged += OnCalendarSizeChanged;
        }

        void OnCalendarSizeChanged(object sender, SizeChangedEventArgs e)
        {
            _applicationViewState = ApplicationView.Value;
            UpdateVisualsBasedOnState();
        }

        private void UpdateVisualsBasedOnState()
        {
            VisualStateManager.GoToState(
                 this, _applicationViewState.ToString(), false);
        }

As you can see, once a size change occurs, I can test current view state using Application view state enumeration (see below) that is built into WinRT and then I can just use XAML and VisualStateManager to adjust my visuals for the calendar.

  public enum ApplicationViewState
  {
    FullScreenLandscape,
    Filled,
    Snapped,
    FullScreenPortrait,
  }

For example, for calendar item I adjust border radius and font size for day number

    <Style TargetType="local:CalendarItem">
        <Setter Property="Foreground"  Value="White"/>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate  TargetType="local:CalendarItem">
                    <Grid x:Name="root" d:DesignWidth="272" d:DesignHeight="246" Margin="3,3,3,5">
                        <VisualStateManager.VisualStateGroups>
                            <VisualStateGroup x:Name="Orientation">
                                <VisualState x:Name="FullScreenLandscape"/>
                                <VisualState x:Name="FullScreenPortrait"/>
                                <VisualState x:Name="Snapped">
                                    <Storyboard>
                                        <DoubleAnimation Duration="0" To="10" Storyboard.TargetProperty="(TextBlock.FontSize)" Storyboard.TargetName="DayNumberBlock" d:IsOptimized="True"/>
                                        <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(Border.CornerRadius)" Storyboard.TargetName="border1">
                                            <DiscreteObjectKeyFrame KeyTime="0">
                                                <DiscreteObjectKeyFrame.Value>
                                                    <CornerRadius>2</CornerRadius>
                                                </DiscreteObjectKeyFrame.Value>
                                            </DiscreteObjectKeyFrame>
                                        </ObjectAnimationUsingKeyFrames>
                                        <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(Border.CornerRadius)" Storyboard.TargetName="border">
                                            <DiscreteObjectKeyFrame KeyTime="0">
                                                <DiscreteObjectKeyFrame.Value>
                                                    <CornerRadius>2</CornerRadius>
                                                </DiscreteObjectKeyFrame.Value>
                                            </DiscreteObjectKeyFrame>
                                        </ObjectAnimationUsingKeyFrames>
                                        <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(Border.CornerRadius)" Storyboard.TargetName="glow">
                                            <DiscreteObjectKeyFrame KeyTime="0">
                                                <DiscreteObjectKeyFrame.Value>
                                                    <CornerRadius>2</CornerRadius>
                                                </DiscreteObjectKeyFrame.Value>
                                            </DiscreteObjectKeyFrame>
                                        </ObjectAnimationUsingKeyFrames>
                                        <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(Border.CornerRadius)" Storyboard.TargetName="shine">
                                            <DiscreteObjectKeyFrame KeyTime="0">
                                                <DiscreteObjectKeyFrame.Value>
                                                    <CornerRadius>2</CornerRadius>
                                                </DiscreteObjectKeyFrame.Value>
                                            </DiscreteObjectKeyFrame>
                                        </ObjectAnimationUsingKeyFrames>
                                        <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(FrameworkElement.Margin)" Storyboard.TargetName="DayNumberBlock">
                                            <DiscreteObjectKeyFrame KeyTime="0">
                                                <DiscreteObjectKeyFrame.Value>
                                                    <Thickness>0,0,2,2</Thickness>
                                                </DiscreteObjectKeyFrame.Value>
                                            </DiscreteObjectKeyFrame>
                                        </ObjectAnimationUsingKeyFrames>
                                    </Storyboard>
                                </VisualState>
                                <VisualState x:Name="Filled"/>
                            </VisualStateGroup>
                        </VisualStateManager.VisualStateGroups>
                        <Border x:Name="border1" BorderThickness="1,1,1,1" CornerRadius="30">
                            <Border x:Name="border" BorderBrush="#FF000000" BorderThickness="1,1,1,1" CornerRadius="30">
                                <Border.Background>
                                    <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
                                        <GradientStop Color="#FF17099E"/>
                                        <GradientStop Color="#FF0B1579" Offset="1"/>
                                        <GradientStop Color="#FF4542A1" Offset="0.4"/>
                                        <GradientStop Color="#FF2E32DE" Offset="0.7"/>
                                    </LinearGradientBrush>
                                </Border.Background>
                                <Grid x:Name="grid">
                                    <Grid.RowDefinitions>
                                        <RowDefinition Height="0.5*"/>
                                        <RowDefinition Height="0.5*"/>
                                    </Grid.RowDefinitions>
                                    <Border Opacity="0" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" x:Name="glow" Grid.RowSpan="2" CornerRadius="30">
                                        <Border.Background>
                                            <LinearGradientBrush>
                                                <GradientStop Color="#B22072E4" Offset="0"/>
                                                <GradientStop Color="#001733E6" Offset="1"/>
                                                <GradientStop Color="#5A000002" Offset="0.4"/>
                                                <GradientStop Color="#472844AC" Offset="0.7"/>
                                            </LinearGradientBrush>
                                        </Border.Background>
                                    </Border>
                                    <Border VerticalAlignment="Stretch" HorizontalAlignment="Stretch" 
                                            Margin="0,0,0,0" x:Name="shine" CornerRadius="30" Grid.RowSpan="2">
                                        <Border.Background>
                                            <LinearGradientBrush EndPoint="0.5,0.9" StartPoint="0.5,0.0">
                                                <GradientStop Color="#7F85A2CA" Offset="0"/>
                                                <GradientStop Color="#7F1D1577" Offset="1"/>
                                                <GradientStop Color="#7FA6BCDB" Offset="0.4"/>
                                                <GradientStop Color="#7F8196C1" Offset="0.6"/>
                                                <GradientStop Color="#7F314D7A" Offset="0.8"/>
                                                <GradientStop Color="#7FB5C9E6" Offset="0.2"/>
                                            </LinearGradientBrush>
                                        </Border.Background>
                                    </Border>
                                    <TextBlock 
                                        x:Name="DayNumberBlock"
                                        Text="{Binding DayNumber, RelativeSource={RelativeSource Mode=TemplatedParent}}" 
                                        Foreground="White" 
                                        FontWeight="ExtraBold"
                                        FontSize="40"
                                        HorizontalAlignment="Right" 
                                        VerticalAlignment="Bottom" 
                                        Margin="0,0,10,10"
                                        Grid.Row="1"/>
                                </Grid>
                            </Border>
                        </Border>
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

 

The same I do with the calendar.  I go an extra mile there though, for example changing month name from full to abbreviated name, which I do in code behind, but in the same property – year and month label.

        private string GetMonthName()
        {
            if(_applicationViewState == ApplicationViewState.Snapped)
            {
                return _dateTimeFormatInfo.AbbreviatedMonthNames[_month - 1];
            }
            return _dateTimeFormatInfo.MonthNames[_month - 1];
        }

Effective and easy.

So, what are the benefits od designing a calendar control?  The main is that you separate styling from functionality.  In the same apple that is included with source code, for example, I restyle just calendar item control, adding some custom data.  To do that, you can just add alternate style your style sheet, in this case I am just adding it straight to App.xaml

<Application
    x:Class="Win8CalendarDemo.App"
    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" mc:Ignorable="d"
    xmlns:local="using:Win8CalendarDemo" xmlns:Win8Controls="using:Win8Controls">

    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>

                <!-- 
                    Styles that define common aspects of the platform look and feel
                    Required by Visual Studio project and item templates
                 -->
                <ResourceDictionary Source="Common/StandardStyles.xaml"/>
            </ResourceDictionary.MergedDictionaries>

            <Style TargetType="Win8Controls:CalendarItem">
                <Setter Property="Foreground"  Value="White"/>
                <Setter Property="Template">
                    <Setter.Value>
                        <ControlTemplate  TargetType="Win8Controls:CalendarItem">
                            <Grid x:Name="root" d:DesignWidth="272" d:DesignHeight="246" Margin="3,3,3,5">
                                <VisualStateManager.VisualStateGroups>
                                    <VisualStateGroup x:Name="Orientation">
                                        <VisualState x:Name="FullScreenLandscape"/>
                                        <VisualState x:Name="FullScreenPortrait"/>
                                        <VisualState x:Name="Snapped">
                                            <Storyboard>
                                                <DoubleAnimation Duration="0" To="10" Storyboard.TargetProperty="(TextBlock.FontSize)" Storyboard.TargetName="DayNumberBlock" d:IsOptimized="True"/>
                                                <DoubleAnimation Duration="0" To="10" Storyboard.TargetProperty="(TextBlock.FontSize)" Storyboard.TargetName="ExtraText" d:IsOptimized="True"/>
                                                <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(Border.CornerRadius)" Storyboard.TargetName="border1">
                                                    <DiscreteObjectKeyFrame KeyTime="0">
                                                        <DiscreteObjectKeyFrame.Value>
                                                            <CornerRadius>2</CornerRadius>
                                                        </DiscreteObjectKeyFrame.Value>
                                                    </DiscreteObjectKeyFrame>
                                                </ObjectAnimationUsingKeyFrames>
                                                <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(Border.CornerRadius)" Storyboard.TargetName="border">
                                                    <DiscreteObjectKeyFrame KeyTime="0">
                                                        <DiscreteObjectKeyFrame.Value>
                                                            <CornerRadius>2</CornerRadius>
                                                        </DiscreteObjectKeyFrame.Value>
                                                    </DiscreteObjectKeyFrame>
                                                </ObjectAnimationUsingKeyFrames>
                                                <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(Border.CornerRadius)" Storyboard.TargetName="glow">
                                                    <DiscreteObjectKeyFrame KeyTime="0">
                                                        <DiscreteObjectKeyFrame.Value>
                                                            <CornerRadius>2</CornerRadius>
                                                        </DiscreteObjectKeyFrame.Value>
                                                    </DiscreteObjectKeyFrame>
                                                </ObjectAnimationUsingKeyFrames>
                                                <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(Border.CornerRadius)" Storyboard.TargetName="shine">
                                                    <DiscreteObjectKeyFrame KeyTime="0">
                                                        <DiscreteObjectKeyFrame.Value>
                                                            <CornerRadius>2</CornerRadius>
                                                        </DiscreteObjectKeyFrame.Value>
                                                    </DiscreteObjectKeyFrame>
                                                </ObjectAnimationUsingKeyFrames>
                                                <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(FrameworkElement.Margin)" Storyboard.TargetName="DayNumberBlock">
                                                    <DiscreteObjectKeyFrame KeyTime="0">
                                                        <DiscreteObjectKeyFrame.Value>
                                                            <Thickness>0,0,2,2</Thickness>
                                                        </DiscreteObjectKeyFrame.Value>
                                                    </DiscreteObjectKeyFrame>
                                                </ObjectAnimationUsingKeyFrames>
                                            </Storyboard>
                                        </VisualState>
                                        <VisualState x:Name="Filled"/>
                                    </VisualStateGroup>
                                </VisualStateManager.VisualStateGroups>
                                <Border x:Name="border1" BorderThickness="1,1,1,1" CornerRadius="30">
                                    <Border x:Name="border" BorderBrush="#FF000000" BorderThickness="1,1,1,1" CornerRadius="30">
                                        <Border.Background>
                                            <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
                                                <GradientStop Color="#FF17099E"/>
                                                <GradientStop Color="#FF0B1579" Offset="1"/>
                                                <GradientStop Color="#FF4542A1" Offset="0.4"/>
                                                <GradientStop Color="#FF2E32DE" Offset="0.7"/>
                                            </LinearGradientBrush>
                                        </Border.Background>
                                        <Grid x:Name="grid">
                                            <Grid.RowDefinitions>
                                                <RowDefinition Height="0.5*"/>
                                                <RowDefinition Height="0.5*"/>
                                            </Grid.RowDefinitions>
                                            <Border Opacity="0" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" x:Name="glow" Grid.RowSpan="2" CornerRadius="30">
                                                <Border.Background>
                                                    <LinearGradientBrush>
                                                        <GradientStop Color="#B22072E4" Offset="0"/>
                                                        <GradientStop Color="#001733E6" Offset="1"/>
                                                        <GradientStop Color="#5A000002" Offset="0.4"/>
                                                        <GradientStop Color="#472844AC" Offset="0.7"/>
                                                    </LinearGradientBrush>
                                                </Border.Background>
                                            </Border>
                                            <Border VerticalAlignment="Stretch" HorizontalAlignment="Stretch" 
                                            Margin="0,0,0,0" x:Name="shine" CornerRadius="30" Grid.RowSpan="2">
                                                <Border.Background>
                                                    <LinearGradientBrush EndPoint="0.5,0.9" StartPoint="0.5,0.0">
                                                        <GradientStop Color="#7F85A2CA" Offset="0"/>
                                                        <GradientStop Color="#7F1D1577" Offset="1"/>
                                                        <GradientStop Color="#7FA6BCDB" Offset="0.4"/>
                                                        <GradientStop Color="#7F8196C1" Offset="0.6"/>
                                                        <GradientStop Color="#7F314D7A" Offset="0.8"/>
                                                        <GradientStop Color="#7FB5C9E6" Offset="0.2"/>
                                                    </LinearGradientBrush>
                                                </Border.Background>
                                            </Border>
                                            <TextBlock 
                                                x:Name="DayNumberBlock"
                                                Text="{Binding DayNumber, RelativeSource={RelativeSource Mode=TemplatedParent}}" 
                                                Foreground="White" 
                                                FontWeight="ExtraBold"
                                                FontSize="40"
                                                HorizontalAlignment="Right" 
                                                VerticalAlignment="Bottom" 
                                                Margin="0,0,10,10"
                                                Grid.Row="1"/>
                                            <TextBlock 
                                                x:Name="ExtraText"
                                                Text="{Binding RandomTest}" 
                                                Foreground="Red" 
                                                FontWeight="ExtraBold"
                                                FontSize="30"
                                                HorizontalAlignment="Left" 
                                                VerticalAlignment="Top" 
                                                Margin="0,0,10,10"
                                                Grid.Row="0" Grid.RowSpan="2"/>
                                        </Grid>
                                    </Border>
                                </Border>
                            </Grid>
                        </ControlTemplate>
                    </Setter.Value>
                </Setter>
            </Style>
        </ResourceDictionary>

    </Application.Resources>
</Application>

This code also relies in another feature of calendar control – ability specify custom data with dates.  Once you do that, the control will find the matching date in dates source and assign this object as DataContext to the matching calendar item.  This way you can fully customize the look and the content for each day.

        <win8Controls:Calendar 
            x:Name="Calendar"
            DatePropertyNameForDatesSource="CurrentDate" 
            DatesSource="{Binding Source={StaticResource ViewModel}, Path=TestItems}"/>

If you look inside the view model, you will see an observable collection of custom objects that contain CurrentDate property.  I just have to let the calendar control know about the property name.

        public ViewModel()
        {
            TestItems = new ObservableCollection<TestItem>(
                new TestItem[]
                    {
                        new TestItem { RandomTest = "Item1", CurrentDate = DateTime.Today.AddDays(1) },
                        new TestItem { RandomTest = "Item2", CurrentDate = DateTime.Today.AddDays(2) },
                        new TestItem { RandomTest = "Item3", CurrentDate = DateTime.Today.AddDays(3) }
                    }
                );
        }

        private ObservableCollection<TestItem> _testItems;

        public ObservableCollection<TestItem> TestItems
        {
            get { return _testItems; }
            set { _testItems = value; OnPropertyChanged();}
        }
    public class TestItem : INotifyPropertyChanged
    {

        private string _randomText = string.Empty;

        public string RandomTest
        {
            get { return _randomText; }
            set { _randomText = value; OnPropertyChanged(); }
        }

        private DateTime _currentDate;

        public DateTime CurrentDate
        {
            get { return _currentDate; }
            set { _currentDate = value; OnPropertyChanged();}
        }



        public event PropertyChangedEventHandler PropertyChanged;

        protected void OnPropertyChanged([CallerMemberName] string propertyName = "")
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    }

You can look into a demo project include with the download off CodePlex for this code. 

Enjoy and let me know if you would like any features added.

9 Comments

  1. hi sergey … I’m very newbie in programming, I am very interested in this article / tutorial … and this really helped me for develop my application, but can you help me how if I want to change the color for a specific date (special date)?

    i’m waiting for your response.. 😀

    thank’s before..
    EC

  2. Hey,

    I’ve got this implemented in a project of mine, but I’m trying to style the selected date without much luck. I see that there’s an option to ShowSelectedDate, but it doesn’t seem to do anything. I’ve tried adding a Selected VisualState, but when I select a date (by clicking) it doesn’t seem to actually select the box, though I do get the update in my ViewModel. Any ideas?

    Thanks,
    Xander Dumaine

    • You would need to have a custom template likely. I changed my thought process going from WP to allow for more and cleaner customization, hence item template would allow you to do anything. Is this not working for you? Do you have a sample of what you are trying to achieve?

  3. Hi Sergey,
    I am really impressed with your Calendar Control. One request from my side can you please create a weekly view for this control, similar to Calendar app in Win 8.
    Thanks
    Ahmed Sayed

  4. @kris,
    It is just a visual control, it is up to you what you want to show on it. If you want to add your own events or anything else, just edit the default template or if you are happy with colors, use it as is.

Leave a Reply to Sergey Barskiy Cancel reply

Your email address will not be published. Required fields are marked *