Closed hashitha closed 7 years ago
*Edit - wrong repo.
@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.
Sorry, I got my repos mixed up.
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.
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>
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?
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.
I need a quick refresh on linq 2 xml and I'll implement a parser. Shouldn't take long.
No worries, once you are done I will test it out.
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>
Notes
name
s of input
tags. ModelState provides utillities such as validation and resetting to default values. <DynamicForm>
provides three properties: Model - the form definition source (class instance, class type, FormDefinition, primitive), Value - model instance (class instance=itself, class type=new class(), FormDefinition=ExpandoObject, primitive=itself), Context - where ContextBinding gets its data from, defaults to DataContext of the control.{
and }
you need to escape with double braces or an @
symbol for verbatim strings: message="@literal string {not binding}"
{Binding FirstName}
{Property FirstName}
(one time binding), {ContextBinding FirstName}
(binding to datacontext property instead of model property), {ContextProperty FirstName}
{StaticResource FormTitle}
{DynamicResource FormTitle}
(suitable for localization), and contextual resources e.g. {Value} and {Argument} in validators.or vanilla wpf:
Let me know what you think. Thanks!
I think this will work well. Can I give this a go?
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.
Thanks!
I agree, keep it simple. We only need the basic controls.
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
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=""
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?
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
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);
}
}
}
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
This is great! thanks for the info @EdonGashi
Is there any way we can create dynamic forms? maybe by using a json