diff --git a/demo/Ursa.Demo/DataTemplates/FormDataTemplateSelector.cs b/demo/Ursa.Demo/DataTemplates/FormDataTemplateSelector.cs
new file mode 100644
index 00000000..6351ec38
--- /dev/null
+++ b/demo/Ursa.Demo/DataTemplates/FormDataTemplateSelector.cs
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/demo/Ursa.Demo/Pages/FormDemo.axaml b/demo/Ursa.Demo/Pages/FormDemo.axaml
index a4a4c8d8..fcf23bf9 100644
--- a/demo/Ursa.Demo/Pages/FormDemo.axaml
+++ b/demo/Ursa.Demo/Pages/FormDemo.axaml
@@ -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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demo/Ursa.Demo/ViewModels/FormDemoViewModel.cs b/demo/Ursa.Demo/ViewModels/FormDemoViewModel.cs
index 97e1873e..82970828 100644
--- a/demo/Ursa.Demo/ViewModels/FormDemoViewModel.cs
+++ b/demo/Ursa.Demo/ViewModels/FormDemoViewModel.cs
@@ -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
+ {
+ new FormGroupViewModel
+ {
+ Title = "Basic Information",
+ Items = new ObservableCollection
+ {
+ new FormTextViewModel { Label = "Name" },
+ new FormAgeViewModel { Label = "Age" },
+ new FormTextViewModel { Label = "Email" }
+ }
+ },
+ new FormGroupViewModel
+ {
+ Title = "Education Information",
+ Items = new ObservableCollection
+ {
+ new FormTextViewModel { Label = "College" },
+ new FormDateRangeViewModel { Label = "Study Time" }
+ }
+ },
+ new FormTextViewModel(){ Label = "Other" }
+ };
}
+
+ public ObservableCollection FormGroups { get; set; }
}
-public partial class DataModel : ObservableObject
+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 string Name
set => SetProperty(ref _name, value);
}
- private double _number;
-
[Range(0.0, 10.0)]
public double Number
{
@@ -34,8 +69,6 @@ public double Number
set => SetProperty(ref _number, value);
}
- private string _email = string.Empty;
-
[EmailAddress]
public string Email
{
@@ -43,16 +76,50 @@ public string Email
set => SetProperty(ref _email, value);
}
- private DateTime _date;
-
public DateTime Date
{
get => _date;
set => SetProperty(ref _date, value);
}
+}
- public DataModel()
- {
- Date = DateTime.Today;
- }
+public interface IFormElement
+{
+
+}
+
+public interface IFormGroupViewModel : IFormGroup, IFormElement
+{
+ public string? Title { get; set; }
+ public ObservableCollection Items { get; set; }
+}
+
+public interface IFromItemViewModel: IFormElement
+{
+ public string? Label { get; set; }
+}
+
+public partial class FormGroupViewModel : ObservableObject, IFormGroupViewModel
+{
+ [ObservableProperty] private string? _title;
+ public ObservableCollection 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;
}
\ No newline at end of file
diff --git a/src/Ursa.Themes.Semi/Controls/Form.axaml b/src/Ursa.Themes.Semi/Controls/Form.axaml
index 4a987bd8..24494597 100644
--- a/src/Ursa.Themes.Semi/Controls/Form.axaml
+++ b/src/Ursa.Themes.Semi/Controls/Form.axaml
@@ -70,6 +70,7 @@
@@ -111,6 +112,7 @@
Grid.Column="1"
VerticalAlignment="Stretch"
VerticalContentAlignment="Center"
+ ContentTemplate="{TemplateBinding ContentTemplate}"
Content="{TemplateBinding Content}" />
@@ -120,6 +122,7 @@
diff --git a/src/Ursa/Controls/Form/Form.cs b/src/Ursa/Controls/Form/Form.cs
index 8110b995..9812e811 100644
--- a/src/Ursa/Controls/Form/Form.cs
+++ b/src/Ursa/Controls/Form/Form.cs
@@ -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 @@ protected override bool NeedsContainerOverride(object? item, int index, out obje
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 @@ protected override Control CreateContainerForItemOverride(object? item, int inde
[!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;
+ }
+ }
}
\ No newline at end of file
diff --git a/src/Ursa/Controls/Form/FormGroup.cs b/src/Ursa/Controls/Form/FormGroup.cs
index 42803f9c..54a67c88 100644
--- a/src/Ursa/Controls/Form/FormGroup.cs
+++ b/src/Ursa/Controls/Form/FormGroup.cs
@@ -21,5 +21,13 @@ protected override Control CreateContainerForItemOverride(object? item, int inde
[!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.IsSet(ContentControl.ContentTemplateProperty))
+ {
+ formItem.ContentTemplate = ItemTemplate;
+ }
+ }
}
\ No newline at end of file
diff --git a/tests/HeadlessTest.Ursa/Controls/FormTests/Dynamic_Item_Generation/DataTemplateSelector.cs b/tests/HeadlessTest.Ursa/Controls/FormTests/Dynamic_Item_Generation/DataTemplateSelector.cs
new file mode 100644
index 00000000..e3176643
--- /dev/null
+++ b/tests/HeadlessTest.Ursa/Controls/FormTests/Dynamic_Item_Generation/DataTemplateSelector.cs
@@ -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 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;
+ }
+}
\ No newline at end of file
diff --git a/tests/HeadlessTest.Ursa/Controls/FormTests/Dynamic_Item_Generation/Test.cs b/tests/HeadlessTest.Ursa/Controls/FormTests/Dynamic_Item_Generation/Test.cs
new file mode 100644
index 00000000..ed0266ef
--- /dev/null
+++ b/tests/HeadlessTest.Ursa/Controls/FormTests/Dynamic_Item_Generation/Test.cs
@@ -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
+ {
+ 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