Merge pull request #111 from irihitech/form

Form
This commit is contained in:
Zhang Dian
2024-02-20 17:45:37 +08:00
committed by GitHub
13 changed files with 430 additions and 2 deletions

View File

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

View File

@@ -13,6 +13,7 @@ public static class MenuKeys
public const string MenuKeyDrawer = "Drawer";
public const string MenuKeyDualBadge = "DualBadge";
public const string MenuKeyEnumSelector = "EnumSelector";
public const string MenuKeyForm = "Form";
public const string MenuKeyImageViewer = "ImageViewer";
public const string MenuKeyIpBox = "IPv4Box";
public const string MenuKeyIconButton = "IconButton";

View File

@@ -0,0 +1,71 @@
<UserControl
x:Class="Ursa.Demo.Pages.FormDemo"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:common="clr-namespace:Ursa.Common;assembly=Ursa"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:u="https://irihi.tech/ursa"
xmlns:vm="clr-namespace:Ursa.Demo.ViewModels;assembly=Ursa.Demo"
d:DesignHeight="450"
d:DesignWidth="800"
x:CompileBindings="True"
x:DataType="vm:FormDemoViewModel"
mc:Ignorable="d">
<ScrollViewer>
<StackPanel>
<Grid RowDefinitions="Auto, Auto" ColumnDefinitions="Auto, Auto">
<TextBlock Grid.Row="0" Grid.Column="0" Text="Label Position" VerticalAlignment="Center"></TextBlock>
<TextBlock Grid.Row="1" Grid.Column="0" Text="Label Alignment" VerticalAlignment="Center"></TextBlock>
<u:EnumSelector Grid.Row="0" Grid.Column="1"
Name="position"
EnumType="common:Position"
Value="{x:Static common:Position.Top}" />
<u:EnumSelector Grid.Row="1" Grid.Column="1"
Name="alignment"
EnumType="HorizontalAlignment"
Value="{x:Static HorizontalAlignment.Left}" />
</Grid>
<u:Form
DataContext="{Binding Model}"
LabelAlignment="{Binding #alignment.Value}"
LabelPosition="{Binding #position.Value}"
LabelWidth="*">
<u:FormGroup Header="Information">
<TextBox
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:FormItem Label="Please select a Date">
<CalendarDatePicker SelectedDate="{Binding Date}" />
</u:FormItem>
<u:FormGroup Header="Education">
<TextBox
HorizontalAlignment="Stretch"
u:FormItem.IsRequired="True"
u:FormItem.Label="Name"
Text="{Binding Name}" />
<TextBox
HorizontalAlignment="Stretch"
u:FormItem.Label="Email"
u:FormItem.IsRequired="True"
Text="{Binding Email}" />
</u:FormGroup>
<u:FormItem Label="Click to Submit">
<Button Content="Button With Label" />
</u:FormItem>
<CheckBox Content="I Agree User Agreement" u:FormItem.NoLabel="True"></CheckBox>
<u:FormItem NoLabel="True">
<Button HorizontalAlignment="Left" Content="No Label" />
</u:FormItem>
</u:Form>
</StackPanel>
</ScrollViewer>
</UserControl>

View File

@@ -0,0 +1,13 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace Ursa.Demo.Pages;
public partial class FormDemo : UserControl
{
public FormDemo()
{
InitializeComponent();
}
}

View File

@@ -19,7 +19,7 @@
<!--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 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>

View File

@@ -0,0 +1,48 @@
using System;
using System.ComponentModel.DataAnnotations;
using CommunityToolkit.Mvvm.ComponentModel;
namespace Ursa.Demo.ViewModels;
public partial class FormDemoViewModel: ObservableObject
{
[ObservableProperty] private DataModel _model;
public FormDemoViewModel()
{
Model = new DataModel();
}
}
public partial class DataModel : ObservableObject
{
private string _name;
[MinLength(10)]
public string Name
{
get=>_name;
set => SetProperty(ref _name, value);
}
private string _email;
[EmailAddress]
public string Email
{
get=>_email;
set => SetProperty(ref _email, value);
}
private DateTime _date;
public DateTime Date
{
get => _date;
set => SetProperty(ref _date, value);
}
public DataModel()
{
Date = DateTime.Today;
}
}

View File

@@ -35,6 +35,7 @@ public class MainViewViewModel : ViewModelBase
MenuKeys.MenuKeyDrawer => new DrawerDemoViewModel(),
MenuKeys.MenuKeyDualBadge => new DualBadgeDemoViewModel(),
MenuKeys.MenuKeyEnumSelector => new EnumSelectorDemoViewModel(),
MenuKeys.MenuKeyForm => new FormDemoViewModel(),
MenuKeys.MenuKeyImageViewer => new ImageViewerDemoViewModel(),
MenuKeys.MenuKeyIconButton => new IconButtonDemoViewModel(),
MenuKeys.MenuKeyIpBox => new IPv4BoxDemoViewModel(),

View File

@@ -22,6 +22,7 @@ public class MenuViewModel: ViewModelBase
new() { MenuHeader = "Drawer", Key = MenuKeys.MenuKeyDrawer },
new() { MenuHeader = "DualBadge", Key = MenuKeys.MenuKeyDualBadge },
new() { MenuHeader = "Enum Selector", Key = MenuKeys.MenuKeyEnumSelector },
new() { MenuHeader = "Form", Key = MenuKeys.MenuKeyForm },
new() { MenuHeader = "Icon Button", Key = MenuKeys.MenuKeyIconButton },
new() { MenuHeader = "ImageViewer", Key = MenuKeys.MenuKeyImageViewer },
new() { MenuHeader = "IPv4Box", Key = MenuKeys.MenuKeyIpBox },

View File

@@ -0,0 +1,97 @@
<ResourceDictionary
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:u="https://irihi.tech/ursa">
<!-- Add Resources Here -->
<ControlTheme x:Key="{x:Type u:Form}" TargetType="u:Form">
<Setter Property="Grid.IsSharedSizeScope" Value="False" />
<Setter Property="HorizontalAlignment" Value="Left" />
<Setter Property="Template">
<ControlTemplate TargetType="u:Form">
<DataValidationErrors>
<ItemsPresenter ItemsPanel="{TemplateBinding ItemsPanel}" />
</DataValidationErrors>
</ControlTemplate>
</Setter>
<Style Selector="^:fixed-width">
<Setter Property="Grid.IsSharedSizeScope" Value="True" />
</Style>
</ControlTheme>
<ControlTheme x:Key="{x:Type u:FormGroup}" TargetType="u:FormGroup">
<Setter Property="Template">
<ControlTemplate TargetType="u:FormGroup">
<StackPanel Margin="0 28 0 0">
<ContentPresenter Content="{TemplateBinding Header}" FontWeight="Bold" FontSize="18" />
<Rectangle
Height="1"
Margin="0,8"
HorizontalAlignment="Stretch"
Fill="{DynamicResource SemiColorBorder}" />
<ItemsPresenter ItemsPanel="{TemplateBinding ItemsPanel}" />
</StackPanel>
</ControlTemplate>
</Setter>
</ControlTheme>
<ControlTheme x:Key="{x:Type u:FormItem}" TargetType="u:FormItem">
<Setter Property="Margin" Value="0 8" />
<Setter Property="Template">
<ControlTemplate TargetType="u:FormItem">
<StackPanel>
<StackPanel
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>
</ControlTemplate>
</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>
</ResourceDictionary>

View File

@@ -12,6 +12,7 @@
<ResourceInclude Source="Drawer.axaml" />
<ResourceInclude Source="DualBadge.axaml" />
<ResourceInclude Source="EnumSelector.axaml" />
<ResourceInclude Source="Form.axaml" />
<ResourceInclude Source="IconButton.axaml" />
<ResourceInclude Source="ImageViewer.axaml" />
<ResourceInclude Source="IPv4Box.axaml" />

View File

@@ -0,0 +1,76 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Metadata;
using Avalonia.Layout;
using Ursa.Common;
namespace Ursa.Controls;
[PseudoClasses(PC_FixedWidth)]
public class Form: ItemsControl
{
public const string PC_FixedWidth = ":fixed-width";
public static readonly StyledProperty<GridLength> LabelWidthProperty = AvaloniaProperty.Register<Form, GridLength>(
nameof(LabelWidth));
/// <summary>
/// Behavior:
/// <para>Fixed Width: all labels are with fixed length. </para>
/// <para>Star: all labels are aligned by max length. </para>
/// <para>Auto: labels are not aligned. </para>
/// </summary>
public GridLength LabelWidth
{
get => GetValue(LabelWidthProperty);
set => SetValue(LabelWidthProperty, value);
}
public static readonly StyledProperty<Position> LabelPositionProperty = AvaloniaProperty.Register<Form, Position>(
nameof(LabelPosition), defaultValue: Position.Top);
public Position LabelPosition
{
get => GetValue(LabelPositionProperty);
set => SetValue(LabelPositionProperty, value);
}
public static readonly StyledProperty<HorizontalAlignment> LabelAlignmentProperty = AvaloniaProperty.Register<Form, HorizontalAlignment>(
nameof(LabelAlignment), defaultValue: HorizontalAlignment.Left);
public HorizontalAlignment LabelAlignment
{
get => GetValue(LabelAlignmentProperty);
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)
{
recycleKey = null;
return item is not FormItem && item is not FormGroup;
}
protected override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
{
if (item is not Control control) return new FormItem();
return new FormItem()
{
Content = control,
[!FormItem.LabelProperty] = control[!FormItem.LabelProperty],
[!FormItem.IsRequiredProperty] = control[!FormItem.IsRequiredProperty],
[!FormItem.NoLabelProperty] = control[!FormItem.NoLabelProperty],
};
}
}

View File

@@ -0,0 +1,26 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
namespace Ursa.Controls;
public class FormGroup: HeaderedItemsControl
{
protected override bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
{
recycleKey = null;
return item is not FormItem;
}
protected override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
{
if (item is not Control control) return new FormItem();
return new FormItem
{
Content = control,
[!FormItem.LabelProperty] = control[!FormItem.LabelProperty],
[!FormItem.IsRequiredProperty] = control[!FormItem.IsRequiredProperty],
};
}
}

View File

@@ -0,0 +1,93 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Layout;
using Avalonia.Reactive;
using Avalonia.VisualTree;
using Irihi.Avalonia.Shared.Helpers;
using Ursa.Common;
namespace Ursa.Controls;
[PseudoClasses(PC_Horizontal, PC_NoLabel)]
public class FormItem: ContentControl
{
public const string PC_Horizontal = ":horizontal";
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 static readonly AttachedProperty<bool> IsRequiredProperty =
AvaloniaProperty.RegisterAttached<FormItem, Control, bool>("IsRequired");
public static void SetIsRequired(Control obj, bool value) => obj.SetValue(IsRequiredProperty, value);
public static bool GetIsRequired(Control obj) => obj.GetValue(IsRequiredProperty);
public static readonly AttachedProperty<bool> NoLabelProperty =
AvaloniaProperty.RegisterAttached<FormItem, Control, bool>("NoLabel");
public static void SetNoLabel(Control obj, bool value) => obj.SetValue(NoLabelProperty, value);
public static bool GetNoLabel(Control obj) => obj.GetValue(NoLabelProperty);
#endregion
private List<IDisposable> _formSubscriptions = new List<IDisposable>();
public static readonly StyledProperty<double> LabelWidthProperty = AvaloniaProperty.Register<FormItem, double>(
nameof(LabelWidth));
public double LabelWidth
{
get => GetValue(LabelWidthProperty);
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()
{
PropertyToPseudoClassMixin.Attach<FormItem>(NoLabelProperty, PC_NoLabel);
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
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();
}
}
}