Files
Ursa.Avalonia/src/Ursa/Controls/Descriptions/Descriptions.cs
Dong Bin dcaef1c8ed [New Control] Descriptions (#791)
* feat: add ColumnWrapPanel.

* feat: add Descriptions and DescriptionsItem controls

* WIP: extract a LabeledContentControl for this particular need.

* wip.

* feat: setup demo. fix various initial status issue.

* fix: fix a layout issue.

* feat: improve demo.

* feat: update demo.

* fix: remove a redundant calculation.

* feat: update per copilot feedback.

* feat: add tests, fix a calculation issue.

* feat: refactor to change the default label and value binding assignment logic.

* feat: introduce orientation.

* misc: format codes.

* feat: clarify LabelPosition property.

* feat: extract Font resources.

* revert AvatarDemo.

* feat: assign specific value to Font resources for testing.

* misc: resolve copilot comment.

---------

Co-authored-by: Zhang Dian <54255897+zdpcdt@users.noreply.github.com>
2025-10-22 20:49:02 +08:00

208 lines
7.4 KiB
C#

using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Layout;
using Avalonia.LogicalTree;
using Avalonia.Metadata;
using Ursa.Common;
namespace Ursa.Controls;
[PseudoClasses(PC_FixedWidth)]
public class Descriptions: ItemsControl
{
public const string PC_FixedWidth = ":fixed-width";
public static readonly StyledProperty<IDataTemplate?> LabelTemplateProperty =
LabeledContentControl.LabelTemplateProperty.AddOwner<Descriptions>();
[InheritDataTypeFromItems(nameof(ItemsSource))]
public IDataTemplate? LabelTemplate
{
get => GetValue(LabelTemplateProperty);
set => SetValue(LabelTemplateProperty, value);
}
public static readonly StyledProperty<IBinding?> LabelMemberBindingProperty = AvaloniaProperty.Register<Descriptions, IBinding?>(
nameof(LabelMemberBinding));
[AssignBinding]
[InheritDataTypeFromItems(nameof(ItemsSource))]
public IBinding? LabelMemberBinding
{
get => GetValue(LabelMemberBindingProperty);
set => SetValue(LabelMemberBindingProperty, value);
}
public static readonly StyledProperty<Position> LabelPositionProperty =
DescriptionsItem.LabelPositionProperty.AddOwner<Descriptions>();
/// <summary>
/// The position of the header relative to the content. Only Top and Left are supported.
/// </summary>
public Position LabelPosition
{
get => GetValue(LabelPositionProperty);
set => SetValue(LabelPositionProperty, value);
}
public static readonly StyledProperty<GridLength> LabelWidthProperty =
AvaloniaProperty.Register<Descriptions, GridLength>(nameof(LabelWidth));
public GridLength LabelWidth
{
get => GetValue(LabelWidthProperty);
set => SetValue(LabelWidthProperty, value);
}
public static readonly StyledProperty<ItemAlignment> ItemAlignmentProperty =
DescriptionsItem.ItemAlignmentProperty.AddOwner<Descriptions>();
public ItemAlignment ItemAlignment
{
get => GetValue(ItemAlignmentProperty);
set => SetValue(ItemAlignmentProperty, value);
}
public static readonly StyledProperty<Orientation> OrientationProperty =
AvaloniaProperty.Register<Descriptions, Orientation>(nameof(Orientation), Orientation.Vertical);
public Orientation Orientation
{
get => GetValue(OrientationProperty);
set => SetValue(OrientationProperty, value);
}
static Descriptions()
{
LabelWidthProperty.Changed.AddClassHandler<Descriptions>((x, args) => x.OnLabelWidthChanged(args));
ItemAlignmentProperty.Changed.AddClassHandler<Descriptions, ItemAlignment>((x, args) =>
x.OnItemAlignmentChanged(args));
}
private void OnItemAlignmentChanged(AvaloniaPropertyChangedEventArgs<ItemAlignment> args)
{
PseudoClasses.Set(PC_FixedWidth, args.GetNewValue<ItemAlignment>() != ItemAlignment.Plain);
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
PseudoClasses.Set(PC_FixedWidth, this.ItemAlignment != ItemAlignment.Plain);
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == LabelMemberBindingProperty)
{
if (change.NewValue != null && LabelTemplate != null)
throw new InvalidOperationException("Cannot set both LabelMemberBinding and LabelTemplate.");
_labelDisplayMemberItemTemplate = null;
RefreshContainers();
}
if (change.Property == LabelTemplateProperty)
{
if (change.NewValue != null && LabelMemberBinding != null)
{
throw new InvalidOperationException("Cannot set both LabelMemberBinding and LabelTemplate.");
}
RefreshContainers();
}
}
private void OnLabelWidthChanged(AvaloniaPropertyChangedEventArgs e)
{
foreach (var item in this.GetLogicalDescendants().OfType<DescriptionsItem>())
{
item.LabelWidth = LabelWidth.IsAbsolute ? LabelWidth.Value : double.NaN;
}
}
protected override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
{
return new DescriptionsItem();
}
protected override void PrepareContainerForItemOverride(Control container, object? item, int index)
{
base.PrepareContainerForItemOverride(container, item, index);
if (container is not DescriptionsItem descriptionItem) return;
if (container == item) return;
SetupBindings(descriptionItem, item);
}
protected override bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
{
recycleKey = null;
if (item is not DescriptionsItem descriptionItem) return true;
SetupBindings(descriptionItem, null);
return false;
}
private void SetupBindings(DescriptionsItem container, object? item)
{
var effectiveLabelTemplate = GetLabelTemplate();
if (effectiveLabelTemplate is not null && !container.IsSet(LabeledContentControl.LabelTemplateProperty))
{
container.LabelTemplate = effectiveLabelTemplate;
}
var effectiveContentTemplate = GetContentTemplate();
if (effectiveContentTemplate is not null && !container.IsSet(ContentControl.ContentTemplateProperty))
{
container.ContentTemplate = effectiveContentTemplate;
}
if (!container.IsSet(LabeledContentControl.LabelProperty))
{
container.Label = item;
}
if (!container.IsSet(LabelPositionProperty))
{
container[!LabelPositionProperty] = this[!LabelPositionProperty];
}
if (!container.IsSet(DescriptionsItem.ItemAlignmentProperty))
{
container[!DescriptionsItem.ItemAlignmentProperty] = this[!ItemAlignmentProperty];
}
if (!container.IsSet(DescriptionsItem.LabelWidthProperty))
{
container.LabelWidth = LabelWidth.IsAbsolute ? LabelWidth.Value : double.NaN;
}
}
private IDataTemplate? _valueDisplayMemberItemTemplate;
private IDataTemplate? _labelDisplayMemberItemTemplate;
private IDataTemplate? GetContentTemplate()
{
IDataTemplate? itemTemplate = this.ItemTemplate;
if (itemTemplate != null)
return itemTemplate;
if (this._valueDisplayMemberItemTemplate == null)
{
IBinding? binding = this.DisplayMemberBinding;
if (binding != null)
_valueDisplayMemberItemTemplate =
new FuncDataTemplate<object>((o, s) => new TextBlock { [!TextBlock.TextProperty] = binding });
}
return _valueDisplayMemberItemTemplate;
}
private IDataTemplate? GetLabelTemplate()
{
IDataTemplate? itemTemplate = this.LabelTemplate;
if (itemTemplate != null)
return itemTemplate;
if (this._labelDisplayMemberItemTemplate == null)
{
IBinding? binding = this.LabelMemberBinding;
if (binding != null)
_labelDisplayMemberItemTemplate =
new FuncDataTemplate<object>((o, s) => new TextBlock { [!TextBlock.TextProperty] = binding });
}
return _labelDisplayMemberItemTemplate;
}
}