Search code examples
uwpcalendarselectionhighlightcalendarview

UWP: Automatically highlight a whole week in CalendarView


I'm using a CalendarView inside an UWP application. It is being display in the "Month" mode. What I want is, that when the user selects a date, it should somehow highlight the whole week for the selected day.

I was able to get something working with the following code which selects all days of the week after the user has selected a date, but it doesn't look very nice, the event handling detach/attach is cumbersome and error-prone and I believe there might be more fitting options:

private void cvMain_SelectedDatesChanged(CalendarView sender, CalendarViewSelectedDatesChangedEventArgs args)
{
    // detach event handler to prevent an endless loop
    this.cvMain.SelectedDatesChanged -= cvMain_SelectedDatesChanged;
    try
    {
        if (args.AddedDates.Count > 0)
        {
            // get first date of selection
            var addedDateOffset = args.AddedDates[0];
            var userSelectedDate = addedDateOffset.Date;

            // get first day of the selected way and the last day
            var startOfWeek = userSelectedDate.StartOfWeek();
            var endOfWeek = userSelectedDate.EndOfWeek();

            var selectedDatesList = new List<DateTimeOffset>();
            selectedDatesList.Add(startOfWeek);
            for (var dateInBetween = startOfWeek; dateInBetween <= endOfWeek; dateInBetween = dateInBetween.AddDays(1))
            {
                selectedDatesList.Add(dateInBetween);
            }
            selectedDatesList.Add(endOfWeek);

            cvMain.SelectedDates.Clear();
            foreach (var selectedDate in selectedDatesList)
            {
                cvMain.SelectedDates.Add(selectedDate);
            }
        }
    }
    finally
    {
        // reattach event handler
        this.cvMain.SelectedDatesChanged += cvMain_SelectedDatesChanged;
    }
}

I'm not sure if what I want can really be done with the out-of-the-box control, as it seems we cannot access the day items directly and the CalendarViewDayItemChanging event is only run once on Page_Load, not when the selection changes.

Any ideas?


Solution

  • I have thought of a solution but it is not necessarily a good one, so I am just throwing it out here as an alternative :-D .

    I have tried creating a Behavior for this. First you need to have the Behaviors package installed:

    Install-Package Microsoft.Xaml.Behaviors.Uwp.Managed -Version 2.0.0 
    

    Now the behavior looks like this:

    public class WeekHighlightBehavior : Behavior
    {
        public CalendarView CalendarControl
        {
            get { return (CalendarView)GetValue(CalendarProperty); }
            set { SetValue(CalendarProperty, value); }
        }
    
        // Using a DependencyProperty as the backing store for Calendar.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty CalendarProperty =
            DependencyProperty.Register("CalendarControl", typeof(CalendarView), typeof(WeekHighlightBehavior), new PropertyMetadata(0));
    
        protected override void OnAttached()
        {
            base.OnAttached();
            CalendarControl.SelectedDatesChanged += Calendar_SelectedDatesChanged;
        }
    
        private void Calendar_SelectedDatesChanged(CalendarView sender, CalendarViewSelectedDatesChangedEventArgs args)
        {
            var dayItem = this.AssociatedObject as CalendarViewDayItem;
            var calendar = CultureInfo.CurrentUICulture.Calendar;
            bool highlight = false;
            foreach (var date in args.AddedDates)
            {
                if (calendar.GetWeekOfYear(date.DateTime, CalendarWeekRule.FirstDay, DayOfWeek.Monday) ==
                    calendar.GetWeekOfYear(dayItem.Date.DateTime, CalendarWeekRule.FirstDay, DayOfWeek.Monday))
                {
                    highlight = true;
                    break;
                }
            }
    
            if (highlight)
            {
                dayItem.Background = new SolidColorBrush(Colors.Yellow);
            }
            else
            {
                dayItem.Background = null;
            }
        }
    
        protected override void OnDetaching()
        {
            base.OnDetaching();
            CalendarControl.SelectedDatesChanged -= Calendar_SelectedDatesChanged;
        }
    }
    

    The behavior basically observes the CalendarView's SelectedDatesChanged event, and every time it is executed, it checks if the selected date's week of year matches the week of year of the day the behavior is attached to. If yes, we change the background of the item. This will be called for all dates, so all days in the week will definitely be highlighted.

    Unfortunately, it turns out that you can't attach it via a style setter in XAML:

    <CalendarView x:Name="CalendarView">
        <CalendarView.CalendarViewDayItemStyle>
            <Style TargetType="CalendarViewDayItem">
                <Setter Property="interactivity:Interaction.Behaviors">
                    <Setter.Value>
                        <interactivity:BehaviorCollection>
                            <local:WeekHighlightBehavior CalendarControl="{Binding ElementName=CalendarView}" />
                        </interactivity:BehaviorCollection>
                    </Setter.Value>
                </Setter>
                <Setter Property="Padding" Value="0" />
                <Setter Property="Margin" Value="0" />
    
            </Style>
        </CalendarView.CalendarViewDayItemStyle>
    </CalendarView>
    

    This way the behavior is set only once, for the first item that gets created. You can however set the behavior from the code-behind in the CalendarViewDayItemChanging event:

    private void CalendarView_CalendarViewDayItemChanging(CalendarView sender, CalendarViewDayItemChangingEventArgs args)
    {
        var behavior = new WeekHighlightBehavior();
        behavior.CalendarControl = CalendarView;
        Interaction.GetBehaviors(args.Item).Clear();
        Interaction.GetBehaviors(args.Item).Add(behavior);
    }
    

    I am manually clearing the attached behaviors because I have noticed they did not get cleared automatically, because the CalendarView persists the CalendarDayViewItem instances and reuses them as it sees fit.