feat(Rate): set Value to default to TwoWay binding with data validation support

This commit is contained in:
chinboy
2026-07-06 08:38:02 +08:00
parent fdefbfd928
commit 2c695d54bb
15 changed files with 264 additions and 6 deletions
@@ -15433,6 +15433,8 @@ namespace AtomUI.Theme.Language
dictionary[global::AtomUIGallery.Localization.RateShowCaseLangResourceKind.HalfStarTitle] = global::AtomUIGallery.ShowCases.Rate.en_US.HalfStarTitle;
dictionary[global::AtomUIGallery.Localization.RateShowCaseLangResourceKind.OtherCharacterDescription] = global::AtomUIGallery.ShowCases.Rate.en_US.OtherCharacterDescription;
dictionary[global::AtomUIGallery.Localization.RateShowCaseLangResourceKind.OtherCharacterTitle] = global::AtomUIGallery.ShowCases.Rate.en_US.OtherCharacterTitle;
dictionary[global::AtomUIGallery.Localization.RateShowCaseLangResourceKind.P2ContentClear] = global::AtomUIGallery.ShowCases.Rate.en_US.P2ContentClear;
dictionary[global::AtomUIGallery.Localization.RateShowCaseLangResourceKind.P2ContentSetFourStars] = global::AtomUIGallery.ShowCases.Rate.en_US.P2ContentSetFourStars;
dictionary[global::AtomUIGallery.Localization.RateShowCaseLangResourceKind.P2TextIsallowclearFalse] = global::AtomUIGallery.ShowCases.Rate.en_US.P2TextIsallowclearFalse;
dictionary[global::AtomUIGallery.Localization.RateShowCaseLangResourceKind.P2TextIsallowclearTrue] = global::AtomUIGallery.ShowCases.Rate.en_US.P2TextIsallowclearTrue;
dictionary[global::AtomUIGallery.Localization.RateShowCaseLangResourceKind.P2TooltipBad] = global::AtomUIGallery.ShowCases.Rate.en_US.P2TooltipBad;
@@ -15440,6 +15442,7 @@ namespace AtomUI.Theme.Language
dictionary[global::AtomUIGallery.Localization.RateShowCaseLangResourceKind.P2TooltipNormal] = global::AtomUIGallery.ShowCases.Rate.en_US.P2TooltipNormal;
dictionary[global::AtomUIGallery.Localization.RateShowCaseLangResourceKind.P2TooltipTerrible] = global::AtomUIGallery.ShowCases.Rate.en_US.P2TooltipTerrible;
dictionary[global::AtomUIGallery.Localization.RateShowCaseLangResourceKind.P2TooltipWonderful] = global::AtomUIGallery.ShowCases.Rate.en_US.P2TooltipWonderful;
dictionary[global::AtomUIGallery.Localization.RateShowCaseLangResourceKind.P2TwoWayValueSummaryFormat] = global::AtomUIGallery.ShowCases.Rate.en_US.P2TwoWayValueSummaryFormat;
dictionary[global::AtomUIGallery.Localization.RateShowCaseLangResourceKind.PageDescription] = global::AtomUIGallery.ShowCases.Rate.en_US.PageDescription;
dictionary[global::AtomUIGallery.Localization.RateShowCaseLangResourceKind.PageSubtitle] = global::AtomUIGallery.ShowCases.Rate.en_US.PageSubtitle;
dictionary[global::AtomUIGallery.Localization.RateShowCaseLangResourceKind.ReadOnlyDescription] = global::AtomUIGallery.ShowCases.Rate.en_US.ReadOnlyDescription;
@@ -15461,6 +15464,8 @@ namespace AtomUI.Theme.Language
dictionary[global::AtomUIGallery.Localization.RateShowCaseLangResourceKind.TokenNameStarSizeSM] = global::AtomUIGallery.ShowCases.Rate.en_US.TokenNameStarSizeSM;
dictionary[global::AtomUIGallery.Localization.RateShowCaseLangResourceKind.TokenScopeComponent] = global::AtomUIGallery.ShowCases.Rate.en_US.TokenScopeComponent;
dictionary[global::AtomUIGallery.Localization.RateShowCaseLangResourceKind.TokenStatusStable] = global::AtomUIGallery.ShowCases.Rate.en_US.TokenStatusStable;
dictionary[global::AtomUIGallery.Localization.RateShowCaseLangResourceKind.TwoWayBindingDescription] = global::AtomUIGallery.ShowCases.Rate.en_US.TwoWayBindingDescription;
dictionary[global::AtomUIGallery.Localization.RateShowCaseLangResourceKind.TwoWayBindingTitle] = global::AtomUIGallery.ShowCases.Rate.en_US.TwoWayBindingTitle;
}
catch (System.Exception)
{
@@ -15515,6 +15520,8 @@ namespace AtomUI.Theme.Language
dictionary[global::AtomUIGallery.Localization.RateShowCaseLangResourceKind.HalfStarTitle] = global::AtomUIGallery.ShowCases.Rate.zh_CN.HalfStarTitle;
dictionary[global::AtomUIGallery.Localization.RateShowCaseLangResourceKind.OtherCharacterDescription] = global::AtomUIGallery.ShowCases.Rate.zh_CN.OtherCharacterDescription;
dictionary[global::AtomUIGallery.Localization.RateShowCaseLangResourceKind.OtherCharacterTitle] = global::AtomUIGallery.ShowCases.Rate.zh_CN.OtherCharacterTitle;
dictionary[global::AtomUIGallery.Localization.RateShowCaseLangResourceKind.P2ContentClear] = global::AtomUIGallery.ShowCases.Rate.zh_CN.P2ContentClear;
dictionary[global::AtomUIGallery.Localization.RateShowCaseLangResourceKind.P2ContentSetFourStars] = global::AtomUIGallery.ShowCases.Rate.zh_CN.P2ContentSetFourStars;
dictionary[global::AtomUIGallery.Localization.RateShowCaseLangResourceKind.P2TextIsallowclearFalse] = global::AtomUIGallery.ShowCases.Rate.zh_CN.P2TextIsallowclearFalse;
dictionary[global::AtomUIGallery.Localization.RateShowCaseLangResourceKind.P2TextIsallowclearTrue] = global::AtomUIGallery.ShowCases.Rate.zh_CN.P2TextIsallowclearTrue;
dictionary[global::AtomUIGallery.Localization.RateShowCaseLangResourceKind.P2TooltipBad] = global::AtomUIGallery.ShowCases.Rate.zh_CN.P2TooltipBad;
@@ -15522,6 +15529,7 @@ namespace AtomUI.Theme.Language
dictionary[global::AtomUIGallery.Localization.RateShowCaseLangResourceKind.P2TooltipNormal] = global::AtomUIGallery.ShowCases.Rate.zh_CN.P2TooltipNormal;
dictionary[global::AtomUIGallery.Localization.RateShowCaseLangResourceKind.P2TooltipTerrible] = global::AtomUIGallery.ShowCases.Rate.zh_CN.P2TooltipTerrible;
dictionary[global::AtomUIGallery.Localization.RateShowCaseLangResourceKind.P2TooltipWonderful] = global::AtomUIGallery.ShowCases.Rate.zh_CN.P2TooltipWonderful;
dictionary[global::AtomUIGallery.Localization.RateShowCaseLangResourceKind.P2TwoWayValueSummaryFormat] = global::AtomUIGallery.ShowCases.Rate.zh_CN.P2TwoWayValueSummaryFormat;
dictionary[global::AtomUIGallery.Localization.RateShowCaseLangResourceKind.PageDescription] = global::AtomUIGallery.ShowCases.Rate.zh_CN.PageDescription;
dictionary[global::AtomUIGallery.Localization.RateShowCaseLangResourceKind.PageSubtitle] = global::AtomUIGallery.ShowCases.Rate.zh_CN.PageSubtitle;
dictionary[global::AtomUIGallery.Localization.RateShowCaseLangResourceKind.ReadOnlyDescription] = global::AtomUIGallery.ShowCases.Rate.zh_CN.ReadOnlyDescription;
@@ -15543,6 +15551,8 @@ namespace AtomUI.Theme.Language
dictionary[global::AtomUIGallery.Localization.RateShowCaseLangResourceKind.TokenNameStarSizeSM] = global::AtomUIGallery.ShowCases.Rate.zh_CN.TokenNameStarSizeSM;
dictionary[global::AtomUIGallery.Localization.RateShowCaseLangResourceKind.TokenScopeComponent] = global::AtomUIGallery.ShowCases.Rate.zh_CN.TokenScopeComponent;
dictionary[global::AtomUIGallery.Localization.RateShowCaseLangResourceKind.TokenStatusStable] = global::AtomUIGallery.ShowCases.Rate.zh_CN.TokenStatusStable;
dictionary[global::AtomUIGallery.Localization.RateShowCaseLangResourceKind.TwoWayBindingDescription] = global::AtomUIGallery.ShowCases.Rate.zh_CN.TwoWayBindingDescription;
dictionary[global::AtomUIGallery.Localization.RateShowCaseLangResourceKind.TwoWayBindingTitle] = global::AtomUIGallery.ShowCases.Rate.zh_CN.TwoWayBindingTitle;
}
catch (System.Exception)
{
@@ -15597,6 +15607,8 @@ namespace AtomUI.Theme.Language
dictionary[global::AtomUIGallery.Localization.RateShowCaseLangResourceKind.HalfStarTitle] = global::AtomUIGallery.ShowCases.Rate.zh_TW.HalfStarTitle;
dictionary[global::AtomUIGallery.Localization.RateShowCaseLangResourceKind.OtherCharacterDescription] = global::AtomUIGallery.ShowCases.Rate.zh_TW.OtherCharacterDescription;
dictionary[global::AtomUIGallery.Localization.RateShowCaseLangResourceKind.OtherCharacterTitle] = global::AtomUIGallery.ShowCases.Rate.zh_TW.OtherCharacterTitle;
dictionary[global::AtomUIGallery.Localization.RateShowCaseLangResourceKind.P2ContentClear] = global::AtomUIGallery.ShowCases.Rate.zh_TW.P2ContentClear;
dictionary[global::AtomUIGallery.Localization.RateShowCaseLangResourceKind.P2ContentSetFourStars] = global::AtomUIGallery.ShowCases.Rate.zh_TW.P2ContentSetFourStars;
dictionary[global::AtomUIGallery.Localization.RateShowCaseLangResourceKind.P2TextIsallowclearFalse] = global::AtomUIGallery.ShowCases.Rate.zh_TW.P2TextIsallowclearFalse;
dictionary[global::AtomUIGallery.Localization.RateShowCaseLangResourceKind.P2TextIsallowclearTrue] = global::AtomUIGallery.ShowCases.Rate.zh_TW.P2TextIsallowclearTrue;
dictionary[global::AtomUIGallery.Localization.RateShowCaseLangResourceKind.P2TooltipBad] = global::AtomUIGallery.ShowCases.Rate.zh_TW.P2TooltipBad;
@@ -15604,6 +15616,7 @@ namespace AtomUI.Theme.Language
dictionary[global::AtomUIGallery.Localization.RateShowCaseLangResourceKind.P2TooltipNormal] = global::AtomUIGallery.ShowCases.Rate.zh_TW.P2TooltipNormal;
dictionary[global::AtomUIGallery.Localization.RateShowCaseLangResourceKind.P2TooltipTerrible] = global::AtomUIGallery.ShowCases.Rate.zh_TW.P2TooltipTerrible;
dictionary[global::AtomUIGallery.Localization.RateShowCaseLangResourceKind.P2TooltipWonderful] = global::AtomUIGallery.ShowCases.Rate.zh_TW.P2TooltipWonderful;
dictionary[global::AtomUIGallery.Localization.RateShowCaseLangResourceKind.P2TwoWayValueSummaryFormat] = global::AtomUIGallery.ShowCases.Rate.zh_TW.P2TwoWayValueSummaryFormat;
dictionary[global::AtomUIGallery.Localization.RateShowCaseLangResourceKind.PageDescription] = global::AtomUIGallery.ShowCases.Rate.zh_TW.PageDescription;
dictionary[global::AtomUIGallery.Localization.RateShowCaseLangResourceKind.PageSubtitle] = global::AtomUIGallery.ShowCases.Rate.zh_TW.PageSubtitle;
dictionary[global::AtomUIGallery.Localization.RateShowCaseLangResourceKind.ReadOnlyDescription] = global::AtomUIGallery.ShowCases.Rate.zh_TW.ReadOnlyDescription;
@@ -15625,6 +15638,8 @@ namespace AtomUI.Theme.Language
dictionary[global::AtomUIGallery.Localization.RateShowCaseLangResourceKind.TokenNameStarSizeSM] = global::AtomUIGallery.ShowCases.Rate.zh_TW.TokenNameStarSizeSM;
dictionary[global::AtomUIGallery.Localization.RateShowCaseLangResourceKind.TokenScopeComponent] = global::AtomUIGallery.ShowCases.Rate.zh_TW.TokenScopeComponent;
dictionary[global::AtomUIGallery.Localization.RateShowCaseLangResourceKind.TokenStatusStable] = global::AtomUIGallery.ShowCases.Rate.zh_TW.TokenStatusStable;
dictionary[global::AtomUIGallery.Localization.RateShowCaseLangResourceKind.TwoWayBindingDescription] = global::AtomUIGallery.ShowCases.Rate.zh_TW.TwoWayBindingDescription;
dictionary[global::AtomUIGallery.Localization.RateShowCaseLangResourceKind.TwoWayBindingTitle] = global::AtomUIGallery.ShowCases.Rate.zh_TW.TwoWayBindingTitle;
}
catch (System.Exception)
{
@@ -4872,6 +4872,8 @@ namespace AtomUIGallery.Localization
HalfStarTitle,
OtherCharacterDescription,
OtherCharacterTitle,
P2ContentClear,
P2ContentSetFourStars,
P2TextIsallowclearFalse,
P2TextIsallowclearTrue,
P2TooltipBad,
@@ -4879,6 +4881,7 @@ namespace AtomUIGallery.Localization
P2TooltipNormal,
P2TooltipTerrible,
P2TooltipWonderful,
P2TwoWayValueSummaryFormat,
PageDescription,
PageSubtitle,
ReadOnlyDescription,
@@ -4899,7 +4902,9 @@ namespace AtomUIGallery.Localization
TokenNameStarSizeLG,
TokenNameStarSizeSM,
TokenScopeComponent,
TokenStatusStable
TokenStatusStable,
TwoWayBindingDescription,
TwoWayBindingTitle
}
public class RateShowCaseLangResourceExtension : LanguageResourceExtension<RateShowCaseLangResourceKind>
@@ -23,7 +23,7 @@ internal partial class en_US
public const string ApiPropertyStarColor = "Overrides the filled rating item color.";
public const string ApiPropertyStarBgColor = "Overrides the unselected rating item color.";
public const string ApiPropertyCount = "Total number of rating items.";
public const string ApiPropertyValue = "Current selected rating value.";
public const string ApiPropertyValue = "Current selected rating value. It uses TwoWay binding by default and supports Avalonia data validation.";
public const string ApiPropertyDefaultValue = "Initial rating value used when Value is not explicitly set.";
public const string ApiPropertyIsKeyboardEnabled = "Controls whether keyboard interaction is enabled.";
public const string ApiPropertyToolTips = "Tooltip text list displayed for each rating value.";
@@ -45,6 +45,8 @@ internal partial class en_US
public const string TokenStatusStable = "Stable";
public const string BasicTitle = "Basic";
public const string BasicDescription = "The simplest usage.";
public const string TwoWayBindingTitle = "Two-way binding";
public const string TwoWayBindingDescription = "Value uses TwoWay binding by default, so user selection and ViewModel updates stay synchronized.";
public const string HalfStarTitle = "Half star";
public const string HalfStarDescription = "Support select half star.";
public const string ShowCopywritingTitle = "Show copywriting";
@@ -57,6 +59,9 @@ internal partial class en_US
public const string OtherCharacterDescription = "Replace the default star to other character like alphabet, digit, iconfont or even Chinese word.";
public const string P2TextIsallowclearTrue = "IsAllowClear: true";
public const string P2TextIsallowclearFalse = "IsAllowClear: false";
public const string P2ContentSetFourStars = "Set 4 stars";
public const string P2ContentClear = "Clear";
public const string P2TwoWayValueSummaryFormat = "Selected value: {0:0.#}";
public const string P2TooltipTerrible = "terrible";
public const string P2TooltipBad = "bad";
public const string P2TooltipNormal = "normal";
@@ -23,7 +23,7 @@ internal partial class zh_CN
public const string ApiPropertyStarColor = "覆盖已选评分项颜色。";
public const string ApiPropertyStarBgColor = "覆盖未选评分项颜色。";
public const string ApiPropertyCount = "评分项总数。";
public const string ApiPropertyValue = "当前选中的评分值。";
public const string ApiPropertyValue = "当前选中的评分值,默认 TwoWay 绑定,并支持 Avalonia 数据验证。";
public const string ApiPropertyDefaultValue = "未显式设置 Value 时使用的初始评分值。";
public const string ApiPropertyIsKeyboardEnabled = "控制是否启用键盘交互。";
public const string ApiPropertyToolTips = "每个评分值对应的提示文本列表。";
@@ -45,6 +45,8 @@ internal partial class zh_CN
public const string TokenStatusStable = "稳定";
public const string BasicTitle = "基础用法";
public const string BasicDescription = "最简单的用法。";
public const string TwoWayBindingTitle = "双向绑定";
public const string TwoWayBindingDescription = "Value 默认使用 TwoWay 绑定,用户评分和 ViewModel 更新会保持同步。";
public const string HalfStarTitle = "半星";
public const string HalfStarDescription = "支持选择半星。";
public const string ShowCopywritingTitle = "显示文案";
@@ -57,6 +59,9 @@ internal partial class zh_CN
public const string OtherCharacterDescription = "将默认星形替换为其他字符,例如字母、数字、图标字体,甚至中文文字。";
public const string P2TextIsallowclearTrue = "允许清除:true";
public const string P2TextIsallowclearFalse = "允许清除:false";
public const string P2ContentSetFourStars = "设为 4 星";
public const string P2ContentClear = "清空";
public const string P2TwoWayValueSummaryFormat = "当前评分:{0:0.#}";
public const string P2TooltipTerrible = "糟糕";
public const string P2TooltipBad = "不好";
public const string P2TooltipNormal = "一般";
@@ -23,7 +23,7 @@ internal partial class zh_TW
public const string ApiPropertyStarColor = "覆蓋已選評分項顏色。";
public const string ApiPropertyStarBgColor = "覆蓋未選評分項顏色。";
public const string ApiPropertyCount = "評分項總數。";
public const string ApiPropertyValue = "當前選中的評分值。";
public const string ApiPropertyValue = "當前選中的評分值,預設 TwoWay 綁定,並支援 Avalonia 資料驗證。";
public const string ApiPropertyDefaultValue = "未顯式設置 Value 時使用的初始評分值。";
public const string ApiPropertyIsKeyboardEnabled = "控制是否啟用鍵盤交互。";
public const string ApiPropertyToolTips = "每個評分值對應的提示文本列表。";
@@ -45,6 +45,8 @@ internal partial class zh_TW
public const string TokenStatusStable = "穩定";
public const string BasicTitle = "基礎用法";
public const string BasicDescription = "最簡單的用法。";
public const string TwoWayBindingTitle = "雙向綁定";
public const string TwoWayBindingDescription = "Value 預設使用 TwoWay 綁定,使用者評分和 ViewModel 更新會保持同步。";
public const string HalfStarTitle = "半星";
public const string HalfStarDescription = "支持選擇半星。";
public const string ShowCopywritingTitle = "顯示文案";
@@ -57,6 +59,9 @@ internal partial class zh_TW
public const string OtherCharacterDescription = "將默認星形替換為其他字符,例如字母、數字、圖標字體,甚至中文文字。";
public const string P2TextIsallowclearTrue = "允許清除:true";
public const string P2TextIsallowclearFalse = "允許清除:false";
public const string P2ContentSetFourStars = "設為 4 星";
public const string P2ContentClear = "清空";
public const string P2TwoWayValueSummaryFormat = "目前評分:{0:0.#}";
public const string P2TooltipTerrible = "糟糕";
public const string P2TooltipBad = "不好";
public const string P2TooltipNormal = "一般";
@@ -1,5 +1,7 @@
using System.Collections.ObjectModel;
using System.Collections.Generic;
using System.Globalization;
using System.Reactive;
using AtomUI.Controls;
using AtomUI.Data;
using AtomUIGallery.Localization;
@@ -20,6 +22,8 @@ public class RateViewModel : ReactiveObject, IRoutableViewModel
private ObservableCollection<RateApiRow>? _apiRows;
private ObservableCollection<RateDesignTokenRow>? _designTokenRows;
private IList<string>? _tooltips;
private double _twoWayValue;
private string? _twoWayValueSummary;
public ObservableCollection<RateApiRow>? ApiRows
{
@@ -39,6 +43,27 @@ public class RateViewModel : ReactiveObject, IRoutableViewModel
set => this.RaiseAndSetIfChanged(ref _tooltips, value);
}
public double TwoWayValue
{
get => _twoWayValue;
set
{
if (Math.Abs(_twoWayValue - value) < double.Epsilon)
{
return;
}
this.RaiseAndSetIfChanged(ref _twoWayValue, value);
UpdateTwoWayValueSummary();
}
}
public string? TwoWayValueSummary
{
get => _twoWayValueSummary;
set => this.RaiseAndSetIfChanged(ref _twoWayValueSummary, value);
}
private string? _activeTooltip;
public string? ActiveTooltip
@@ -47,9 +72,38 @@ public class RateViewModel : ReactiveObject, IRoutableViewModel
set => this.RaiseAndSetIfChanged(ref _activeTooltip, value);
}
public ReactiveCommand<Unit, Unit> SetFourStarsCommand { get; }
public ReactiveCommand<Unit, Unit> ClearTwoWayValueCommand { get; }
public RateViewModel(IScreen screen)
{
HostScreen = screen;
_twoWayValue = 2.0;
SetFourStarsCommand = ReactiveCommand.Create(HandleSetFourStars);
ClearTwoWayValueCommand = ReactiveCommand.Create(HandleClearTwoWayValue);
UpdateTwoWayValueSummary();
}
public void RefreshLocalizedState()
{
UpdateTwoWayValueSummary();
}
private void HandleSetFourStars()
{
TwoWayValue = 4.0;
}
private void HandleClearTwoWayValue()
{
TwoWayValue = 0.0;
}
private void UpdateTwoWayValueSummary()
{
TwoWayValueSummary = string.Format(CultureInfo.CurrentCulture,
RateShowCaseLanguage.Get(RateShowCaseLangResourceKind.P2TwoWayValueSummaryFormat, "Selected value: {0:0.#}"),
TwoWayValue);
}
public void EnsureApiRows()
@@ -124,6 +178,11 @@ public class RateViewModel : ReactiveObject, IRoutableViewModel
RateShowCaseLangResourceKind.ApiPropertyIsMotionEnabled => en_US.ApiPropertyIsMotionEnabled,
RateShowCaseLangResourceKind.ApiPropertyValueChanged => en_US.ApiPropertyValueChanged,
RateShowCaseLangResourceKind.ApiPropertyHoverValueChanged => en_US.ApiPropertyHoverValueChanged,
RateShowCaseLangResourceKind.TwoWayBindingTitle => en_US.TwoWayBindingTitle,
RateShowCaseLangResourceKind.TwoWayBindingDescription => en_US.TwoWayBindingDescription,
RateShowCaseLangResourceKind.P2ContentSetFourStars => en_US.P2ContentSetFourStars,
RateShowCaseLangResourceKind.P2ContentClear => en_US.P2ContentClear,
RateShowCaseLangResourceKind.P2TwoWayValueSummaryFormat => en_US.P2TwoWayValueSummaryFormat,
RateShowCaseLangResourceKind.TokenNameStarColor => en_US.TokenNameStarColor,
RateShowCaseLangResourceKind.TokenNameStarSize => en_US.TokenNameStarSize,
RateShowCaseLangResourceKind.TokenNameStarSizeSM => en_US.TokenNameStarSizeSM,
@@ -52,6 +52,29 @@
</gallery:ShowCaseItem.DeferredContentTemplate>
</gallery:ShowCaseItem>
<gallery:ShowCaseItem
Title="{gallery:RateShowCaseLangResource TwoWayBindingTitle}"
Description="{gallery:RateShowCaseLangResource TwoWayBindingDescription}"
BadgeText="v6.0.8"
IsDeferredContentEnabled="True">
<gallery:ShowCaseItem.DeferredContentTemplate>
<DataTemplate x:DataType="vm:RateViewModel">
<StackPanel Orientation="Vertical" Spacing="10">
<atom:Rate Value="{Binding TwoWayValue}" />
<atom:TextBlock Text="{Binding TwoWayValueSummary}" />
<StackPanel Orientation="Horizontal" Spacing="10">
<atom:Button SizeType="Small"
Command="{Binding SetFourStarsCommand}"
Content="{gallery:RateShowCaseLangResource P2ContentSetFourStars}" />
<atom:Button SizeType="Small"
Command="{Binding ClearTwoWayValueCommand}"
Content="{gallery:RateShowCaseLangResource P2ContentClear}" />
</StackPanel>
</StackPanel>
</DataTemplate>
</gallery:ShowCaseItem.DeferredContentTemplate>
</gallery:ShowCaseItem>
<gallery:ShowCaseItem
Title="{gallery:RateShowCaseLangResource HalfStarTitle}"
Description="{gallery:RateShowCaseLangResource HalfStarDescription}"
@@ -86,6 +86,7 @@ public partial class RateShowCase : GalleryReactiveUserControl<RateViewModel>
RateShowCaseLanguage.Get(RateShowCaseLangResourceKind.P2TooltipWonderful, "wonderful"),
};
SyncActiveTooltip(viewModel, 0);
viewModel.RefreshLocalizedState();
}
private void HandleValueChanged(object? sender, RateValueChangedEventArgs e)
@@ -2,6 +2,14 @@
本文档记录 Rate 控件级设计、API、主题契约、Token 和实现结构的变化。它不替代仓库根目录 `CHANGELOG.md`,也不作为正式版本发布说明。
## 2026-07-06
- Behavior
- Make `Rate.Value` default to TwoWay binding and enable Avalonia data validation.
- Keep user rating, Form value and ViewModel state on the same `Value` source of truth.
- Gallery
- Add a `v6.0.8` Rate binding example showing user selection and ViewModel updates staying synchronized.
## 2026-06-26
- Docs
@@ -72,6 +72,8 @@ Public API / ItemsSource / Command / Event
- 伪类和 internal state 必须从单一 owner 推导,避免双向同步导致循环更新。
- Gallery API 表中的状态说明应与源码实际状态流一致。
`AbstractRate.Value` 是评分控件的 Form value 和当前评分 source of truth。属性注册默认 `BindingMode.TwoWay` 并启用 Avalonia data validation;pointer、keyboard 和 Form set/clear 都应更新 `Value`,再由 `Value` 回放到 `EffectiveValue` 和 `RateItem.SelectedState`。
## 5. 生命周期与模板接入
生命周期规则:
@@ -106,6 +108,7 @@ Rate 的交互事件应从输入源收敛到控件级语义事件:
- Template part 重新应用时的状态回放。
- 主题资源、Token 和 SharedToken 计算后的视觉更新。
- 内容、命令和视觉状态在模板节点之间的同步。
- `Value` 的 ViewModel 更新、用户评分、Form set/get/clear、`ValueChanged` 和 hover 状态通知。
- 动效启停、初始加载阶段 transition 抑制和卸载取消。
实现文档不逐行解释私有方法。若某个私有算法成为稳定维护入口,应在本节补充算法不变量,而不是把代码复述为说明书。
@@ -78,6 +78,7 @@ Public API / inherited command / item source / user input
- Disabled 或不可交互状态优先屏蔽 pointer、keyboard、motion 和提交类反馈。
- selection/checked/active、input/value、motion、visual option 状态由控件实例或明确的数据 owner 推导,不能在 template part 之间双向竞争。
- `Rate.Value` 是评分控件的外部值 owner,默认 `BindingMode.TwoWay` 并启用 Avalonia data validation;用户评分、键盘调整、Form value 和 ViewModel 更新必须收敛到同一份数值状态。
- 模板重套用时必须把 public API 对应状态回放到新的 part、伪类和主题变量。
- 集合、弹层、异步、动效或窗口相关状态必须能处理 reset、close、cancel、detach 和 owner 释放。
@@ -138,6 +139,8 @@ Rate 与同分类控件共享尺寸、状态、Token、Gallery 展示和验证
Rate 的当前项状态必须由单一 owner 推导。public 选择属性、集合项容器和伪类之间只能做单向同步,集合替换、清空和模板重套用时必须回放当前状态。
`Value` 作为 Form value 和受控评分值暴露给用户。控件从 `Value` 同步 `EffectiveValue` 与每个 `RateItem.SelectedState`,也会在 pointer 或 keyboard 交互后更新 `Value`,默认绑定模式负责把该值回写到 ViewModel。
### 8.2 动效模型
Rate 的动效只表达状态变化反馈,不应改变 public API 语义。初始加载、禁用态和卸载路径应能抑制或取消动效,避免保留旧控件实例。
+7 -2
View File
@@ -5,6 +5,7 @@ using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Media;
@@ -39,7 +40,11 @@ public abstract class AbstractRate : TemplatedControl,
AvaloniaProperty.Register<AbstractRate, int>(nameof(Count), 5);
public static readonly StyledProperty<double> ValueProperty =
AvaloniaProperty.Register<AbstractRate, double>(nameof(Value), double.NaN);
AvaloniaProperty.Register<AbstractRate, double>(
nameof(Value),
double.NaN,
defaultBindingMode: BindingMode.TwoWay,
enableDataValidation: true);
public static readonly StyledProperty<double> DefaultValueProperty =
AvaloniaProperty.Register<AbstractRate, double>(nameof(DefaultValue), 0);
@@ -543,4 +548,4 @@ public abstract class AbstractRate : TemplatedControl,
{
}
#endregion
}
}
@@ -0,0 +1,95 @@
using System.ComponentModel;
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Threading;
using Shouldly;
using Xunit;
using AvaloniaWindow = Avalonia.Controls.Window;
namespace AtomUI.Desktop.Controls.Tests.Rate;
public class RateBindingTests
{
static RateBindingTests()
{
AvaloniaTestApp.EnsureInitialized();
}
[Fact]
public void Value_Is_TwoWay_And_DataValidation_Enabled()
{
var metadata = Desktop.Controls.Rate.ValueProperty.GetMetadata(typeof(Desktop.Controls.Rate));
metadata.DefaultBindingMode.ShouldBe(BindingMode.TwoWay);
metadata.EnableDataValidation.ShouldBe(true);
}
[Fact]
public void Value_DefaultBindingMode_Updates_ViewModel()
{
var viewModel = new RateBindingViewModel
{
Value = 2.0
};
var rate = new Desktop.Controls.Rate();
rate.Bind(
Desktop.Controls.Rate.ValueProperty,
new Binding(nameof(RateBindingViewModel.Value))
{
Source = viewModel
});
ShowInWindow(rate, () =>
{
rate.Value.ShouldBe(2.0);
rate.Value = 4.0;
Dispatcher.UIThread.RunJobs();
viewModel.Value.ShouldBe(4.0);
});
}
private static void ShowInWindow(Control content, Action assertion)
{
var window = new AvaloniaWindow
{
Width = 360,
Height = 240,
Content = content
};
try
{
window.Show();
Dispatcher.UIThread.RunJobs();
assertion();
}
finally
{
window.Close();
}
}
private sealed class RateBindingViewModel : INotifyPropertyChanged
{
private double _value;
public double Value
{
get => _value;
set
{
if (Math.Abs(_value - value) < double.Epsilon)
{
return;
}
_value = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Value)));
}
}
public event PropertyChangedEventHandler? PropertyChanged;
}
}
@@ -4,6 +4,23 @@ Description="{gallery:RateShowCaseLangResource BasicDescription}">
<atom:Rate/>
</gallery:ShowCaseItem>
<gallery:ShowCaseItem
Title="{gallery:RateShowCaseLangResource TwoWayBindingTitle}"
Description="{gallery:RateShowCaseLangResource TwoWayBindingDescription}"
BadgeText="v6.0.8">
<StackPanel Orientation="Vertical" Spacing="10">
<atom:Rate Value="{Binding TwoWayValue}" />
<atom:TextBlock Text="{Binding TwoWayValueSummary}" />
<StackPanel Orientation="Horizontal" Spacing="10">
<atom:Button SizeType="Small"
Command="{Binding SetFourStarsCommand}"
Content="{gallery:RateShowCaseLangResource P2ContentSetFourStars}" />
<atom:Button SizeType="Small"
Command="{Binding ClearTwoWayValueCommand}"
Content="{gallery:RateShowCaseLangResource P2ContentClear}" />
</StackPanel>
</StackPanel>
</gallery:ShowCaseItem>
<gallery:ShowCaseItem
Title="{gallery:RateShowCaseLangResource HalfStarTitle}"
Description="{gallery:RateShowCaseLangResource HalfStarDescription}">
<atom:Rate DefaultValue="3.5" IsAllowHalf="True"/>
@@ -41,6 +41,8 @@ public class RateShowCasePageTests
source.ShouldContain("Description=\"{gallery:RateShowCaseLangResource PageDescription}\"");
source.ShouldContain("<gallery:ShowCaseItem");
source.ShouldContain("RateShowCaseLangResource BasicTitle");
source.ShouldContain("RateShowCaseLangResource TwoWayBindingTitle");
source.ShouldContain("BadgeText=\"v6.0.8\"");
source.ShouldContain("RateShowCaseLangResource HalfStarTitle");
source.ShouldContain("RateShowCaseLangResource ShowCopywritingTitle");
source.ShouldContain("RateShowCaseLangResource ReadOnlyTitle");
@@ -67,6 +69,10 @@ public class RateShowCasePageTests
pageSource.ShouldContain("Tag=\"DesignToken\"");
pageSource.ShouldNotContain("ItemsSource=\"{Binding ApiRows}\"");
pageSource.ShouldNotContain("ItemsSource=\"{Binding DesignTokenRows}\"");
pageSource.ShouldContain("Value=\"{Binding TwoWayValue}\"");
pageSource.ShouldContain("Text=\"{Binding TwoWayValueSummary}\"");
pageSource.ShouldContain("Command=\"{Binding SetFourStarsCommand}\"");
pageSource.ShouldContain("Command=\"{Binding ClearTwoWayValueCommand}\"");
codeBehindSource.ShouldContain("new GalleryShowCaseScenarioController");
codeBehindSource.ShouldContain("_scenarioController.Attach(DataContext)");
@@ -125,11 +131,14 @@ public class RateShowCasePageTests
source.ShouldContain("ScenarioApi");
source.ShouldContain("ScenarioDesignToken");
source.ShouldContain("PageSubtitle");
source.ShouldContain("TwoWayBindingTitle");
source.ShouldContain("TwoWayBindingDescription");
source.ShouldNotContain("InfoNamespaceLabel");
source.ShouldContain("ApiPropertyIsAllowClear");
source.ShouldContain("ApiPropertyIsAllowHalf");
source.ShouldContain("ApiPropertyToolTips");
source.ShouldContain("ApiPropertyValueChanged");
source.ShouldContain("P2TwoWayValueSummaryFormat");
source.ShouldContain("TokenNameStarColor");
source.ShouldContain("TokenNameStarHoverScale");
}