WPF-Forge/Forge.Forms

Dynamic form setting initial values

hashitha opened this issue · 17 comments

Once we load the xml string and build the form can we load some initial values. e.g. Patient name and date of birth in this case.

CompiledDefinition = FormBuilder.Default.GetDefinition(xmlString);
Maybe the returned CompiledDefinition can have the ExpandoObject which we can modify and call something like CompiledDefinition.Reload()

<form>
  <title>ULTRASOUND WORKSHEET ABDOMEN</title>
  <heading>Patient details</heading>
  <row>
    <col width="2">
 
    
        <input type="string" name="FirstName" label="Patient name" tooltip="Enter your name here."  >
      <validate must="NotBeEmpty" />
   </input>
  
    </col>
    <col>
   
        <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>
 
    </col>
  </row>
  <row>
    <col width="2">
     <input type="datetime?" name="AppointmentDate" label="Date" icon="calendar" conversionError="Invalid date string." /> 
    </col>
    <col> 
         <input type="string" name="Age" label="Age" > 
   </input> 
    </col>
  </row>

 <heading>Findings</heading>

  <row>
        <input type="string" name="RightKidney" label="Right Kidney" ></input>
  </row>
  <row>
    <input type="string" name="LeftKidney" label="Left Kidney" ></input>
  </row>
  <row>
    <col>
      <row>
        <col>
          <text>Bladder</text>
        </col>
        <col>
          <input type="string" name="BladderVol" label="Bladder Vol (mm)" ></input>
        </col>
      </row>
      <row>
        <col>
          <text>Post Mkt.</text>
        </col>
        <col>
          <row>
            <text>VOL</text>
            <input />
          </row>
        </col>
      </row>
      <row>
        <col>
          <text>Prostate:</text>
        </col>
        <col>
          <row>
            <text>VOL</text>
            <input />
          </row>
        </col>
      </row>
      <row>
        <col>
          <text>Ureteric Jets:</text>
        </col>
        <col>
          <input />
        </col>
      </row>
      <br />
      <heading>Female</heading>
      <row>
        <col>
          <text>Uterus</text>
        </col>
        <col>
          <input />
        </col>
      </row>
      <row>
        <col>
          <text>Endromet.</text>
        </col>
        <col>
          <input />
        </col>
      </row>
      <row>
        <col>
          <text>Rt. Ovary</text>
        </col>
        <col>
          <input />
        </col>
      </row>
      <row>
        <col>
          <text>Left Ovary</text>
        </col>
        <col>
          <input />
        </col>
      </row>
    </col>
    <col>
      <img src="https://upload.wikimedia.org/wikipedia/commons/thumb/a/ab/KidneyStructures_PioM.svg/590px-KidneyStructures_PioM.svg.png" />
    </col>
  </row>
  <row>
    <text>Comments</text>
    <input />
  </row>
</form>

You mean for example have the name field prepopulated or something like that?

I'll write some possibilities to see if either of them can help you:

  • When an action has resets="true"it resets fields to their defaultValue. Fields need to have a name if you want to reset them, otherwise we have no way of accessing the data.
<action name="reset" content="RESET" icon="close" resets="true" />
  • Inputs can have a defaultValue, for example <input name="FirstName" defaultValue="Patient". When the model resets the FirstName textbox will have the value Patient.

  • defaultValue can be dynamic if you need to fetch the name from a database or somewhere. For example <input name="FirstName" defaultValue="{ContextBinding PatientName}" />`. Whenever a value reset is requested the value is taken from property PatientName of the ViewModel (Form.Context property, which defaults to DataContext). For more info about bindings refer to https://wpf-forge.github.io/Forge.Forms/guides/dynamic-resources

  • You can add an indexer in your viewmodel if you want dynamic behavior {ContextBinding [Username]}, what this does is it calls Context["Username"].

  • If you have access to the expando from code, you can call ModelState.Reset(myModel), which does the same thing as clicking a button with resets="true". If you want to reset only some properties then you can pass those parameters to an overload of that method. Note: the form must be currently mounted in order for ModelState utilities to work.

  • A FormDefinition is a "class", which means that it contains information how to construct a model, but does not contain the model as a singleton. Once you create a definition, it can be used multiple times to create the same form with new models. In your form you can think of the definition as a class PatientForm, and only when you mount the form the model becomes model = new PatientForm().

  • You can access the model inside action handlers with actionContext.Model. If you are not inside an actionhandler you can access it by referencing the DynamicForm control MyForm.Value. MyForm.Model in this case will return your compiled definition because that's the "model" used to construct the form. Sometimes Model and Value are the same but Value will always contain the constructed object to which the form is bound to. If you have a single main form you can easily create an abstraction something like interface IModelLocator { object GetModel(); } in order to prevent viewmodel code from being polluted with control references.

I pushed an update that will be helpful. You can now send a parameter to GetDefinition which specifies if you want to freeze the definition or not after it's built.

var definition = FormBuilder.Default.GetDefinition(xmlString, freeze: false);

// Here we can manipulate the definition to our liking.
var firstNameElement = (DataFormField)definition.GetElements().FirstOrDefault(e => e is DataFormField d && d.Key == "FirstName");
if (firstNameElement != null)
{
    firstNameElement.DefaultValue = new LiteralValue("FirstName default value");
}

// By passing false, we need to manually freeze the definition.
// Without this the form won't work.
definition.FreezeAll();

// After freeze you should not modify the definition.

I will try the send a parameter to GetDefinition and <input name="FirstName" defaultValue="{ContextBinding PatientName}" /> and get back to you

I think you have enough tools without relying on manipulating the form elements manually, as that might cause bugs. For example I modify my form definition to have a default value "Patient 123". but then I can't reuse my form because you have to construct a new form for "Patient 456".

I suggest you provide auxiliary data from your viewmodel. That's why it's called (data)context (on which the form is hosted).

If you don't know what data you need at compile time, you can create an indexer or even make your VM dynamic by extending DynamicObject and overriding TryGetMember.

Indexer example:

class MyViewModel : ViewModel {
  // This can get data from DB or anywhere.
  public object this[string name] => GetPropertySomehow(name, CurrentPatientId);
}

in XML:

<input defaultValue="{ContextBinding [FirstName]" />

this sends FirstName as string name in the indexer

On second thought, there is the possibility of injecting an IValueProvider inside default value of dataformfields, so you are not restricted to putting a LiteralValue. Maybe you can provide a custom FirstNameValueProvider etc. on default value.

Almost all the dynamic forms that we load will share the same fields such as patient name, dob etc which we need to populate from the database to minimise data entry.

So I tried this code and it works well for us

var definition = FormBuilder.Default.GetDefinition(dynamicFormContent, freeze: false);

// Here we can manipulate the definition to our liking.
var firstNameElement = (DataFormField)definition.GetElements().FirstOrDefault(e => e is DataFormField d && d.Key == "FirstName");
if (firstNameElement != null)
{
    firstNameElement.DefaultValue = new LiteralValue("FirstName default value");
}

// By passing false, we need to manually freeze the definition.
// Without this the form won't work.
definition.FreezeAll();

CompiledDefinition = definition;
// After freeze you should not modify the definition.
 

Does this method work with all types? I just tried this with the field and it doesn't seem to work.

<form>
<row>
<text name="FirstName">Name:  {Binding FirstName}</text>
<text name="ScanDate" >Scan Date: </text>
</row>
<row>
<text name="Age" >Age: {Binding Age}</text>
<text name="DateOfBirth" >Date of Birth: {Binding DateOfBirth}</text>
</row>
<hr />
</form>

Text is readonly, it does not support names. The binding auto updates if firstname changes. Does it not?

I tried this with no luck. Do I need to set datacontext or something like that

        private string firstName;
        public string FirstName
        {
            get
            {
                return firstName;
            }
            set
            {
                firstName = value;
                RaisePropertyChanged("FirstName");
            }
        }

Can you try it like this:

<DynamicForm Model={Binding MyXmlDefinition} Context={Binding MyDataProvider} />

where MyDataProvider is the object that has FirstName.

Also note: {Binding} binds to current model instance while {ContextBinding} binds to form context

Just tried this and couln't get it to work

image

Can you make sure that you are using {ContextBinding FirstName} instead of {Binding FirstName}?

Excellent! {ContextBinding FirstName} works

Great! I'm working on something cool with resources, it will be a new way to inject data into the view with fewer complications.

That is great. I will be creating more forms and do some more testing this week. Then I can give some sample forms to put on the wiki as examples.

JSON can now be viewed in XML demo route:

image

This will make life easier to inspect and debug the model values.

About the resources, I did some experiments and ended up with something like the context again... I think it's good as it is. We can provide utilities and guidelines for easier usage of that mechanism.

The context works fine. Will let you know if I run into any issues