Welcome to the second and final post in my blog series "Creating a calendar control in Xamarin Forms". If you have not read Part 1 where I discussed how to create the base Xamarin.Form shell project I used to create the control you can find it here
If you have not completed or want to just start from part 2 you can find a starter project here
Note this control is built in a mvvm style application, if mvvm is not something you have seen before you can read about it here
Create the View Models
Base View Model
The first view model to create is a view model that will be used as the base view model that will contain plumbing code to implement INotifyPropertyChanged. In this post I am not diving in to the depths of this but I will just say that this is needed so the UI will update when a property you have "bound" to is changed, you will see how to do this in the other view models.
Lets create a new class named BaseViewModel.cs
in the ViewModels folder with the following code you might need to update the namepsace if you have not named your project the same as the starter project (if the view models folder in missing because you downloaded the starter project mention above, just create it at the same level as the views folder).
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace CalendarControl.ViewModels
{
public class BaseViewModel : INotifyPropertyChanged
{
string title = string.Empty;
public string Title
{
get { return title; }
set { SetProperty(ref title, value); }
}
protected bool SetProperty<T>(ref T backingStore, T value, [CallerMemberName] string propertyName = "", Action onChanged = null)
{
if (EqualityComparer<T>.Default.Equals(backingStore, value))
return false;
backingStore = value;
onChanged?.Invoke();
OnPropertyChanged(propertyName);
return true;
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = "")
{
var changed = PropertyChanged;
if (changed == null)
return;
changed.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Calendar Item View Model
The next view model to create is CalendarItemViewModel.cs
with the following code. Every part of the calendar in the control will be made up of these view models. I will dive in to this view model a little more to provide some context but I won't be doing this for other view model.
using System;
namespace CalendarControl.ViewModels
{
public class CalendarItemViewModel : BaseViewModel
{
private string _type;
public DateTime Date { get; }
public string Label { get; }
public string Type
{
get => _type;
set
{
SetProperty(ref _type, value);
}
}
public CalendarItemViewModel(DateTime date)
{
Date = date.Date;
Type = "date";
Label = Date.Day.ToString();
}
public CalendarItemViewModel(string label = " ", string type = "empty")
{
Type = type;
Label = label;
}
}
}
Lets pay attention to some parts of this view model.
public class CalendarItemViewModel : BaseViewModel
This view model inherits from BaseViewModel
so that you can use the plumbing code provided in it.
There are a number of properties defined in this view model.
- Type The type property is used to defined how the calendar item should be shown or interacted with within the control.
- Date: The date is required for when the calendar item is defined as a
date
type. - Label: The label property is used when the calendar item needs to display some text on the screen.
Looking at all the properties you will notice that Type is implemented differently to the other properties. This is because when Type is updated we want the UI to update and deal with the change, this achieved by calling the SetProperty(ref ..., value)
method that is inherited from the BaseViewModel. SetProperty requires a field so this property is using a field named _type and you pass this backing field to the SetProperty method.
Calendar View Model
This view model is used to control all the elements in the calendar control, I won't be diving in to this view model unlike the previous but it is commented heavily to provide context for each element. Create a new view model named CalendarViewModel.cs
in the view models folder with the following code.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Input;
using Xamarin.CommunityToolkit.ObjectModel;
using Xamarin.Forms;
namespace CalendarControl.ViewModels
{
public class CalendarViewModel : BaseViewModel
{
//Backing fields
private CalendarItemViewModel _selectedItem;
private CalendarItemViewModel _previousSelectedItem;
private CalendarItemViewModel _selectedDate;
private DateTime _calendarMonth;
//Property to show the current month
public string MonthText => _calendarMonth.ToString("MMMM yyyy");
//Commands used to change between months
public ICommand PreviousMonth { get; }
public ICommand NextMonth { get; }
//The current datetime object for the month the control is showing
public DateTime CalendarMonth
{
get => _calendarMonth;
set
{
SetProperty(ref _calendarMonth, value);
//As the month has been changed you need to tell the UI that the MonthText property has also changed
OnPropertyChanged(nameof(MonthText));
//Lets create all the items month selected
CreateItemsForMonth(CalendarMonth);
}
}
//The command to bind to for when the selected item is changed
public ICommand SelectedItemChangedCommand { get; set; }
//A property to show how you can use a date when it is selected. In this example this just shown in a label
//but it be that you show a list of appointments on the selected date etc.
public CalendarItemViewModel SelectedDate
{
get => _selectedDate;
set
{
SetProperty(ref _selectedDate, value);
}
}
//The property that is set by the view when a user selects in the calendar control
public CalendarItemViewModel SelectedItem
{
get => _selectedItem;
set
{
//if the selects the same item just return (nothing to be done)
if (_selectedItem == value) return;
//if the item has the type or empty or header lets not actually do anything
if (value.Type == "empty" || value.Type == "header") return;
//If we get here it must be a date so set the selected date
SelectedDate = value;
if (_selectedItem != null)
{
//if this is not the first time the user has selected something then lets set _previousSelectedItem before updating the backing field
_previousSelectedItem = _selectedItem;
}
SetProperty(ref _selectedItem, value);
}
}
//This Collection is used to store all items in the calendar control
public ObservableRangeCollection<CalendarItemViewModel> Items { get; set; }
public CalendarViewModel()
{
SelectedItemChangedCommand = new Command(() => {
ItemChangedCommand();
});
Items = new ObservableRangeCollection<CalendarItemViewModel>();
//On initialistion lets set the calendar month to the current date
CalendarMonth = DateTime.Now;
//Setup the previous and next month commands by just adding or subtracting a month from the calendar month property
PreviousMonth = new Command(() => { CalendarMonth = CalendarMonth.AddMonths(-1); CheckSelected(); });
NextMonth = new Command(() => { CalendarMonth = CalendarMonth.AddMonths(1); CheckSelected(); });
}
//Method to create all items for the control
public void CreateItemsForMonth(DateTime date)
{
//lets start by creating the header row
var items = new List<CalendarItemViewModel>
{
new CalendarItemViewModel("S", "header"),
new CalendarItemViewModel("M", "header"),
new CalendarItemViewModel("T", "header"),
new CalendarItemViewModel("W", "header"),
new CalendarItemViewModel("T", "header"),
new CalendarItemViewModel("F", "header"),
new CalendarItemViewModel("S", "header")
};
//default to first of the month for the date past in
var dateToCalc = new DateTime(date.Year, date.Month, 1);
//This is done to add blank items for when the month does not start on a Sunday to the items align with the header
if (dateToCalc.DayOfWeek != DayOfWeek.Sunday)
{
for (var i = 0; i < (int)dateToCalc.DayOfWeek; i++)
{
items.Add(new CalendarItemViewModel());
}
}
//This loop adds an item for every day of the current month selected
var currentMonth = date.Month;
while (dateToCalc.Month == currentMonth)
{
var day = new CalendarItemViewModel(dateToCalc);
//if the date for the item it is creating then flag it as today so it can be styled differently
if (day.Date.Date == DateTime.Now.Date) day.Type = "today";
items.Add(day);
dateToCalc = dateToCalc.AddDays(1);
}
//Replace Range is used so only one call to update the UI is called
Items.ReplaceRange(items);
}
//When the item is changed then we need to update the previous items type to no longer be selected and the new Item type to to be selected
public void ItemChangedCommand()
{
if (_previousSelectedItem != null)
{
var item = Items.FirstOrDefault(x => x == _previousSelectedItem);
if (item != null)
{
item.Type = DateTime.Now.Date == _previousSelectedItem.Date.Date ? "today" : "date";
}
}
var selectedItem = Items.FirstOrDefault(x => x == _selectedItem);
if (selectedItem != null)
{
selectedItem.Type = "selected";
}
}
//On changing the month lets check if the current month contains the selected date so it can be set up correctly
private void CheckSelected()
{
if (SelectedDate != null)
{
var d = Items.FirstOrDefault(x => x.Date == SelectedDate.Date);
if (d != null)
{
SelectedItem = d;
}
}
}
}
}
Lets Create some converters
The last thing that needs to be created before we actually update the view is a converter that is used to select the right style that you define for each item type available.
Lets create CalendarItemStyleConverter.cs
in the converters folder (again create this folder at the same level as the views folder if it doesn't not exist) with the following code. If you have not seen a IValueConverter
you can find the docs for it here
using System;
using System.Globalization;
using Xamarin.Forms;
namespace CalendarControl.Converters
{
public class CalendarItemStyleConverter : IValueConverter
{
public Style DayStyle { get; set; }
public Style HeaderStyle { get; set; }
public Style TodayStyle { get; set; }
public Style SelectedStyle { get; set; }
public Style EmptyStyle { get; set; }
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var type = value as string;
if (type == null) throw new InvalidOperationException();
if (EmptyStyle != null && type == "empty") return EmptyStyle;
if (HeaderStyle != null && type == "header") return HeaderStyle;
if (SelectedStyle != null && type == "selected") return SelectedStyle;
if (TodayStyle != null && type == "today") return TodayStyle;
if (DayStyle != null) return DayStyle;
throw new InvalidOperationException("Item style is required");
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new InvalidOperationException();
}
}
}
Update Calendar Page
Now it is time to update the CalendarPage.xaml
and CalendarPage.xaml.cs
that was created in the starter project.
Lets start by adding the following line BindingContext = new CalendarViewModel();
to below the call to InitializeComponent();
in the constructor inCalendarPage.xaml.cs
. This is saying for this view create a new instance of the CalendarViewModel
and set it as the binding context.
Now lets update CalendarPage.xaml
with the following code.
<?xml version="1.0" encoding="UTF-8"?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:CalendarControl.Converters"
x:Class="CalendarControl.Views.CalendarPage"
Background="{AppThemeBinding Dark=Black, Light=White}">
<ContentPage.Resources>
<!--This is the list of styles that are defined for each type used in the control-->
<Style TargetType="Grid" x:Key="EmptyStyle">
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="{AppThemeBinding Dark=Black, Light=White}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Selected">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="{AppThemeBinding Dark=Black, Light=White}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="Grid" x:Key="SelectedStyle" BasedOn="{StaticResource EmptyStyle}">
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="Green" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="Grid" x:Key="TodayStyle" BasedOn="{StaticResource EmptyStyle}">
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="Fuschia" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<!--This is setting up the converter to use the styles above-->
<local:CalendarItemStyleConverter x:Key="CalendarItemStyleConverter"
DayStyle="{StaticResource EmptyStyle}"
EmptyStyle="{StaticResource EmptyStyle}"
SelectedStyle="{StaticResource SelectedStyle}"
TodayStyle="{StaticResource TodayStyle}"
HeaderStyle="{StaticResource EmptyStyle}"/>
</ContentPage.Resources>
<ContentPage.Content>
<StackLayout>
<!--Set up the top row with the buttons to change the month and label show the current month-->
<Grid ColumnDefinitions="50,*,50">
<Button Text="Prev" Grid.Column="0" Command="{Binding PreviousMonth}"></Button>
<Label Text="{Binding MonthText}" Grid.Column="1" HorizontalTextAlignment="Center"></Label>
<Button Text="Next" Grid.Column="2" Command="{Binding NextMonth}"></Button>
</Grid>
<!--This is the collection view that is the main view for the control, it is setup to be a vertical grid with 7 columns-->
<CollectionView x:Name="LayoutRoot"
SelectionMode="Single"
ItemsSource="{Binding Items}"
ItemsLayout="VerticalGrid, 7"
SelectedItem="{Binding SelectedItem}"
SelectionChangedCommand="{Binding SelectedItemChangedCommand}">
<CollectionView.ItemTemplate>
<DataTemplate>
<!--The Style is bound the item type using the converter above to convert it to a converter-->
<Grid Style="{Binding Type, Converter={StaticResource CalendarItemStyleConverter}}">
<Label Text="{Binding Label}"></Label>
</Grid>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
<!--This is just here to show how you can use the selected date when it is selected from the control above-->
<Label Text="{Binding SelectedDate.Date, StringFormat='{0:dd MMMM yyyy}'}" HorizontalOptions="Center"/>
</StackLayout>
</ContentPage.Content>
</ContentPage>
It is fairly well commented but I just want to point out a few things. If you have never seen how binding works in xaml, lets take a look the following line.
<Button Text="Prev" Grid.Column="0" Command="{Binding PreviousMonth}"></Button>
This button has a command that you can bind to so you can see the command has a value of "{Binding PreviousMonth}"
this tells xaml to bind to a property in the binding context (CalendarViewModel, this was set above in the code behind CalendarPage.xaml.cs
) called PreviousMonth.
Now to note if you want to read any more on some other things like Styles in Xamarin.Forms or CollectionView then check out the Xamarin.Forms documentation that is available at docs.microsoft.com
Time to run the application
If all has gone well you should now be able to the run the application and have a control that looks like the following.
If you want to get a copy of the final code it is available here CalendarControl-FinalProject
Notes
Just to note the final project has an extra page to show the licenses for the open source software used in the application that has not been discussed in this post.