mirror of
https://github.com/AtomUI/AtomUI.git
synced 2026-10-29 15:19:12 +08:00
feat(Calendar): add typed header context with change-value/mode commands
CalendarHeaderContext exposes Value/Mode plus ICommands. ChangeValueCommand commits as Customize source; both commands validate parameter type in CanExecute. Adds internal CalendarRelayCommand helper.
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Windows.Input;
|
||||
using AtomUI.Desktop.Controls.Internal.Calendar;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
@@ -177,4 +178,22 @@ public class Calendar : TemplatedControl
|
||||
|
||||
PanelChanged?.Invoke(this, new CalendarPanelChangedEventArgs(Value.Date, mode));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为自定义 Header 模板构建强类型上下文(spec §5.4)。命令的 CanExecute 校验参数类型;
|
||||
/// ChangeValueCommand 以 <see cref="CalendarSelectSource.Customize"/> 提交,且不自动应用
|
||||
/// ValidRange 或 DisabledDate(约束由自定义 Header 负责)。
|
||||
/// </summary>
|
||||
internal CalendarHeaderContext BuildHeaderContext()
|
||||
{
|
||||
var changeValue = new CalendarRelayCommand(
|
||||
execute: p => CommitUserSelection((DateTime)p!, CalendarSelectSource.Customize),
|
||||
canExecute: p => p is DateTime);
|
||||
|
||||
var changeMode = new CalendarRelayCommand(
|
||||
execute: p => CommitModeChange((CalendarMode)p!),
|
||||
canExecute: p => p is CalendarMode mode && (mode == CalendarMode.Month || mode == CalendarMode.Year));
|
||||
|
||||
return new CalendarHeaderContext(Value.Date, Mode, changeValue, changeMode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace AtomUI.Desktop.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// 自定义 Header 模板(<see cref="Calendar.HeaderTemplate"/>)的数据上下文。
|
||||
/// 不暴露 Calendar 实例;模板只能通过命令提交用户意图。
|
||||
/// </summary>
|
||||
public sealed class CalendarHeaderContext
|
||||
{
|
||||
public CalendarHeaderContext(DateTime value, CalendarMode mode, ICommand changeValueCommand, ICommand changeModeCommand)
|
||||
{
|
||||
Value = value;
|
||||
Mode = mode;
|
||||
ChangeValueCommand = changeValueCommand;
|
||||
ChangeModeCommand = changeModeCommand;
|
||||
}
|
||||
|
||||
/// <summary>当前 Value。</summary>
|
||||
public DateTime Value { get; }
|
||||
|
||||
/// <summary>当前 Mode。</summary>
|
||||
public CalendarMode Mode { get; }
|
||||
|
||||
/// <summary>提交 <see cref="DateTime"/>,来源为 <see cref="CalendarSelectSource.Customize"/>。参数非 <see cref="DateTime"/> 时 CanExecute 为 false。</summary>
|
||||
public ICommand ChangeValueCommand { get; }
|
||||
|
||||
/// <summary>提交 <see cref="CalendarMode"/>。参数非合法枚举时 CanExecute 为 false。</summary>
|
||||
public ICommand ChangeModeCommand { get; }
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using System;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace AtomUI.Desktop.Controls.Internal.Calendar;
|
||||
|
||||
/// <summary>
|
||||
/// Calendar Header Context 使用的轻量 <see cref="ICommand"/>。参数校验交给 <c>canExecute</c>,
|
||||
/// 非法参数时 <see cref="CanExecute"/> 返回 false 且 <see cref="Execute"/> 不动作。
|
||||
/// </summary>
|
||||
internal sealed class CalendarRelayCommand : ICommand
|
||||
{
|
||||
private readonly Action<object?> _execute;
|
||||
private readonly Func<object?, bool> _canExecute;
|
||||
|
||||
public CalendarRelayCommand(Action<object?> execute, Func<object?, bool> canExecute)
|
||||
{
|
||||
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
|
||||
_canExecute = canExecute ?? throw new ArgumentNullException(nameof(canExecute));
|
||||
}
|
||||
|
||||
public event EventHandler? CanExecuteChanged;
|
||||
|
||||
public bool CanExecute(object? parameter) => _canExecute(parameter);
|
||||
|
||||
public void Execute(object? parameter)
|
||||
{
|
||||
if (!_canExecute(parameter))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_execute(parameter);
|
||||
}
|
||||
|
||||
public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using AtomUICalendar = AtomUI.Desktop.Controls.Calendar;
|
||||
|
||||
namespace AtomUI.Desktop.Controls.Tests.Calendar;
|
||||
|
||||
public class CalendarHeaderContextTests
|
||||
{
|
||||
static CalendarHeaderContextTests()
|
||||
{
|
||||
AvaloniaTestApp.EnsureInitialized();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HeaderContext_ExposesValueAndMode()
|
||||
{
|
||||
var c = new AtomUICalendar { Value = new DateTime(2026, 7, 1), Mode = CalendarMode.Year };
|
||||
var ctx = BuildHeaderContext(c);
|
||||
ctx.Value.ShouldBe(new DateTime(2026, 7, 1));
|
||||
ctx.Mode.ShouldBe(CalendarMode.Year);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ChangeValueCommand_RejectsNonDateParameter()
|
||||
{
|
||||
var c = new AtomUICalendar { Value = new DateTime(2026, 7, 1) };
|
||||
var ctx = BuildHeaderContext(c);
|
||||
ctx.ChangeValueCommand.CanExecute("not-a-date").ShouldBeFalse();
|
||||
ctx.ChangeValueCommand.CanExecute(null).ShouldBeFalse();
|
||||
ctx.ChangeValueCommand.CanExecute(new DateTime(2026, 8, 1)).ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ChangeValueCommand_CommitsAsCustomizeSource()
|
||||
{
|
||||
var c = new AtomUICalendar { Value = new DateTime(2026, 7, 1) };
|
||||
CalendarSelectSource? seen = null;
|
||||
c.Selected += (_, e) => seen = e.Source;
|
||||
|
||||
var ctx = BuildHeaderContext(c);
|
||||
ctx.ChangeValueCommand.Execute(new DateTime(2026, 9, 3));
|
||||
|
||||
seen.ShouldBe(CalendarSelectSource.Customize);
|
||||
c.Value.ShouldBe(new DateTime(2026, 9, 3));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ChangeModeCommand_RejectsInvalidParameter()
|
||||
{
|
||||
var c = new AtomUICalendar();
|
||||
var ctx = BuildHeaderContext(c);
|
||||
ctx.ChangeModeCommand.CanExecute("nope").ShouldBeFalse();
|
||||
ctx.ChangeModeCommand.CanExecute((CalendarMode)999).ShouldBeFalse();
|
||||
ctx.ChangeModeCommand.CanExecute(CalendarMode.Year).ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ChangeModeCommand_SwitchesModeAndRaisesPanelChanged()
|
||||
{
|
||||
var c = new AtomUICalendar { Value = new DateTime(2026, 7, 1) };
|
||||
var panelRaised = 0;
|
||||
c.PanelChanged += (_, _) => panelRaised++;
|
||||
|
||||
var ctx = BuildHeaderContext(c);
|
||||
ctx.ChangeModeCommand.Execute(CalendarMode.Year);
|
||||
|
||||
c.Mode.ShouldBe(CalendarMode.Year);
|
||||
panelRaised.ShouldBe(1);
|
||||
}
|
||||
|
||||
private static CalendarHeaderContext BuildHeaderContext(AtomUICalendar c)
|
||||
{
|
||||
var m = typeof(AtomUICalendar).GetMethod(
|
||||
"BuildHeaderContext", BindingFlags.Instance | BindingFlags.NonPublic)!;
|
||||
return (CalendarHeaderContext)m.Invoke(c, Array.Empty<object>())!;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user