edongashi / WpfMaterialForms

Dynamically generated forms and dialogs in WPF
MIT License
51 stars 14 forks source link

Expose rows for custom controls #12

Closed redbaty closed 6 years ago

redbaty commented 6 years ago

It would be nice to be able to manually add some custom controls, like a combobox.

edongashi commented 6 years ago

It is possible to register custom hooks in FormBuilder. They will get called for each property, where you can decide if you want to return something custom, or null if you want to pass to other builders.

There are two types of builders PropertyBuilders and TypeBuilders. PropertyBuilders are queried first to see if the property has any attribute that modifies the generated field. If no such builder takes responsibility then TypeBuilders are checked, which return fields for types like string and boolean.

The idea is that you can hook builders when you encounter an attribute such as SelectFrom in PropertyBuilders, while you can hook controls for custom types in TypeBuilders.

A SelectFromBuilder is implemented, but the control is not yet implemented. I think I can do that quickly.

The syntax is as follows:

[SelectFrom(new string[] { "Designer", "Engineer", "Manager" })]
public string Occupation { get; set; }

or

[SelectFrom("{ContextBinding Occupations}")] // Gets the list from datacontext
public string Occupation { get; set; }

or

[SelectFrom("{Binding Occupations}", SelectionType = SelectionType.RadioButtons]
public string Occupation { get; set; }

public string[] Occupations => new string[] { "Designer", "Engineer", "Manager" };

etc.. check SelectFromAttribute.cs for attributes. I will tag you when I get the time to implement the control.

redbaty commented 6 years ago

All right then! Anyway I'd like to congratulate you for your work so far, this is really amazing and I'm really surprised this is not being talked about in the "WPF-UWP" community, keep up the good work! 😄

I really wish I could help, but I am not that familiar with the MVMM and this project seems a bit over-complicated with the Old stuff, if you could do a Diagram showing how stuff are made it would be pretty neat.

edongashi commented 6 years ago

Thanks! :smile:

I agree that without proper documentation it can be a bit complex to dig into the internals. Let me try to write a brief explanation, which I can copy to a doc file later.

The MaterialForms project has 4 folders:

Old folder and Resources folder - These contain the old version of the library. The API was stiff and hardcoded in so many places, which caused a lot of issues like no localization, no theming, no XAML support, hosted within predefined containers (dialogs and windows) and so many more. So basically ignore these folders, as they are there for backwards compatibility,


The hot new stuff is located in Wpf and Themes.

Themes contains XAML resource dictionaries for 3 form styles: WPF, Metro and Material. So for example when I want to add a combobox I need to consider all 3 styles for uniform support.

Form building and logic is located in Wpf folder. The philosophy of creating forms has shifted from declaring a bunch of Schemas to rich classes and annotations. I thought this approach was true to MVVM, plus you could reuse your domain objects by adding unobtrusive annotations.

Forms are built as follows: A FormBuilder takes a type of instance of whatever and returns a IFormDefinition. This IFormDefinition is the blueprint for generating forms.

<DynamicForm Model="{Binding Model}"> will get some object or type in Model prop and display a MVVM style bound form for that object. You can specify a FormBuilder property or FormBuilder.Default is used by default.

Form definitions can contain FormElements. They can be anything, including text, headers, icons, images, and most importantly editable fields: DataFormField.

As mentioned above FormBuilder is designed to be modifiable even in runtime. You can register custom behavior for example if I do an Image annotation

[Image]
public string MyImage { get; set; }

I can hook a PropertyBuilder to check if the property contains an ImageAttribute, if yes, then we can intercept and create a custom control instead of the default text box. That control can then show a WPF Image with src="{FormBinding Value}".

Similarly, if we have a custom type for example a RoomReservation with DateTime Date and int NumberOfGuests

public RoomReservation ReservationDetails { get; set; }

We can hook a TypeBuilder for type RoomReservation so that we can return a custom control whenever this property type is encountered in a class.

FormBindings can be imagined as "delayed bindings", since you won't know in compile time what field you will be binding to. For example {FormBinding Value} would resolve to a {Binding MyImage} when the form definition is actually compiled.

Cool features resulting from the model approach:

ModelState.Reset(model); // reset model to default value, you can define custom defaultvalues in [Field(DefaultValue = "...")]
ModelState.Validate(model); // check if model satisfies validators
ModelState.UpdateFields(model); // refresh view without INotifyPropertyChanged

One issue requested XML support, and it was surprisingly easy to implement (like 2 hours of work) due to how well it's structured.

Another crazy idea that comes to mind is the possibility generating generic CRUD routes with EntityFramework's .Set<T>, display a DataGrid for read, and you can generate a dynamic form for Insert and Update. Maybe even a details page if you render all fields readonly.

I appreciate your issues that request features, because these give me a vision on what to focus for my limited time (I wish I could have been more active in this project).

edongashi commented 6 years ago

@redbaty I have added combobox support. Check out Selection and FoodSelection demos. There were quite a few cases to consider (may have bugs), but the feature has come out great!

redbaty commented 6 years ago

@EdonGashi Awesome dude! Really good job, will test it right now 👍

redbaty commented 6 years ago

Hey it would be nice to update the nuget package, but anyway I'm closing this for now, great work 😄

redbaty commented 6 years ago

Oh and don't know if it was intentional but it won't support .NET 4.5 on the preview package

edongashi commented 6 years ago

No clue about the versions... PRs with fixes are appreciated!

I thought I had appveyor setup to publish NuGet pre-releases. I'll check that quickly

redbaty commented 6 years ago

@EdonGashi I just checked it the target framework has been updated since the last stable nuget relase, but it's all right since 4.6 is supported on every platform 4.5 is supported in

redbaty commented 6 years ago

@EdonGashi Hey actually I've seen that it works on your demo but on my project it still won't work, I'm debugging it right now.

edongashi commented 6 years ago

I reconfigured publishing from AppVeyor. Could you check 2.0.0-ci20? Since we are in pre-release territory we can test things out

redbaty commented 6 years ago

Yeah still not luck, I'll post the code that I'm using so if you want too you can debug it too.

XAML

                <Button Content="Adicionar movimentação rápida"
                        Command="{x:Static materialDesign:DialogHost.OpenDialogCommand}"
                        Style="{DynamicResource MaterialDesignFlatButton}" Foreground="White">
                    <Button.CommandParameter>
                        <controls:DynamicForm Model="{Binding Selection}"></controls:DynamicForm>
                    </Button.CommandParameter>
                </Button>

Model

    class HomeModel
    {
        public FoodSelection Selection { get; set; } = new FoodSelection();
    }
     public class FoodSelection : INotifyPropertyChanged
    {
        private string firstFood = "Pizza";
        private string secondFood = "Steak";
        private string thirdFood = "Salad";
        private string yourFavoriteFood;

        [Field(DefaultValue = "Pizza")]
        [Value(Must.NotBeEmpty)]
        public string FirstFood
        {
            get => firstFood;
            set
            {
                firstFood = value;
                OnPropertyChanged();
            }
        }

        [Field(DefaultValue = "Steak")]
        [Value(Must.NotBeEmpty)]
        public string SecondFood
        {
            get => secondFood;
            set
            {
                secondFood = value;
                OnPropertyChanged();
            }
        }

        [Field(DefaultValue = "Salad")]
        [Value(Must.NotBeEmpty)]
        public string ThirdFood
        {
            get => thirdFood;
            set
            {
                thirdFood = value;
                OnPropertyChanged();
            }
        }

        [Text("You have selected {Binding YourFavoriteFood}", InsertAfter = true)]

        [SelectFrom(new[] { "{Binding FirstFood}, obviously.", "{Binding SecondFood} is best!", "I love {Binding ThirdFood}" })]
        public string YourFavoriteFood
        {
            get => yourFavoriteFood;
            set
            {
                yourFavoriteFood = value;
                OnPropertyChanged();
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;

        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }

Result image

edongashi commented 6 years ago

Reproduced, my guess is that a resource dictionary reference is missing somewhere. Shouldn't be too difficult to find.

quick note: comboboxes will work only with material theme atm

redbaty commented 6 years ago

@EdonGashi even if you have already found the solution please don't commit it yet, I'd like to help you out on this one

edongashi commented 6 years ago

Found it! That's no bug, I totally forgot that by default WPF theme is used. You can notice by the textbox labels :)

untitled

Add this reference for Material Theme

https://github.com/EdonGashi/WpfMaterialForms/blob/master/src/Demo/MaterialForms.Demo/App.xaml#L19

redbaty commented 6 years ago

@EdonGashi LOL, I was typing the comment! You beat me to it 😄

redbaty commented 6 years ago

@EdonGashi Hey you should work on documenting the more complex stuff like the IFieldBuilder, I'll send a PR documenting the new stuff but I think it would be best for you to document the more complex ones

edongashi commented 6 years ago

Alright, I will gradually document everything. Thanks!