Bring Holiday Cheer to Friends & Family: Build a Cross-Platform Advent Calendar App for Every Device

Agnès Zitte
21 min readDec 2, 2024

--

The holiday season is upon us, and what better way to bring joy and excitement to your loved ones than with a custom advent calendar app? 🎄 Whether it’s sharing holiday tips, daily surprises, or personalized messages, this app is your perfect festive companion. And the best part? You’ll create it with one codebase that runs seamlessly on Windows, Android, iOS, macOS, Linux, and even the web! And using the latest tech stack, including .NET 9, which brings another layer of performance.

In this guide, you’ll learn how to build a cross-platform advent calendar app using the open-source Uno Platform, a powerful framework for building single-codebase native mobile, web, desktop, and embedded apps quickly. With a sprinkle of C# magic and XAML's versatility, you can turn your creative ideas into a real-world application that brings holiday cheer across devices.

Whether you’re a seasoned developer or just exploring cross-platform development, this step-by-step guide will walk you through everything, from setting up your environment to finishing creating your app.
Ready to spread some festive vibes with code? Let’s get started! 🎁

🎁 Building a Festive Advent Calendar with Uno Platform’s Hot Design ❄️

🎅 Join the Holiday Tech Fun!

This post is part of two fantastic advent calendar projects that celebrate the season with engaging tech content:

  • ❄️ C# Advent Calendar 2024: A community-driven event where C# enthusiasts contribute insightful articles “unlocked” daily from December 1st to December 25th, fostering knowledge-sharing and the growth of the C# community.
  • 🌟 Festive Tech Calendar 2024: A global initiative bringing a variety of tech-related content throughout December, while raising funds for the Beatson Cancer Charity this year. Discover new ideas, learn, and give back to a meaningful cause.

Discover a wealth of creative tech ideas and join us in celebrating this festive season with innovation and learning!

What You’ll Learn in This Guide

  • 🛠️ Setting Up Your Development Environment
    Prepare your machine with all the necessary tools for Uno Platform development and ensure you’re ready to dive into cross-platform app building.
  • 📅 Building the Festive Advent Calendar App Step-by-Step
    Follow along as we create a festive cross-platform advent calendar app with C# tips, daily surprises, and personalized messages. Learn how to make your app festive, engaging, and full of holiday cheer while leveraging tools like the Windows Community Toolkit for converters and EnqueueAsync, Uno Toolkit ResponsiveExtension for responsive design. You’ll also see how to utilize ApplicationData.Current.LocalSettings for saving the state of daily tips and loading content via StorageFile for a seamless cross-platform experience.
  • ❄️ Adding More Holiday Magic
    Discover how to include a bonus snowfall animation using the Canvas control.
  • Shared Festive Advent Calendar Sample Project for Reference
    Access the complete sample project on GitHub, designed to help you create your own festive advent calendar. Add your unique content, leverage tools like responsive extensions, dialog click events, and state-saving, and share the joy of your app with the world!
  • 🎁 Sneak Peek Gift: Festive Advent Calendar featuring Hot Design™
    Stick around until the end of this guide to learn more about the recently announced Uno Platform Studio featuring Hot Design™, unveiled during Day 3 of the .NET Conference 2024. In the video, I demonstrate how to adjust the UI design in real-time using the Festive Advent Calendar app. Plus, there’s a bonus gift waiting for you at the end — stay tuned! 😉
    Missed the announcement? Check out the video here and the blog post here. Don’t forget to join the waitlist for beta testing!

🛠️ Setting Up Your Development Environment

To get started, you’ll need to prepare your development environment for Uno Platform. Uno Platform supports development on Windows, macOS, and Linux, with the choice of the IDE, such as Visual Studio, Visual Studio Code, or Rider. Follow the official Uno Platform Getting Started documentation for step-by-step setup instructions.

On my end, I used Visual Studio on Windows.

For reference, at the time of writing this article, I am using:

  • Visual Studio Stable Version 17.12.2
  • Uno Platform Visual Studio Extension (5.5.47.61)
  • Uno.Sdk 5.5.49
  • Uno.Check Version 1.27.2

Getting started with Uno Platform:

Creating Your Festive Advent Calendar App

For this project, I started by creating an app named FestiveAdventCalendar using the Uno Platform Template Wizard in Visual Studio. I chose the following options in the wizard:

  • Blank Preset: A minimal starting point for customization.
  • .NET 9 (under Framework): To take advantage of the latest performance improvements and features.
  • Toolkit (under Features): Provides additional features like SafeArea and responsive helpers to enhance your app’s functionality and user experience.

Here’s a screenshot of the initial project setup:

Once the project is created, you are ready to dive into adding features to build my cross-platform advent calendar app. Keep following along as we move to the next steps! 🎄

📅 Building the Festive Advent Calendar App Step-by-Step

In this section, we’ll build the advent calendar app from scratch. Follow along as we create the core features, including the countdown timer, calendar grid, and daily tips functionality. Each step brings you closer to the final version of the app, piece by piece.

Step 1: Designing the XAML Layout

The first step is to set up the user interface in MainPage.xaml. This file contains the structure for the countdown timer, calendar grid, and background visuals. The key elements include:

  1. Styles and Page Background: A gradient background in addition to styles to create a festive atmosphere.
  2. Snowfall Canvas: A Canvas element for rendering snowfall animations (covered later in the guide).
  3. Header Section: Displays the app title, current date, and a countdown timer.
  4. Calendar Grid: A grid layout representing the advent calendar.

Copy and paste the following code into your MainPage.xaml file:

<Page x:Class="FestiveAdventCalendar.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:FestiveAdventCalendar"
xmlns:utu="using:Uno.Toolkit.UI"
utu:SafeArea.Insets="VisibleBounds"
xmlns:converters="using:CommunityToolkit.WinUI.Converters">

<Page.Background>
<!-- Gradient Page Background -->
<LinearGradientBrush StartPoint="0,0"
EndPoint="0,1">
<GradientStop Color="#3D8DF3"
Offset="0.0" />
<GradientStop Color="#B3E5FC"
Offset="0.5" />
<GradientStop Color="#E0F7FA"
Offset="0.97" />
<GradientStop Color="#FFFFFF"
Offset="1.0" />
</LinearGradientBrush>
</Page.Background>

<Page.Resources>

<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />

<!-- CalendarDay Content TextBlock Style -->
<Style x:Key="CalendarDayTextBlockStyle"
TargetType="TextBlock">
<Setter Property="Foreground"
Value="#FF00A6ED" />
<Setter Property="Margin"
Value="10" />
</Style>

<!-- CalendarDay Button Content Template -->
<DataTemplate x:Key="CalendarDayTemplate">
<Grid>
<Border Background="#90FFFFFF"
BorderBrush="White"
BorderThickness="3"
CornerRadius="15"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Visibility="{Binding IsTipOpened, Converter={StaticResource BoolToVisibilityConverter}}" />
<TextBlock Text="{Binding DayNumber}"
Style="{StaticResource CalendarDayTextBlockStyle}"
HorizontalAlignment="{Binding TextHorizontalAlignment}"
VerticalAlignment="{Binding TextVerticalAlignment}"
FontSize="{Binding WideTextSize}"
Visibility="{utu:Responsive Narrow=Collapsed, Wide=Visible}" />
<TextBlock Text="{Binding DayNumber}"
Style="{StaticResource CalendarDayTextBlockStyle}"
HorizontalAlignment="{Binding TextHorizontalAlignment}"
VerticalAlignment="{Binding TextVerticalAlignment}"
FontSize="{Binding NarrowTextSize}"
Visibility="{utu:Responsive Narrow=Visible, Wide=Collapsed}" />
<TextBlock Text="❄"
Style="{StaticResource CalendarDayTextBlockStyle}"
HorizontalAlignment="{Binding IconHorizontalAlignment}"
VerticalAlignment="{Binding IconVerticalAlignment}"
FontSize="{Binding WideIconSize}"
Margin="5"
Visibility="{utu:Responsive Narrow=Collapsed, Wide=Visible}" />
<TextBlock Text="❄"
Style="{StaticResource CalendarDayTextBlockStyle}"
HorizontalAlignment="{Binding IconHorizontalAlignment}"
VerticalAlignment="{Binding IconVerticalAlignment}"
FontSize="{Binding NarrowIconSize}"
Margin="5"
Visibility="{utu:Responsive Narrow=Visible, Wide=Collapsed}" />
</Grid>
</DataTemplate>

<!-- CalendarDay Button Style -->
<Style x:Key="CalendarDayButtonStyle"
TargetType="Button">
<Setter Property="Background"
Value="#78FFFFFF" />
<Setter Property="HorizontalAlignment"
Value="Stretch" />
<Setter Property="VerticalAlignment"
Value="Stretch" />
<Setter Property="HorizontalContentAlignment"
Value="Stretch" />
<Setter Property="VerticalContentAlignment"
Value="Stretch" />
<Setter Property="CornerRadius"
Value="15" />
<Setter Property="BorderThickness"
Value="0" />
<Setter Property="Padding"
Value="0" />
<Setter Property="ContentTemplate"
Value="{StaticResource CalendarDayTemplate}" />
</Style>

<!-- Countdown StackPanel Style -->
<Style x:Key="CountdownStackPanelStyle"
TargetType="StackPanel">
<Setter Property="Margin"
Value="5" />
<Setter Property="Background"
Value="#CCFFFFFF" />
<Setter Property="Padding"
Value="10" />
<Setter Property="Width"
Value="90" />
<Setter Property="Height"
Value="90" />
<Setter Property="BorderThickness"
Value="4" />
<Setter Property="BorderBrush"
Value="White" />
<Setter Property="HorizontalAlignment"
Value="Center" />
<Setter Property="VerticalAlignment"
Value="Center" />
<Setter Property="CornerRadius"
Value="5" />
</Style>

<!-- Countdown Separator TextBlock Style -->
<Style x:Key="SeparatorTextBlockStyle"
TargetType="TextBlock">
<Setter Property="Text"
Value=":" />
<Setter Property="FontSize"
Value="28" />
<Setter Property="Foreground"
Value="#E1E1E1" />
<Setter Property="VerticalAlignment"
Value="Center" />
<Setter Property="HorizontalAlignment"
Value="Center" />
</Style>

<!-- Countdown Number TextBlock Style -->
<Style x:Key="CountdownNumberTextBlockStyle"
TargetType="TextBlock">
<Setter Property="FontSize"
Value="28" />
<Setter Property="Foreground"
Value="#00A6ED" />
<Setter Property="FontWeight"
Value="Bold" />
<Setter Property="HorizontalAlignment"
Value="Center" />
</Style>

<!-- Countdown Label TextBlock Style -->
<Style x:Key="CountdownLabelTextBlockStyle"
TargetType="TextBlock">
<Setter Property="FontSize"
Value="14" />
<Setter Property="Foreground"
Value="#3D8DF3" />
<Setter Property="HorizontalAlignment"
Value="Center" />
</Style>
</Page.Resources>

<Grid>
<!-- Snowfall Canvas -->
<Canvas x:Name="SnowCanvas"
IsHitTestVisible="False"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" />

<!-- Calendar Content -->
<Viewbox Margin="0,0,0,20">
<Grid Grid.RowDefinitions="Auto,*"
Margin="{utu:Responsive Narrow=20, Wide=50}">

<!-- Header -->
<StackPanel Orientation="{utu:Responsive Narrow=Vertical, Wide=Horizontal}"
HorizontalAlignment="Center">
<!-- Header Texts -->
<StackPanel HorizontalAlignment="{utu:Responsive Narrow=Center, Wide=Left}"
VerticalAlignment="Center">
<TextBlock Text="Festive Advent Calendar"
Foreground="White"
FontWeight="Bold"
FontSize="{utu:Responsive Narrow=38, Wide=60}"
HorizontalAlignment="{utu:Responsive Narrow=Center, Wide=Left}" />

<TextBlock Text="{x:Bind CurrentDateTimeFormatted, Mode=OneWay}"
Foreground="White"
FontWeight="SemiBold"
FontSize="{utu:Responsive Narrow=14, Wide=20}"
Margin="0,0,0,10"
HorizontalAlignment="{utu:Responsive Narrow=Center, Wide=Left}" />
</StackPanel>

<!-- Header Countdown -->
<Border Grid.Column="1"
DoubleTapped="Countdown_DoubleTapped"
HorizontalAlignment="{utu:Responsive Narrow=Center, Wide=Right}"
VerticalAlignment="Center">
<StackPanel Padding="{utu:Responsive Narrow='0,0,0,40', Wide='20,0,0,0'}"
Orientation="Horizontal"
HorizontalAlignment="Center">
<!-- Days -->
<StackPanel Style="{StaticResource CountdownStackPanelStyle}">
<TextBlock Text="{x:Bind DaysLeft, Mode=OneWay}"
Style="{StaticResource CountdownNumberTextBlockStyle}" />
<TextBlock Text="Days"
Style="{StaticResource CountdownLabelTextBlockStyle}" />
</StackPanel>

<!-- Separator -->
<TextBlock Style="{StaticResource SeparatorTextBlockStyle}" />

<!-- Hours -->
<StackPanel Style="{StaticResource CountdownStackPanelStyle}">
<TextBlock Text="{x:Bind HoursLeft, Mode=OneWay}"
Style="{StaticResource CountdownNumberTextBlockStyle}" />
<TextBlock Text="Hours"
Style="{StaticResource CountdownLabelTextBlockStyle}" />
</StackPanel>

<!-- Separator -->
<TextBlock Style="{StaticResource SeparatorTextBlockStyle}" />

<!-- Minutes -->
<StackPanel Style="{StaticResource CountdownStackPanelStyle}">
<TextBlock Text="{x:Bind MinutesLeft, Mode=OneWay}"
Style="{StaticResource CountdownNumberTextBlockStyle}" />
<TextBlock Text="Minutes"
Style="{StaticResource CountdownLabelTextBlockStyle}" />
</StackPanel>

<!-- Separator -->
<TextBlock Style="{StaticResource SeparatorTextBlockStyle}" />

<!-- Seconds -->
<StackPanel Style="{StaticResource CountdownStackPanelStyle}">
<TextBlock Text="{x:Bind SecondsLeft, Mode=OneWay}"
Style="{StaticResource CountdownNumberTextBlockStyle}" />
<TextBlock Text="Seconds"
Style="{StaticResource CountdownLabelTextBlockStyle}" />
</StackPanel>
</StackPanel>
</Border>
</StackPanel>

<!-- CalendarGrid defined with 5 rows and 7 columns -->
<Grid x:Name="CalendarGrid"
Grid.Row="1"
MinHeight="650"
Margin="0,10,0,0"
ColumnSpacing="10"
RowSpacing="10"
RowDefinitions="*,*,*,*,*"
ColumnDefinitions="*,*,*,*,*,*,*" />
</Grid>
</Viewbox>
</Grid>
</Page>

Step 2: Creating the Data Models

To manage the calendar and C# daily tips, you will need to create two classes. Right-click on FestiveAdventCalender.csproj in your solution explorer, and select Add > New Item.

  • CalendarDay.cs: Represents each calendar day, including its layout and state (e.g., whether a tip has been opened).
using System.ComponentModel;

namespace FestiveAdventCalendar;

public class CalendarDay : INotifyPropertyChanged
{
public int DayNumber { get; set; }
public int Row { get; set; }
public int Column { get; set; }
public int RowSpan { get; set; } = 1;
public int ColumnSpan { get; set; } = 1;
public double WideIconSize { get; set; }
public double WideTextSize { get; set; }
public double NarrowIconSize { get; set; }
public double NarrowTextSize { get; set; }
public HorizontalAlignment IconHorizontalAlignment { get; set; }
public VerticalAlignment IconVerticalAlignment { get; set; }
public HorizontalAlignment TextHorizontalAlignment { get; set; }
public VerticalAlignment TextVerticalAlignment { get; set; }

private bool _isTipOpened;
public bool IsTipOpened
{
get => _isTipOpened;
set
{
if (_isTipOpened != value)
{
_isTipOpened = value;
OnPropertyChanged(nameof(IsTipOpened));
}
}
}

public event PropertyChangedEventHandler? PropertyChanged;
protected void OnPropertyChanged(string propertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
  • Tip.cs: Represents the tips to be shown for each day.
namespace FestiveAdventCalendar;

public class Tip
{
public int Day { get; set; }
public string TipContent { get; set; } = string.Empty;
}

Step 3: Adding References for Windows Community Toolkit

To use the CommunityToolkit features such as converters and EnqueueAsync, follow these steps:

  1. Update the project file (FestiveAdventCalendar.csproj)
    Add the following references:
    <ItemGroup>
<PackageReference Include="CommunityToolkit.WinUI.Controls.Primitives" />
<PackageReference Include="CommunityToolkit.WinUI.Converters" />
<!-- Add more community toolkit references here -->
</ItemGroup>

2. Update Directory.Packages.props
Include these package versions:

    <ItemGroup>
<PackageVersion Include="CommunityToolkit.WinUI.Controls.Primitives" Version="8.1.240916" />
<PackageVersion Include="CommunityToolkit.WinUI.Converters" Version="8.1.240916" />
<!-- Add more community toolkit references here -->
</ItemGroup>

For more information regarding WCT, see the related links:

Step 4: Implementing the Logic in MainPage.xaml.cs

The next step is to implement the logic for the app in MainPage.xaml.cs. This file contains the code-behind for the XAML layout and handles the core functionalities of the app, including:

  1. Countdown Timer: Updates the remaining time until Christmas and displays the countdown dynamically.
  2. Calendar Grid Management: Populates the advent calendar with clickable buttons representing each day.
  3. Daily Tips: Loads the tips from a JSON file and displays a tip for each calendar day when clicked.
  4. State Management: Persists whether a tip has already been opened for a specific day.
  5. Event Handling: Manages button clicks, resetting tip states, and countdown timer updates.

Copy and paste the following complete code snippet into your MainPage.xaml.cs file:

using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Text.Json;
using Microsoft.UI.Xaml.Input;
using CommunityToolkit.WinUI;


namespace FestiveAdventCalendar;

public sealed partial class MainPage : Page, INotifyPropertyChanged
{
// Private Fields
private readonly DispatcherTimer _timer;
private List<Tip> _tips = new(); // Initialize to an empty list

private string _currentDateTimeFormatted = string.Empty; // Default to empty
private int _daysLeft;
private int _hoursLeft;
private int _minutesLeft;
private int _secondsLeft;

private const string TipStateKey = "FestiveAdventCalendarTips";
private static readonly DateTimeOffset TestDate = new(2024, 12, 23, 0, 0, 0, TimeSpan.Zero);
private readonly bool _useTestDate = false;
private DateTimeOffset EffectiveDate => _useTestDate ? TestDate : DateTimeOffset.Now;

// Public Properties
public ObservableCollection<CalendarDay> Days { get; } = new();

public string CurrentDateTimeFormatted
{
get => _currentDateTimeFormatted;
set
{
_currentDateTimeFormatted = value;
OnPropertyChanged(nameof(CurrentDateTimeFormatted));
}
}

public int DaysLeft
{
get => _daysLeft;
set
{
_daysLeft = value;
OnPropertyChanged(nameof(DaysLeft));
}
}

public int HoursLeft
{
get => _hoursLeft;
set
{
_hoursLeft = value;
OnPropertyChanged(nameof(HoursLeft));
}
}

public int MinutesLeft
{
get => _minutesLeft;
set
{
_minutesLeft = value;
OnPropertyChanged(nameof(MinutesLeft));
}
}

public int SecondsLeft
{
get => _secondsLeft;
set
{
_secondsLeft = value;
OnPropertyChanged(nameof(SecondsLeft));
}
}

public MainPage()
{
InitializeComponent();

_tips = new List<Tip>(); // Initialize _tips
_currentDateTimeFormatted = string.Empty; // Initialize to an empty string

_timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
SetupCountdownTimer();

_ = InitializeAsync();
PopulateCalendar();
}

// Event Handlers
private void DayButton_Click(object sender, RoutedEventArgs e)
{
if (sender is Button button && button.DataContext is CalendarDay day)
{
if (day.DayNumber <= EffectiveDate.Day)
{
// Find the tip for the day
var tip = _tips.FirstOrDefault(t => t.Day == day.DayNumber)?.TipContent;

if (!string.IsNullOrEmpty(tip))
{
ShowTipForDay(day.DayNumber, tip);
day.IsTipOpened = true;
SaveTipState(day.DayNumber, true);
}
else
{
ShowWaitMessage(day.DayNumber);
}
}
else
{
ShowWaitMessage(day.DayNumber);
}
}
}

private async void Countdown_DoubleTapped(object sender, DoubleTappedRoutedEventArgs e)
{
ResetTipStates();

foreach (var day in Days)
{
day.IsTipOpened = false;
}

await ShowDialogAsync(
"🎁 Surprise Gift Unwrapped! 🎁",
"🥚 You've discovered a hidden Easter Egg! 🥚\n✨ All tips have been magically reset for testing. Enjoy your festive fun! 🎉",
"Ho Ho OK! 🎅"
);
}

// Public Events
public event PropertyChangedEventHandler? PropertyChanged; // Allow null

// Protected Methods
private void OnPropertyChanged(string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

// Private Methods
private async Task InitializeAsync()
{
await LoadTipsAsync();
LoadTipStates();
}

private void SetupCountdownTimer()
{
UpdateCountdown(this, EventArgs.Empty);
_timer.Tick += UpdateCountdown;
_timer.Start();
}

private void UpdateCountdown(object? sender, object? e)
{
DateTimeOffset targetDate = new DateTimeOffset(EffectiveDate.Year, 12, 25, 0, 0, 0, EffectiveDate.Offset);
TimeSpan timeLeft = targetDate - EffectiveDate;

UpdateProperty(() => CurrentDateTimeFormatted =
$"Today: {EffectiveDate:dddd, MMMM d, yyyy - h:mm tt} (UTC{EffectiveDate.Offset.Hours:+00;-00}:{EffectiveDate.Offset.Minutes:00})");
UpdateProperty(() => DaysLeft = (int)timeLeft.TotalDays);
UpdateProperty(() => HoursLeft = timeLeft.Hours);
UpdateProperty(() => MinutesLeft = timeLeft.Minutes);
UpdateProperty(() => SecondsLeft = timeLeft.Seconds);
}

private void UpdateProperty(Action updateAction)
{
DispatcherQueue.TryEnqueue(() =>
{
updateAction();
OnPropertyChanged();
});
}

private void PopulateCalendar()
{
foreach (var item in GetCalendarItems())
{
Days.Add(item);
CalendarGrid.Children.Add(CreateCalendarButton(item));
}
}

private Button CreateCalendarButton(CalendarDay item)
{
var button = new Button
{
Style = (Style)Resources["CalendarDayButtonStyle"],
DataContext = item
};
button.Click += DayButton_Click;

Grid.SetRow(button, item.Row);
Grid.SetColumn(button, item.Column);
Grid.SetRowSpan(button, item.RowSpan);
Grid.SetColumnSpan(button, item.ColumnSpan);

return button;
}

private async Task LoadTipsAsync()
{
try
{
var file = await StorageFile.GetFileFromApplicationUriAsync(new Uri("ms-appx:///AppData/CSharpTips.json"));
string json = await FileIO.ReadTextAsync(file);
_tips = JsonSerializer.Deserialize<List<Tip>>(json) ?? new List<Tip>();
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Error loading tips: {ex.Message}");
}
}

private void LoadTipStates()
{
string savedState = ApplicationData.Current.LocalSettings.Values[TipStateKey] as string;
if (string.IsNullOrEmpty(savedState)) return;

var tipStates = JsonSerializer.Deserialize<Dictionary<int, bool>>(savedState) ?? new();
foreach (var (dayNumber, isOpened) in tipStates)
{
var day = Days.FirstOrDefault(d => d.DayNumber == dayNumber);
if (day != null)
{
day.IsTipOpened = isOpened; // Directly set the property.
}
}
}

private void SaveTipState(int dayNumber, bool isOpened)
{
// Get the saved state as a string
string savedState = ApplicationData.Current.LocalSettings.Values[TipStateKey] as string ?? string.Empty;

// Deserialize existing states or create a new dictionary if the saved state is invalid
var tipStates = !string.IsNullOrWhiteSpace(savedState)
? JsonSerializer.Deserialize<Dictionary<int, bool>>(savedState) ?? new Dictionary<int, bool>()
: new Dictionary<int, bool>();

// Update the state for the specific day
tipStates[dayNumber] = isOpened;

// Save the updated state back to local settings
ApplicationData.Current.LocalSettings.Values[TipStateKey] = JsonSerializer.Serialize(tipStates);
}


private async void ShowTipForDay(int dayNumber, string tip)
{
await DispatcherQueue.EnqueueAsync(async () =>
{
var closeButtonText = dayNumber == 25 ? "Enjoy the Holidays! 🎉" : "Unwrap More 🎁";

var dialog = new ContentDialog
{
Title = $"🎄 C# Festive Tip for Day {dayNumber} 🎁",
Content = $"{tip}\n\n🎅 Ho Ho Ho! Keep coding merrily along!",
CloseButtonText = closeButtonText,
XamlRoot = this.XamlRoot
};

await dialog.ShowAsync();
});
}

private async void ShowWaitMessage(int dayNumber)
{
await DispatcherQueue.EnqueueAsync(async () =>
{
DateTime targetDay = new DateTime(EffectiveDate.Year, 12, dayNumber);
var daysLeft = (targetDay.Date - EffectiveDate.Date).Days;

string title = daysLeft >= 10 ? "🎅 Hold Your Sleigh! 🎄" : "🎄 Almost There! 🎁";
string message = $"The gift for Day {dayNumber} isn't ready yet!\nYou need to wait {daysLeft} more day{(daysLeft > 1 ? "s" : "")} until it's time to unwrap this C# tip!";

var dialog = new ContentDialog
{
Title = title,
Content = message,
CloseButtonText = "🎁 Got it! 🎄",
XamlRoot = this.XamlRoot
};

await dialog.ShowAsync();
});
}

private async Task ShowDialogAsync(string title, string content, string closeButtonText)
{
var dialog = new ContentDialog
{
Title = title,
Content = content,
CloseButtonText = closeButtonText,
XamlRoot = XamlRoot
};

await DispatcherQueue.EnqueueAsync(() => dialog.ShowAsync());
}

private ObservableCollection<CalendarDay> GetCalendarItems()
{
return new()
{
new CalendarDay
{
DayNumber = 1,
WideTextSize = 60,
WideIconSize = 40,
NarrowTextSize = 44,
NarrowIconSize = 20,
IconHorizontalAlignment = HorizontalAlignment.Left,
IconVerticalAlignment = VerticalAlignment.Top,
TextHorizontalAlignment = HorizontalAlignment.Center,
TextVerticalAlignment = VerticalAlignment.Center,
Row = 0,
Column = 0
},
new CalendarDay
{
DayNumber = 2,
WideTextSize = 60,
WideIconSize = 38,
NarrowTextSize = 39,
NarrowIconSize = 35,
IconHorizontalAlignment = HorizontalAlignment.Right,
IconVerticalAlignment = VerticalAlignment.Top,
TextHorizontalAlignment = HorizontalAlignment.Center,
TextVerticalAlignment = VerticalAlignment.Bottom,
Row = 1,
Column = 1
},
new CalendarDay
{
DayNumber = 3,
WideTextSize = 80,
WideIconSize = 50,
NarrowTextSize = 60,
NarrowIconSize = 29,
IconHorizontalAlignment = HorizontalAlignment.Left,
IconVerticalAlignment = VerticalAlignment.Top,
TextHorizontalAlignment = HorizontalAlignment.Right,
TextVerticalAlignment = VerticalAlignment.Bottom,
Row = 2,
Column = 3,
ColumnSpan = 2
},
new CalendarDay
{
DayNumber = 4,
WideTextSize = 70,
WideIconSize = 38,
NarrowTextSize = 40,
NarrowIconSize = 28,
IconHorizontalAlignment = HorizontalAlignment.Right,
IconVerticalAlignment = VerticalAlignment.Top,
TextHorizontalAlignment = HorizontalAlignment.Center,
TextVerticalAlignment = VerticalAlignment.Bottom,
Row = 4,
Column = 0
},
new CalendarDay
{
DayNumber = 5,
WideTextSize = 90,
WideIconSize = 25,
NarrowTextSize = 45,
NarrowIconSize = 32,
IconHorizontalAlignment = HorizontalAlignment.Right,
IconVerticalAlignment = VerticalAlignment.Top,
TextHorizontalAlignment = HorizontalAlignment.Center,
TextVerticalAlignment = VerticalAlignment.Bottom,
Row = 5,
Column = 4
},
new CalendarDay
{
DayNumber = 6,
WideTextSize = 80,
WideIconSize = 32,
NarrowTextSize = 36,
NarrowIconSize = 28,
IconHorizontalAlignment = HorizontalAlignment.Left,
IconVerticalAlignment = VerticalAlignment.Bottom,
TextHorizontalAlignment = HorizontalAlignment.Right,
TextVerticalAlignment = VerticalAlignment.Top,
Row = 3,
Column = 0
},
new CalendarDay
{
DayNumber = 7,
WideTextSize = 90,
WideIconSize = 42,
NarrowTextSize = 39,
NarrowIconSize = 36,
IconHorizontalAlignment = HorizontalAlignment.Right,
IconVerticalAlignment = VerticalAlignment.Top,
TextHorizontalAlignment = HorizontalAlignment.Left,
TextVerticalAlignment = VerticalAlignment.Bottom,
Row = 0,
Column = 3
},
new CalendarDay
{
DayNumber = 8,
WideTextSize = 90,
WideIconSize = 26,
NarrowTextSize = 32,
NarrowIconSize = 21,
IconHorizontalAlignment = HorizontalAlignment.Right,
IconVerticalAlignment = VerticalAlignment.Bottom,
TextHorizontalAlignment = HorizontalAlignment.Center,
TextVerticalAlignment = VerticalAlignment.Center,
Row = 4,
Column = 1
},
new CalendarDay
{
DayNumber = 9,
WideTextSize = 90,
WideIconSize = 16,
NarrowTextSize = 38,
NarrowIconSize = 25,
IconHorizontalAlignment = HorizontalAlignment.Left,
IconVerticalAlignment = VerticalAlignment.Top,
TextHorizontalAlignment = HorizontalAlignment.Center,
TextVerticalAlignment = VerticalAlignment.Bottom,
Row = 2,
Column = 2
},
new CalendarDay
{
DayNumber = 10,
WideTextSize = 55,
WideIconSize = 22,
NarrowTextSize = 42,
NarrowIconSize = 29,
IconHorizontalAlignment = HorizontalAlignment.Right,
IconVerticalAlignment = VerticalAlignment.Bottom,
TextHorizontalAlignment = HorizontalAlignment.Center,
TextVerticalAlignment = VerticalAlignment.Top,
Row = 2,
Column = 5
},
new CalendarDay
{
DayNumber = 11,
WideTextSize = 60,
WideIconSize = 27,
NarrowTextSize = 40,
NarrowIconSize = 32,
IconHorizontalAlignment = HorizontalAlignment.Left,
IconVerticalAlignment = VerticalAlignment.Top,
TextHorizontalAlignment = HorizontalAlignment.Center,
TextVerticalAlignment = VerticalAlignment.Bottom,
Row = 3,
Column = 1
},
new CalendarDay
{
DayNumber = 12,
WideTextSize = 60,
WideIconSize = 15,
NarrowTextSize = 43,
NarrowIconSize = 26,
IconHorizontalAlignment = HorizontalAlignment.Right,
IconVerticalAlignment = VerticalAlignment.Top,
TextHorizontalAlignment = HorizontalAlignment.Center,
TextVerticalAlignment = VerticalAlignment.Center,
Row = 1,
Column = 3
},
new CalendarDay
{
DayNumber = 13,
WideTextSize = 90,
WideIconSize = 18,
NarrowTextSize = 44,
NarrowIconSize = 30,
IconHorizontalAlignment = HorizontalAlignment.Right,
IconVerticalAlignment = VerticalAlignment.Top,
TextHorizontalAlignment = HorizontalAlignment.Center,
TextVerticalAlignment = VerticalAlignment.Center,
Row = 3,
Column = 4,
ColumnSpan = 2
},
new CalendarDay
{
DayNumber = 14,
WideTextSize = 60,
WideIconSize = 42,
NarrowTextSize = 38,
NarrowIconSize = 32,
IconHorizontalAlignment = HorizontalAlignment.Right,
IconVerticalAlignment = VerticalAlignment.Bottom,
TextHorizontalAlignment = HorizontalAlignment.Center,
TextVerticalAlignment = VerticalAlignment.Center,
Row = 0,
Column = 2,
RowSpan = 2
},
new CalendarDay
{
DayNumber = 15,
WideTextSize = 60,
WideIconSize = 35,
NarrowTextSize = 44,
NarrowIconSize = 27,
IconHorizontalAlignment = HorizontalAlignment.Left,
IconVerticalAlignment = VerticalAlignment.Bottom,
TextHorizontalAlignment = HorizontalAlignment.Center,
TextVerticalAlignment = VerticalAlignment.Top,
Row = 3,
Column = 3
},
new CalendarDay
{
DayNumber = 16,
WideTextSize = 50,
WideIconSize = 18,
NarrowTextSize = 42,
NarrowIconSize = 30,
IconHorizontalAlignment = HorizontalAlignment.Right,
IconVerticalAlignment = VerticalAlignment.Top,
TextHorizontalAlignment = HorizontalAlignment.Center,
TextVerticalAlignment = VerticalAlignment.Bottom,
Row = 0,
Column = 1
},
new CalendarDay
{
DayNumber = 17,
WideTextSize = 50,
WideIconSize = 35,
NarrowTextSize = 43,
NarrowIconSize = 28,
IconHorizontalAlignment = HorizontalAlignment.Left,
IconVerticalAlignment = VerticalAlignment.Bottom,
TextHorizontalAlignment = HorizontalAlignment.Center,
TextVerticalAlignment = VerticalAlignment.Top,
Row = 0,
Column = 6
},
new CalendarDay
{
DayNumber = 18,
WideTextSize = 80,
WideIconSize = 38,
NarrowTextSize = 70,
NarrowIconSize = 31,
IconHorizontalAlignment = HorizontalAlignment.Right,
IconVerticalAlignment = VerticalAlignment.Bottom,
TextHorizontalAlignment = HorizontalAlignment.Left,
TextVerticalAlignment = VerticalAlignment.Top,
Row = 4,
Column = 2,
ColumnSpan = 2
},
new CalendarDay
{
DayNumber = 19,
WideTextSize = 45,
WideIconSize = 25,
NarrowTextSize = 40,
NarrowIconSize = 28,
IconHorizontalAlignment = HorizontalAlignment.Left,
IconVerticalAlignment = VerticalAlignment.Top,
TextHorizontalAlignment = HorizontalAlignment.Right,
TextVerticalAlignment = VerticalAlignment.Bottom,
Row = 1,
Column = 6
},
new CalendarDay
{
DayNumber = 20,
WideTextSize = 45,
WideIconSize = 22,
NarrowTextSize = 43,
NarrowIconSize = 19,
IconHorizontalAlignment = HorizontalAlignment.Left,
IconVerticalAlignment = VerticalAlignment.Bottom,
TextHorizontalAlignment = HorizontalAlignment.Center,
TextVerticalAlignment = VerticalAlignment.Center,
Row = 2,
Column = 1
},
new CalendarDay
{
DayNumber = 21,
WideTextSize = 60,
WideIconSize = 40,
NarrowTextSize = 44,
NarrowIconSize = 30,
IconHorizontalAlignment = HorizontalAlignment.Right,
IconVerticalAlignment = VerticalAlignment.Bottom,
TextHorizontalAlignment = HorizontalAlignment.Center,
TextVerticalAlignment = VerticalAlignment.Center,
Row = 2,
Column = 6,
RowSpan = 3
},
new CalendarDay
{
DayNumber = 22,
WideTextSize = 35,
WideIconSize = 32,
NarrowTextSize = 39,
NarrowIconSize = 20,
IconHorizontalAlignment = HorizontalAlignment.Right,
IconVerticalAlignment = VerticalAlignment.Bottom,
TextHorizontalAlignment = HorizontalAlignment.Center,
TextVerticalAlignment = VerticalAlignment.Center,
Row = 3,
Column = 2
},
new CalendarDay
{
DayNumber = 23,
WideTextSize = 38,
WideIconSize = 22,
NarrowTextSize = 40,
NarrowIconSize = 25,
IconHorizontalAlignment = HorizontalAlignment.Left,
IconVerticalAlignment = VerticalAlignment.Bottom,
TextHorizontalAlignment = HorizontalAlignment.Center,
TextVerticalAlignment = VerticalAlignment.Top,
Row = 4,
Column = 5
},
new CalendarDay
{
DayNumber = 24,
WideTextSize = 60,
WideIconSize = 26,
NarrowTextSize = 42,
NarrowIconSize = 30,
IconHorizontalAlignment = HorizontalAlignment.Left,
IconVerticalAlignment = VerticalAlignment.Top,
TextHorizontalAlignment = HorizontalAlignment.Center,
TextVerticalAlignment = VerticalAlignment.Center,
Row = 1,
Column = 0,
RowSpan = 2
},
new CalendarDay
{
DayNumber = 25,
WideTextSize = 120,
WideIconSize = 60,
NarrowTextSize = 100,
NarrowIconSize = 45,
IconHorizontalAlignment = HorizontalAlignment.Right,
IconVerticalAlignment = VerticalAlignment.Bottom,
TextHorizontalAlignment = HorizontalAlignment.Center,
TextVerticalAlignment = VerticalAlignment.Center,
Row = 0,
Column = 4,
ColumnSpan = 2,
RowSpan = 2
}
};
}

private void ResetTipStates()
{
// Reset to an empty dictionary
ApplicationData.Current.LocalSettings.Values[TipStateKey] = JsonSerializer.Serialize(new Dictionary<int, bool>());
}
}

Step 5: Adding the JSON File for Daily Tips

To enable the advent calendar to display daily tips, you need to add a JSON file containing the tips data to your project. This step involves creating a new folder, adding the JSON file, and configuring its build action to make it accessible at runtime.

  1. Create an AppData Folder:
    In your project, create a new folder named AppData. This folder will store the CSharpTips.json file containing the daily tips.
    Right-click on FestiveAdventCalendar.csproj in your solution explorer and select Add > New Folder.
  2. Download CSharpTips.json:
    You can download the file and its content here on GitHub.
  3. Add the JSON File:
    Right-click on the AppData folder in your solution explorer and select Add > Existing Item.
    Select the location of the CSharpTips.json you downloaded, and click Add.
  4. Change the Build Action of the File:
    -
    Select the CSharpTips.json file in the solution explorer.
    - In the Properties window, set the Build Action to Content.

Step 6: Build, Run, and Test

With everything in place, you’re ready to build and run your Festive Advent Calendar app on your desired platform (Windows, WebAssembly, Android, iOS, etc.).

  • Debugging: For platform-specific debugging information, refer to the Uno Platform Debugging Guide.
  • Test Specific Dates:
    Set _useTestDate = true in MainPage.xaml.cs and update TestDate
private static readonly DateTimeOffset TestDate = new(2024, 12, 15, 0, 0, 0, TimeSpan.Zero);

*This allows testing as if it’s a specific day (e.g., Dec 15).

🎉 Congrats! Your Festive Advent Calendar app is ready to enjoy!

❄️ Adding More Holiday Magic

Let’s enhance your Festive Advent Calendar app with a festive snowfall animation. This feature adds a magical touch to your app, making it even more immersive and fun.

Simply add the following snippet to your MainPage.xaml.cs to enable the snowfall animation:

using Microsoft.UI;
using Microsoft.UI.Xaml.Shapes;

...

private readonly Random _random = new Random();
private bool _isRunningSnowfall = true;

public MainPage()

{
...

SnowCanvas.Loaded += (_, _) => StartSnowfall();

Unloaded += (_, _) =>

{

_isRunningSnowfall = false; // Stop the snowfall process

SnowCanvas.Children.Clear(); // Clear snowflakes to avoid lingering operations

};

...
}

// Start the snowfall animation, generating snowflakes in batches.

private async void StartSnowfall()

{

// Ensure DispatcherQueue is initialized properly

if (Microsoft.UI.Dispatching.DispatcherQueue.GetForCurrentThread() == null)

{

System.Diagnostics.Debug.WriteLine("DispatcherQueue is not available. Snowfall cannot be started.");

return;

}



int totalSnowflakes = 100;

int flakesPerBatch = 5;



await Microsoft.UI.Dispatching.DispatcherQueue.GetForCurrentThread().EnqueueAsync(async () =>

{

for (int i = 0; i < totalSnowflakes; i += flakesPerBatch)

{

for (int j = 0; j < flakesPerBatch; j++)

{

var snowFlake = CreateSnowflake();

if (snowFlake != null)

{

SnowCanvas.Children.Add(snowFlake);

AnimateSnowflake(snowFlake); // Fire-and-forget animation.

}

}



// Stagger snowflake generation.

await Task.Delay(500);

}

});

}



// Create an individual snowflake with random properties.

private Ellipse? CreateSnowflake()

{

int size = _random.Next(3, 8);



// Ensure valid dimensions for the canvas.

if (SnowCanvas.ActualWidth <= 0 || SnowCanvas.ActualHeight <= 0)

{

System.Diagnostics.Debug.WriteLine("SnowCanvas dimensions are not valid.");

return null;

}



var snowflake = new Ellipse

{

Width = size,

Height = size,

Fill = new SolidColorBrush(Colors.White),

Opacity = _random.NextDouble() * 0.8 + 0.2 // Random opacity for natural effect.

};



// Random starting position for snowflake.

double leftPosition = _random.Next(0, (int)SnowCanvas.ActualWidth);

double topPosition = -size;



Canvas.SetLeft(snowflake, leftPosition);

Canvas.SetTop(snowflake, topPosition);



return snowflake;

}



// Animate a snowflake falling down the screen.

private async void AnimateSnowflake(Ellipse snowFlake)

{

double duration = _random.Next(10000, 20000);



while (_isRunningSnowfall)

{

// Ensure UI access using DispatcherQueue and check for valid canvas and snowflake

if (Microsoft.UI.Dispatching.DispatcherQueue.GetForCurrentThread() == null || snowFlake == null || SnowCanvas == null || !this.IsLoaded)

{

return; // Exit if DispatcherQueue, snowflake, or canvas is no longer valid.

}



double fullHeight = Math.Max(XamlRoot.Size.Height, SnowCanvas.ActualHeight);

double startTop = Canvas.GetTop(snowFlake);

double endTop = fullHeight + snowFlake.Height;



double startLeft = Canvas.GetLeft(snowFlake);

double maxDrift = _random.Next(-20, 20);



// Simulate falling with drift.

for (double t = 0; t < 1; t += 0.01)

{

if (!_isRunningSnowfall) return;



await Microsoft.UI.Dispatching.DispatcherQueue.GetForCurrentThread().EnqueueAsync(() =>

{

if (snowFlake == null || SnowCanvas == null || !this.IsLoaded) return;



double newTop = startTop + (endTop - startTop) * t;

Canvas.SetTop(snowFlake, newTop);



// Increase the drift by multiplying maxDrift by a factor (e.g., 3.0)

double drift = startLeft + maxDrift * Math.Sin(t * Math.PI); // Add lateral drift

Canvas.SetLeft(snowFlake, drift);

});



await Task.Delay(TimeSpan.FromMilliseconds(duration / 100));

}



// Reset snowflake position after it falls.

await Microsoft.UI.Dispatching.DispatcherQueue.GetForCurrentThread().EnqueueAsync(() =>

{

if (SnowCanvas == null || snowFlake == null)

{

return;

}

Canvas.SetTop(snowFlake, -snowFlake.Height);

Canvas.SetLeft(snowFlake, _random.Next(0, (int)SnowCanvas.ActualWidth));

});

}

}

🎄 Note: The SnowCanvas element is already in the XAML layout from previous Step 1. Once this code is added, your app will create and animate snowflakes, delivering a magical snowfall effect.

Enjoy the holiday magic! 🎅❄️

Windows, Android, Desktop
iOS and WebAssembly

✨ Shared Festive Advent Calendar Sample Project for Reference

Congratulations! If you’ve followed along, you now have a fully functional and responsive Festive Advent Calendar app running on your chosen platform.

🎉 The sample project is available for reference on GitHub here. Feel free to customize the design, the surprises for each day, or even the overall concept to create your own unique version. Whether you’re sharing it with family, loved ones, or the world, your festive cross-platform app is ready to spread holiday joy! 🎄✨

For more information on how to publish your app across platforms — including Windows, iOS, Android, macOS, Linux, and more, please refer to the Uno Platform documentation in regards to app publishing.

Happy coding and happy holidays! 🎅💻

🎁 Sneak Peek Gift: Festive Advent Calendar featuring Hot Design

Still with me? Here’s a sneak peek of Hot Design™, showcased in action with the Festive Advent Calendar App.

What is Hot Design™?

Hot Design™ is the next-generation Visual Designer for cross-platform .NET Applications. It seamlessly transforms your running app into a design interface, accessible from any IDE on any OS.

Why is it a game-changer?

  • Works with Visual Studio, VS Code, or Rider
    Keep using the IDE you love. Hot Design integrates seamlessly across Visual Studio, VS Code, and Rider, on all supported platforms.
  • Round-Trip Sync Between Designer and Code
    Real-time updates between Designer and XAML keep your design-to-code workflow consistent and fast.
  • Custom and Third-Party UI Controls Supported!
    Fully supports your custom and third-party UI components, simplifying complex design systems.
  • Work with Real Data
    No more mock data! Use actual data from your running app or mock it, if needed, for quick previews.
  • Use Your Favorite Design Pattern
    Compatible with MVVM, MVUX, and other patterns, ensuring flexibility with your preferred state management.
  • Design UI for a Remote Device
    Fine-tune your app’s UI in real time on remote hardware, without redeployment hassles.

Watch the video below for an exclusive look at how Uno Platform’s Studio featuring Hot Design can transform your development workflow.

🎁 Building a Festive Advent Calendar with Uno Platform’s Hot Design ❄️

Again, if you missed the announcement, check out the video here and the announcement blog post here. Don’t forget to join the waitlist for beta testing and be among the first to experience this game-changing tool! 🚀

🎁 As promised, here’s an exclusive bonus gift just for you!
🚀 Share this blog post on social media and tag me along with Uno Platform, and we will make sure with the Uno Platform team that you get fast-tracked access to the highly-anticipated beta testing of Hot Design™. Don’t miss this chance to experience the future of cross-platform design before anyone else! 🎉

🎄 Happy Coding and Happy Holidays! 🎁
I hope this guide inspires you to create something magical for the holiday season. Whether it’s a gift for loved ones, a fun project for yourself, or something to share with the world, your Festive Advent Calendar app is sure to bring joy across platforms.

Looking ahead to 2025? Use this guide as inspiration to create a New Year’s countdown app or calendar to celebrate with friends and family as you welcome the new year! ✨

If you’ve built your own version or added your unique twist, I’d love to see it! Feel free to share it and tag me on social media. Let’s celebrate creativity together. 🌟

📫 Reach me via: Discord | GitHub | Bluesky | X | LinkedIn

Wishing you a joyful holiday season and an amazing year of coding ahead! 🎅✨

Happy Holidays,

Agnès Zitte.

--

--

Agnès Zitte
Agnès Zitte

Written by Agnès Zitte

Senior Software Engineer passionate about cross-platform apps with .NET (C# / XAML). When not coding, I’m exploring creative hobbies and new projects.

Responses (2)