WPF-Forge/Forge.Forms

Check all binding

hashitha opened this issue · 11 comments

I have the following form with a lot of checkboxes. Is there any way to bind one checkbox to all the others. The reason for this is to quickly tick all the checkboxes instead of going one by one.

<form>
  <title>SECOND TRIMESTER ULTRASOUND</title>
  <heading>Patient details</heading>
  <row>
    <col>
      <text>Name: {ContextBinding FirstName} {ContextBinding LastName} ({ContextBinding GenderAsString})</text>
    </col>
    <col>
      <text>DOB: {ContextBinding DateOfBirth} ({ContextBinding AgeString})</text>
    </col>
  </row>
  <row>
    <col>
      <text>Patient ID: {ContextBinding PatientId}</text>
    </col>
    <col>
      <text>Accession: {ContextBinding PatientId}</text>
    </col>
  </row>
  <hr />
  <row>
    <col>
      <input
        type="string"
        name="EDD"
        label="EDD"></input>
    </col>
    <col>
      <input
        type="string"
        name="Weeks"
        label="Weeks"></input>
    </col>
    <col>
      <input
        type="string"
        name="Days"
        label="Days"></input>
    </col>
  </row>
  <title>Placenta</title>
  <row>
    <col>
      <row>
        <input
          type="bool"
          name="has-Anterior"
          label="Anterior"></input>
      </row>
      <row>
        <input
          type="bool"
          name="has-Posterior"
          label="Posterior"></input>
      </row>
      <row>
        <input
          type="bool"
          name="has-Fundal"
          label="Fundal"></input>
      </row>
      <row>
        <col>
          <input
            type="bool"
            name="has-Clear"
            label="Clear (&gt;2cm)"></input>
        </col>
        <col
          width="0.7">
          <input
            type="string"
            name="has-ClearLength"
            visible="{Binding has-Clear}"
            label="mm"></input>
        </col>
      </row>
      <row>
        <input
          type="bool"
          name="has-Low"
          label="Low"></input>
      </row>
      <row>
        <input
          type="bool"
          name="has-Covering"
          label="Covering"></input>
      </row>
      <row>
        <input
          type="bool"
          name="has-Liquor"
          label="Liquor"></input>
      </row>
      <row>
        <input
          type="string"
          name="CervicalLength"
          label="Cervical Length (mm)"></input>
      </row>
    </col>
    <col>
      <textarea
        name="PlacentaComments"
        label="Placenta comments" />
    </col>
  </row>
  <hr />
  <title>Growth Parameters</title>
  <row>
    <col>
      <row>
        <col>
          <input
            type="string"
            name="BPD"
            label="BPD (mm)"></input>
        </col>
        <col
          width="0.5">
          <text>=</text>
        </col>
      </row>
      <row>
        <col>
          <input
            type="string"
            name="HC"
            label="HC (mm)"></input>
        </col>
        <col
          width="0.5">
          <text>=</text>
        </col>
      </row>
      <row>
        <col>
          <input
            type="string"
            name="AC"
            label="AC (mm)"></input>
        </col>
        <col
          width="0.5">
          <text>=</text>
        </col>
      </row>
      <row>
        <col>
          <input
            type="string"
            name="FL"
            label="FL (mm)"></input>
        </col>
        <col
          width="0.5">
          <text>=</text>
        </col>
      </row>
      <row>
        <col>
          <input
            type="string"
            name="H2"
            label="H2 (mm)"></input>
        </col>
        <col
          width="0.5">
          <text>=</text>
        </col>
      </row>
      <row>
        <col>
          <input
            type="string"
            name="meanusage"
            label="Mean U/S Age"></input>
        </col>
        <col
          width="0.5">
          <text>=</text>
        </col>
      </row>
    </col>
    <col>
      <row>
        <col>
          <input
            type="string"
            name="bpd_weeks"
            label="Weeks"></input>
        </col>
        <col>
          <input
            type="string"
            name="bpd_days"
            label="Days"></input>
        </col>
      </row>
      <row>
        <col>
          <input
            type="string"
            name="hc_weeks"
            label="Weeks"></input>
        </col>
        <col>
          <input
            type="string"
            name="hc_days"
            label="Days"></input>
        </col>
      </row>
      <row>
        <col>
          <input
            type="string"
            name="ac_weeks"
            label="Weeks"></input>
        </col>
        <col>
          <input
            type="string"
            name="ac_days"
            label="Days"></input>
        </col>
      </row>
      <row>
        <col>
          <input
            type="string"
            name="fl_weeks"
            label="Weeks"></input>
        </col>
        <col>
          <input
            type="string"
            name="fl_days"
            label="Days"></input>
        </col>
      </row>
      <row>
        <col>
          <input
            type="string"
            name="h2_weeks"
            label="Weeks"></input>
        </col>
        <col>
          <input
            type="string"
            name="h2_days"
            label="Days"></input>
        </col>
      </row>
      <row>
        <col>
          <input
            type="string"
            name="mean_weeks"
            label="Weeks"></input>
        </col>
        <col>
          <input
            type="string"
            name="mean_days"
            label="Days"></input>
        </col>
      </row>
    </col>
    <col>
      <row>
        <col>
          <text>Heart rate:</text>
        </col>
        <col>
          <input
            type="string"
            name="Heart_Rate"
            label="bpm"></input>
        </col>
      </row>
      <row>
        <col>
          <text></text>
        </col>
        <col>
          <input
            type="string"
            name="BPDf"
            label=""></input>
        </col>
      </row>
      <row>
        <col>
          <text></text>
        </col>
        <col>
          <input
            type="string"
            name="BPDf"
            label=""></input>
        </col>
      </row>
    </col>
  </row>
  <hr />
  <title>Foetal anatomy</title>
  <row>
    <col
      width="1.1">
      <row>
        <col
          width="0.27">
          <heading>Head</heading>
        </col>
        <col>
          <input
            type="bool"
            name="has-Tick_All_Head"
            label=""></input>
        </col>
      </row>
      <row>
        <input
          type="bool"
          name="has-Faix"
          label="Faix"></input>
      </row>
      <row>
        <input
          type="bool"
          name="has-Cavum_Septum"
          label="Cavum Septum"></input>
      </row>
      <row>
        <input
          type="bool"
          name="has-Skull_Bones"
          label="Skull Bones"></input>
      </row>
      <row>
        <input
          type="bool"
          name="has-Choroid_Plexus"
          label="Choroid Plexus"></input>
      </row>
      <row>
        <col>
          <input
            type="bool"
            name="has-Ventricles"
            label="Ventricles"></input>
        </col>
        <col
          width="0.7">
          <input
            type="string"
            name="has-VentriclesLength"
            visible="{Binding has-Ventricles}"
            label="mm"></input>
        </col>
      </row>
      <row>
        <col>
          <input
            type="bool"
            name="has-Cerebellum"
            label="Cerebellum"></input>
        </col>
        <col
          width="0.7">
          <input
            type="string"
            name="has-CerebellumLength"
            visible="{Binding has-Cerebellum}"
            label="mm"></input>
        </col>
      </row>
      <row>
        <col>
          <input
            type="bool"
            name="has-Cisterna_Magna"
            label="Cisterna Magna"></input>
        </col>
        <col
          width="0.7">
          <input
            type="string"
            name="has-Cisterna_MagnaLength"
            visible="{Binding has-Cisterna_Magna}"
            label="mm"></input>
        </col>
      </row>
      <row>
        <col>
          <input
            type="bool"
            name="has-Nuchal_Fold"
            label="Nuchal Fold"></input>
        </col>
        <col
          width="0.7">
          <input
            type="string"
            name="has-Nuchal_FoldLength"
            visible="{Binding has-Nuchal_Fold}"
            label="mm"></input>
        </col>
      </row>
    </col>
    <col>
      <row>
        <heading
          icon="heart">Heart</heading>
      </row>
      <row>
        <input
          type="bool"
          name="has-Position"
          label="Position/Axis"></input>
      </row>
      <row>
        <input
          type="bool"
          name="has-Four_Chambers"
          label="Four Chambers"></input>
      </row>
      <row>
        <input
          type="bool"
          name="has-LVOT"
          label="LVOT"></input>
      </row>
      <row>
        <input
          type="bool"
          name="has-RVOT"
          label="RVOT"></input>
      </row>
      <row>
        <input
          type="bool"
          name="has-Aortic_Arch"
          label="Aortic Arch"></input>
      </row>
      <row>
        <input
          type="bool"
          name="has-Ductal_Arch"
          label="Ductal Arch"></input>
      </row>
    </col>
  </row>
  <hr />
  <br />
  <row>
    <col
      width="1.1">
      <row>
        <heading>Face</heading>
      </row>
      <row>
        <input
          type="bool"
          name="has-Orbits"
          label="Orbits"></input>
      </row>
      <row>
        <input
          type="bool"
          name="has-Lips_Nose"
          label="Lips/Nose"></input>
      </row>
      <row>
        <input
          type="bool"
          name="has-Jaw"
          label="Jaw"></input>
      </row>
      <row>
        <input
          type="bool"
          name="has-Profile"
          label="Profile"></input>
      </row>
    </col>
    <col>
      <row>
        <heading>Abdomen</heading>
      </row>
      <row>
        <input
          type="bool"
          name="has-Diaphragm"
          label="Diaphragm"></input>
      </row>
      <row>
        <input
          type="bool"
          name="has-Stomach"
          label="Stomach"></input>
      </row>
      <row>
        <input
          type="bool"
          name="has-LeftKidney"
          label="Left Kidney"></input>
      </row>
      <row>
        <input
          type="bool"
          name="has-RightKidney"
          label="Right Kidney"></input>
      </row>
      <row>
        <input
          type="bool"
          name="has-Bladder"
          label="Bladder"></input>
      </row>
      <row>
        <input
          type="bool"
          name="has-Abdo_Wall"
          label="Abdo Wall"></input>
      </row>
    </col>
  </row>
  <hr />
  <br />
  <row>
    <col
      width="1.1">
      <row>
        <heading>Spine</heading>
      </row>
      <row>
        <input
          type="bool"
          name="has-Coronal"
          label="Coronal"></input>
      </row>
      <row>
        <input
          type="bool"
          name="has-Sagital"
          label="Sagital"></input>
      </row>
      <row>
        <input
          type="bool"
          name="has-Axial"
          label="Axial"></input>
      </row>
      <row>
        <input
          type="bool"
          name="has-Skinline"
          label="Skinline"></input>
      </row>
    </col>
    <col>
      <row>
        <heading>Limbs</heading>
      </row>
      <row>
        <input
          type="bool"
          name="has-12_Long_Bones"
          label="12 Long Bones"></input>
      </row>
      <row>
        <input
          type="bool"
          name="has-Hands"
          label="Hands"></input>
      </row>
      <row>
        <input
          type="bool"
          name="has-Feet"
          label="Feet"></input>
      </row>
      <row>
        <input
          type="bool"
          name="has-Position_of_joins"
          label="Position of joins"></input>
      </row>
    </col>
  </row>
  <hr />
  <br />
  <row>
    <col
      width="1.1">
      <row>
        <heading>Umbilical cord</heading>
      </row>
      <row>
        <input
          type="bool"
          name="has-Insertion"
          label="Insertion"></input>
      </row>
      <row>
        <input
          type="bool"
          name="has-3VC"
          label="3VC"></input>
      </row>
    </col>
    <col></col>
  </row>
  <hr />
  <row>
    <input
      type="string"
      name="Sonographer"
      label="Sonographer"></input>
  </row>
  <row>
    <action
      name="reset"
      content="RESET"
      icon="close"
      resets="true" />
    <action
      name="submit"
      content="SUBMIT"
      icon="check"
      validates="true" />
  </row>
</form>

See this images. If I tick the "Head" then everything in the head category should tick
image

This is quite useful, but I cannot think of a straightforward way to do this declaratively.

The dynamic object that holds the values implements INotifyPropertyChanged, which you could use to listen to property changes, and do your side effects when a specific value changes.

((INotifyPropertyChanged)Form.Value).PropertyChanged += (s, e) => {
  if (e.PropertyName == "Head") {
    dynamic model = s;
    if (model .Head == true) {
      model.Property1 = true;
      model.Property2 = true;
      model.Property3 = true;
    } // or use IDictionary<string,object> if you don't like dynamic
  }
};

One could create some abstractions around this if we could have a way to attach metadata to properties.
For example:

<input type="bool" name="Head" data-sets="Property1,Property2,Property3" />

then we could do (pseudocode):

((INotifyPropertyChanged)Form.Value).PropertyChanged += (s, e) => {
  var metadata = GetMetadataSomehow(e.PropertyName);
  if (metadata.TryGetValue("data-sets", out var sets)) {
    var props = sets.Split(",");
    var dict = (IDictionary<string, object>)s;
    foreach (var prop in props) {
      dict[prop] = true;
    }
  }
};

if we could pull that metadata from the definition you could implement new behaviors for your form declaratively.

I'll think of other approaches meanwhile and will keep you updated. But I like this metadata concept so it will likely be implemented.

If you can implement <input type="bool" data-sets="Property1,Property2,Property3" /> this would work I think.

Maybe another way would be to bind the value of all the checkboxes to the main checkbox. Do you think this will work?

In a wpf binding there is the binding source and binding target. Binding source is always the model property, and we cannot change that, otherwise we could no longer read values from the model (when serializing to json etc).

In a WPF class form side effects could be done easily:

public bool CheckAll {
  get {
    return checkAll; // or maybe compute tis
  }
  set {
    checkAll = true;
    OnPropertyChanged();
    if (value) {
      Property1 = true;
      Property2 = true;
      Property3 = true;
    }
  }
}

In XML we have no way of executing side effects, unless we allow embedding javascript or cs roslyn scripts inside the form. But considering your form is hosted in a controlled and predictable environment, you can create underlying infrastructure for features you need, such as those special attributes for different behaviors.

I am working on this currently. Syntax will be like this: attr-myvalue="something", and then you can query metadata from formDefinition.GetAttributes("MyField").TryGetValue("myvalue", out value)

Feature is done, usages:

XML:

<form meta-myformattr="myvalue1">
  <input name="FirstName" meta-myfieldattr="myvalue2" />
</form>

Accessing data:

string formAttr = myDefinition.Metadata["MyFormAttr"]; // dictionary is case-insensitive
string fieldAttr = myDefinition.GetDataFIeld("FirstName").Metadata["myfieldattr"];

Class/prop decoration:

[Meta("myformattr", "myvalue1")]
public class Form {
  [Meta("myfieldattr", "myvalue2")]
  public string FirstName { get; set; }
}

Accessing metadata is same from both xml and code-first.

Unrelated note, the row wrappers here are not necessary:

    <col>
      <row>
        <heading>Limbs</heading>
      </row>
      <row>
        <input
          type="bool"
          name="has-12_Long_Bones"
          label="12 Long Bones"></input>
      </row>
      <row>
        <input
          type="bool"
          name="has-Hands"
          label="Hands"></input>
      </row>
      <row>
        <input
          type="bool"
          name="has-Feet"
          label="Feet"></input>
      </row>
      <row>
        <input
          type="bool"
          name="has-Position_of_joins"
          label="Position of joins"></input>
      </row>
    </col>

you can simplify it like this:

    <col>
        <heading>Limbs</heading>
        <input
          type="bool"
          name="has-12_Long_Bones"
          label="12 Long Bones"></input>
        <input
          type="bool"
          name="has-Hands"
          label="Hands"></input>
        <input
          type="bool"
          name="has-Feet"
          label="Feet"></input>
        <input
          type="bool"
          name="has-Position_of_joins"
          label="Position of joins"></input>
    </col>

This will save some unnecessary ActionPanels from being created.

@edongashi sorry I don't understand how to use this. Can you please give an example. I only need one checkbox ticking all other checkboxes

I have a great idea, i will update here when i get time to do that (within the next 12 hours).

A plugin system named "behaviors", where you can inject various handlers, in this case a property changed behavior. You'll see soon.

Behaviors feature is done, well mostly done:

  • DynamicForm class supports following methods: DynamicForm.AddBehavior(object behavior) and DynamicForm.RemoveBehavior(object behavior)
  • As you can see above, behaviors are only applied at class/global level
  • Instance level (per form control) behaviors can be done if they are needed
  • A behavior to be useful should implement IPropertyChangedBehavior to do side effects on property changed, IModelChangedBehavior when a new form is mounted, or both.
  • More behaviors types will be added in time.
  • This is basically a glorified observer pattern, but it's easier for the consumer because you don't have to worry about event leaks from manually hooking into inpcs.

For your task you need this behavior:

class CheckAllBehavior : IPropertyChangedBehavior
{
    public void PropertyChanged(IPropertyChangedContext context)
    {
        if (!(context.Model is IDictionary<string, object> model))
        {
            return;
        }

        // Select all only on checked.
        if (model[context.PropertyName] is true)
        {
            var meta = context
                .FormDefinition
                .GetElements()
                .FirstOrDefault(elem => elem is DataFormField d && d.Key == context.PropertyName)
                ?.Metadata;
            if (meta == null || !meta.TryGetValue("sets", out var value))
            {
                return;
            }

            var props = value?.Split(new[] { ' ', ',' }, StringSplitOptions.RemoveEmptyEntries);
            if (props != null)
            {
                foreach (var prop in props)
                {
                    model[prop] = true;
                }
            }
        }
    }
}

This is already done in the demo (this specific solution will not be supported from the library). To see it in action write the following form:

<form>
  <input type="bool" label="All" name="all" meta-sets="first,second,third" />
  <input type="bool" label="First" name="first" />
  <input type="bool" label="Second" name="second" />
  <input type="bool" label="Third" name="third" />
</form>

Copy the class above into your project and modify it as you see fit. Don't forget to inject it somewhere in startup/init code.

Also I forgot, be careful when setting properties and reacting to their changes, as you might cause an infinite loop.

Did you get this working? I had updated the behavior to be much better. You can find it in the demo here. It handles mixed state and two way events (parent-child).

Just run the demo and type this:

<form>
  <input type="bool" label="All" name="all" meta-sets="first,second,third" />
  <input type="bool" label="First" name="first" />
  <input type="bool" label="Second" name="second" />
  <input type="bool" label="Third" name="third" />
</form>

@hashitha also what could be improved here: when serializing, you can check if the field is just an "all" setter, which you can ignore when serializing json.

if (field.Metadata.ContainsKey("sets")) {
  // don't serialize this
  continue;
}

because otherwise we get this:

{
  "all": true,
  "first": true,
  "second": true,
  "third": true
}

depending if you need the "all" flag in your json or not.