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 @@ - +