feat: add more properties.

This commit is contained in:
rabbitism
2024-02-18 17:10:59 +08:00
parent 14c7e6b21f
commit 66835a8efa
8 changed files with 232 additions and 127 deletions

View File

@@ -1,6 +1,6 @@
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<AvaloniaVersion>11.0.0</AvaloniaVersion> <AvaloniaVersion>11.0.9</AvaloniaVersion>
</PropertyGroup> </PropertyGroup>
</Project> </Project>

View File

@@ -1,27 +1,58 @@
<UserControl xmlns="https://github.com/avaloniaui" <UserControl
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="Ursa.Demo.Pages.FormDemo"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns="https://github.com/avaloniaui"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" xmlns:common="clr-namespace:Ursa.Common;assembly=Ursa"
xmlns:vm="clr-namespace:Ursa.Demo.ViewModels;assembly=Ursa.Demo" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:u="https://irihi.tech/ursa" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
x:DataType="vm:FormDemoViewModel" xmlns:u="https://irihi.tech/ursa"
x:CompileBindings="True" xmlns:vm="clr-namespace:Ursa.Demo.ViewModels;assembly=Ursa.Demo"
x:Class="Ursa.Demo.Pages.FormDemo"> d:DesignHeight="450"
d:DesignWidth="800"
x:CompileBindings="True"
x:DataType="vm:FormDemoViewModel"
mc:Ignorable="d">
<StackPanel> <StackPanel>
<u:Form DataContext="{Binding Model}" LabelWidth="Auto"> <StackPanel>
<u:EnumSelector
Name="position"
EnumType="common:Position"
Value="{x:Static common:Position.Top}" />
<u:EnumSelector
Name="alignment"
EnumType="HorizontalAlignment"
Value="{x:Static HorizontalAlignment.Left}" />
</StackPanel>
<u:Form
DataContext="{Binding Model}"
LabelAlignment="{Binding #alignment.Value}"
LabelPosition="{Binding #position.Value}"
LabelWidth="*">
<u:FormGroup Header="Information"> <u:FormGroup Header="Information">
<TextBox u:Form.Label="Name" Text="{Binding Name}" Width="300"/> <TextBox
<TextBox u:Form.Label="Email" Text="{Binding Email}" Width="300"/> Width="300"
u:FormItem.IsRequired="True"
u:FormItem.Label="Name"
Text="{Binding Name}" />
<TextBox
Width="300"
u:FormItem.Label="Email"
Text="{Binding Email}" />
</u:FormGroup> </u:FormGroup>
<u:FormItem> <u:FormItem Label="Please select a Date">
<CalendarDatePicker u:Form.Label="Date" SelectedDate="{Binding Date}" /> <Calendar SelectedDate="{Binding Date}" />
</u:FormItem>
<u:FormItem Label="Click to Submit">
<Button Content="With Label" />
</u:FormItem>
<u:FormItem NoLabel="True">
<Button HorizontalAlignment="Left" Content="No Label" />
</u:FormItem> </u:FormItem>
</u:Form> </u:Form>
<StackPanel> <StackPanel>
<TextBlock Text="{Binding Model.Name}"></TextBlock> <TextBlock Text="{Binding Model.Name}" />
<TextBlock Text="{Binding Model.Email}"></TextBlock> <TextBlock Text="{Binding Model.Email}" />
<TextBlock Text="{Binding Model.Date}"></TextBlock> <TextBlock Text="{Binding Model.Date}" />
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
</UserControl> </UserControl>

View File

@@ -19,7 +19,7 @@
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.--> <!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="$(AvaloniaVersion)" /> <PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="$(AvaloniaVersion)" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.1.0" /> <PackageReference Include="CommunityToolkit.Mvvm" Version="8.1.0" />
<PackageReference Include="Semi.Avalonia" Version="11.0.0" /> <PackageReference Include="Semi.Avalonia" Version="11.0.7" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -1,36 +1,97 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui" <ResourceDictionary
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns="https://github.com/avaloniaui"
xmlns:u="https://irihi.tech/ursa"> xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
<!-- Add Resources Here --> xmlns:u="https://irihi.tech/ursa">
<!-- Add Resources Here -->
<ControlTheme x:Key="{x:Type u:Form}" TargetType="u:Form"> <ControlTheme x:Key="{x:Type u:Form}" TargetType="u:Form">
<Setter Property="Grid.IsSharedSizeScope" Value="False" />
<Setter Property="HorizontalAlignment" Value="Left" />
<Setter Property="Template"> <Setter Property="Template">
<ControlTemplate TargetType="u:Form"> <ControlTemplate TargetType="u:Form">
<DataValidationErrors> <DataValidationErrors>
<ItemsPresenter ItemsPanel="{TemplateBinding ItemsPanel}"></ItemsPresenter> <ItemsPresenter ItemsPanel="{TemplateBinding ItemsPanel}" />
</DataValidationErrors> </DataValidationErrors>
</ControlTemplate> </ControlTemplate>
</Setter> </Setter>
<Style Selector="^:fixed-width">
<Setter Property="Grid.IsSharedSizeScope" Value="True" />
</Style>
</ControlTheme> </ControlTheme>
<ControlTheme x:Key="{x:Type u:FormGroup}" TargetType="u:FormGroup"> <ControlTheme x:Key="{x:Type u:FormGroup}" TargetType="u:FormGroup">
<Setter Property="Template"> <Setter Property="Template">
<ControlTemplate TargetType="u:FormGroup"> <ControlTemplate TargetType="u:FormGroup">
<StackPanel> <StackPanel>
<ContentPresenter Content="{TemplateBinding Header}"/> <ContentPresenter Content="{TemplateBinding Header}" FontWeight="Bold" />
<ItemsPresenter ItemsPanel="{TemplateBinding ItemsPanel}"/> <Rectangle
Height="1"
Margin="0,8"
HorizontalAlignment="Stretch"
Fill="LightGray" />
<ItemsPresenter ItemsPanel="{TemplateBinding ItemsPanel}" />
</StackPanel> </StackPanel>
</ControlTemplate> </ControlTemplate>
</Setter> </Setter>
</ControlTheme> </ControlTheme>
<ControlTheme x:Key="{x:Type u:FormItem}" TargetType="u:FormItem"> <ControlTheme x:Key="{x:Type u:FormItem}" TargetType="u:FormItem">
<Setter Property="Margin" Value="0 8" />
<Setter Property="Template"> <Setter Property="Template">
<ControlTemplate TargetType="u:FormItem"> <ControlTemplate TargetType="u:FormItem">
<StackPanel Orientation="Horizontal"> <StackPanel>
<TextBlock Text="{TemplateBinding Label}" /> <StackPanel
<ContentPresenter Content="{TemplateBinding Content}"/> Name="PART_LabelPanel"
Margin="0,0,0,4"
HorizontalAlignment="{TemplateBinding LabelAlignment}"
Orientation="Horizontal">
<ContentPresenter Content="{TemplateBinding Label}" FontWeight="Bold" />
<TextBlock
Foreground="{DynamicResource SemiRed6}"
IsVisible="{TemplateBinding IsRequired}"
Text="*" />
</StackPanel>
<ContentPresenter Content="{TemplateBinding Content}" />
</StackPanel> </StackPanel>
</ControlTemplate> </ControlTemplate>
</Setter> </Setter>
<Style Selector="^:not(:no-label):horizontal">
<Setter Property="Template">
<ControlTemplate TargetType="u:FormItem">
<Grid RowDefinitions="*, *">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="Label" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Border
Grid.Row="0"
Grid.Column="0"
Width="{TemplateBinding LabelWidth}">
<StackPanel
Name="PART_LabelPanel"
Margin="8,8,8,0"
HorizontalAlignment="{TemplateBinding LabelAlignment}"
Orientation="Horizontal">
<ContentPresenter Content="{TemplateBinding Label}" FontWeight="Bold" />
<TextBlock
Foreground="{DynamicResource SemiRed6}"
IsVisible="{TemplateBinding IsRequired}"
Text="*" />
</StackPanel>
</Border>
<ContentPresenter
Grid.Row="0"
Grid.Column="1"
Content="{TemplateBinding Content}" />
</Grid>
</ControlTemplate>
</Setter>
</Style>
<Style Selector="^:no-label">
<Setter Property="Template">
<ControlTemplate TargetType="u:FormItem">
<ContentPresenter Content="{TemplateBinding Content}" />
</ControlTemplate>
</Setter>
</Style>
</ControlTheme> </ControlTheme>
</ResourceDictionary> </ResourceDictionary>

View File

@@ -1,59 +1,65 @@
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Metadata;
using Avalonia.Layout;
using Ursa.Common; using Ursa.Common;
namespace Ursa.Controls; namespace Ursa.Controls;
[PseudoClasses(PC_FixedWidth)]
public class Form: ItemsControl public class Form: ItemsControl
{ {
#region Attached Properties public const string PC_FixedWidth = ":fixed-width";
public static readonly AttachedProperty<object?> LabelProperty =
AvaloniaProperty.RegisterAttached<Form, Control, object?>("Label");
public static void SetLabel(Control obj, object? value) => obj.SetValue(LabelProperty, value);
public static object? GetLabel(Control obj) => obj.GetValue(LabelProperty);
public static readonly StyledProperty<GridLength> LabelWidthProperty = AvaloniaProperty.Register<Form, GridLength>(
nameof(LabelWidth));
public static readonly AttachedProperty<bool> IsRequiredProperty = /// <summary>
AvaloniaProperty.RegisterAttached<Form, Control, bool>("IsRequired"); /// Behavior:
public static void SetIsRequired(Control obj, bool value) => obj.SetValue(IsRequiredProperty, value); /// <para>Fixed Width: all labels are with fixed length. </para>
public static bool GetIsRequired(Control obj) => obj.GetValue(IsRequiredProperty); /// <para>Star: all labels are aligned by max length. </para>
/// <para>Auto: labels are not aligned. </para>
public static readonly AttachedProperty<GridLength> LabelWidthProperty = /// </summary>
AvaloniaProperty.RegisterAttached<Form, Control, GridLength>("LabelWidth"); public GridLength LabelWidth
{
public static void SetLabelWidth(Control obj, GridLength value) => obj.SetValue(LabelWidthProperty, value); get => GetValue(LabelWidthProperty);
public static GridLength GetLabelWidth(Control obj) => obj.GetValue(LabelWidthProperty); set => SetValue(LabelWidthProperty, value);
}
#endregion
public static readonly StyledProperty<Position> LabelPositionProperty = AvaloniaProperty.Register<Form, Position>( public static readonly StyledProperty<Position> LabelPositionProperty = AvaloniaProperty.Register<Form, Position>(
nameof(LabelPosition)); nameof(LabelPosition), defaultValue: Position.Top);
/// <summary>
/// Only Left and Top work.
/// </summary>
public Position LabelPosition public Position LabelPosition
{ {
get => GetValue(LabelPositionProperty); get => GetValue(LabelPositionProperty);
set => SetValue(LabelPositionProperty, value); set => SetValue(LabelPositionProperty, value);
} }
public static readonly StyledProperty<Position> LabelAlignmentProperty = AvaloniaProperty.Register<Form, Position>( public static readonly StyledProperty<HorizontalAlignment> LabelAlignmentProperty = AvaloniaProperty.Register<Form, HorizontalAlignment>(
nameof(LabelAlignment)); nameof(LabelAlignment), defaultValue: HorizontalAlignment.Left);
/// <summary>
/// Only Left and Right work. public HorizontalAlignment LabelAlignment
/// </summary>
public Position LabelAlignment
{ {
get => GetValue(LabelAlignmentProperty); get => GetValue(LabelAlignmentProperty);
set => SetValue(LabelAlignmentProperty, value); set => SetValue(LabelAlignmentProperty, value);
} }
static Form()
{
LabelWidthProperty.Changed.AddClassHandler<Form, GridLength>((x, args) => x.LabelWidthChanged(args));
}
private void LabelWidthChanged(AvaloniaPropertyChangedEventArgs<GridLength> args)
{
var newValue = args.NewValue.Value;
bool isFixed = newValue.IsStar || newValue.IsAbsolute;
PseudoClasses.Set(PC_FixedWidth, isFixed);
}
protected override bool NeedsContainerOverride(object? item, int index, out object? recycleKey) protected override bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
{ {
recycleKey = null; recycleKey = null;
return item is FormItem or FormGroup; return item is not FormItem && item is not FormGroup;
} }
protected override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey) protected override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
@@ -62,27 +68,8 @@ public class Form: ItemsControl
return new FormItem() return new FormItem()
{ {
Content = control, Content = control,
[!LabelProperty] = control.GetObservable(Form.LabelProperty).ToBinding(), [!FormItem.LabelProperty] = control[!FormItem.LabelProperty],
[!IsRequiredProperty] = control.GetObservable(Form.IsRequiredProperty).ToBinding(), [!FormItem.IsRequiredProperty] = control[!FormItem.IsRequiredProperty],
}; };
} }
protected override void PrepareContainerForItemOverride(Control container, object? item, int index)
{
base.PrepareContainerForItemOverride(container, item, index);
if (container is FormGroup group)
{
if (!group.IsSet(FormGroup.LabelWidthProperty))
{
group[!LabelWidthProperty] = this.GetObservable(LabelWidthProperty).ToBinding();
}
}
else if (container is FormItem formItem)
{
if(!formItem.IsSet(FormItem.LabelWidthProperty))
{
formItem[!LabelWidthProperty] = this.GetObservable(LabelWidthProperty).ToBinding();
}
}
}
} }

View File

@@ -6,13 +6,6 @@ namespace Ursa.Controls;
public class FormGroup: HeaderedItemsControl public class FormGroup: HeaderedItemsControl
{ {
public static readonly StyledProperty<GridLength> LabelWidthProperty = Form.LabelWidthProperty.AddOwner<FormGroup>();
public GridLength LabelWidth
{
get => GetValue(LabelWidthProperty);
set => SetValue(LabelWidthProperty, value);
}
protected override bool NeedsContainerOverride(object? item, int index, out object? recycleKey) protected override bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
{ {
recycleKey = null; recycleKey = null;
@@ -25,17 +18,9 @@ public class FormGroup: HeaderedItemsControl
return new FormItem return new FormItem
{ {
Content = control, Content = control,
[!FormItem.LabelProperty] = control.GetObservable(Form.LabelProperty).ToBinding(), [!FormItem.LabelProperty] = control[!FormItem.LabelProperty],
[!FormItem.IsRequiredProperty] = control.GetObservable(Form.IsRequiredProperty).ToBinding(), [!FormItem.IsRequiredProperty] = control[!FormItem.IsRequiredProperty],
}; };
} }
protected override void PrepareContainerForItemOverride(Control container, object? item, int index)
{
base.PrepareContainerForItemOverride(container, item, index);
if (container is FormItem formItem)
{
formItem.LabelWidth = LabelWidth;
}
}
} }

View File

@@ -1,46 +1,93 @@
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Layout;
using Avalonia.Reactive;
using Avalonia.VisualTree;
using Irihi.Avalonia.Shared.Helpers;
using Ursa.Common;
namespace Ursa.Controls; namespace Ursa.Controls;
[PseudoClasses(PC_Horizontal, PC_NoLabel)]
public class FormItem: ContentControl public class FormItem: ContentControl
{ {
public static readonly StyledProperty<object?> LabelProperty = AvaloniaProperty.Register<FormItem, object?>( public const string PC_Horizontal = ":horizontal";
nameof(Label)); public const string PC_NoLabel = ":no-label";
#region Attached Properties
public static readonly AttachedProperty<object?> LabelProperty =
AvaloniaProperty.RegisterAttached<FormItem, Control, object?>("Label");
public static void SetLabel(Control obj, object? value) => obj.SetValue(LabelProperty, value);
public static object? GetLabel(Control obj) => obj.GetValue(LabelProperty);
public object? Label public static readonly AttachedProperty<bool> IsRequiredProperty =
{ AvaloniaProperty.RegisterAttached<FormItem, Control, bool>("IsRequired");
get => GetValue(LabelProperty); public static void SetIsRequired(Control obj, bool value) => obj.SetValue(IsRequiredProperty, value);
set => SetValue(LabelProperty, value); public static bool GetIsRequired(Control obj) => obj.GetValue(IsRequiredProperty);
}
public static readonly StyledProperty<bool> IsRequiredProperty = AvaloniaProperty.Register<FormItem, bool>( public static readonly AttachedProperty<bool> NoLabelProperty =
nameof(IsRequired)); AvaloniaProperty.RegisterAttached<FormItem, Control, bool>("NoLabel");
public bool IsRequired public static void SetNoLabel(Control obj, bool value) => obj.SetValue(NoLabelProperty, value);
{ public static bool GetNoLabel(Control obj) => obj.GetValue(NoLabelProperty);
get => GetValue(IsRequiredProperty); #endregion
set => SetValue(IsRequiredProperty, value);
} private List<IDisposable> _formSubscriptions = new List<IDisposable>();
public static readonly StyledProperty<GridLength> LabelWidthProperty = AvaloniaProperty.Register<FormItem, GridLength>( public static readonly StyledProperty<double> LabelWidthProperty = AvaloniaProperty.Register<FormItem, double>(
nameof(LabelWidth)); nameof(LabelWidth));
public GridLength LabelWidth public double LabelWidth
{ {
get => GetValue(LabelWidthProperty); get => GetValue(LabelWidthProperty);
set => SetValue(LabelWidthProperty, value); set => SetValue(LabelWidthProperty, value);
} }
public static readonly StyledProperty<HorizontalAlignment> LabelAlignmentProperty = AvaloniaProperty.Register<FormItem, HorizontalAlignment>(
nameof(LabelAlignment));
public HorizontalAlignment LabelAlignment
{
get => GetValue(LabelAlignmentProperty);
set => SetValue(LabelAlignmentProperty, value);
}
static FormItem() static FormItem()
{ {
LabelWidthProperty.Changed.AddClassHandler<FormItem, GridLength>((x, args) => x.LabelWidthChanged(args)); PropertyToPseudoClassMixin.Attach<FormItem>(NoLabelProperty, PC_NoLabel);
} }
private void LabelWidthChanged(AvaloniaPropertyChangedEventArgs<GridLength> args) protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{ {
GridLength? length = args.GetNewValue<GridLength>(); base.OnAttachedToVisualTree(e);
var form = this.GetVisualAncestors().OfType<Form>().FirstOrDefault();
if (form is not null)
{
_formSubscriptions.Clear();
var labelSubscription = form
.GetObservable(Form.LabelWidthProperty)
.Subscribe(new AnonymousObserver<GridLength>(length => { LabelWidth = length.IsAbsolute ? length.Value : double.NaN; }));
var positionSubscription = form
.GetObservable(Form.LabelPositionProperty)
.Subscribe(new AnonymousObserver<Position>(position => { PseudoClasses.Set(PC_Horizontal, position == Position.Left);}));
var alignmentSubscription = form
.GetObservable(Form.LabelAlignmentProperty)
.Subscribe(new AnonymousObserver<HorizontalAlignment>(alignment => { LabelAlignment = alignment; }));
_formSubscriptions.Add(labelSubscription);
_formSubscriptions.Add(positionSubscription);
_formSubscriptions.Add(alignmentSubscription);
}
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
foreach (var subscription in _formSubscriptions)
{
subscription.Dispose();
}
} }
} }

View File

@@ -1,6 +0,0 @@
namespace Ursa.Controls;
public class FormPanel
{
}