diff --git a/controlgallery/AtomUIGallery/GeneratedFiles/AtomUI.Generator/AtomUI.Generator.LanguageGenerator/LanguageProviderPool.g.cs b/controlgallery/AtomUIGallery/GeneratedFiles/AtomUI.Generator/AtomUI.Generator.LanguageGenerator/LanguageProviderPool.g.cs index 2a9fe2c32..80e780e7a 100644 --- a/controlgallery/AtomUIGallery/GeneratedFiles/AtomUI.Generator/AtomUI.Generator.LanguageGenerator/LanguageProviderPool.g.cs +++ b/controlgallery/AtomUIGallery/GeneratedFiles/AtomUI.Generator/AtomUI.Generator.LanguageGenerator/LanguageProviderPool.g.cs @@ -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) { diff --git a/controlgallery/AtomUIGallery/GeneratedFiles/AtomUI.Generator/AtomUI.Generator.LanguageGenerator/LanguageResourceConst.g.cs b/controlgallery/AtomUIGallery/GeneratedFiles/AtomUI.Generator/AtomUI.Generator.LanguageGenerator/LanguageResourceConst.g.cs index 8022917c0..21869cfd8 100644 --- a/controlgallery/AtomUIGallery/GeneratedFiles/AtomUI.Generator/AtomUI.Generator.LanguageGenerator/LanguageResourceConst.g.cs +++ b/controlgallery/AtomUIGallery/GeneratedFiles/AtomUI.Generator/AtomUI.Generator.LanguageGenerator/LanguageResourceConst.g.cs @@ -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 diff --git a/controlgallery/AtomUIGallery/ShowCases/DataEntry/Rate/Localization/en_US.cs b/controlgallery/AtomUIGallery/ShowCases/DataEntry/Rate/Localization/en_US.cs index f5e9821d7..7f51a044c 100644 --- a/controlgallery/AtomUIGallery/ShowCases/DataEntry/Rate/Localization/en_US.cs +++ b/controlgallery/AtomUIGallery/ShowCases/DataEntry/Rate/Localization/en_US.cs @@ -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"; diff --git a/controlgallery/AtomUIGallery/ShowCases/DataEntry/Rate/Localization/zh_CN.cs b/controlgallery/AtomUIGallery/ShowCases/DataEntry/Rate/Localization/zh_CN.cs index dd4c5a454..2295a8195 100644 --- a/controlgallery/AtomUIGallery/ShowCases/DataEntry/Rate/Localization/zh_CN.cs +++ b/controlgallery/AtomUIGallery/ShowCases/DataEntry/Rate/Localization/zh_CN.cs @@ -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 = "一般"; diff --git a/controlgallery/AtomUIGallery/ShowCases/DataEntry/Rate/Localization/zh_TW.cs b/controlgallery/AtomUIGallery/ShowCases/DataEntry/Rate/Localization/zh_TW.cs index 61f8a6947..fef0ae9de 100644 --- a/controlgallery/AtomUIGallery/ShowCases/DataEntry/Rate/Localization/zh_TW.cs +++ b/controlgallery/AtomUIGallery/ShowCases/DataEntry/Rate/Localization/zh_TW.cs @@ -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 = "一般"; diff --git a/controlgallery/AtomUIGallery/ShowCases/DataEntry/Rate/ViewModels/RateViewModel.cs b/controlgallery/AtomUIGallery/ShowCases/DataEntry/Rate/ViewModels/RateViewModel.cs index 7a35e2588..fcda625ff 100644 --- a/controlgallery/AtomUIGallery/ShowCases/DataEntry/Rate/ViewModels/RateViewModel.cs +++ b/controlgallery/AtomUIGallery/ShowCases/DataEntry/Rate/ViewModels/RateViewModel.cs @@ -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? _apiRows; private ObservableCollection? _designTokenRows; private IList? _tooltips; + private double _twoWayValue; + private string? _twoWayValueSummary; public ObservableCollection? 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 SetFourStarsCommand { get; } + public ReactiveCommand 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, diff --git a/controlgallery/AtomUIGallery/ShowCases/DataEntry/Rate/Views/RateShowCase.axaml b/controlgallery/AtomUIGallery/ShowCases/DataEntry/Rate/Views/RateShowCase.axaml index c1f0c7915..63c77b043 100644 --- a/controlgallery/AtomUIGallery/ShowCases/DataEntry/Rate/Views/RateShowCase.axaml +++ b/controlgallery/AtomUIGallery/ShowCases/DataEntry/Rate/Views/RateShowCase.axaml @@ -52,6 +52,29 @@ + + + + + + + + + + + + + + + RateShowCaseLanguage.Get(RateShowCaseLangResourceKind.P2TooltipWonderful, "wonderful"), }; SyncActiveTooltip(viewModel, 0); + viewModel.RefreshLocalizedState(); } private void HandleValueChanged(object? sender, RateValueChangedEventArgs e) diff --git a/docs/controls/desktop/data-entry/rate/changelog.md b/docs/controls/desktop/data-entry/rate/changelog.md index b07854425..3cad2afe4 100644 --- a/docs/controls/desktop/data-entry/rate/changelog.md +++ b/docs/controls/desktop/data-entry/rate/changelog.md @@ -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 diff --git a/docs/controls/desktop/data-entry/rate/implementation.md b/docs/controls/desktop/data-entry/rate/implementation.md index 8301f799c..ff9ba10bb 100644 --- a/docs/controls/desktop/data-entry/rate/implementation.md +++ b/docs/controls/desktop/data-entry/rate/implementation.md @@ -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 抑制和卸载取消。 实现文档不逐行解释私有方法。若某个私有算法成为稳定维护入口,应在本节补充算法不变量,而不是把代码复述为说明书。 diff --git a/docs/controls/desktop/data-entry/rate/overview.md b/docs/controls/desktop/data-entry/rate/overview.md index 34b9a22c1..1d26b8bb1 100644 --- a/docs/controls/desktop/data-entry/rate/overview.md +++ b/docs/controls/desktop/data-entry/rate/overview.md @@ -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 语义。初始加载、禁用态和卸载路径应能抑制或取消动效,避免保留旧控件实例。 diff --git a/src/AtomUI.Controls/Rate/AbstractRate.cs b/src/AtomUI.Controls/Rate/AbstractRate.cs index 61cdd869c..28df8cf96 100644 --- a/src/AtomUI.Controls/Rate/AbstractRate.cs +++ b/src/AtomUI.Controls/Rate/AbstractRate.cs @@ -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(nameof(Count), 5); public static readonly StyledProperty ValueProperty = - AvaloniaProperty.Register(nameof(Value), double.NaN); + AvaloniaProperty.Register( + nameof(Value), + double.NaN, + defaultBindingMode: BindingMode.TwoWay, + enableDataValidation: true); public static readonly StyledProperty DefaultValueProperty = AvaloniaProperty.Register(nameof(DefaultValue), 0); @@ -543,4 +548,4 @@ public abstract class AbstractRate : TemplatedControl, { } #endregion -} \ No newline at end of file +} diff --git a/tests/AtomUI.Desktop.Controls.Tests/Rate/RateBindingTests.cs b/tests/AtomUI.Desktop.Controls.Tests/Rate/RateBindingTests.cs new file mode 100644 index 000000000..45be3e6cd --- /dev/null +++ b/tests/AtomUI.Desktop.Controls.Tests/Rate/RateBindingTests.cs @@ -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; + } +} diff --git a/tests/AtomUIGallery.Tests/ShowCases/RateShowCaseExamples.snapshot b/tests/AtomUIGallery.Tests/ShowCases/RateShowCaseExamples.snapshot index c57d7b45e..f06e39c27 100644 --- a/tests/AtomUIGallery.Tests/ShowCases/RateShowCaseExamples.snapshot +++ b/tests/AtomUIGallery.Tests/ShowCases/RateShowCaseExamples.snapshot @@ -4,6 +4,23 @@ Description="{gallery:RateShowCaseLangResource BasicDescription}"> + + + + + + + + + + diff --git a/tests/AtomUIGallery.Tests/ShowCases/RateShowCasePageTests.cs b/tests/AtomUIGallery.Tests/ShowCases/RateShowCasePageTests.cs index 5eac31578..4acbbca48 100644 --- a/tests/AtomUIGallery.Tests/ShowCases/RateShowCasePageTests.cs +++ b/tests/AtomUIGallery.Tests/ShowCases/RateShowCasePageTests.cs @@ -41,6 +41,8 @@ public class RateShowCasePageTests source.ShouldContain("Description=\"{gallery:RateShowCaseLangResource PageDescription}\""); source.ShouldContain("