Merge pull request #482 from irihitech/form

Form Dynamic Generation
This commit is contained in:
Zhang Dian
2024-11-15 23:15:24 +08:00
committed by GitHub
11 changed files with 420 additions and 13 deletions

View File

@@ -0,0 +1,24 @@
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using Ursa.Demo.ViewModels;
namespace Ursa.Demo.Converters;
public class FormDataTemplateSelector: ResourceDictionary, IDataTemplate
{
public Control? Build(object? param)
{
if (param is null) return null;
var type = param.GetType();
if (this.TryGetResource(type, null, out var template) && template is IDataTemplate dataTemplate)
{
return dataTemplate.Build(param);
}
return null;
}
public bool Match(object? data)
{
return data is IFromItemViewModel;
}
}

View File

@@ -7,6 +7,7 @@
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"
xmlns:converters="clr-namespace:Ursa.Demo.Converters"
d:DesignHeight="450"
d:DesignWidth="800"
x:CompileBindings="True"
@@ -50,6 +51,31 @@
<TextBox Width="300" u:FormItem.Label="Name" Text="{Binding Name}"/>
<TextBox Width="300" u:FormItem.Label="Email" Text="{Binding Email}" />
</u:Form>
<u:Divider Content="MVVM setup for dynamic items. " />
<u:Form ItemsSource="{Binding FormGroups}" HorizontalAlignment="Stretch" LabelPosition="Left" LabelWidth="*">
<u:Form.Styles>
<Style Selector="u|FormGroup" x:DataType="vm:IFormGroupViewModel">
<Setter Property="Header" Value="{Binding Title}" />
<Setter Property="ItemsSource" Value="{Binding Items}" />
</Style>
<Style Selector="u|FormItem" x:DataType="vm:IFromItemViewModel">
<Setter Property="Label" Value="{Binding Label}" />
</Style>
</u:Form.Styles>
<u:Form.ItemTemplate>
<converters:FormDataTemplateSelector>
<DataTemplate x:Key="{x:Type vm:FormTextViewModel}" DataType="vm:FormTextViewModel">
<TextBox Text="{Binding Value}"/>
</DataTemplate>
<DataTemplate x:Key="{x:Type vm:FormAgeViewModel}" DataType="vm:FormAgeViewModel">
<u:NumericUIntUpDown Value="{Binding Age}"/>
</DataTemplate>
<DataTemplate x:Key="{x:Type vm:FormDateRangeViewModel}" DataType="vm:FormDateRangeViewModel">
<u:DateRangePicker SelectedStartDate="{Binding Start}" SelectedEndDate="{Binding End}"/>
</DataTemplate>
</converters:FormDataTemplateSelector>
</u:Form.ItemTemplate>
</u:Form>
</StackPanel>
</ScrollViewer>
</UserControl>

View File

@@ -1,6 +1,8 @@
using System;
using System.Collections.ObjectModel;
using System.ComponentModel.DataAnnotations;
using CommunityToolkit.Mvvm.ComponentModel;
using Irihi.Avalonia.Shared.Contracts;
namespace Ursa.Demo.ViewModels;
@@ -11,13 +13,48 @@ public partial class FormDemoViewModel : ObservableObject
public FormDemoViewModel()
{
Model = new DataModel();
FormGroups = new ObservableCollection<IFormElement>
{
new FormGroupViewModel
{
Title = "Basic Information",
Items = new ObservableCollection<IFromItemViewModel>
{
new FormTextViewModel { Label = "Name" },
new FormAgeViewModel { Label = "Age" },
new FormTextViewModel { Label = "Email" }
}
},
new FormGroupViewModel
{
Title = "Education Information",
Items = new ObservableCollection<IFromItemViewModel>
{
new FormTextViewModel { Label = "College" },
new FormDateRangeViewModel { Label = "Study Time" }
}
},
new FormTextViewModel(){ Label = "Other" }
};
}
public partial class DataModel : ObservableObject
public ObservableCollection<IFormElement> FormGroups { get; set; }
}
public class DataModel : ObservableObject
{
private DateTime _date;
private string _email = string.Empty;
private string _name = string.Empty;
private double _number;
public DataModel()
{
Date = DateTime.Today;
}
[MinLength(10)]
public string Name
{
@@ -25,8 +62,6 @@ public partial class DataModel : ObservableObject
set => SetProperty(ref _name, value);
}
private double _number;
[Range(0.0, 10.0)]
public double Number
{
@@ -34,8 +69,6 @@ public partial class DataModel : ObservableObject
set => SetProperty(ref _number, value);
}
private string _email = string.Empty;
[EmailAddress]
public string Email
{
@@ -43,16 +76,50 @@ public partial class DataModel : ObservableObject
set => SetProperty(ref _email, value);
}
private DateTime _date;
public DateTime Date
{
get => _date;
set => SetProperty(ref _date, value);
}
}
public DataModel()
public interface IFormElement
{
Date = DateTime.Today;
}
public interface IFormGroupViewModel : IFormGroup, IFormElement
{
public string? Title { get; set; }
public ObservableCollection<IFromItemViewModel> Items { get; set; }
}
public interface IFromItemViewModel: IFormElement
{
public string? Label { get; set; }
}
public partial class FormGroupViewModel : ObservableObject, IFormGroupViewModel
{
[ObservableProperty] private string? _title;
public ObservableCollection<IFromItemViewModel> Items { get; set; } = [];
}
public partial class FormTextViewModel : ObservableObject, IFromItemViewModel
{
[ObservableProperty] private string? _label;
[ObservableProperty] private string? _value;
}
public partial class FormAgeViewModel : ObservableObject, IFromItemViewModel
{
[ObservableProperty] private uint? _age;
[ObservableProperty] private string? _label;
}
public partial class FormDateRangeViewModel : ObservableObject, IFromItemViewModel
{
[ObservableProperty] private DateTime? _end;
[ObservableProperty] private string? _label;
[ObservableProperty] private DateTime? _start;
}

View File

@@ -70,6 +70,7 @@
</StackPanel>
<ContentPresenter
Name="PART_ContentPresenter"
ContentTemplate="{TemplateBinding ContentTemplate}"
Content="{TemplateBinding Content}" />
</StackPanel>
</ControlTemplate>
@@ -111,6 +112,7 @@
Grid.Column="1"
VerticalAlignment="Stretch"
VerticalContentAlignment="Center"
ContentTemplate="{TemplateBinding ContentTemplate}"
Content="{TemplateBinding Content}" />
</Grid>
</ControlTemplate>
@@ -120,6 +122,7 @@
<Setter Property="Template">
<ControlTemplate TargetType="u:FormItem">
<ContentPresenter
ContentTemplate="{TemplateBinding ContentTemplate}"
Content="{TemplateBinding Content}" />
</ControlTemplate>
</Setter>

View File

@@ -2,6 +2,7 @@
using Avalonia.Controls;
using Avalonia.Controls.Metadata;
using Avalonia.Layout;
using Irihi.Avalonia.Shared.Contracts;
using Ursa.Common;
namespace Ursa.Controls;
@@ -64,7 +65,11 @@ public class Form: ItemsControl
protected override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
{
if (item is not Control control) return new FormItem();
if (item is not Control control)
{
if (item is IFormGroup) return new FormGroup();
return new FormItem();
}
return new FormItem()
{
Content = control,
@@ -73,4 +78,17 @@ public class Form: ItemsControl
[!FormItem.NoLabelProperty] = control[!FormItem.NoLabelProperty],
};
}
protected override void PrepareContainerForItemOverride(Control container, object? item, int index)
{
base.PrepareContainerForItemOverride(container, item, index);
if(container is FormItem formItem && !formItem.IsSet(ContentControl.ContentTemplateProperty))
{
formItem.ContentTemplate = ItemTemplate;
}
if (container is FormGroup group && !group.IsSet(FormGroup.ItemTemplateProperty))
{
group.ItemTemplate = ItemTemplate;
}
}
}

View File

@@ -22,4 +22,12 @@ public class FormGroup: HeaderedItemsControl
};
}
protected override void PrepareContainerForItemOverride(Control container, object? item, int index)
{
base.PrepareContainerForItemOverride(container, item, index);
if (container is FormItem formItem && !formItem.IsSet(ContentControl.ContentTemplateProperty))
{
formItem.ContentTemplate = ItemTemplate;
}
}
}

View File

@@ -0,0 +1,22 @@
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using Avalonia.Metadata;
namespace HeadlessTest.Ursa.Controls.FormTests.Dynamic_Item_Generation;
internal class DataTemplateSelector : IDataTemplate
{
[Content] public Dictionary<Type, IDataTemplate?> Templates { get; } = new();
public Control? Build(object? param)
{
if (param is null) return null;
var type = param.GetType();
return Templates.TryGetValue(type, out var template) ? template?.Build(param) : null;
}
public bool Match(object? data)
{
return data is IFromItemViewModel;
}
}

View File

@@ -0,0 +1,124 @@
using System.Collections.ObjectModel;
using Avalonia.Controls;
using Avalonia.Headless.XUnit;
using Avalonia.LogicalTree;
using Ursa.Controls;
namespace HeadlessTest.Ursa.Controls.FormTests.Dynamic_Item_Generation;
public class Test
{
[AvaloniaFact]
public void FormItem_Generation()
{
// Arrange
var viewModel = new TestViewModel();
viewModel.Items = new ObservableCollection<IFormElement>
{
new FormTextViewModel { Label = "First Name" },
new FormTextViewModel { Label = "Last Name" },
new FormAgeViewModel { Label = "Age" },
new FormDateRangeViewModel { Label = "Date of Birth" }
};
var window = new TestWindow { DataContext = viewModel };
// Act
window.Show();
var form = window.FindControl<Form>("Form");
var logicalChildren = form.GetLogicalChildren().ToList();
Assert.True(logicalChildren.All(a=>a is FormItem));
Assert.IsType<TextBox>(logicalChildren[0].LogicalChildren[0]);
Assert.IsType<TextBox>(logicalChildren[1].LogicalChildren[0]);
Assert.IsType<NumericUIntUpDown>(logicalChildren[2].LogicalChildren[0]);
Assert.IsType<DateRangePicker>(logicalChildren[3].LogicalChildren[0]);
Assert.Equal("First Name", FormItem.GetLabel((Control)logicalChildren[0]));
Assert.Equal("Last Name", FormItem.GetLabel((Control)logicalChildren[1]));
Assert.Equal("Age", FormItem.GetLabel((Control)logicalChildren[2]));
Assert.Equal("Date of Birth", FormItem.GetLabel((Control)logicalChildren[3]));
window.Close();
}
[AvaloniaFact]
public void FormGroup_Generation()
{
// Arrange
var viewModel = new TestViewModel();
viewModel.Items = new ObservableCollection<IFormElement>
{
new FormGroupViewModel
{
Title = "Basic Information",
Items = new ObservableCollection<IFromItemViewModel>
{
new FormTextViewModel { Label = "First Name" },
new FormTextViewModel { Label = "Last Name" },
new FormAgeViewModel { Label = "Age" },
new FormDateRangeViewModel { Label = "Date of Birth" }
}
}
};
var window = new TestWindow { DataContext = viewModel };
// Act
window.Show();
var form = window.FindControl<Form>("Form");
var logicalChildren = form.GetLogicalChildren().ToList();
Assert.True(logicalChildren.All(a=>a is FormGroup));
var formGroup = (FormGroup)logicalChildren[0];
Assert.Equal("Basic Information", formGroup.Header);
var formItems = formGroup.GetLogicalChildren().ToList();
Assert.True(formItems.All(a=>a is FormItem));
Assert.IsType<TextBox>(formItems[0].LogicalChildren[0]);
Assert.IsType<TextBox>(formItems[1].LogicalChildren[0]);
Assert.IsType<NumericUIntUpDown>(formItems[2].LogicalChildren[0]);
Assert.IsType<DateRangePicker>(formItems[3].LogicalChildren[0]);
Assert.Equal("First Name", FormItem.GetLabel((Control)formItems[0]));
Assert.Equal("Last Name", FormItem.GetLabel((Control)formItems[1]));
Assert.Equal("Age", FormItem.GetLabel((Control)formItems[2]));
Assert.Equal("Date of Birth", FormItem.GetLabel((Control)formItems[3]));
window.Close();
}
[AvaloniaFact]
public void Mixture_Generation()
{
// Arrange
var viewModel = new TestViewModel();
viewModel.Items = new ObservableCollection<IFormElement>
{
new FormTextViewModel { Label = "First Name" },
new FormGroupViewModel
{
Title = "Basic Information",
Items = new ObservableCollection<IFromItemViewModel>
{
new FormTextViewModel { Label = "Last Name" },
new FormAgeViewModel { Label = "Age" },
new FormDateRangeViewModel { Label = "Date of Birth" }
}
}
};
var window = new TestWindow { DataContext = viewModel };
// Act
window.Show();
var form = window.FindControl<Form>("Form");
var logicalChildren = form.GetLogicalChildren().ToList();
Assert.True(logicalChildren.All(a=>a is FormItem || a is FormGroup));
Assert.IsType<TextBox>(logicalChildren[0].LogicalChildren[0]);
var formGroup = (FormGroup)logicalChildren[1];
Assert.Equal("Basic Information", formGroup.Header);
var formItems = formGroup.GetLogicalChildren().ToList();
Assert.True(formItems.All(a=>a is FormItem));
Assert.IsType<TextBox>(formItems[0].LogicalChildren[0]);
Assert.IsType<NumericUIntUpDown>(formItems[1].LogicalChildren[0]);
Assert.IsType<DateRangePicker>(formItems[2].LogicalChildren[0]);
window.Close();
}
}

View File

@@ -0,0 +1,51 @@
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using Irihi.Avalonia.Shared.Contracts;
namespace HeadlessTest.Ursa.Controls.FormTests.Dynamic_Item_Generation;
public class TestViewModel
{
public ObservableCollection<IFormElement> Items { get; set; } = [];
}
public interface IFormElement
{
}
public interface IFormGroupViewModel : IFormGroup, IFormElement
{
public string? Title { get; set; }
public ObservableCollection<IFromItemViewModel> Items { get; set; }
}
public interface IFromItemViewModel: IFormElement
{
public string? Label { get; set; }
}
public partial class FormGroupViewModel : ObservableObject, IFormGroupViewModel
{
[ObservableProperty] private string? _title;
public ObservableCollection<IFromItemViewModel> Items { get; set; } = [];
}
public partial class FormTextViewModel : ObservableObject, IFromItemViewModel
{
[ObservableProperty] private string? _label;
[ObservableProperty] private string? _value;
}
public partial class FormAgeViewModel : ObservableObject, IFromItemViewModel
{
[ObservableProperty] private uint? _age;
[ObservableProperty] private string? _label;
}
public partial class FormDateRangeViewModel : ObservableObject, IFromItemViewModel
{
[ObservableProperty] private DateTime? _end;
[ObservableProperty] private string? _label;
[ObservableProperty] private DateTime? _start;
}

View File

@@ -0,0 +1,51 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
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:HeadlessTest.Ursa.Controls.FormTests.Dynamic_Item_Generation"
mc:Ignorable="d" d:DesignWidth="800"
d:DesignHeight="450"
x:DataType="vm:TestViewModel"
x:CompileBindings="True"
x:Class="HeadlessTest.Ursa.Controls.FormTests.Dynamic_Item_Generation.TestWindow"
Title="TestWindow">
<u:Form Name="Form" ItemsSource="{Binding Items}">
<u:Form.Styles>
<Style Selector="u|FormGroup"
x:DataType="vm:IFormGroupViewModel">
<Setter Property="Header"
Value="{Binding Title}" />
<Setter Property="ItemsSource"
Value="{Binding Items}" />
</Style>
<Style Selector="u|FormItem"
x:DataType="vm:IFromItemViewModel">
<Setter Property="Label"
Value="{Binding Label}" />
</Style>
</u:Form.Styles>
<u:Form.ItemTemplate>
<vm:DataTemplateSelector>
<DataTemplate
x:Key="{x:Type vm:FormTextViewModel}"
DataType="vm:FormTextViewModel">
<TextBox Text="{Binding Value}" />
</DataTemplate>
<DataTemplate
x:Key="{x:Type vm:FormAgeViewModel}"
DataType="vm:FormAgeViewModel">
<u:NumericUIntUpDown
Value="{Binding Age}" />
</DataTemplate>
<DataTemplate
x:Key="{x:Type vm:FormDateRangeViewModel}"
DataType="vm:FormDateRangeViewModel">
<u:DateRangePicker
SelectedStartDate="{Binding Start}"
SelectedEndDate="{Binding End}" />
</DataTemplate>
</vm:DataTemplateSelector>
</u:Form.ItemTemplate>
</u:Form>
</Window>

View File

@@ -0,0 +1,13 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace HeadlessTest.Ursa.Controls.FormTests.Dynamic_Item_Generation;
public partial class TestWindow : Window
{
public TestWindow()
{
InitializeComponent();
}
}