Git Product home page Git Product logo

cognitive's Introduction

🚧 Work in progress 🚧 Please view the Wiki here

What's new? Release notes

  • 1.3.0 09/04/2024 - Enums for property name lookups. Added SLF4J, JUnit5, began unit tests.
  • 1.2.0 08/05/2024 - Validators support multiple validation messages.
  • 1.1.0 06/28/2024 - PropertyIdentifier type objects to reference properties.
  • 1.0.0 05/30/2024 - Initial Release. FXMLMvvmLoader, SimpleViewModel, ValidationViewModel, Validator(s).

Cognitive

A lightweight JavaFX (21+) forms framework based on the MVVM UI architecture pattern.

View Models maintain the state of a view (Form) and, in principle, should contain a controller's presentation logic. This MVVM allows the developer to test the presentation logic without having to wire up a JavaFX controller during runtime.

Quick Start

To use Cognitive in your project, download and install Java 21 JDK. The library depends on JavaFX 21+ To see the demo's code see Form demo

Gradle:

implementation 'org.carlfx:cognitive:1.3.0'

Maven:

<dependency>
    <groupId>org.carlfx</groupId>
    <artifactId>cognitive</artifactId>
    <version>1.3.0</version>
</dependency>

Project using Java Modules (JPMS) will want to do the following in the consuming module:

requires org.carlfx.cognitive;

// open for FXML loaders
opens com.mycompany.myproject.controller to javafx.fxml, org.carlfx.cognitive;

As you can see, the opens allow FXML and Controller code to inject view models and UI controls using reflection.

Introduction

Software developers creating form based applications will inevitably stumble across the single most important UI architectural design pattern Model View Controller or MVC in short. This concept has paved the way for many frameworks which provide a good (acceptable) separation of concerns.

However, there are drawbacks to the MVC pattern (especially in JavaFX). One main drawback is that the controller layer can be difficult (if not impossible) to test. It is especially concerning when you are JavaFX developer who has worked with FXML and controller classes.

The pattern will avoid coupling the UI components, model (data) and presentation logic within a controller class. Because UI components are available during runtime it difficult to test interactions (presentation logic) between the model layer and UI.

So, what is a solution?

You guessed it! The MVVM UI architecture pattern.

What is MVVM?

MVVM is an architectural pattern that isolates the business logic/back-end data(Presentation Logic) from the view (UI code). Its goal is to simplify user interface development. According to Wikipedia, the MVVM is a variation of Martin Fowler's perspective of the Presentation Model design pattern.

Next, let's see how to apply the MVVM UI pattern to an existing JavaFX Application

Converting JavaFX MVC to the MVVM UI pattern

Before I show you how to convert a JavaFX MVC structure, let's compare the differences between MVC and the MVVM UI architecture patterns.

MVC (Model View Controller)

Typically MVC is used in Java Swing or JavaFX UI form type apps.

JavaFX is MVC-based, where developers often follow the Supervising Controller Pattern. An excerpt describing the controller's ability to manipulate the view with more complex view logic:

"Supervising Controller uses a controller both to handle input response but also to manipulate the view to handle more complex view logic. It leaves simple view behavior to the declarative system, intervening only when effects are needed that are beyond what can be achieved declaratively."

In the case of JavaFX, the declarative part would be FXML & CSS (XML representing the View).

JavaFX can also bind (Properties API) UI controls and listeners to synchronize the model data. While it is convenient to do this inside a controller(Presenter) class, the code is tightly coupled regarding presentation logic, (complex) view logic, and (possibly) business logic, making code difficult to maintain, debug, and test.

It is difficult to test especially when many UI controls are realized (dependency injected) during runtime, such as @FXML annotated UI controls inside controller classes.

mvc-ui-pattern

Now, let's look at the MVVM pattern and how it differs.

MVVM (Model View ViewModel)

The MVVM UI pattern is a variation of the Presentation Model Pattern. Both MVVM and Presentation Model pattern both achieve the separation of the View and the Model. An excerpt from Martin Fowler's section on When to use it?

"Presentation Model allows you to write logic completely independent of the views used for display. You also do not need to rely on the view to store state."

What's important is that the ViewModel manages the state. Later, we'll look at how to bind data between the View and ViewModel to synchronize model data.

The following is how MVVM is normally depected:

mvvm-ui-pattern

As you will notice, the model does not update the view layer, which he main difference between MVVM and MVC.

The main advantage is testing presentation logic separately from the UI form and its associated JavaFX Controller class.

Now that you've seen the pros and cons between MVC and MVVM how do we conceptually convert a JavaFX MVC UI form into the MVVM pattern?

Hint: Refactor (pull out) presentation logic and business logic away from JavaFX controllers into ViewModels or Model (services).

Converting a JavaFX MVC Form UI using the MVVM pattern

Below is a conceptual way to think of JavaFX using the MVVM UI pattern.

Cognitive is an un-opinionated library that let's you to refactor things at your own pace. As you get comfortable, you'll notice that JavaFX controller classes contain less code where presentation logic is move off to the ViewModels.

javafx-mvvm-ui-pattern

As shown above, you can treat the JavaFX FXML & Controller class as the View, and the Model will remain the same. The only difference is that the ViewModels will contain much of the state of the UI form and presentation logic. The objective is to make the view very stupid.

Now that you know conceptually, let's look at a code example of an MVC-style controller with a save operation.

This is a typical controller without the use of a view model.

Below is an example of how a UI form is about to save data.

/**
 * User clicks on save button to save contact information.
 * @param ae event 
 */
@FXML
private void saveAction(ActionEvent ae) {
   // copy user's input
   String firstName = firstNameTextField.getText();
   String lastName = lastNameTextField.getText();
   
   // validate user's input
   // if valid write to database and reset ui.
   // db.write(new Person(firstName, lastName));

}

Now, let's look at how to use a view model in a controller class.

Using ValidationViewModels in a controller

Using view models, you can have presentation logic or business logic. When testing presentation logic, you can populate a view model with the correct values without modifying the UI. Remember, a view model does not contain any UI controls. Shown below is an example of using a view model.

@FXML
private void initialize() {
   firstNameTextField.textProperty().bidirectionalBind(personViewModel.getProperty(FIRST_NAME));
   lastNameTextField.textProperty().bidirectionalBind(personViewModel.getProperty(LAST_NAME));
}

@FXML
private void saveAction(ActionEvent ae) {
    personViewModel.save(); // validates
    if (personViewModel.hasErrorMsgs()) {
       // apply messages to badges or decorate control for fields or global messages.  
    } else {
       // view model get model values and has logic to persist data.
       String firstName = personViewModel.getValue(FIRST_NAME);
       String lastName = personViewModel.getValue(LAST_NAME);
       // personViewModel.writePerson(new Person(firstName, lastName)); 
    }
}

Above you can see there are 4 steps to using View Models:

  1. Binding - Bind JavaFX controls and their properties to the view model's properties (property value layer).
  2. Validation - Upon saveAction() method perform a view model's save() method (which calls the validate() method)
  3. Error Messages - Check if there are any error messages if so, these can be applied to controls for user feedback.
  4. Model Values - Once you have valid values (model value layer) the code calls to view model's .getValue() method to return raw values. NOTE: Think of a view model with two layers a Property Values and Model Values. ValidationViewModel`'s save Now that you see, much of the work uses view models instead of methods or UI code inside the controller class. The developer aims to remove state and presentation logic from the controller class.

Let's look at the two main implementations of the ViewModel interface SimpleViewModel and ValidationViewModel.

SimpleViewModel

Let's start by creating a SimpleViewModel with one property with a String such as a first name. The objective is To create a JavaFX text field and bind the value with a view model's property.

To bind properties do the following:

final String FIRST_NAME = "firstName";

// A text field
var firstNameTextField = new TextField();

// Create a 
var personVm = new SimpleViewModel()
        .addProperty(FIRST_NAME, "");

// Bidirectional bind of the first name property and text field's text property.
firstNameTextField.textProperty().bidirectional(personVm.getProperty(FIRST_NAME));

// Set view model property value.
personVm.setPropertyValue(FIRST_NAME, "Fred");

// Output Text field's text property
System.out.println("First name = " + firstNameTextField.getText());

Output:

First name = Fred

As you can see whenever a user enters text into the text field (TextField) the view model's property (first name) gets populated and visa-versa.

Usually if you have a UI Form that has read-only or no validation needed a SimpleViewModel can be used. A form controls bound to properties on a view model you can call the reset() method to copy initial model values back into the property values, thus clearing the screen. The save() method will copy the property values into the model values layer. For simple UIs you can validate fields manually.

ValidationViewModel

Next, let's look at ValidationViewModel(s). These allow the developer to add validation to properties. The following example shows you how to create properties and add validators. These use cases are typically when a user is about to save information. Here they would need to validate before obtaining model values. New in version 1.3.0 are Enums as property names!

public enum PersonField {
    FIRST_NAME("First Name"),
    LAST_NAME("Last Name"),
    AGE("Age"),
    PHONE("Phone"),
    HEIGHT("Height"),
    COLORS("Colors"),
    FOODS("Foods"),
    THING("thing"),
    MPG("Mpg"),
    CUSTOM_PROP("Custom Prop");

    public final String name;
    PersonField(String name){
        this.name = name;
    }
}

var personVm = new ValidationViewModel()
        .addProperty(FIRST_NAME, "")
        .addValidator(FIRST_NAME, FIRST_NAME.name(), (ReadOnlyStringProperty prop, ViewModel vm) -> {
            if (prop.isEmpty().get()) {
                return new ValidationMessage(FIRST_NAME, MessageType.ERROR, "${%s} is required".formatted(FIRST_NAME));
            }
            return VALID;
        })
        // Adding multiple validation messages
        .addValidator(FIRST_NAME, FIRST_NAME.name(), (ReadOnlyStringProperty prop, ValidationResult validationResult, ViewModel viewModel) -> {
            if (prop.isEmpty().get() || prop.isNotEmpty().get() && prop.get().length() < 3) {
                validationResult.error("${%s} must be greater than 3 characters.".formatted(FIRST_NAME));
            }
            String firstChar = String.valueOf(prop.get().charAt(0));
            if (firstChar.equals(firstChar.toLowerCase())) {
                validationResult.error("${%s} first character must be upper case.".formatted(FIRST_NAME));
            }
        })
        .addProperty(PHONE, "111-1111111")
        .addValidator(PHONE, PHONE.name(), (ReadOnlyStringProperty prop, ValidationResult validationResult, ViewModel vm) -> {
            String ph = prop.get();
            Pattern pattern = Pattern.compile("([0-9]{3}\\-[0-9]{3}\\-[0-9]{4})");
            Matcher matcher = pattern.matcher(ph);
            if (!matcher.matches()) {
                validationResult.error("${%s} must be formatted XXX-XXX-XXXX. Entered as %s".formatted(PHONE, ph);
            }
        });

// validate view model
personVm.validate();

if (personVm.hasErrors()) {
        for (ValidationMessage vMsg : personVm.getValidationMessages()) {
        System.out.println("msg Type: %s errorcode: %s, msg: %s".formatted(vMsg.messageType(), vMsg.errorCode(), vMsg.interpolate(personVm)) );
        }
        }

Output:

msg Type: ERROR errorcode: -1, msg: First Name is required
msg Type: ERROR errorcode: -1, msg: First Name must be greater than 3 characters.
msg Type: ERROR errorcode: -1, msg: Phone Number must be formatted XXX-XXX-XXXX. Entered as 111-1111111

Above you'll notice the first name field is required and must be more than 3 characters. The phone number is formatted incorrectly.

As each validation message contains a property of the field in question the code can create decorators or badges affixed on a UI control to allow the user to see the error or warning.

ValidationMessage vMsg = ...;
Label firstNameError = ...;
        if (FIRST_NAME.equals(vMsg.propertyName())) {
        firstNameError.setText(vMsg.message());
        }

Now let's fix the validation issues but instead of calling validate() you should call save(). A ValidatationViewModel overrides the SimpleViewModel's save() method.

The save() method essentially copies property values into the model value layer. Since a call to the validate() method happens before the save() method, property values will not be copied when errors occur.

personVm.setPropertyValue(FIRST_NAME, "Fred");
personVm.setPropertyValue(PHONE, "123-867-5309");
personVm.save();

The correct thing to do is obtain the view model's model values by calling the following:

if (personVm.hasErrorMsgs()) {
        return;
        }
// Valid!
// Obtain valid values from the view model.
String validFirstName = personVm.getValue(FIRST_NAME); // You should not use personVm.getPropertyValue(FIRST_NAME);

// Write to database 
db.write(...);

You can think of the property values of a view model used for the form ui and the model values used on the backend.

How to inject view models into JavaFX controllers?

When creating JavaFX controller classes you can add view models by using the annotation as follows:

// ... A controllers instance variables

@InjectViewModel
SimpleViewModel personViewModel;

When you've created a FXML (view) and a controller you must use the FXMLMvvmLoader.make() facility.

Config config = ...;
JFXNode<Pane, PersonController> personJFXNode = FXMLMvvmLoader.make(config);
Pane personPane = personJFXNode.node();
PersonController personController = personJFXNode.controller();
// perform work

Demo - Account Creation Form

Here's a demo UI form without values. As a user types into fields, the validator for populating the form will update the submit button state. If any fields are not populated, the save button will be disabled.

When pressing the submit button the validation behavior occurs afterwards. To see the demo's code see Form demo

demo1

Input in error (after save to validate)

demo2

When the submit is pressed show the overlay icons for each field with an error.

demo3

As you can see the user entered an initial character as an upper case 'F' only one error message alerts user that it must be 3 characters or more. With the new support of multiple error messages when using validator let show multiple messages related to the first name field.

Here are the following requirements or validation rules for First Name:

  • Must not be blank
  • Must be greater than 3 characters
  • First character must be upper case

Let's enter one lowercase character into the First Name field and click on submit to evaluate error messages. Shown below is the new support for multiple validation messages using ConsumerValidators.

Screenshot 2024-08-05 at 2 37 32 PM

Above you will notice the first name field the user entered one lowercase 'f' character getting 2 validation messages. To see how to add multiple validation messages shown below is a StringConsumerValidator for the first name field.

viewModel.addValidator(FIRST_NAME, "First Name", (ReadOnlyStringProperty prop, ValidationResult validationResult, ViewModel viewModel) -> {
        if (prop.isEmpty().get() || prop.isNotEmpty().get() && prop.get().length() < 3) {
        validationResult.error(FIRST_NAME, "${%s} must be greater than 3 characters.".formatted(FIRST_NAME));
        }
String firstChar = String.valueOf(prop.get().charAt(0));
    if (firstChar.equals(firstChar.toLowerCase())) {
        validationResult.error(FIRST_NAME, "${%s} first character must be upper case.".formatted(FIRST_NAME));
        }
        });

Now let's add correct data with valid input.

demo4

References

The following are links on the topic of UI/UX and Patterns:

cognitive's People

Contributors

carldea avatar realthanhpv avatar

Stargazers

 avatar  avatar Hiram Kamau avatar  avatar sciPher80s avatar Diego Schulz avatar David Weber avatar Oliver Kopp avatar Loay Ghreeb avatar Oliver Löffler avatar John avatar Christoph avatar  avatar Mateus Porto avatar Vidhan avatar  avatar

Watchers

 avatar

cognitive's Issues

Instead of a string as a key to lookup Property and Model values can we enhance it using a Property Identifier type object?

Question: Can properties get looked up other than a property name of type String?

Answer: No (not yet), the APIs are currently only using a property name of type String.

Let’s talk about the idea of a Property Identifier type object. This will offer another way to lookup property & model values by using a unique id or property name.

Current State

The current MVVM capability of Cognitive only looks up property and model values based on a key of type String of a property name (used to reference a field in a UI form).

Enhancement

An enhancement updates the APIs to allow a Property Identifier type object that represents a unique id, a property name, and an underlying user data type object. The user data type object can be any object that contains a unique value. e.g. Person class may have a getter method returning a unique id.

With this newer PropertyIdentifier it allows you to continue using it for property and model value lookup and also validation.

/**
 * A property id and name to uniquely represent a field.
 * @param <T> type of unique identifier. e.g. UUID, Integer, String
 * @param <U> The domain object entity type. e.g. Concept record, or String.
 */
public interface PropertyIdentifier<T, U>{
    T getPropertyId();
    U getUserData();
    String getPropertyName();
}

Add (create) a property using a PropertyIdentifier with validation

PropertyIdentifier<UUID, UUID> spId = new SimplePropertyIdentifier<>("firstName", uId -> uId, UUID.randomUUID());

IdValidationViewModel personVm =  new IdValidationViewModel()
   .addProperty(spId, "fred")
   .addValidator(spId, "First Name", (ReadOnlyStringProperty prop, ViewModel vm) -> {
      // Check if first character is capitalized
      String firstChar = prop.get().substring(0,1);
      if (firstChar.toUpperCase().equals(firstChar))
         return VALID;

      return new ValidationMessage(spId.idToString(), MessageType.ERROR, "${%s} first character must be capitalized. Entered as %s ".formatted(spId.idToString(), prop.get()));

   });

String firstName = personVm.getPropertyValue(spId); // fred

Above you'll notice personVm.getPropertyValue(spId) is looked up by a PropertyIdentifier instance. As a convenience the APIs should allow the caller to also lookup properties by using the following (as a key type):

  • Property name - To be consistent with the current APIs still able to lookup by property name.
  • Unique identifier (String) - In certain scenarios the unique id is passed as a string such as a UUID.toString().
  • Unique identifier (Generic type def) - In certain scenarios the actual unique id object is used such as a UUID instance.

Additional example of convenient ways to lookup or set property values on the view model.

// spId.getPropertyId().toString() = 12628b52-2ba5-418c-b6ad-02f87edb97ce
UUID uniqueId = spId.getPropertyId();

// Get property value using any of the three other key types.
String firstName = personVm.getPropertyValue(uniqueId);
String firstName = personVm.getPropertyValue("firstName");
String firstName = personVm.getPropertyValue("12628b52-2ba5-418c-b6ad-02f87edb97ce");

// Validate view model
personVm.validate();  // error message: First Name first character must be capitalized. Entered as fred 

// Set the property value to Fred. All equivalent to personVm.setPropertyValue(spId, "Fred");
personVm.setPropertyValue(uniqueId, "Fred");
personVm.setPropertyValue("firstName", "Fred");
personVm.setPropertyValue("12628b52-2ba5-418c-b6ad-02f87edb97ce", "Fred");

When we validate personVm.validate() the error message will be returned as follows:
First Name first character must be capitalized. Entered as fred

After validation above you'll notice the calls to setPropertyValue() to fix the validation error.

Now that things are valid a save() will move property values into the model values. Below are examples of using the different key types to get model values.

personVm.save(); // override validation

String firstName = personVm.getValue(firstNameFieldPropId);  // PropertyIdentifier
String firstName = personVm.getValue(propId);         // property id (unique id)
String firstName = personVm.getValue("firstName");   // property name
String firstName = personVm.getValue("12628b52-2ba5-418c-b6ad-02f87edb97ce"); // property id as a String

Java Classes & Interfaces to create.

The following is a high level list of items to create.

  • Create an interface PropertIdentifier
  • Create a SimplePropertyIdentifier
  • Add propertys to IdSimpleViewModel - addProperty(PropertyIdentifier propId, T value)
  • Add validators to IdValidationViewModel - addValidator(PropertyIdentifier propId, String friendlyName, Validator validator)
  • Create tests for IdSimpleViewModel
  • Create tests for IdValidationViewModel
  • Refactor Validation code to be a delegate (ValidationManager)

A high-level UML class hierarchy
property-identifier-concept-oriented

Some additional notes:

PropertyIdentifier

The following is an interface to allow for custom PropertyIdentifiers:

package org.carlfx.cognitive.viewmodel;

/**
 * A property id and name to uniquely represent a field. See setUserData() methods to allow the field to reference any object.
 * @param <T> type of unique identifier. e.g. UUID, Integer, String
 * @param <U> The domain object entity type. e.g. Concept record, or String.
 */
public interface PropertyIdentifier<T, U>{
    /**
     * Returns the unique id of this object.
     * @return Returns the unique id of this object.
     */
    T getPropertyId();

    /**
     * Returns the user data set into this object.
     * @return Returns the user data set into this object.
     */
    U getUserData();
    String getPropertyName();
    /**
     * Compares other PropertyIdentifier instances for equality.
     * @param other Other property identifier to compare.
     * @return Returns true if equals otherwise false.
     */
    default boolean equals(PropertyIdentifier other) {
        return getPropertyId().equals(other.getPropertyId());
    }

    /**
     * A unique integer hash code based on the property id.
     * @return A unique integer hash code based on the property id.
     */
    default int hashcode() {
        return getPropertyId().hashCode();
    }

    default String idToString() {
        return getPropertyId().toString();
    }
}

Here's a possible custom property identifier (concrete implementation):

public record ConceptPropertyIdentifier(/* Db db, */ConceptRecord concept) implements PropertyIdentifier<UUID, ConceptRecord> {
    @Override
    public UUID getPropertyId() {
        return concept.uuid();
    }

    @Override
    public ConceptRecord getUserData() {
        return concept;
    }


    /**
     * Additional methods to provide full names, short names, etc.
     * @return The full name of the concept
     */
    public String fullName() {
        // able to retrieve concepts full name from database.
        // return db.query(id: concept.uuid() ).fqn()
        return concept.fullName();
    }
    /**
     * A default readable property name for field often used for the UIs label.
     * @return property name to lookup field
     */
    @Override
    public String getPropertyName() {
        // able to retrieve concepts full name from database.
        // return db.query(id: concept.uuid() ).fqn()
        return concept.shortName();
    }

    /**
     * A default readable property name for field often used for the UIs label.
     * @return property name to lookup field
     */
    public String shortName() {
        // able to retrieve concepts full name from database.
        // return db.query(id: concept.uuid() ).fqn()
        return concept.shortName();
    }

}

Adding a new concept oriented property values and validation

// create a field (label) for Case significants
ConceptRecord caseSigConceptRecord = new ConceptRecord(UUID.randomUUID(), "Case Significance", "Case");

// Two concept values (insensitive & initial capital)
ConceptRecord caseInsenstiveConcept = new ConceptRecord(UUID.randomUUID(), "Case Insensitive", "Insensitive");
ConceptRecord caseCapInitialConcept = new ConceptRecord(UUID.randomUUID(), "Capitalize initial character", "Cap 1st Character");

// Create an IdValidationViewModel
IdValidationViewModel personVm =  new IdValidationViewModel()
   .addProperty(new ConceptPropertyIdentifier(caseSigConceptRecord), caseInsenstiveConcept) // Custom PropertyIdentifier Concept oriented value. propertyId (case sig) -> Property Value (Case insensitive).
   .addValidator(caseSigConceptRecord.uuid(), "Case significance", (ReadOnlyObjectProperty prop, ViewModel vm) -> {
         ConceptRecord conceptRecord = (ConceptRecord) prop.get();
         if (!conceptRecord.uuid().equals(caseCapInitialConcept.uuid())) {
            return new ValidationMessage(caseSigConceptRecord.uuid().toString(), MessageType.ERROR, "Case Significance must be %s. Entered as %s ".formatted(caseCapInitialConcept.shortName(), prop.get()));
         }
         return VALID;
   });

personVm.validate();

Output of error:

{ msgType=ERROR, errorCode=-1, msg="Case Significance must be Cap 1st Character. Entered as ConceptRecord[uuid=4b28f6fb-45a1-4bee-b1fa-ff95eec356b3, fullName=Case Insensitive, shortName=Insensitive] "}

The error says the Case significance must be the concept that is a (Capitalize first character concept) value and not the concept (Case insensitive).

To fix the validation error simply set property value to the concept record representing capitalize initial character concept (variable: caseCapInitialConcept).

personVm.setPropertyValue(caseSigConceptRecord.uuid(), caseCapInitialConcept);
personVm.save();

ConceptRecord value = personVm.getValue(caseSigConceptRecord.uuid());
System.out.println("Case significance: %s".formatted(value.getFullName()));

Output is the following:

Case significance: Capitalize initial character

Instead of a string as a key to lookup Property and Model values can we enhance it using a Enum type object?

Question: Can properties get looked up using Enums by instead of type String?

Answer: No (not yet), the APIs are currently only using a property name of type String and PropertyIdentifier.

Let’s talk about the idea of a Enum type object. This will offer another way to lookup property & model values by using a Enums. Enums in Java allow a concise way to create enumerated types such as the following:

public enum PersonField {
   FIRST_NAME("First Name"),
   LAST_NAME("Last Name"),
   AGE("Age");

   public final String name;
   PersonField(String name){
      this.name = name;
   }
}

As you can see enums are more concise compared to a public static String FIRST_NAME = "firstName"; as a property name.

Current State

The current MVVM capability of Cognitive only looks up property and model values based on a key of type String of a property name (used to reference a field in a UI form). Additionally there is new support for looking up PropertyIdentifier type objects.

Enhancement

An enhancement updates the APIs to allow a Enum type object that represents a Java enumerated type. e.g. Color enum can have a label, red, green and blue color channel as shown above.

Add (create) a property using a Enum with validation

ValidationViewModel personVm =  new ValidationViewModel()
   .addProperty(FIRST_NAME, "Fred")
   .addValidator(FIRST_NAME, FIRST_NAME.name(), (ReadOnlyStringProperty prop, ValidationResult validationResult, ViewModel vm) -> {
      // Check if first character is capitalized
      String firstChar = prop.get().substring(0,1);
      if (!firstChar.toUpperCase().equals(firstChar)) {
         validationResult.error("${%s} first character must be capitalized. Entered as %s ".formatted(FIRST_NAME, prop.get());
    }
});

String firstName = personVm.getPropertyValue(FIRST_NAME); // fred

Above you'll notice personVm.getPropertyValue(FIRST_NAME) is looked up by a PersonField.FIRST_NAME enum.

Multiple validation messages generated by validators.

Question: Can validators allow you to create multiple validation messages?
Answer: Currently no. You have to return one per validator.

Feature: Validators allows developers to create multiple ValidationMessage(s) instead of returning one or VALID (null) value.

Cognitive version 1.1.0 currently has all validators are type specific and one that is a custom validator the returns one validation message when evaluating validator.

Let's look at a simple example 'First Name' when defining a validator for the property field.

For example given the following is how it currently works (return one error message at a time).

ValidationViewModel personVm = new ValidationViewModel()
    .addProperty(FIRST_NAME, "")
    .addValidator(FIRST_NAME, "First Name", (ReadOnlyStringProperty prop, ViewModel vm) -> {
        if (prop.isEmpty().get()) {
            return new ValidationMessage(FIRST_NAME, MessageType.ERROR, "${%s} is required".formatted(FIRST_NAME));
        }
        return VALID;
    })
    .addValidator(FIRST_NAME, "First Name", (ReadOnlyStringProperty prop, ViewModel vm) -> {
        if (prop.isEmpty().get() || prop.isNotEmpty().get() && prop.get().length() < 3) {
            return new ValidationMessage(FIRST_NAME, MessageType.ERROR, "${%s} must be greater than 3 characters.".formatted(FIRST_NAME));
        }
        return VALID;
    });

Output of the validators:

msg Type: ERROR errorcode: -1, msg: First Name is required
msg Type: ERROR errorcode: -1, msg: First Name must be greater than 3 characters.

Wouldn't it be nice to just validate and add as many errors as you wish all in one validator. Look at below to refactor.

ValidationViewModel personVm = new ValidationViewModel()
    .addProperty(FIRST_NAME, "")
    .addValidator(FIRST_NAME, "First Name", (ReadOnlyStringProperty prop, ValidationResult vr, ViewModel vm) -> {
        if (prop.isEmpty().get()) {
            vr.error("${%s} is required".formatted(FIRST_NAME));
        }
        String value = prop.get();
        if (value.length() < 3) {
            vr.error("${%s} must be greater than 2 characters.".formatted(FIRST_NAME));
            vr.warn("${%s} must be greater than 2 characters.".formatted(FIRST_NAME));
        }
        if (value.length() == 0) {
            vr.info("${%s} must not be blank.".formatted(FIRST_NAME));
        }
    });

Here are the following error messages created:

msg Type: ERROR errorcode: -1, msg: First Name is required
msg Type: ERROR errorcode: -1, msg: First Name must be greater than 2 characters.
msg Type: WARN errorcode: -1, msg: First Name must be greater than 2 characters.
msg Type: INFO errorcode: -1, msg: First Name must not be blank.

Some things to consider:

Create a TypeConsumerValidator type functional interface.

public interface TypeConsumerValidator<T, ValidationResult, ViewModel> {
  /**
     * Performs this operation on the given arguments.
     * @param t
     * @param validationResult
     * @param viewModel
     */
    void accept(T t, ValidationResult validationResult, ViewModel viewModel);

    default TypeConsumerValidator<T, ValidationResult, ViewModel> andThen(TypeConsumerValidator<? super T, ValidationResult, ViewModel> after) {
        Objects.requireNonNull(after);
        return (l, r, x) -> {
            accept(l, r, x);
            after.accept(l, r, x);
        };
    }
}

This will allow Type specific ConsumerValdiators such as a boolean type.

public interface BooleanConsumerValidator extends TypeConsumerValidator<ReadOnlyBooleanProperty, ValidationResult, ViewModel> {
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.