diff --git a/demo/UraniumApp/AppShell.xaml b/demo/UraniumApp/AppShell.xaml
index 247e3496..c75bc64b 100644
--- a/demo/UraniumApp/AppShell.xaml
+++ b/demo/UraniumApp/AppShell.xaml
@@ -144,6 +144,10 @@
+
+
+
+
diff --git a/demo/UraniumApp/MauiProgram.cs b/demo/UraniumApp/MauiProgram.cs
index 32751b82..ca51b176 100644
--- a/demo/UraniumApp/MauiProgram.cs
+++ b/demo/UraniumApp/MauiProgram.cs
@@ -6,6 +6,8 @@
using System.Reactive;
using UraniumUI;
using UraniumUI.Dialogs;
+using UraniumUI.Options;
+using UraniumUI.Validations;
namespace UraniumApp;
@@ -31,6 +33,11 @@ public static MauiApp CreateMauiApp()
fonts.AddFluentIconFonts();
});
+ builder.Services.Configure(options =>
+ {
+ options.ValidationFactory = DataAnnotationValidation.CreateValidations;
+ });
+
RxApp.DefaultExceptionHandler = new AnonymousObserver(ex =>
{
App.Current.MainPage.DisplayAlert("Error", ex.Message, "OK");
diff --git a/demo/UraniumApp/Pages/AutoFormViewPage.xaml b/demo/UraniumApp/Pages/AutoFormViewPage.xaml
new file mode 100644
index 00000000..7e98dd28
--- /dev/null
+++ b/demo/UraniumApp/Pages/AutoFormViewPage.xaml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/demo/UraniumApp/Pages/AutoFormViewPage.xaml.cs b/demo/UraniumApp/Pages/AutoFormViewPage.xaml.cs
new file mode 100644
index 00000000..fb7d4fc6
--- /dev/null
+++ b/demo/UraniumApp/Pages/AutoFormViewPage.xaml.cs
@@ -0,0 +1,9 @@
+namespace UraniumApp.Pages;
+
+public partial class AutoFormViewPage : ContentPage
+{
+ public AutoFormViewPage()
+ {
+ InitializeComponent();
+ }
+}
\ No newline at end of file
diff --git a/demo/UraniumApp/ViewModels/AutoFormViewPageViewModel.cs b/demo/UraniumApp/ViewModels/AutoFormViewPageViewModel.cs
new file mode 100644
index 00000000..ed253b37
--- /dev/null
+++ b/demo/UraniumApp/ViewModels/AutoFormViewPageViewModel.cs
@@ -0,0 +1,23 @@
+using DotNurse.Injector.Attributes;
+using ReactiveUI;
+using ReactiveUI.Fody.Helpers;
+using System.ComponentModel.DataAnnotations;
+
+namespace UraniumApp.ViewModels;
+
+[RegisterAs(typeof(AutoFormViewPageViewModel))]
+public class AutoFormViewPageViewModel : ReactiveObject
+{
+ [EmailAddress]
+ [Required]
+ [MinLength(5)]
+ [Reactive] public string Email { get; set; }
+ [Reactive] public string FullName { get; set; }
+ [Reactive] public Gender Gender { get; set; }
+ [Reactive] public DateTime? BirthDate { get; set; }
+ [Reactive] public TimeSpan? MeetingTime { get; set; }
+ [Reactive] public int? NumberOfSeats { get; set; }
+
+ [Required]
+ [Reactive] public bool IsTermsAndConditionsAccepted { get; set; }
+}
diff --git a/docs/en/infrastructure/AutoFormView.md b/docs/en/infrastructure/AutoFormView.md
new file mode 100644
index 00000000..6fdeb6ec
--- /dev/null
+++ b/docs/en/infrastructure/AutoFormView.md
@@ -0,0 +1,143 @@
+# AutoFormView
+
+The `AutoFormView` is a view that automatically generates a form based on the properties of a model. It is a subclass of `FormView` and uses the same APIs.
+
+## Usage
+
+`AutoFormView` is defined in `UraniumUI.Controls` namespace. You can use it in XAML like this:
+
+```xml
+xmlns:uranium="http://schemas.enisn-projects.io/dotnet/maui/uraniumui"
+```
+
+Then you can use it like this:
+
+```xml
+
+```
+
+### Example
+
+```csharp
+public class AutoFormViewPageViewModel : ViewModelBase
+{
+ [Reactive] public string Email { get; set; }
+ [Reactive] public string FullName { get; set; }
+ [Reactive] public Gender Gender { get; set; }
+ [Reactive] public DateTime? BirthDate { get; set; }
+ [Reactive] public TimeSpan? MeetingTime { get; set; }
+ [Reactive] public int? NumberOfSeats { get; set; }
+ [Reactive] public bool IsTermsAndConditionsAccepted { get; set; }
+}
+```
+
+```xml
+
+```
+
+![AutoFormView](images/autoformview-example-dark.png)
+
+
+## Configuration
+
+AutoFormView can be configured using the `AutoFormViewOptions` in the MauiProgram.cs file. Here is an example of how to configure the `AutoFormView`:
+
+```csharp
+builder.Services.Configure(options =>
+{
+ // configure options here
+});
+```
+
+### DataAnnotations
+It's not supported DataAnnotations by default. You can add `UraniumUI.Validations.DataAnnotations` package to project and configure `AutoFormViewOptions` to use DataAnnotations.
+
+```csharp
+builder.Services.Configure(options =>
+{
+ options.ValidationFactory = DataAnnotationValidation.CreateValidations;
+});
+```
+
+### EditorMapping
+You can configure the `AutoFormView` to use a specific editor for a type. For example, you can configure the `AutoFormViewOptions` to use a `Editor` for `string` properties.
+
+```csharp
+builder.Services.Configure(options =>
+{
+ options.EditorMapping[typeof(string)] = (property, source) =>
+ {
+ var editor = new Entry();
+ editor.SetBinding(Entry.TextProperty, new Binding(property.Name, source: source));
+ return editor;
+ };
+});
+```
+
+> Note: The following types are already mapped by default: `string`, `int`, `float`, `double`, `DateTime`, `TimeSpan`, `bool`, `Enum`, `Keyboard`.
+
+
+### Property Name Mapping
+You can configure the `PropertyNameFactory` property of `AutoFormViewOptions` to use a custom factory to get the property name. For example, you can implement a localization factory to get the property name from a resource file.
+
+```csharp
+builder.Services.Configure(options =>
+{
+ options.PropertyNameFactory = property =>
+ {
+ return Localize(property.Name);
+ };
+});
+```
+
+## Customization
+
+You can customize the `AutoFormView`.
+
+
+## ItemsLayout
+You can customize the `ItemsLayout` of the `AutoFormView` using the `ItemsLayout` property. For example, you can use a `GridLayout` to display the properties in a grid.
+
+> **Note:** It's not the same as the `ItemsLayout` of the `CollectionView`. This is a **real** layout that will be used to place editors into children. Such as `StackLayout`, `Grid`, `FlexLayout`, etc.
+
+```xml
+
+
+
+
+
+```
+
+![AutoFormView](images/autoformview-itemslayout-grid-dark.png)
+
+
+## FooterLayout
+You can customize the `FooterLayout` of the `AutoFormView` using the `FooterLayout` property. For example, you can use a `HorizontalStackLayout` to display the buttons in a horizontal stack.
+
+```xml
+
+
+
+
+
+```
+
+![AutoFormView](images/autoformview-footerlayout-dark.png)
+
+## ShowMissingProperties
+
+You can configure the `AutoFormView` to show missing properties using the `ShowMissingProperties` property. For example, you can set the `ShowMissingProperties` to `true` to show all properties of the model.
+
+```xml
+
+```
+
+![AutoFormView](images/autoformview-showmissingproperties-dark.png)
+
+
+## Other Properties
+
+- `ShowSubmitButton`: Indicates whether the submit button is visible. The default value is `true`.
+- `SohwResetButton`: Indicates whether the reset button is visible. The default value is `true`.
+- `SubmitButtonText`: The text of the submit button. The default value is `Submit`.
+- `ResetButtonText`: The text of the reset button. The default value is `Reset`.
\ No newline at end of file
diff --git a/docs/en/infrastructure/images/autoformview-example-dark.png b/docs/en/infrastructure/images/autoformview-example-dark.png
new file mode 100644
index 00000000..d24ebca3
Binary files /dev/null and b/docs/en/infrastructure/images/autoformview-example-dark.png differ
diff --git a/docs/en/infrastructure/images/autoformview-footerlayout-dark.png b/docs/en/infrastructure/images/autoformview-footerlayout-dark.png
new file mode 100644
index 00000000..80c8fee5
Binary files /dev/null and b/docs/en/infrastructure/images/autoformview-footerlayout-dark.png differ
diff --git a/docs/en/infrastructure/images/autoformview-itemslayout-grid-dark.png b/docs/en/infrastructure/images/autoformview-itemslayout-grid-dark.png
new file mode 100644
index 00000000..894ba805
Binary files /dev/null and b/docs/en/infrastructure/images/autoformview-itemslayout-grid-dark.png differ
diff --git a/docs/en/infrastructure/images/autoformview-showmissingproperties-dark.png b/docs/en/infrastructure/images/autoformview-showmissingproperties-dark.png
new file mode 100644
index 00000000..6c228531
Binary files /dev/null and b/docs/en/infrastructure/images/autoformview-showmissingproperties-dark.png differ
diff --git a/src/UraniumUI.Material/Extensions/AutoFormViewMaterialConfigurationExtensions.cs b/src/UraniumUI.Material/Extensions/AutoFormViewMaterialConfigurationExtensions.cs
new file mode 100644
index 00000000..a427ce6d
--- /dev/null
+++ b/src/UraniumUI.Material/Extensions/AutoFormViewMaterialConfigurationExtensions.cs
@@ -0,0 +1,135 @@
+using InputKit.Shared.Controls;
+using System.Reflection;
+using UraniumUI.Controls;
+using UraniumUI.Extensions;
+using UraniumUI.Material.Controls;
+using UraniumUI.Options;
+using UraniumUI.Resources;
+
+namespace UraniumUI.Material.Extensions;
+
+public static class AutoFormViewMaterialConfigurationExtensions
+{
+ public static MauiAppBuilder ConfigureAutoFormViewForMaterial(this MauiAppBuilder builder)
+ {
+ builder.Services.Configure(options =>
+ {
+ options.EditorMapping[typeof(string)] = EditorForString;
+ options.EditorMapping[typeof(int)] = EditorForNumeric;
+ options.EditorMapping[typeof(double)] = EditorForNumeric;
+ options.EditorMapping[typeof(float)] = EditorForNumeric;
+ options.EditorMapping[typeof(bool)] = EditorForBoolean;
+ options.EditorMapping[typeof(Keyboard)] = EditorForKeyboard;
+ options.EditorMapping[typeof(Enum)] = EditorForEnum;
+ options.EditorMapping[typeof(DateTime)] = EditorForDateTime;
+ options.EditorMapping[typeof(TimeSpan)] = EditorForTimeSpan;
+ });
+
+ return builder;
+ }
+
+ public static View EditorForString(PropertyInfo property, object source)
+ {
+ var editor = new TextField();
+ editor.SetBinding(TextField.TextProperty, new Binding(property.Name, source: source));
+ editor.AllowClear = true;
+ editor.Title = property.Name;
+
+ return editor;
+ }
+
+ public static View EditorForNumeric(PropertyInfo property, object source)
+ {
+ var editor = new TextField();
+ editor.SetBinding(TextField.TextProperty, new Binding(property.Name, source: source));
+ editor.Title = property.Name;
+ editor.AllowClear = false;
+ editor.Keyboard = Keyboard.Numeric;
+
+ return editor;
+ }
+
+ public static View EditorForBoolean(PropertyInfo property, object source)
+ {
+ var editor = new UraniumUI.Material.Controls.CheckBox();
+ editor.SetBinding(UraniumUI.Material.Controls.CheckBox.IsCheckedProperty, new Binding(property.Name, source: source));
+ editor.Text = property.Name;
+
+ return editor;
+ }
+
+ public static View EditorForEnum(PropertyInfo property, object source)
+ {
+ var editor = new PickerField();
+
+ var values = Enum.GetValues(property.PropertyType.AsNonNullable());
+ if (values.Length <= 5)
+ {
+ return CreateSelectionViewForValues(values, property, source);
+ }
+
+ editor.ItemsSource = values;
+ editor.SetBinding(PickerField.SelectedItemProperty, new Binding(property.Name, source: source));
+ editor.Title = property.Name;
+ editor.AllowClear = false;
+ return editor;
+ }
+
+ private static View CreateSelectionViewForValues(Array values, PropertyInfo property, object source)
+ {
+ var shouldUseSingleColumn = values.Length > 3;
+ var editor = new SelectionView
+ {
+ Color = ColorResource.GetColor("Primary", "PrimaryDark"),
+ ColumnSpacing = -2,
+ RowSpacing = shouldUseSingleColumn ? 5 : -2,
+ SelectionType = shouldUseSingleColumn ? InputKit.Shared.SelectionType.RadioButton : InputKit.Shared.SelectionType.Button,
+ ColumnNumber = shouldUseSingleColumn ? 1 : values.Length,
+ ItemsSource = values
+ };
+
+ editor.SetBinding(SelectionView.SelectedItemProperty, new Binding(property.Name, source: source));
+
+ return new VerticalStackLayout
+ {
+ Spacing = 6,
+ Children = {
+ new Label { Text = property.Name },
+ editor
+ }
+ };
+ }
+
+ public static View EditorForKeyboard(PropertyInfo property, object source)
+ {
+ var editor = new PickerField();
+
+ editor.ItemsSource = typeof(Keyboard)
+ .GetProperties(BindingFlags.Public | BindingFlags.Static)
+ .Select(x => x.GetValue(null))
+ .ToArray();
+
+ editor.SetBinding(PickerField.SelectedItemProperty, new Binding(property.Name, source: source));
+ editor.Title = property.Name;
+ editor.AllowClear = false;
+ return editor;
+ }
+
+ public static View EditorForDateTime(PropertyInfo property, object source)
+ {
+ var editor = new DatePickerField();
+ editor.SetBinding(DatePickerField.DateProperty, new Binding(property.Name, source: source));
+ editor.Title = property.Name;
+ editor.AllowClear = false;
+ return editor;
+ }
+
+ public static View EditorForTimeSpan(PropertyInfo property, object source)
+ {
+ var editor = new TimePickerField();
+ editor.SetBinding(TimePickerField.TimeProperty, new Binding(property.Name, source: source));
+ editor.Title = property.Name;
+ editor.AllowClear = false;
+ return editor;
+ }
+}
diff --git a/src/UraniumUI.Material/UraniumUIMaterialMauiProgramExtensions.cs b/src/UraniumUI.Material/UraniumUIMaterialMauiProgramExtensions.cs
index c498d317..b66f744a 100644
--- a/src/UraniumUI.Material/UraniumUIMaterialMauiProgramExtensions.cs
+++ b/src/UraniumUI.Material/UraniumUIMaterialMauiProgramExtensions.cs
@@ -1,4 +1,5 @@
using UraniumUI.Material.Controls;
+using UraniumUI.Material.Extensions;
using UraniumUI.Material.Handlers;
namespace UraniumUI;
@@ -10,7 +11,9 @@ public static MauiAppBuilder UseUraniumUIMaterial(this MauiAppBuilder builder)
{
handlers.AddHandler(typeof(ButtonView), typeof(ButtonViewHandler));
});
-
+
+ builder.ConfigureAutoFormViewForMaterial();
+
return builder;
}
}
diff --git a/src/UraniumUI.Validations.DataAnnotations/UraniumUI.Validations.DataAnnotations.csproj b/src/UraniumUI.Validations.DataAnnotations/UraniumUI.Validations.DataAnnotations.csproj
index 09c752bc..65e78583 100644
--- a/src/UraniumUI.Validations.DataAnnotations/UraniumUI.Validations.DataAnnotations.csproj
+++ b/src/UraniumUI.Validations.DataAnnotations/UraniumUI.Validations.DataAnnotations.csproj
@@ -27,7 +27,7 @@
-
+
diff --git a/src/UraniumUI.Validations.DataAnnotations/Validations/DataAnotationValidation.cs b/src/UraniumUI.Validations.DataAnnotations/Validations/DataAnotationValidation.cs
index af04b4ac..4b06adc1 100644
--- a/src/UraniumUI.Validations.DataAnnotations/Validations/DataAnotationValidation.cs
+++ b/src/UraniumUI.Validations.DataAnnotations/Validations/DataAnotationValidation.cs
@@ -1,5 +1,6 @@
using InputKit.Shared.Validations;
using System.ComponentModel.DataAnnotations;
+using System.Reflection;
namespace UraniumUI.Validations;
public class DataAnnotationValidation : IValidation
@@ -7,7 +8,7 @@ public class DataAnnotationValidation : IValidation
public string Message { get; protected set; }
public ValidationAttribute Attribute { get; }
-
+
public string PropertyName { get; }
public DataAnnotationValidation(ValidationAttribute attribute, string propertyName)
@@ -30,4 +31,14 @@ public bool Validate(object value)
return false;
}
}
+
+ public static IEnumerable CreateValidations(PropertyInfo property)
+ {
+ var attributes = property.GetCustomAttributes();
+
+ foreach (var attribute in attributes)
+ {
+ yield return new DataAnnotationValidation(attribute, property.Name);
+ }
+ }
}
\ No newline at end of file
diff --git a/src/UraniumUI/Controls/AutoFormView.cs b/src/UraniumUI/Controls/AutoFormView.cs
new file mode 100644
index 00000000..79a06dc0
--- /dev/null
+++ b/src/UraniumUI/Controls/AutoFormView.cs
@@ -0,0 +1,360 @@
+using InputKit.Shared.Controls;
+using System.Reflection;
+using UraniumUI.Extensions;
+using UraniumUI.Resources;
+using UraniumUI.Options;
+using Microsoft.Extensions.Options;
+using InputKit.Shared.Abstraction;
+
+namespace UraniumUI.Controls;
+public class AutoFormView : FormView
+{
+ public object Source { get => GetValue(SourceProperty); set => SetValue(SourceProperty, value); }
+ public static readonly BindableProperty SourceProperty = BindableProperty.Create(
+ nameof(Source),
+ typeof(object),
+ typeof(AutoFormView),
+ propertyChanged: (bindable, oldValue, newValue) => (bindable as AutoFormView).OnSourceChanged());
+
+ public PropertyInfo[] EditingProperties { get; protected set; }
+
+ public bool ShowSubmitButton { get => (bool)GetValue(ShowSubmitbuttonProperty); set => SetValue(ShowSubmitbuttonProperty, value); }
+
+ public static readonly BindableProperty ShowSubmitbuttonProperty = BindableProperty.Create(
+ nameof(ShowSubmitButton), typeof(bool), typeof(AutoFormView), defaultValue: true,
+ propertyChanged: (bindable, oldValue, newValue) => (bindable as AutoFormView).OnShowSubmitButtonChanged());
+
+ public bool ShowResetButton { get => (bool)GetValue(ShowResetButtonProperty); set => SetValue(ShowResetButtonProperty, value); }
+
+ public static readonly BindableProperty ShowResetButtonProperty = BindableProperty.Create(
+ nameof(ShowResetButton), typeof(bool), typeof(AutoFormView), defaultValue: true,
+ propertyChanged: (bindable, oldValue, newValue) => (bindable as AutoFormView).OnShowResetButtonChanged());
+
+ public string SubmitButtonText { get => (string)GetValue(SubmitButtonTextProperty); set => SetValue(SubmitButtonTextProperty, value); }
+
+ public static readonly BindableProperty SubmitButtonTextProperty = BindableProperty.Create(
+ nameof(SubmitButtonText), typeof(string), typeof(AutoFormView), defaultValue: "Submit",
+ propertyChanged: (bindable, oldValue, newValue) => (bindable as AutoFormView).OnSubmitButtonTextChanged());
+
+ public string ResetButtonText { get => (string)GetValue(ResetButtonTextProperty); set => SetValue(ResetButtonTextProperty, value); }
+
+ public static readonly BindableProperty ResetButtonTextProperty = BindableProperty.Create(
+ nameof(ResetButtonText), typeof(string), typeof(AutoFormView), defaultValue: "Reset",
+ propertyChanged: (bindable, oldValue, newValue) => (bindable as AutoFormView).OnResetButtonTextChanged());
+
+ public bool ShowMissingProperties { get; set; } = true;
+
+ public bool Hierarchical { get; set; } = false;
+
+ public Type HierarchyLimitType { get; set; } = typeof(object);
+
+ private Layout _itemsLayout = new VerticalStackLayout
+ {
+ Spacing = 20,
+ Padding = 10,
+ };
+
+ public Layout ItemsLayout
+ {
+ get => _itemsLayout;
+ set
+ {
+ Children.Remove(_itemsLayout);
+ while (_itemsLayout.Children.Count > 0)
+ {
+ var item = _itemsLayout.Children.First();
+ _itemsLayout.Children.Remove(item);
+ value?.Children.Add(item);
+ }
+ _itemsLayout = value;
+ if (_itemsLayout != null)
+ {
+ Children.Add(_itemsLayout);
+ }
+ }
+ }
+
+ private Layout _footerLayout = new VerticalStackLayout { Spacing = 12 };
+
+ public Layout FooterLayout
+ {
+ get => _footerLayout;
+ set
+ {
+ ItemsLayout.Remove(_footerLayout);
+ while (ItemsLayout.Children.Count > 0)
+ {
+ var item = _footerLayout.Children.First();
+ ItemsLayout.Children.Remove(item);
+ value?.Children.Add(item);
+ }
+ _footerLayout = value;
+ if (ItemsLayout.Children.Count > 0 && _footerLayout != null)
+ {
+ ItemsLayout.Children.Add(_footerLayout);
+ }
+ }
+ }
+
+ protected Dictionary EditorMapping { get; }
+
+ protected AutoFormViewOptions Options { get; }
+ public AutoFormView()
+ {
+ Children.Add(_itemsLayout);
+
+ Options = UraniumServiceProvider.Current.GetRequiredService>().Value;
+ EditorMapping = Options.EditorMapping;
+ }
+
+ protected void OnSourceChanged()
+ {
+ var flags = BindingFlags.Public | BindingFlags.Instance;
+ if (Hierarchical)
+ {
+ flags |= BindingFlags.FlattenHierarchy;
+ }
+ var props = Source.GetType()
+ .GetProperties(flags)
+ .Where(x => HierarchyLimitType.IsAssignableFrom(x.PropertyType))
+ .ToArray();
+
+ EditingProperties = props;
+
+ Render();
+ }
+
+ private void Render()
+ {
+ if (Source is null)
+ {
+ _itemsLayout.Children.Clear();
+ return;
+ }
+
+ foreach (var property in EditingProperties)
+ {
+ var createEditor = EditorMapping.FirstOrDefault(x => x.Key.IsAssignableFrom(property.PropertyType.AsNonNullable())).Value;
+ if (createEditor != null)
+ {
+ var editor = createEditor(property, Source);
+
+ foreach (var action in Options.PostEditorActions)
+ {
+ action(editor, property);
+ }
+
+ if (editor is IValidatable validatable && Options.ValidationFactory != null)
+ {
+ validatable.Validations.AddRange(Options.ValidationFactory(property));
+ }
+
+ _itemsLayout.Children.Add(editor);
+ }
+ else if (ShowMissingProperties)
+ {
+ _itemsLayout.Children.Add(new Label
+ {
+ Text = $"No editor for {property.Name} ({property.PropertyType})",
+ FontAttributes = FontAttributes.Italic
+ });
+ }
+ }
+
+ if (!_itemsLayout.Children.Contains(_footerLayout))
+ {
+ _itemsLayout.Children.Add(_footerLayout);
+ OnShowSubmitButtonChanged();
+ OnShowResetButtonChanged();
+ }
+ }
+
+ Button? submitButton;
+ protected virtual void OnShowSubmitButtonChanged()
+ {
+ if (ShowSubmitButton)
+ {
+ if (submitButton is null)
+ {
+ submitButton = new Button
+ {
+ Text = SubmitButtonText,
+ StyleClass = new[] { "FilledButton" },
+ Command = buttonSubmitCommand,
+ };
+ _footerLayout.Children.Insert(0, submitButton);
+ }
+ }
+ else
+ {
+ if (submitButton is not null)
+ {
+ _footerLayout.Children.Remove(submitButton);
+ submitButton = null;
+ }
+ }
+ }
+
+ Button? resetButton;
+ protected virtual void OnShowResetButtonChanged()
+ {
+ if (ShowResetButton)
+ {
+ if (resetButton is null)
+ {
+ resetButton = new Button
+ {
+ Text = ResetButtonText,
+ StyleClass = new[] { "OutlinedButton" },
+ Command = buttonResetCommand,
+ };
+ _footerLayout.Children.Add(resetButton);
+ }
+ }
+ else
+ {
+ if (resetButton != null)
+ {
+ _footerLayout.Children.Remove(resetButton);
+ resetButton = null;
+ }
+ }
+ }
+
+ protected virtual void OnSubmitButtonTextChanged()
+ {
+ if (submitButton != null)
+ {
+ submitButton.Text = SubmitButtonText;
+ }
+ }
+
+ protected virtual void OnResetButtonTextChanged()
+ {
+ if (resetButton != null)
+ {
+ resetButton.Text = ResetButtonText;
+ }
+ }
+
+ public static View EditorForString(PropertyInfo property, object source)
+ {
+ var editor = new Entry();
+ editor.SetBinding(Entry.TextProperty, new Binding(property.Name, source: source));
+
+ return new VerticalStackLayout
+ {
+ new Label { Text = property.Name },
+ editor
+ };
+ }
+
+ public static View EditorForNumeric(PropertyInfo property, object source)
+ {
+ var editor = new Entry();
+ editor.SetBinding(Entry.TextProperty, new Binding(property.Name, source: source));
+ editor.Keyboard = Keyboard.Numeric;
+
+ return new VerticalStackLayout
+ {
+ new Label { Text = property.Name },
+ editor
+ };
+ }
+
+ public static View EditorForBoolean(PropertyInfo property, object source)
+ {
+ var editor = new InputKit.Shared.Controls.CheckBox();
+ editor.SetBinding(InputKit.Shared.Controls.CheckBox.IsCheckedProperty, new Binding(property.Name, source: source));
+ editor.Text = property.Name;
+
+ return editor;
+ }
+
+ public static View EditorForEnum(PropertyInfo property, object source)
+ {
+ var editor = new Picker();
+
+ var values = Enum.GetValues(property.PropertyType.AsNonNullable());
+ if (values.Length <= 5)
+ {
+ return CreateSelectionViewForValues(values, property, source);
+ }
+
+ editor.ItemsSource = values;
+ editor.SetBinding(Picker.SelectedItemProperty, new Binding(property.Name, source: source));
+ editor.Title = property.Name;
+ return new VerticalStackLayout
+ {
+ new Label { Text = property.Name },
+ editor
+ };
+ }
+
+ public static View CreateSelectionViewForValues(Array values, PropertyInfo property, object source)
+ {
+ var shouldUseSingleColumn = values.Length > 3;
+ var editor = new SelectionView
+ {
+ Color = ColorResource.GetColor("Primary", "PrimaryDark"),
+ ColumnSpacing = -2,
+ RowSpacing = shouldUseSingleColumn ? 5 : -2,
+ SelectionType = shouldUseSingleColumn ? InputKit.Shared.SelectionType.RadioButton : InputKit.Shared.SelectionType.Button,
+ ColumnNumber = shouldUseSingleColumn ? 1 : values.Length,
+ ItemsSource = values
+ };
+
+ editor.SetBinding(SelectionView.SelectedItemProperty, new Binding(property.Name, source: source));
+
+ return new VerticalStackLayout
+ {
+ Spacing = 6,
+ Children = {
+ new Label { Text = property.Name },
+ editor
+ }
+ };
+ }
+
+ public static View EditorForKeyboard(PropertyInfo property, object source)
+ {
+ var editor = new Picker();
+
+ editor.ItemsSource = typeof(Keyboard)
+ .GetProperties(BindingFlags.Public | BindingFlags.Static)
+ .Select(x => x.GetValue(null))
+ .ToArray();
+
+ editor.SetBinding(Picker.SelectedItemProperty, new Binding(property.Name, source: source));
+ editor.Title = property.Name;
+ return new VerticalStackLayout
+ {
+ new Label { Text = property.Name },
+ editor
+ };
+ }
+
+ public static View EditorForDateTime(PropertyInfo property, object source)
+ {
+ var editor = new DatePicker();
+ editor.SetBinding(DatePicker.DateProperty, new Binding(property.Name, source: source));
+
+ return new VerticalStackLayout
+ {
+ new Label { Text = property.Name },
+ editor
+ };
+ }
+
+ public static View EditorForTimeSpan(PropertyInfo property, object source)
+ {
+ var editor = new TimePicker();
+ editor.SetBinding(TimePicker.TimeProperty, new Binding(property.Name, source: source));
+
+ return new VerticalStackLayout
+ {
+ new Label { Text = property.Name },
+ editor
+ };
+ }
+}
diff --git a/src/UraniumUI/Extensions/NullableExtensions.cs b/src/UraniumUI/Extensions/NullableExtensions.cs
new file mode 100644
index 00000000..5ef9a274
--- /dev/null
+++ b/src/UraniumUI/Extensions/NullableExtensions.cs
@@ -0,0 +1,35 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace UraniumUI.Extensions;
+public static class NullableExtensions
+{
+ public static bool IsNullable(this Type type)
+ {
+ if (type is null)
+ {
+ return false;
+ }
+
+ return type.Name == typeof(Nullable<>).Name;
+ }
+
+ public static T? MakeNullable(this T value)
+ where T : struct
+ {
+ var genericNullableType = typeof(Nullable<>).MakeGenericType(typeof(T));
+ return Activator.CreateInstance(genericNullableType, args: new object[] { value }) as T?;
+ }
+
+ public static Type AsNonNullable(this Type type)
+ {
+ if (type.IsNullable())
+ {
+ return type.GetGenericArguments()[0];
+ }
+ return type;
+ }
+}
diff --git a/src/UraniumUI/MauiProgramExtensions.cs b/src/UraniumUI/MauiProgramExtensions.cs
index b3108fd2..c394e1b5 100644
--- a/src/UraniumUI/MauiProgramExtensions.cs
+++ b/src/UraniumUI/MauiProgramExtensions.cs
@@ -3,6 +3,7 @@
using UraniumUI.Controls;
using UraniumUI.Dialogs;
using UraniumUI.Handlers;
+using UraniumUI.Options;
using UraniumUI.Views;
namespace UraniumUI;
@@ -21,6 +22,9 @@ public static MauiAppBuilder UseUraniumUI(this MauiAppBuilder builder)
});
builder.Services.AddTransient();
+
+ builder.ConfigureAutoFormViewDefaults();
+
return builder;
}
diff --git a/src/UraniumUI/Options/AutoFormViewOptions.cs b/src/UraniumUI/Options/AutoFormViewOptions.cs
new file mode 100644
index 00000000..13bdb8f9
--- /dev/null
+++ b/src/UraniumUI/Options/AutoFormViewOptions.cs
@@ -0,0 +1,28 @@
+using InputKit.Shared.Validations;
+using System.ComponentModel.DataAnnotations;
+using System.Reflection;
+
+namespace UraniumUI.Options;
+public sealed class AutoFormViewOptions
+{
+ public Dictionary EditorMapping { get; } = new();
+
+ public delegate View EditorForType(PropertyInfo property, object source);
+
+ public List> PostEditorActions { get; } = new();
+
+ public Func PropertyNameFactory { get; set; } = DefaultPropertyNameFactory;
+
+ public Func> ValidationFactory { get; set; }
+
+ private static string DefaultPropertyNameFactory(PropertyInfo property)
+ {
+ var attribute = property.GetCustomAttribute();
+ if (attribute != null)
+ {
+ return attribute.Name;
+ }
+
+ return property.Name;
+ }
+}
diff --git a/src/UraniumUI/Options/AutoFormViewOptionsExtensions.cs b/src/UraniumUI/Options/AutoFormViewOptionsExtensions.cs
new file mode 100644
index 00000000..c021795b
--- /dev/null
+++ b/src/UraniumUI/Options/AutoFormViewOptionsExtensions.cs
@@ -0,0 +1,23 @@
+using UraniumUI.Controls;
+
+namespace UraniumUI.Options;
+public static class AutoFormViewOptionsExtensions
+{
+ public static MauiAppBuilder ConfigureAutoFormViewDefaults(this MauiAppBuilder builder)
+ {
+ builder.Services.Configure(options =>
+ {
+ options.EditorMapping[typeof(string)] = AutoFormView.EditorForString;
+ options.EditorMapping[typeof(int)] = AutoFormView.EditorForNumeric;
+ options.EditorMapping[typeof(double)] = AutoFormView.EditorForNumeric;
+ options.EditorMapping[typeof(float)] = AutoFormView.EditorForNumeric;
+ options.EditorMapping[typeof(bool)] = AutoFormView.EditorForBoolean;
+ options.EditorMapping[typeof(Keyboard)] = AutoFormView.EditorForKeyboard;
+ options.EditorMapping[typeof(Enum)] = AutoFormView.EditorForEnum;
+ options.EditorMapping[typeof(DateTime)] = AutoFormView.EditorForDateTime;
+ options.EditorMapping[typeof(TimeSpan)] = AutoFormView.EditorForTimeSpan;
+ });
+
+ return builder;
+ }
+}
diff --git a/src/UraniumUI/UraniumUI.csproj b/src/UraniumUI/UraniumUI.csproj
index 33ddc8c9..431bb7fa 100644
--- a/src/UraniumUI/UraniumUI.csproj
+++ b/src/UraniumUI/UraniumUI.csproj
@@ -25,7 +25,7 @@
-
+