Friday, June 7, 2013

Windows Store App - Calendar Control (XAML)

Recently I have been researching and playing around with the Windows Store App development for the Windows 8 and RT. I am used to develop enterprise solution or business application and I thought of may be creating something useful that can be used with a tablet PC. When I was playing with the Windows Store App UI and the available controls, I realize that the most important or commonly used controls such as Calendar, DateTime Picker, GridView, etc are not available in Windows Store App. :(

Therefore, I have no choice but to create my own Calendar control. And, yeah, I know there are 3rd party Calendar controls available such as Telerik but I just want to try to create one by my own. I made it and today I want to share about the making of custom Calendar control in XAML for Windows Store App. This is how my Calendar control look like:



Concept

The logic of the calendar construction is to construct the previous month date boxes first. If the last day of the previous month is on Saturday (last day of the week), then skip making date boxes, otherwise create number of boxes until before the first day of current month.

Then, start appending the current month date boxes and if the row contain 7 boxes, then create a new row and then append more boxes until the last day of the month.

Finally, append the remaining boxes start with the first day of next month until the last day of the week.


Implementation

So, how to implement the above concept?
First, create a UserControl for the Calendar control and design the grid in such way:

<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*" />
        <ColumnDefinition Width="*" />
        <ColumnDefinition Width="*" />
    </Grid.ColumnDefinitions>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />
        <RowDefinition Height="*" />
    </Grid.RowDefinitions>
   
    <!-- Header -->
    <Rectangle Grid.Row="0" Grid.ColumnSpan="3"
                Style="{StaticResource CalendarHeaderBox}" />
    <TextBlock Grid.Column="1" Name="CurrentDateText"
                Style="{StaticResource CalendarHeader}" />
   
    <!-- Navigation Button -->
    <Button Name="PreviousButton" Grid.Column="0" Content="&lt;"
            HorizontalAlignment="Left" Margin="20"
            Tapped="PreviousButton_Tapped" />
    <Button Name="NextButton" Grid.Column="2" Content="&gt;"
            HorizontalAlignment="Right" Margin="20"
            Tapped="NextButton_Tapped" />
   
    <!-- Calendar Grid -->
    <Grid Grid.Row="1" Grid.ColumnSpan="3" Name="CalendarGrid">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
    </Grid>

</Grid>


And then, create another UserControl for the individual box of the day.

<Grid Name="ItemBox" Style="{StaticResource CalendarItemBox}">
    <Rectangle Stroke="Gainsboro" StrokeThickness="1" ></Rectangle>
    <TextBlock Name="ItemValue" Text="1" Style="{StaticResource CalendarItem}" />

</Grid>


This box user control contain very simple logic which is to display the box background color and text after the user control is loaded.

private void UserControl_Loaded(object sender, RoutedEventArgs e)
{
    ItemValue.Text = this.Text;
    ItemBox.Style = this.GridStyle;

    if (this.Value.CompareTo(DateTime.Today) == 0)
        ItemBox.Background = new SolidColorBrush(Colors.Orange);

}


Now back to the Calendar user control, create a method to display the day of the week (the blue color header boxes as you see from above screenshot). I will just skip the detail, and below is the code snippet of my logic to form the calendar with the boxes (user controls). If you want to see the full detail, scroll down to the end of this post and download my source code.

The calendar formation is divided into 3 parts: Previous Month + Current Month + Next Month.

Before that, I need to create a new event handler to handle the value change after the user click at the the box. Also, I need to create the properties to store the current calendar viewing month value and the selected date value.

public DateTime CurrentDate { get; set; }
public DateTime SelectedDate { get; set; }

private event EventHandler<TappedRoutedEventArgs> _selectionChange;
public event EventHandler<TappedRoutedEventArgs> SelectionChange
{
    add
    {
        _selectionChange += value;
    }
    remove
    {
        _selectionChange -= value;
    }
}

public void OnSelectionChange(object sender, TappedRoutedEventArgs e)
{
    if (_selectionChange != null)
        _selectionChange(sender, e);

}

Next, create the methods to form the Calendar.

This method is to construct the previous month calendar.
private void InitializePreviousMonthBoxes()
{
    DateTime previousMonthDate = this.CurrentDate.AddMonths(-1);
    DateTime previousMonthDateIteration = new DateTime(previousMonthDate.Year, previousMonthDate.Month, DateTime.DaysInMonth(previousMonthDate.Year, previousMonthDate.Month));
    CalendarGrid.RowDefinitions.Add(new RowDefinition() { Height = new GridLength(1, GridUnitType.Star) });

    for (int dayOfWeek = (int)previousMonthDateIteration.DayOfWeek; dayOfWeek >= 0; dayOfWeek--)
    {

        CalendarItem item = new CalendarItem(previousMonthDateIteration, previousMonthDateIteration.Day.ToString(), Application.Current.Resources["CalendarOtherMonthItemBox"] as Style);
        item.PointerEntered += (sender, args) =>
        {
            ((CalendarItem)sender).GridStyle = Application.Current.Resources["CalendarMouseOverItemBox"] as Style;
        };

        item.PointerExited += (sender, args) =>
        {
            if (((CalendarItem)sender).Value == this.SelectedDate)
                ((CalendarItem)sender).GridStyle = Application.Current.Resources["CalendarSelectedItemBox"] as Style;
            else
                ((CalendarItem)sender).GridStyle = Application.Current.Resources["CalendarOtherMonthItemBox"] as Style;
        };

        //delegate the tapped event to selection change event
        item.Tapped += (sender, args) =>
        {
            //update the selected date value
            this.SelectedDate = ((CalendarItem)sender).Value;
            this.CurrentDate = this.CurrentDate.AddMonths(-1);
            InitializeCalendar();

            OnSelectionChange(sender, args);
        };

        item.SetValue(Grid.RowProperty, 1);
        item.SetValue(Grid.ColumnProperty, dayOfWeek);

        CalendarGrid.Children.Add(item);

        previousMonthDateIteration = previousMonthDateIteration.AddDays(-1);
    }

}


This method is to construct current month calendar.

private void InitializeCurrentMonthBoxes()
{
    int row = 1;
    int maxDay = DateTime.DaysInMonth(this.CurrentDate.Year, this.CurrentDate.Month);
    for (int day = 1; day <= maxDay; day++)
    {
        DateTime dateIteration = new DateTime(this.CurrentDate.Year, this.CurrentDate.Month, day);
        int dayOfWeek = (int)dateIteration.DayOfWeek;

        CalendarItem item = new CalendarItem(dateIteration, day.ToString(), Application.Current.Resources["CalendarItemBox"] as Style);
        item.PointerEntered += (sender, args) =>
        {
            ((CalendarItem)sender).GridStyle = Application.Current.Resources["CalendarMouseOverItemBox"] as Style;
        };

        item.PointerExited += (sender, args) =>
        {
            if (((CalendarItem)sender).Value == this.SelectedDate)
                ((CalendarItem)sender).GridStyle = Application.Current.Resources["CalendarSelectedItemBox"] as Style;
            else
                ((CalendarItem)sender).GridStyle = Application.Current.Resources["CalendarItemBox"] as Style;
        };

        //delegate the tapped event to selection change event
        item.Tapped += (sender, args) =>
        {
            //get the box day value
            ((CalendarItem)sender).GridStyle = Application.Current.Resources["CalendarSelectedItemBox"] as Style;

            //get the box before selected value and reset the color
            var selectedItem = (CalendarItem)CalendarGrid.Children.Single(x =>
                                    x.GetType() == typeof(CalendarItem) &&
                                    ((CalendarItem)x).Value == this.SelectedDate);

            selectedItem.GridStyle = Application.Current.Resources["CalendarItemBox"] as Style;

            //update the selected date value
            this.SelectedDate = ((CalendarItem)sender).Value;

            OnSelectionChange(sender, args);
        };

        //highlight selected date
        if (this.SelectedDate.CompareTo(dateIteration) == 0)
            item.GridStyle = Application.Current.Resources["CalendarSelectedItemBox"] as Style;

        item.SetValue(Grid.RowProperty, row);
        item.SetValue(Grid.ColumnProperty, dayOfWeek);

        CalendarGrid.Children.Add(item);

        if (dayOfWeek == 6 && day != maxDay)
        {
            row++;
            CalendarGrid.RowDefinitions.Add(new RowDefinition() { Height = new GridLength(1, GridUnitType.Star) });
        }
    }

}

This method is to construct next month calendar.

private void InitializeNextMonthBoxes()
{
    DateTime nextMonthDate = this.CurrentDate.AddMonths(1);
    DateTime nextMonthDateIteration = new DateTime(nextMonthDate.Year, nextMonthDate.Month, 1);

    int lastRow = CalendarGrid.RowDefinitions.Count - 1;

    if (nextMonthDateIteration.DayOfWeek != DayOfWeek.Sunday)
        for (int dayOfWeek = (int)nextMonthDateIteration.DayOfWeek; dayOfWeek < 7; dayOfWeek++)
        {
            CalendarItem item = new CalendarItem(nextMonthDateIteration, nextMonthDateIteration.Day.ToString(), Application.Current.Resources["CalendarOtherMonthItemBox"] as Style);
            item.PointerEntered += (sender, args) =>
            {
                ((CalendarItem)sender).GridStyle = Application.Current.Resources["CalendarMouseOverItemBox"] as Style;
            };

            item.PointerExited += (sender, args) =>
            {
                if (((CalendarItem)sender).Value == this.SelectedDate)
                    ((CalendarItem)sender).GridStyle = Application.Current.Resources["CalendarSelectedItemBox"] as Style;
                else
                    ((CalendarItem)sender).GridStyle = Application.Current.Resources["CalendarOtherMonthItemBox"] as Style;
            };

            //delegate the tapped event to selection change event
            item.Tapped += (sender, args) =>
            {
                //update the selected date value
                this.SelectedDate = ((CalendarItem)sender).Value;
                this.CurrentDate = this.CurrentDate.AddMonths(1);
                InitializeCalendar();

                OnSelectionChange(sender, args);
            };

            item.SetValue(Grid.RowProperty, lastRow);
            item.SetValue(Grid.ColumnProperty, dayOfWeek);

            CalendarGrid.Children.Add(item);

            nextMonthDateIteration = nextMonthDateIteration.AddDays(1);
        }

}

In the end, you need to execute these above 3 methods to form a complete calendar of the month. Finally, use the above custom control in the page like this with correct namespace:

<Page
    x:Class="CalendarControl.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:CalendarControl"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:custom="using:CalendarControl.CustomControl"
    mc:Ignorable="d">

    <Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <TextBox Grid.Row="0" Name="SelectedDateText"
                 Width="1024" Margin="10" />
        <custom:Calendar Grid.Row="1" x:Name="MyCalendar"
                            Width="1024"
                            Height="768"
                            HorizontalAlignment="Center"
                            VerticalAlignment="Center"
                            Margin="10" SelectionChange="Calendar_SelectionChange" />
    </Grid>

</Page>


Then, how to get the selected date value? Just use the following code.

private void Calendar_SelectionChange(object sender, TappedRoutedEventArgs e)
{
    //if you get the out of context error, make sure to use the namespace x:
    //in your XAML - x:Name
    SelectedDateText.Text = MyCalendar.SelectedDate.ToString("yyyy-MM-dd");

}


If you are interested with my source code, feel free to download from HERE.
Customize the Calendar for your own purpose and please let me know or drop me a comment if you detect any bug with the Calendar control.

My next post is going to be DateTime Picker for Windows Store App (XAML). Stay tuned.




4 comments:

  1. This looks good. I am going to try it out. Did you also have a week view in this calendar? where we can schedule appointments at differetn times of the day.

    ReplyDelete
    Replies
    1. Thanks, but sorry to inform you that I have not further extend this control yet. Anyway, feel free to modify the code to suit your need.

      Delete
  2. I think there is a bug in the following piece of code (the InitializeCurrentMonthBoxes method in Calendar.xaml.ca):

    //get the box before selected value and reset the color
    var selectedItem = (CalendarItem)CalendarGrid.Children.Single(x =>
    x.GetType() == typeof(CalendarItem) &&
    ((CalendarItem)x).Value == this.SelectedDate);
    selectedItem.GridStyle = Application.Current.Resources["CalendarItemBox"] as Style;

    It manifests itself by throwing an exception when a user clicks a day in the calendar. Follow these steps:
    - Click any day in the calendar different than the current day.
    - Go to the next month.
    - Click any day again. Note that the selected day is not on the screen any more.

    A solution would be to take into account that the selected day may not be displayed on the screen:

    //get the box before selected value and reset the color
    // BUG FIX: changed Single to SingleOrDefault and added the null check.
    var selectedItem = (CalendarItem)CalendarGrid.Children.SingleOrDefault(x =>
    x.GetType() == typeof(CalendarItem) &&
    ((CalendarItem)x).Value == this.SelectedDate);
    if (selectedItem != null)
    selectedItem.GridStyle = Application.Current.Resources["CalendarItemBox"] as Style;


    Is that correct or am I missing something?

    ReplyDelete

Send Transactional SMS with API

This post cover how to send transactional SMS using the Alibaba Cloud Short Message Service API. Transactional SMS usually come with One Tim...