diff --git a/src/Avalonia/HandyControlDemo_Avalonia/Data/DemoInfo.json b/src/Avalonia/HandyControlDemo_Avalonia/Data/DemoInfo.json
index b81c0c7f..2dfd239b 100644
--- a/src/Avalonia/HandyControlDemo_Avalonia/Data/DemoInfo.json
+++ b/src/Avalonia/HandyControlDemo_Avalonia/Data/DemoInfo.json
@@ -54,7 +54,8 @@
[ "UniformSpacingPanel", "UniformSpacingPanelDemo", "Brush.IncreaseHorizontalSpacing", "", "Layout" ],
[ "ProgressBar", "ProgressBarDemo", "Brush.ProgressBar", "", "Feedback" ],
[ "Rate", "RateDemo", "Brush.Favorite", "", "Feedback" ],
- [ "Loading", "LoadingDemo", "Brush.Loading", "", "Feedback" ]
+ [ "Loading", "LoadingDemo", "Brush.Loading", "", "Feedback" ],
+ [ "Tag", "TagDemo", "Brush.Tag", "", "DataDisplay" ]
]
}
]
diff --git a/src/Avalonia/HandyControlDemo_Avalonia/UserControl/Controls/TagDemo.axaml b/src/Avalonia/HandyControlDemo_Avalonia/UserControl/Controls/TagDemo.axaml
new file mode 100644
index 00000000..14dc4d10
--- /dev/null
+++ b/src/Avalonia/HandyControlDemo_Avalonia/UserControl/Controls/TagDemo.axaml
@@ -0,0 +1,79 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Avalonia/HandyControlDemo_Avalonia/UserControl/Controls/TagDemo.axaml.cs b/src/Avalonia/HandyControlDemo_Avalonia/UserControl/Controls/TagDemo.axaml.cs
new file mode 100644
index 00000000..97357120
--- /dev/null
+++ b/src/Avalonia/HandyControlDemo_Avalonia/UserControl/Controls/TagDemo.axaml.cs
@@ -0,0 +1,63 @@
+using System.Collections.ObjectModel;
+using Avalonia.Interactivity;
+
+namespace HandyControlDemo.UserControl;
+
+public class TagDemoItem : Avalonia.AvaloniaObject
+{
+ public static readonly Avalonia.StyledProperty NameProperty =
+ Avalonia.AvaloniaProperty.Register(nameof(Name));
+
+ public static readonly Avalonia.StyledProperty IsSelectedProperty =
+ Avalonia.AvaloniaProperty.Register(nameof(IsSelected));
+
+ public string? Name
+ {
+ get => GetValue(NameProperty);
+ set => SetValue(NameProperty, value);
+ }
+
+ public bool IsSelected
+ {
+ get => GetValue(IsSelectedProperty);
+ set => SetValue(IsSelectedProperty, value);
+ }
+}
+
+public partial class TagDemo : Avalonia.Controls.UserControl
+{
+ public ObservableCollection DataList { get; } = new();
+
+ public TagDemo()
+ {
+ InitializeComponent();
+
+ for (var i = 1; i <= 10; i++)
+ {
+ DataList.Add(new TagDemoItem
+ {
+ Name = $"Name{i}",
+ IsSelected = i % 2 == 0
+ });
+ }
+
+ BoundContainer.ItemsSource = DataList;
+ AddButton.Click += AddButton_Click;
+ }
+
+ private void AddButton_Click(object? sender, RoutedEventArgs e)
+ {
+ var name = TagNameBox.Text;
+ if (string.IsNullOrWhiteSpace(name))
+ {
+ return;
+ }
+
+ DataList.Insert(0, new TagDemoItem
+ {
+ Name = name,
+ IsSelected = DataList.Count % 2 == 0
+ });
+ TagNameBox.Text = string.Empty;
+ }
+}
diff --git a/src/Avalonia/HandyControl_Avalonia/Controls/Tag/Tag.cs b/src/Avalonia/HandyControl_Avalonia/Controls/Tag/Tag.cs
new file mode 100644
index 00000000..fd95ac3b
--- /dev/null
+++ b/src/Avalonia/HandyControl_Avalonia/Controls/Tag/Tag.cs
@@ -0,0 +1,131 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.Metadata;
+using Avalonia.Controls.Primitives;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+
+namespace HandyControl.Controls;
+
+[TemplatePart(PartCloseButton, typeof(Button))]
+public class Tag : HeaderedContentControl
+{
+ public const string PartCloseButton = "PART_CloseButton";
+
+ private Button? _closeButton;
+
+ public static readonly StyledProperty ShowCloseButtonProperty =
+ AvaloniaProperty.Register(nameof(ShowCloseButton), true);
+
+ public bool ShowCloseButton
+ {
+ get => GetValue(ShowCloseButtonProperty);
+ set => SetValue(ShowCloseButtonProperty, value);
+ }
+
+ public static readonly StyledProperty SelectableProperty =
+ AvaloniaProperty.Register(nameof(Selectable));
+
+ public bool Selectable
+ {
+ get => GetValue(SelectableProperty);
+ set => SetValue(SelectableProperty, value);
+ }
+
+ public static readonly StyledProperty IsSelectedProperty =
+ AvaloniaProperty.Register(nameof(IsSelected));
+
+ public bool IsSelected
+ {
+ get => GetValue(IsSelectedProperty);
+ set => SetValue(IsSelectedProperty, value);
+ }
+
+ public static readonly RoutedEvent SelectedEvent =
+ RoutedEvent.Register(nameof(Selected), RoutingStrategies.Bubble);
+
+ public event System.EventHandler Selected
+ {
+ add => AddHandler(SelectedEvent, value);
+ remove => RemoveHandler(SelectedEvent, value);
+ }
+
+ public static readonly RoutedEvent ClosingEvent =
+ RoutedEvent.Register(nameof(Closing), RoutingStrategies.Bubble);
+
+ public event System.EventHandler Closing
+ {
+ add => AddHandler(ClosingEvent, value);
+ remove => RemoveHandler(ClosingEvent, value);
+ }
+
+ public static readonly RoutedEvent ClosedEvent =
+ RoutedEvent.Register(nameof(Closed), RoutingStrategies.Bubble);
+
+ public event System.EventHandler Closed
+ {
+ add => AddHandler(ClosedEvent, value);
+ remove => RemoveHandler(ClosedEvent, value);
+ }
+
+ static Tag()
+ {
+ IsSelectedProperty.Changed.AddClassHandler((tag, e) => tag.OnIsSelectedChanged(e));
+ }
+
+ private void OnIsSelectedChanged(AvaloniaPropertyChangedEventArgs e)
+ {
+ var newValue = e.GetNewValue();
+ PseudoClasses.Set(":selected", newValue);
+ RaiseEvent(new RoutedEventArgs(SelectedEvent, this));
+ }
+
+ protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
+ {
+ base.OnApplyTemplate(e);
+
+ if (_closeButton != null)
+ {
+ _closeButton.Click -= CloseButton_Click;
+ }
+
+ _closeButton = e.NameScope.Find