edongashi / WpfMaterialForms

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

Dynamic forms #7

Closed hashitha closed 7 years ago

hashitha commented 7 years ago

Is there any way we can create dynamic forms? maybe by using a json

BrettKinny commented 7 years ago

*Edit - wrong repo.

hashitha commented 7 years ago

@bkinny isn't that this same code? I have checked this project out and I don't think you can do what I am asking for.

BrettKinny commented 7 years ago

Sorry, I got my repos mixed up.

edongashi commented 7 years ago

Hey @hashitha. I haven't merged to master yet but that's the current aim for this project. Forms will be generated by classes and customized using attributes. Class properties compile to low level objects - FormElements. You can create true runtime dynamic forms using those FormElements but it will be complex so there is a builder layer planned for convenience. I find that classes were the best approach because of mvvm patterns and elements bind automatically to properties.

edongashi commented 7 years ago

Here's an example of what's currently possible:

[Title("Log in to continue")]
[Action("cancel", "CANCEL")]
[Action("login", "LOG IN", Validates = true)]
public class Login
{
    [Field(Icon = "Account")]
    [Value(Must.NotBeEmpty, Message = "Enter your username.")]
    public string Username { get; set; }

    [Field(Icon = "Key")]
    public string Password { get; set; }

    public bool RememberMe { get; set; }
}
<md:Card>
    <DynamicForm Margin="12" Model={Binding LoginInstance} />
</md:Card>

login

This implies that you must have a class at compile time. For a serializable solution JSON may be too verbose, I'm thinking of an XML-like approach:

<form name="Login">
    <title>Log in to continue</title>
    <string name="Username" icon="Account">
        <validator type="MustNotBeEmpty" message="Enter your username." />
    </string>
    <password name="Password" icon="Account" />
    <bool name="RememberMe" />
    <action name="cancel" content="CANCEL" />
    <action name="login" content="LOGIN" validates="true" />
</form>

The generated FormElements for these two descriptions may be the same, though in the second case we're losing strong typing, so these properties will be assigned to a dynamic object.

Any thoughts on this idea?

hashitha commented 7 years ago

Hi @EdonGashi,

The second XML approach would work better for us. I think dynamic object would be fine because we just need the final results as a key-value pair to generate the pdf. Also having a separate class for each form won't work in this case.

I am looking for something like typeform.com for WPF. Basically, the end user will create the form and we would send the data to a pdf merge service like https://www.webmerge.me/ and get the final pdf.

edongashi commented 7 years ago

I need a quick refresh on linq 2 xml and I'll implement a parser. Shouldn't take long.

hashitha commented 7 years ago

No worries, once you are done I will test it out.

edongashi commented 7 years ago

I think I have a working prototype. I added the following example to the demo. This is equivalent to the User.cs class:

<form>
    <title>Create account</title>
    <heading>Personal details</heading>
    <input type="string" name="FirstName"
           label="First name"
           tooltip="Enter your first name here."
           icon="pencil">
        <validate must="NotBeEmpty" />
    </input>
    <input type="string" name="LastName"
           label="Last name" icon="empty"
           tooltip="Enter your last name here." />
    <input type="datetime?"
           name="DateOfBirth"
           label="Date of birth"
           icon="calendar"
           conversionError="Invalid date string.">
        <validate must="NotBeEmpty" />
        <validate must="BeLessThan" value="2020-01-01">
            You said you are born in the year {Value:yyyy}. Are you really from the future?
        </validate>
    </input>
    <heading>Account details</heading>
    <input type="string" name="Username"
           label="Username" icon="account" >
        <validate must="MatchPattern" value="^[a-zA-Z][a-zA-Z0-9]*$"
                  message="'{Value}' is not a valid username." />
    </input>
    <input type="string" name="Password"
           label="Password" icon="key">
        <validate converter="Length" must="BeGreaterThanOrEqualTo" value="6">
            Your password has {Value|Length} characters, which is less than the required {Argument}.
        </validate>
    </input>
    <input type="string" name="PasswordConfirm"
           label="Confirm password" icon="empty">
        <validate must="BeEqualTo" value="{Binding Password}"
                  onValueChanged="ClearErrors"
                  message="The entered passwords do not match." />
    </input>
    <br />
    <heading icon="checkall">Review entered information</heading>
    <text>Name: {Binding FirstName} {Binding LastName}</text>
    <text>Date of birth: {Binding DateOfBirth:yyyy-MM-dd}</text>
    <text>Username: {Binding Username}</text>
    <br />
    <heading>License agreement</heading>
    <text>By signing up, you agree to our terms of use, privacy policy, and cookie policy.</text>
    <input type="bool" name="Agree" label="Agree to license" defaultValue="false">
        <validate must="BeTrue">You must accept the license agreement.</validate>
    </input>
    <action name="reset" content="RESET" icon="close" resets="true" />
    <action name="submit" content="SUBMIT" icon="check" validates="true" />
</form>

user_md

Notes

user_metro

or vanilla wpf:

user_wpf

Let me know what you think. Thanks!

hashitha commented 7 years ago

I think this will work well. Can I give this a go?

edongashi commented 7 years ago

Sure, it's pushed to master. Consider this syntax as 95% final. I'll try to stay backwards compatible with future updates. I'll add a <row> element to support grouping, some more controls like <select> <slider> <progress> <image>, nothing too complex though because we don't want to reinvent xaml. There's also a MD stepper proposal which would fit so well with this model - separate your controls in <step>s and it'd feel quite close to the typeform you linked.

If you need any of the above controls drop an issue and I'll prioritize that.

hashitha commented 7 years ago

Thanks!

I agree, keep it simple. We only need the basic controls.

hashitha commented 7 years ago

I have tried the code and it all seems to work fine. To get the entered data out when the user clicks submit do I have to go through CompiledDefinition.FormRows ?

I want to get the following for each input data

  1. name of field
  2. field type e.g. string, int etc
  3. value
edongashi commented 7 years ago

DynamicForm.Value property holds the dynamic object. I wanted something like <DynamicForm Model="{Binding MyXmlDefinition}" Value="{Binding MyKeyValues, Mode=OneWayToSource}" />. Wpf doesnt allow the latter though since it's a readonly dependency property and you can't assign bindings to it. So I'm going to change it to a two way property which ignores source changes. It's a hack but it'll allow MVVM binding.

Meanwhile you can use: var dictionary = (IDictionary <string,object>)MyDynamicForm.Value;

This casts the expando to a key value map. Keys are prop names and values are current values. This should cover most cases as you can call value.GetType (). The exception is nulls where you won't be able to determine the property type.

If you need to go deeper to "reflect" the form types we need to get the DataFormFields present in the current definition. They are stored internally in the form (need to make an api) or we can query the children of formrows for Elements of type DataFormField which hold the Key property and the PropertyType.

The DataFormField is the "blueprint" so it wont hold the current values. They are held in the .Value. One can be considered the "class" the other the "instance".

I'll write some documentation soon.

About actions - if your ViewModel implements IActionHandler it will receive callbacks from action clicks. If validates=""

edongashi commented 7 years ago

Misclicked cause of phone. If validates="true" callbacks may be suspended it model fails validation. It seems a good idea to pass model instance here too. Maybe I'll change that quickly?

edongashi commented 7 years ago

Added an object model parameter to IActionHandler. It's a breaking change but it's too convenient to have it there.

Edit: Also added DynamicForm.GetDataFields(), which returns a Dictionary<string, DataFormField> of the current form

hashitha commented 7 years ago

object model parameter works great for us. I can use something like this to generate the final JSON

 public void HandleAction(object model, string action, object parameter)
        {
            notificationService.Notify($"Action '{action}'");

            if (action.Equals("submit"))
            {
                var dictionary = (IDictionary<string, object>)model;

                foreach (KeyValuePair<string, object> kvp in dictionary)
                {
                    Console.WriteLine("Key = {0} ", kvp.Key);

                    Type t = kvp.Value.GetType();
                    if (t.Equals(typeof(String)))
                        Console.WriteLine("{0} is String.", kvp.Value);
                    else if (t.Equals(typeof(DateTime)))
                        Console.WriteLine("{0} is a DateTime.", kvp.Value);
                    else if (t.Equals(typeof(int)))
                        Console.WriteLine("{0} is a 32-bit integer.", kvp.Value);
                    else if (t.Equals(typeof(long)))
                        Console.WriteLine("{0} is a 32-bit integer.", kvp.Value);
                    else if (t.Equals(typeof(double)))
                        Console.WriteLine("{0} is a double-precision floating point.", kvp.Value);
                    else if (t.Equals(typeof(Boolean)))
                        Console.WriteLine("{0} is a Boolean.", kvp.Value);
                    else
                        Console.WriteLine("'{0}' is another data type.", kvp.Value);

                }
            }

        }
edongashi commented 7 years ago

With the c# 7 pattern matching it gets even better:

switch (kvp.Value)
{
    case int i:
        Console.WriteLine($"Integer {i}");
        break;
    case string s:
        Console.WriteLine($"String {s}");
        break;
    case long l:
        Console.WriteLine($"Long {l}");
        break;
        // other cases
    case null:
        Console.WriteLine("Field has no value. Cannot determine type.");
        break;
    default:
        Console.WriteLine("Other type.");
        break;
}

Note: I think JSON.NET allows you to serialize ExpandoObjects so you don't have to do it manually.

If not, you can do something like this:

Dictionary<string,object> clonedPairs = new Dictionary<string,object>(dictionary); // clones the original dict.

string json = JsonConvert.SerializeObject(clonedPairs);

This generates a JObject as in the following example: http://www.newtonsoft.com/json/help/html/SerializeDictionary.htm

hashitha commented 7 years ago

This is great! thanks for the info @EdonGashi