miken.mkndev
miken.mkndev

Reputation: 1951

UWP CalendarView update on command

I am building a Windows Phone 10 application using Universal Windows Platform. In my app I have a standard CalendarView that I would like to show density colors on for dates that have events. The idea is to load the calendar as soon as the page is loaded, make an API request, and upon successful data retrieval have the CalendarView refresh it's UI so that the CalendarViewDayItemChanging event is called. From there I can set my density colors for the cells that have events.

I have pretty much everything working correctly except for one part. When the calendar first loads I set it's min/max date ranges to the current month so that we only see one month at a time. This causes the calendar's UI to refresh as expected. However, after my API request completes if I try to set the min/max date ranges again, to the same dates, then the calendar does not refresh it's UI. Due to this I have no way to force the CalendarView to refresh it's UI.

I've tried calling UpdateLayout, I've tried to reset the min/max date ranges, and I've tried to bind the calendar's DataContext to an ObservableCollection in my code behind that is updated when my data updates. None of this works and I do not see any method to just update the UI.

I'm pretty new to UWP so am unsure of what I am doing wrong. I know the concept of Data Binding is a big part of UWP, but I am unsure how I would bind my data to this CalendarView so that it refreshes when my data is refreshed. Any suggestions?

Below is a quick excerpt of my code as it stands now.

XAML

<CalendarView 
    Name="Calendar"
    NumberOfWeeksInView="6"
    CalendarViewDayItemChanging="CalendarView_DayItemChanging"
    DataContext="{Binding CalendarDates}">
</CalendarView>

Code-behind

namespace Pages
{
    public sealed partial class CalendarPage : BasePage
    {
        #region Private Variables

        private CalendarPageModel PageModel = new CalendarPageModel();
        private ObservableCollection<DateTime> CalendarDates;

        #endregion

        #region Constructor

        public CalendarPage()
        {
            this.InitializeComponent();
            CalendarDates = new ObservableCollection<DateTime>();
        }

        #endregion

        #region Events

        private void Page_Loaded(object sender, RoutedEventArgs args)
        {
            SetCalendarDateRange(); //NOTE: This is done here so that my UI consistantly shows the correct dates on the screen
            LoadData();
        }

        private void CalendarView_DayItemChanging(CalendarView sender, CalendarViewDayItemChangingEventArgs args)
        {
            if (!PageModel.DateHasEvent(args.Item.Date))
            {
                args.Item.SetDensityColors(null);
            }
            else
            {
                List<Color> colors = new List<Color>();
                Color? color = Application.Current.Resources["CalendarHasEventDensityColor"] as Color?;
                if (color != null)
                {
                    colors.Add((Color)color);
                }

                args.Item.SetDensityColors(colors);
            }
        }

        #endregion

        #region Data

        private void SetCalendarDateRange()
        {
            Calendar.MinDate = PageModel.StartDate;
            Calendar.MaxDate = PageModel.EndDate;
        }

        private async void LoadData()
        {
            // get data
            await PageModel.RefreshData(PageModel.StartDate, PageModel.EndDate);

            // force calendar to update
            //NOTE: This only works if the date range is actually different than what it's currently set to
            SetCalendarDateRange();

            //NOTE: I have tried to just manually add a date to my observable collection to see if it'll kick off the calendar refresh, but it doesn't
            CalendarDates.add(DateTime.Now);
        }

        #endregion
    }
}

Upvotes: 1

Views: 1524

Answers (1)

Matt Lacey
Matt Lacey

Reputation: 65556

The Bad news
The CalendarView control isn't unfortunately designed for this scenario. Because it is optimized for performance when showing a large number of days, it only refreshes the UI when an individual day is loaded.

However...

The good news
It is possible to modify the control to create this behavior but it takes a little bit of work.

The basic principle is to take responsibility for drawing the Density color blocks and bind them to something that can update through bindings.

As an example of this working, add the following to a page

<Page.Resources>
    <local:ColorBrushConverter x:Key="BrushConverter" />
</Page.Resources>
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <StackPanel>
        <CalendarView Name="Calendar"
                      DisplayMode="Month"
                      CalendarViewDayItemChanging="CalendarView_DayItemChanging"
                      >
            <CalendarView.CalendarViewDayItemStyle>
                <Style TargetType="CalendarViewDayItem" >
                    <Setter Property="Template">
                        <Setter.Value>
                            <ControlTemplate TargetType="CalendarViewDayItem">
                                <Grid Opacity="0.5">
                                    <Grid.RowDefinitions>
                                        <RowDefinition Height="*"/>
                                        <RowDefinition Height="*"/>
                                        <RowDefinition Height="*"/>
                                        <RowDefinition Height="*"/>
                                        <RowDefinition Height="*"/>
                                    </Grid.RowDefinitions>
                                    <Rectangle Grid.Row="0" Fill="{Binding FifthColor, Converter={StaticResource BrushConverter}}" />
                                    <Rectangle Grid.Row="1" Fill="{Binding FourthColor, Converter={StaticResource BrushConverter}}" />
                                    <Rectangle Grid.Row="2" Fill="{Binding ThirdColor, Converter={StaticResource BrushConverter}}" />
                                    <Rectangle Grid.Row="3" Fill="{Binding SecondColor, Converter={StaticResource BrushConverter}}" />
                                    <Rectangle Grid.Row="4" Fill="{Binding FirstColor, Converter={StaticResource BrushConverter}}" />
                                </Grid>
                            </ControlTemplate>
                        </Setter.Value>
                    </Setter>
                </Style>
            </CalendarView.CalendarViewDayItemStyle>
        </CalendarView>
        <Button Click="AddEventClicked">Add random event</Button>
    </StackPanel>
</Grid>

And accompanying code behind:

public sealed partial class MainPage : Page
{
    private MyViewModel ViewModel;

    private DateTime today;
    private DateTime minDate;
    private DateTimeOffset maxDate;

    public MainPage()
    {
        this.InitializeComponent();

        // Keep these for reference
        this.today = DateTime.Now.Date;
        this.minDate = new DateTime(today.Year, today.Month, 1);
        this.maxDate = minDate.AddMonths(1);

        // Create our viewmodel
        ViewModel = MyViewModel.Generate(minDate.Date, maxDate.Date);
        Calendar.MinDate = minDate;
        Calendar.MaxDate = maxDate;

        // Add data for the next three days - will be shown when page loads
        ViewModel.Dates[today.AddDays(1)].Add(Colors.Red);
        ViewModel.Dates[today.AddDays(2)].Add(Colors.Purple);
        ViewModel.Dates[today.AddDays(2)].Add(Colors.Blue);
        ViewModel.Dates[today.AddDays(3)].Add(Colors.Green);
    }

    private void CalendarView_DayItemChanging(CalendarView sender, CalendarViewDayItemChangingEventArgs args)
    {
        // When the DayItem in the calendar is loaded
        var itemDate = args?.Item?.Date.Date ?? DateTime.MinValue;

        if (ViewModel.Dates.ContainsKey(itemDate))
        {
            // Set the datacontext for our custom control
            // - Which does support 2way binding :)
            args.Item.DataContext = ViewModel.Dates[itemDate];
        }
    }

    private void AddEventClicked(object sender, RoutedEventArgs e)
    {
        var rand = new Random();
        var randomColor = Color.FromArgb(
                                         255,
                                         (byte) rand.Next(0, 254),
                                         (byte)rand.Next(0, 254),
                                         (byte)rand.Next(0, 254));

        var randomDay = rand.Next(1, 29);
        var randDateInMonth = new DateTime(today.Year, today.Month, randomDay);

        if (ViewModel.Dates.ContainsKey(randDateInMonth))
        {
            ViewModel.Dates[randDateInMonth].Add(randomColor);
        }
    }
}

public class MyViewModel
{
    // The VM really just holds this dictionary
    public Dictionary<DateTime, DensityColors> Dates { get; }

    private MyViewModel()
    {
        this.Dates = new Dictionary<DateTime, DensityColors>();
    }

    // Static constructor to limit the dates and populate dictionary
    public static MyViewModel Generate(DateTime minDate, DateTime maxDate)
    {
        var generated = new MyViewModel();

        for (var i = 0; i < (maxDate - minDate).TotalDays; i++)
        {
            generated.Dates.Add(minDate.AddDays(i), new DensityColors());
        }

        return generated;
    }
}

public class DensityColors : ObservableCollection<Color>, INotifyPropertyChanged
{
    // Properties that expose items in underlying OC
    public Color FirstColor => Items.Any() ? Items.First() : Colors.Transparent;
    public Color SecondColor => Items.Count > 1 ? Items.Skip(1).First() : Colors.Transparent;
    public Color ThirdColor => Items.Count > 2 ? Items.Skip(2).First() : Colors.Transparent;
    public Color FourthColor => Items.Count > 3 ? Items.Skip(3).First() : Colors.Transparent;
    public Color FifthColor => Items.Count > 4 ? Items.Skip(4).First() : Colors.Transparent;

    protected override void InsertItem(int index, Color item)
    {
        base.InsertItem(index, item);

        // Hacky forcing of updating UI for all properties
        OnPropertyChanged(nameof(FirstColor));
        OnPropertyChanged(nameof(SecondColor));
        OnPropertyChanged(nameof(ThirdColor));
        OnPropertyChanged(nameof(FourthColor));
        OnPropertyChanged(nameof(FifthColor));
    }

    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

public class ColorBrushConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, string language)
    {
        if (value is Color)
        {
            return new SolidColorBrush((Color)value);
        }

        return value;
    }

    public object ConvertBack(object value, Type targetType, object parameter, string language)
    {
        throw new NotImplementedException();
    }
}

This is limited to 5 entries per day (not 10 like the built in control, any more are ignored) but should give you an idea of how to achieve what you're after or be modified as necessary.

Upvotes: 2

Related Questions