Page tree

Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

...

Showdemo
pagenameto.etc.domuidemo.pages.searchpanel.SearchPanelMetadata1

Mixing metadata and user configuration

By default the SearchPanel does not use metadata when the panel is configured manually. To combine metadata with manually added controls call the following:

Code Block
lf.addDefault();

This adds the metadata to the definition as follows:

  • When this call is done the metadata defined search items are added after what is already configured
  • When a user defined item is added for a given property then metadata for that same property is not used. This acts a bit special:
    • If the user defined item has been added before the call to addDefault() then the metadata items are added behind all defined items, and the metadata item with the existing property is just skipped
    • If the user defined item is added after the metadata items have been added (so after the call to addDefault()) then the user defined item replaces the metadata defined item. The net result is that the user defined item will be at the same position that the metadata item would have been.
Warning

One word of warning though: it is dangerous to assume a lot about how the metadata for an object looks. So manipulating the result of the metadata a lot inside a form is madness: any time the metadata changes the form becomes unstable. If you need to make large changes to how a form would look when it only uses metadata consider configuring the thing completely - that makes you independent of metadata.

Customizing the SearchPanel

Using default values

To have default values for controls we use the builder. To define a default value we need to know the data type of the control that is being used for searching. This data type is often not the same as the data type of the field we search on! This can be seen in the examples above:

  • The lookup control for the "total amount" field behaves as a String, because you can input things like "> 1200". The actual value type for this control is NumberLookupValue, which contains:
    • Number from: the "from" amount or the first number entered in the string (always present)
    • QOperation fromOperation: an enum representing the possible operations to issue on that "from", like ">=" or "<".
    • We also have Number to and QOperation to which are used when there are two conditions in the field, like ">= 1000 < 10000"
  • The invoiceDate property uses a DateLookupControl which consists of two dates. Consequently it returns a datatype "DatePeriod" which consists of two Date fields (from and to) representing the values in both DateInput controls.

To set a default value you must use the data types that are actually used by the control or things will fail.

An example of using default values is this:

Code Block
@Override public void createContent() throws Exception {
   ContentPanel cp = new ContentPanel();
   add(cp);

   SearchPanel<Invoice> lf = new SearchPanel<>(Invoice.class);
   cp.add(lf);
   lf.setClicked(a -> search(lf.getCriteria()));

   //-- Find customer by ID
   Customer defaultCustomer = getSharedContext().get(Customer.class, Long.valueOf(10));
   lf.add().property("customer").defaultValue(defaultCustomer).control(); // Default customer

   //-- Default the search total to >= 5.0
   NumberLookupValue nlv = new NumberLookupValue(QOperation.GE, BigDecimal.valueOf(5.0));
   lf.add().property("total").defaultValue(nlv).control();             // Allow searching for a total

   //-- Default the date to before 2010.
   DatePeriod period = new DatePeriod(null, DateUtil.dateFor(2010, 0, 1));
   lf.add().property("invoiceDate").defaultValue(period).control();         // And the date.
}

We use the appropriate data type for the controls being used and define the value for the control from there. The net result is that the form shows with default search values loaded:

Showdemo
pagenameto.etc.domuidemo.pages.searchpanel.SearchPanelManual2

Intermezzo: SearchPanel under the hood

To have a SearchPanel we need the following parts:

  • Each property we want to search on must have a Lookup Control which lets you enter the value to search for.
  • Once a Lookup Value has been entered in a Lookup Control we need a LookupQueryBuilder to add the LookupValue to a QCriteria.
  • And when all of the data is known we need a ISearchFormBuilder to add the control and its label to a form in some layout.
Info

The earlier LookupForm (now deprecated) combined all of this into a single cruddy interface. This made it very hard to customize. The SearchPanel separates all these concerns making it easier to change.

Lookup Controls

The search starts with a Lookup control. A Lookup Control is some class implementing IControl<> which can be used to enter data for a lookup. Some data types (as we've seen above) require a special control to be made to handle the search, as their search is somehow special. But many other data types can be looked up with normal controls:

  • Any String can be searched on by using a normal Text2<String> control.
  • Enumerable values like boolean and enum can be looked up by using a combo box (ComboLookup2) which just contains the values from the enumeration
  • Parent relations can be looked up by a LookupInput as long as that LookupInput is configured to look for the specified parent entity.

If you do not like the default controls used by the SearchPanel it is relatively easy to create your own: just create a new class implementing IControl<> and make it do what you want. This often also requires that you define the proper datatype for the control, which must be able to hold all of the information that you use to search for.

Examples of Lookup controls can be found by looking at DomUI's own implementations under the lookupcontrols package inside the searchpanel package.

Tip

Because LookupControls are just normal IControl<> instances it is very easy to manipulate them: you can add change listeners or play with their values just as you would do with "normal" controls. Making one search control "react" to changes of another is done by just adding a change listener!

Lookup Query Builders

Once you have a value from a Lookup Control we need to somehow translate that value into a part of a QCriteria query. This is the responsibility of the ILookupQueryBuilder instances which are defined as follows:

Code Block
public interface ILookupQueryBuilder<D> {
   @Nonnull
   <T> LookupQueryBuilderResult appendCriteria(@Nonnull QCriteria<T> criteria, @Nullable D lookupValue);
}

The type D is defined as the type of the value of the associated LookupControl. Implementations of this interface must convert the value entered by the user and represented by the lookupValue into something edible inside the QCriteria instance passed to the method.

The simplest QueryBuilder implementation is ObjectLookupQueryBuilder which is defined as follows:

Code Block
@DefaultNonNull
final public class ObjectLookupQueryBuilder<D> implements ILookupQueryBuilder<D> {
   private final String m_propertyName;

   public ObjectLookupQueryBuilder(String propertyName) {
      m_propertyName = requireNonNull(propertyName);
   }

   @Override public <T> LookupQueryBuilderResult appendCriteria(QCriteria<T> criteria, @Nullable D value) {
      if(value == null || (value instanceof String && ((String) value).trim().length() == 0))
         return LookupQueryBuilderResult.EMPTY;       // Is okay but has no data

      // FIXME Handle minimal-size restrictions on input (search field metadata)
      //-- Put the value into the criteria..
      if(value instanceof String) {
         String str = (String) value;
         str = str.trim().replace("*", "%") + "%";        // FIXME Do not search with wildcard by default 8-(
         criteria.ilike(m_propertyName, str);
      } else {
         criteria.eq(m_propertyName, value);             // property == value
      }
      return LookupQueryBuilderResult.VALID;
   }
}


The thing works as follows:

  • Instances are created with the property name that the search takes place on.
  • Once it's time to search the code finds out if the value is a String. In that case it will default to an "ILIKE" operation inside the QCriteria.
  • If it is not a String the value is treated as a literal that needs to be equal to the database field. This latter is used by all literal matches like:
    • Searching for any enumerated value (boolean, enum)
    • Searching for a parent relation (like the specific Customer)

Because the value type can be anything the Query Builder can built very complex queries to do the actual searching.

Using your own lookup components

It is quite simple to replace the default components by your own. Depending on what you want you use/create a lookup component and you use/create a QueryBuilder. As an example we can replace the customer lookup with a Combobox (bad idea) easily as follows:

Code Block
@Override public void createContent() throws Exception {
   ContentPanel cp = new ContentPanel();
   add(cp);

   SearchPanel<Invoice> lf = new SearchPanel<>(Invoice.class);
   cp.add(lf);
   lf.setClicked(a -> search(lf.getCriteria()));

   //-- Create a combobox of customers
   QCriteria<Customer> q = QCriteria.create(Customer.class)   // All customers with last names starting with A
      .ilike("lastName", "B%");
   ComboLookup2<Customer> customerC = new ComboLookup2<>(q);
   customerC.setContentRenderer((node, value) -> node.add(value.getFirstName() + " " + value.getLastName()));

   lf.add().property("customer").control(customerC);

   lf.add().property("total").control();           // Allow searching for a total
   lf.add().property("invoiceDate").control();          // And the date.
}

In this case, because the control returns a value (Customer) that needs to be compared with equals we only pass a new control; the SearchPanel will use the ObjectLookupQueryBuilder to create the query. The net results looks like this:

Showdemo
pagenameto.etc.domuidemo.pages.searchpanel.SearchPanelManual3

When you make a control that has a more complex value you need to create the LookupBuilder too.

Comparison with LookupForm's components

LookupForm combined everything about a single search property in a single interface (ILookupControlInstance). This interface was responsible for everything: the control to use, the search to perform, how to render the control and its label and the shoe size of the builder. To customize this was hard because everything needed to be constructed as one class. The controls used by this code were not real IControl instances so manipulating them was very special. In addition because both controls and search code needed to be together there was no clear separation of tasks which again reduced reusability - and caused some quite bad code in the process.

The form that was constructed by the LookupForm was fixed as there was no reasonable way to change the layout without adding more crud to the LookupForm.

The SearchPanels separates all of this into separate well-defined and reusable parts, and delegates "special use" into special parts - that themselves then become reusable again.

Lookup Factories

When just defining properties to search for the SearchPanel will try to create the proper lookup controls and query builder by itself. It does that by asking the LookupControlRegistry2 class for control factories that can handle the specified property. A factory instance is defined as follows:

Code Block
public interface ILookupFactory<D> {
   @Nonnull FactoryPair<D> createControl(@Nonnull SearchPropertyMetaModel spm);
}

and gets registered like this:

Code Block
register(new DateLookupFactory2(), a -> Date.class.isAssignableFrom(a.getActualType()) ? 10 : 0);
register(new EnumAndBoolLookupFactory2<>(), LookupControlRegistry2::scoreEnumerable);
register(new NumberLookupFactory2(), pmm -> DomUtil.isIntegerType(pmm.getActualType()) || DomUtil.isRealType(pmm.getActualType()) || pmm.getActualType() == BigDecimal.class ? 10 : 0);
register(new RelationLookupFactory2<>(), pmm -> pmm.getRelationType() ==  PropertyRelationType.UP ? 10 : 0);
register(new RelationComboLookupFactory2<>(), pmm -> pmm.getRelationType() == PropertyRelationType.UP && Constants.COMPONENT_COMBO.equals(pmm.getComponentTypeHint()) ? 10 : 0);
register(new StringLookupFactory2<>(), pmm -> 1);        // Accept all

The factory is combined with a lambda that checks the characteristics of the property against whatever the factory would accept. This comparison returns a score. The factory returning the highest > 0 score is the one that is asked to create the control and the query factory.

This makes it very easy to create your own defaults for SearchPanel controls: just create a factory, register it and make it return the correct score when you recognise a property you'd want to handle.