Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions samples/ControlCatalog/Pages/CalendarDatePickerPage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@

<TextBlock Text="Validation Example"/>
<CalendarDatePicker SelectedDate="{CompiledBinding ValidatedDateExample, Mode=TwoWay}"/>

<TextBlock Text="Show WeekNumbers" />
<CalendarDatePicker ShowWeekNumbers="True"
FirstDayOfWeek="Monday"
WeekNumberRule="FirstFourDayWeek" />
</StackPanel>

</StackPanel>
Expand Down
6 changes: 6 additions & 0 deletions samples/ControlCatalog/Pages/CalendarPage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@
<Calendar Name="BlackoutDatesCalendar"
SelectionMode="SingleDate" />
</StackPanel>
<StackPanel>
<TextBlock Text="Weeknumbers"/>
<Calendar SelectionMode="SingleDate"
ShowWeekNumbers="True"
WeekNumberHeader="CW" />
</StackPanel>
</WrapPanel>
</StackPanel>
</ContentPage>
54 changes: 54 additions & 0 deletions src/Avalonia.Controls/Calendar/Calendar.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Globalization;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Data;
Expand Down Expand Up @@ -352,6 +353,56 @@ public IBrush? HeaderBackground
set => SetValue(HeaderBackgroundProperty, value);
}

/// <summary>
/// Defines the <see cref="ShowWeekNumbers"/> property.
/// </summary>
public static readonly StyledProperty<bool> ShowWeekNumbersProperty =
AvaloniaProperty.Register<Calendar, bool>(nameof(ShowWeekNumbers));

/// <summary>
/// Gets or sets a value indicating whether week numbers are shown in the month view.
/// </summary>
public bool ShowWeekNumbers
{
get => GetValue(ShowWeekNumbersProperty);
set => SetValue(ShowWeekNumbersProperty, value);
}
Comment thread
timunie marked this conversation as resolved.
Outdated

/// <summary>
/// Defines the <see cref="WeekNumberRule"/> property.
/// </summary>
public static readonly StyledProperty<CalendarWeekRule> WeekNumberRuleProperty =
AvaloniaProperty.Register<Calendar, CalendarWeekRule>(
nameof(WeekNumberRule),
defaultValue: DateTimeHelper.GetCurrentDateFormat().CalendarWeekRule);

/// <summary>
/// Gets or sets the rule used to determine the first week of the year for week number display.
/// The default is taken from the current culture.
/// </summary>
public CalendarWeekRule WeekNumberRule
{
get => GetValue(WeekNumberRuleProperty);
set => SetValue(WeekNumberRuleProperty, value);
}

/// <summary>
/// Defines the <see cref="WeekNumberHeader"/> property.
/// </summary>
public static readonly StyledProperty<object?> WeekNumberHeaderProperty =
AvaloniaProperty.Register<Calendar, object?>(nameof(WeekNumberHeader), "#");

/// <summary>
/// Gets or sets the content displayed in the week-number column header cell.
/// Set this to a localized string such as <c>"CW"</c>, <c>"KW"</c>, or <c>"Wk"</c>
/// to give users context for the week-number column. Defaults to <c>null</c> (blank).
Comment thread
timunie marked this conversation as resolved.
Outdated
/// </summary>
public object? WeekNumberHeader
{
get => GetValue(WeekNumberHeaderProperty);
set => SetValue(WeekNumberHeaderProperty, value);
}

public static readonly StyledProperty<CalendarMode> DisplayModeProperty =
AvaloniaProperty.Register<Calendar, CalendarMode>(
nameof(DisplayMode),
Expand Down Expand Up @@ -2208,6 +2259,9 @@ static Calendar()
DisplayDateProperty.Changed.AddClassHandler<Calendar>((x, e) => x.OnDisplayDateChanged(e));
DisplayDateStartProperty.Changed.AddClassHandler<Calendar>((x, e) => x.OnDisplayDateStartChanged(e));
DisplayDateEndProperty.Changed.AddClassHandler<Calendar>((x, e) => x.OnDisplayDateEndChanged(e));
ShowWeekNumbersProperty.Changed.AddClassHandler<Calendar>((x, _) => x.UpdateMonths());
WeekNumberRuleProperty.Changed.AddClassHandler<Calendar>((x, _) => x.UpdateMonths());
WeekNumberHeaderProperty.Changed.AddClassHandler<Calendar>((x, _) => x.UpdateMonths());
KeyDownEvent.AddClassHandler<Calendar>((x, e) => x.Calendar_KeyDown(e));
KeyUpEvent.AddClassHandler<Calendar>((x, e) => x.Calendar_KeyUp(e));
}
Expand Down
69 changes: 64 additions & 5 deletions src/Avalonia.Controls/Calendar/CalendarItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ namespace Avalonia.Controls.Primitives
[TemplatePart(PART_ElementNextButton, typeof(Button))]
[TemplatePart(PART_ElementPreviousButton, typeof(Button))]
[TemplatePart(PART_ElementYearView, typeof(Grid))]
[PseudoClasses(":calendardisabled")]
[PseudoClasses(":calendardisabled", ":showweeknumbers")]
Comment thread
timunie marked this conversation as resolved.
Outdated
public sealed class CalendarItem : TemplatedControl
{
/// <summary>
Expand All @@ -49,6 +49,9 @@ public sealed class CalendarItem : TemplatedControl

private readonly System.Globalization.Calendar _calendar = new GregorianCalendar();

private CalendarWeekNumberLabel[] _weekNumberLabels = Array.Empty<CalendarWeekNumberLabel>();
private CalendarWeekNumberLabel? _weekNumberHeaderLabel;

internal Calendar? Owner { get; set; }
internal CalendarDayButton? CurrentButton { get; set; }

Expand Down Expand Up @@ -168,16 +171,18 @@ private void PopulateGrids()
{
if (MonthView != null)
{
var childCount = Calendar.RowsPerMonth + Calendar.RowsPerMonth * Calendar.ColumnsPerMonth;
// +7 for the week-number column (1 header + 6 data cells)
var childCount = Calendar.RowsPerMonth + Calendar.RowsPerMonth * Calendar.ColumnsPerMonth + Calendar.RowsPerMonth;
using var children = new PooledList<Control>(childCount);

// Day-of-week title cells — day columns are at 1-7 (shifted right by 1)
for (int i = 0; i < Calendar.ColumnsPerMonth; i++)
{
if (DayTitleTemplate?.Build() is Control cell)
{
cell.DataContext = string.Empty;
cell.SetValue(Grid.RowProperty, 0);
cell.SetValue(Grid.ColumnProperty, i);
cell.SetValue(Grid.ColumnProperty, i + 1);
children.Add(cell);
}
}
Expand All @@ -198,15 +203,34 @@ private void PopulateGrids()
cell.Owner = Owner;
}
cell.SetValue(Grid.RowProperty, i);
cell.SetValue(Grid.ColumnProperty, j);
cell.SetValue(Grid.ColumnProperty, j + 1);
cell.CalendarDayButtonMouseDown += cellMouseLeftButtonDown;
cell.CalendarDayButtonMouseUp += cellMouseLeftButtonUp;
cell.PointerEntered += cellMouseEntered;
cell.Click += cellClick;
children.Add(cell);
}
}


// Week-number cells — added after existing cells to preserve child indices 0–48.
// Header placeholder (row 0, col 0)
_weekNumberHeaderLabel = new CalendarWeekNumberLabel();
_weekNumberHeaderLabel.IsHeader = true;
_weekNumberHeaderLabel.SetValue(Grid.RowProperty, 0);
_weekNumberHeaderLabel.SetValue(Grid.ColumnProperty, 0);
children.Add(_weekNumberHeaderLabel);

// Data labels (rows 1–6, col 0)
_weekNumberLabels = new CalendarWeekNumberLabel[Calendar.RowsPerMonth - 1];
for (int i = 1; i < Calendar.RowsPerMonth; i++)
{
var label = new CalendarWeekNumberLabel();
label.SetValue(Grid.RowProperty, i);
label.SetValue(Grid.ColumnProperty, 0);
_weekNumberLabels[i - 1] = label;
children.Add(label);
}

MonthView.Children.AddRange(children);
}

Expand Down Expand Up @@ -254,6 +278,10 @@ protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
NextButton = e.NameScope.Find<Button>(PART_ElementNextButton);
MonthView = e.NameScope.Find<Grid>(PART_ElementMonthView);
YearView = e.NameScope.Find<Grid>(PART_ElementYearView);

// Prepend a column for week numbers; day columns follow in positions 1–7.
// Done here so PopulateGrids() can place cells at the correct indices.
MonthView?.ColumnDefinitions.Insert(0, new ColumnDefinition(GridLength.Auto));

if (Owner != null)
{
Expand Down Expand Up @@ -373,6 +401,7 @@ internal void UpdateMonthMode()
{
SetDayTitles();
SetCalendarDayButtons(_currentMonth);
UpdateWeekNumberLabels(_currentMonth);
}
}
private void SetMonthModeHeaderButton()
Expand Down Expand Up @@ -589,6 +618,36 @@ private void SetCalendarDayButtons(DateTime firstDayOfMonth)
}
}

private void UpdateWeekNumberLabels(DateTime firstDayOfMonth)
{
if (_weekNumberLabels.Length == 0)
return;

int lastMonthToDisplay = PreviousMonthDays(firstDayOfMonth);
DateTime firstDateDisplayed = DateTimeHelper.CompareYearMonth(firstDayOfMonth, DateTime.MinValue) > 0
? _calendar.AddDays(firstDayOfMonth, -lastMonthToDisplay)
: firstDayOfMonth;

bool show = Owner?.ShowWeekNumbers ?? false;
var rule = Owner?.WeekNumberRule ?? DateTimeHelper.GetCurrentDateFormat().CalendarWeekRule;
var firstDayOfWeek = Owner?.FirstDayOfWeek ?? DateTimeHelper.GetCurrentDateFormat().FirstDayOfWeek;

PseudoClasses.Set(":showweeknumbers", show);

for (int i = 0; i < _weekNumberLabels.Length; i++)
{
DateTime firstDayOfRow = _calendar.AddDays(firstDateDisplayed, i * NumberOfDaysPerWeek);
_weekNumberLabels[i].Content = DateTimeHelper.GetWeekOfYear(firstDayOfRow, rule, firstDayOfWeek).ToString();
_weekNumberLabels[i].IsVisible = show;
}

if (_weekNumberHeaderLabel != null)
{
_weekNumberHeaderLabel.Content = Owner?.WeekNumberHeader;
_weekNumberHeaderLabel.IsVisible = show;
}
}

internal void UpdateYearMode()
{
if (Owner != null)
Expand Down
29 changes: 29 additions & 0 deletions src/Avalonia.Controls/Calendar/CalendarWeekNumberLabel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
namespace Avalonia.Controls.Primitives
{
/// <summary>
/// Displays a week number in the month view of a <see cref="Calendar"/>.
/// Apply styles targeting <see cref="CalendarWeekNumberLabel"/> to customise
/// the appearance — for example <c>FontWeight="Bold"</c>.
/// Use the <c>:header</c> pseudo-class to target the column header cell (row 0).
/// </summary>
public sealed class CalendarWeekNumberLabel : ContentControl

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO an entire new primitive probably isn't needed for this concept. It can be done with text and light-weight styling resources alone.

That in turn allows room for future expansion in this area. For example -- we might want to make it interactive as a button in some way, the calendar might get a week ONLY view, etc.

I'm also not a fan of the legacy term "Label" being used for new controls. That just isn't done in modern XAML.

{
private bool _isHeader;
Comment thread
timunie marked this conversation as resolved.
Outdated

/// <summary>
/// Gets or sets a value indicating whether this label is the column header cell
/// (placed in row 0 of the month grid, above the week-number data cells).
/// Themes can target this with the <c>:header</c> pseudo-class.
/// </summary>
public bool IsHeader
{
get => _isHeader;
internal set
{
if (_isHeader == value) return;
_isHeader = value;
PseudoClasses.Set(":header", value);
}
}
}
}
18 changes: 18 additions & 0 deletions src/Avalonia.Controls/Calendar/DateTimeHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -131,5 +131,23 @@ public static string ToYearString(DateTime date)
var format = GetCurrentDateFormat();
return date.Year.ToString(format);
}

/// <summary>
/// Gets the week-of-year number for the specified date.
/// </summary>
/// <param name="date">The date.</param>
/// <param name="rule">The rule that defines what constitutes the first week of the year.</param>
/// <param name="firstDayOfWeek">The first day of the week.</param>
/// <returns>The week number that <paramref name="date"/> falls in.</returns>
public static int GetWeekOfYear(DateTime date, CalendarWeekRule rule, DayOfWeek firstDayOfWeek)
{
// .NET's Calendar.GetWeekOfYear does not correctly implement ISO 8601 for the combination
// of FirstFourDayWeek + Monday: late-December dates that belong to week 1 of the next year
// are incorrectly returned as week 53. Use ISOWeek for that specific combination.
if (rule == CalendarWeekRule.FirstFourDayWeek && firstDayOfWeek == DayOfWeek.Monday)
return ISOWeek.GetWeekOfYear(date);

return CultureInfo.CurrentCulture.Calendar.GetWeekOfYear(date, rule, firstDayOfWeek);
Comment thread
timunie marked this conversation as resolved.
Outdated
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Globalization;
using Avalonia.Controls.Primitives;
using Avalonia.Data;
using Avalonia.Layout;
Expand Down Expand Up @@ -140,6 +141,24 @@ public partial class CalendarDatePicker
public static readonly StyledProperty<VerticalAlignment> VerticalContentAlignmentProperty =
ContentControl.VerticalContentAlignmentProperty.AddOwner<CalendarDatePicker>();

/// <summary>
/// Defines the <see cref="ShowWeekNumbers"/> property.
/// </summary>
public static readonly StyledProperty<bool> ShowWeekNumbersProperty =
Calendar.ShowWeekNumbersProperty.AddOwner<CalendarDatePicker>();

/// <summary>
/// Defines the <see cref="WeekNumberRule"/> property.
/// </summary>
public static readonly StyledProperty<CalendarWeekRule> WeekNumberRuleProperty =
Calendar.WeekNumberRuleProperty.AddOwner<CalendarDatePicker>();

/// <summary>
/// Defines the <see cref="WeekNumberHeader"/> property.
/// </summary>
public static readonly StyledProperty<object?> WeekNumberHeaderProperty =
Calendar.WeekNumberHeaderProperty.AddOwner<CalendarDatePicker>();

/// <summary>
/// Gets a collection of dates that are marked as not selectable.
/// </summary>
Expand Down Expand Up @@ -358,5 +377,36 @@ public VerticalAlignment VerticalContentAlignment
get => GetValue(VerticalContentAlignmentProperty);
set => SetValue(VerticalContentAlignmentProperty, value);
}

/// <summary>
/// Gets or sets a value indicating whether week numbers are shown in the month view
/// of the drop-down <see cref="Calendar"/>.
/// </summary>
Comment thread
timunie marked this conversation as resolved.
Outdated
public bool ShowWeekNumbers
{
get => GetValue(ShowWeekNumbersProperty);
set => SetValue(ShowWeekNumbersProperty, value);
}

/// <summary>
/// Gets or sets the rule used to determine the first week of the year for week number display.
/// The default is taken from the current culture.
/// </summary>
public CalendarWeekRule WeekNumberRule
{
get => GetValue(WeekNumberRuleProperty);
set => SetValue(WeekNumberRuleProperty, value);
}

/// <summary>
/// Gets or sets the content displayed in the week-number column header cell of the drop-down
/// <see cref="Calendar"/>. Set to a localized string such as <c>"CW"</c>, <c>"KW"</c>,
/// or <c>"Wk"</c> to give users context. Defaults to <c>null</c> (blank).
/// </summary>
public object? WeekNumberHeader
{
get => GetValue(WeekNumberHeaderProperty);
set => SetValue(WeekNumberHeaderProperty, value);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,8 @@
<StaticResource x:Key="CalendarViewBorderBrush" ResourceKey="SystemControlForegroundChromeMediumBrush" />
<StaticResource x:Key="CalendarViewWeekDayForegroundDisabled"
ResourceKey="SystemControlDisabledBaseMediumLowBrush" />
<StaticResource x:Key="CalendarViewWeekNumberSeparatorForeground"
ResourceKey="SystemControlDisabledBaseMediumLowBrush" />
Comment on lines +405 to +406

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned above, if the CalendarWeekNumberLabel goes away it would be needed to add CalendarViewWeekNumberForeground and related resources here.

<StaticResource x:Key="CalendarViewNavigationButtonBackground" ResourceKey="SystemControlTransparentBrush" />
<StaticResource x:Key="CalendarViewNavigationButtonForegroundPointerOver"
ResourceKey="SystemControlForegroundBaseHighBrush" />
Expand Down Expand Up @@ -1228,6 +1230,8 @@
<StaticResource x:Key="CalendarViewBorderBrush" ResourceKey="SystemControlForegroundChromeMediumBrush" />
<StaticResource x:Key="CalendarViewWeekDayForegroundDisabled"
ResourceKey="SystemControlDisabledBaseMediumLowBrush" />
<StaticResource x:Key="CalendarViewWeekNumberSeparatorForeground"
ResourceKey="SystemControlDisabledBaseMediumLowBrush" />
<StaticResource x:Key="CalendarViewNavigationButtonBackground" ResourceKey="SystemControlTransparentBrush" />
<StaticResource x:Key="CalendarViewNavigationButtonForegroundPointerOver"
ResourceKey="SystemControlForegroundBaseHighBrush" />
Expand Down
5 changes: 4 additions & 1 deletion src/Avalonia.Themes.Fluent/Controls/CalendarDatePicker.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,10 @@
SelectedDate="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=SelectedDate, Mode=TwoWay}"
DisplayDate="{TemplateBinding DisplayDate}"
DisplayDateStart="{TemplateBinding DisplayDateStart}"
DisplayDateEnd="{TemplateBinding DisplayDateEnd}" />
DisplayDateEnd="{TemplateBinding DisplayDateEnd}"
ShowWeekNumbers="{TemplateBinding ShowWeekNumbers}"
WeekNumberRule="{TemplateBinding WeekNumberRule}"
WeekNumberHeader="{TemplateBinding WeekNumberHeader}" />
</Popup>
</Grid>
</Panel>
Expand Down
Loading