22 Jun 2013

Building Custom LOV with searchContent Facet

List of Values UI components, such as inputListOfValues and inputComboboxListOfValues have a special facet searchContent. The facet is supposed to be used as an extension point which allows us to build our custom LOV's. The content of the facet is going to be rendered in the LOV's search dialog window. This feature looks pretty attractive, since there are lots of use cases when we would like to modify the search dialog content and functionality. And what is important, we basically don't want to throw away the existing LOV's search engine and build it ourselves from scratch. Our goal is to enhance a little bit the search dialog in terms of look and feel and to inherit the existing functionality.

In order to transform a LOV into the custom one, we've got to implement a couple of listeners and design the content of the searchContent facet. So, there is a LOV:

<af:inputComboboxListOfValues id="deptId"
    popupTitle="Search and Select: #{bindings.DepartmentId.hints.label}"
    value="#{bindings.DepartmentId.inputValue}"
    label="#{bindings.DepartmentId.hints.label}"
    model="#{bindings.DepartmentId.listOfValuesModel}"
    required="#{bindings.DepartmentId.hints.mandatory}"
    columns="#{bindings.DepartmentId.hints.displayWidth}"
    shortDesc="#{bindings.DepartmentId.hints.tooltip}"
    autoSubmit="true"
    
    returnPopupListener="#{LOVUtilBean.lovSearchListener}"
    launchPopupListener="#{LOVUtilBean.lovPopupListener}"
    >    


Attributes returnPopupListener and launchPopupListener refer to some managed bean methods. And the searchContent facet:
 <f:facet name="searchContent">
  <af:panelGroupLayout layout="vertical" id="pgl1">
    <af:query headerText="Search" disclosed="true"
      value="#{bindings.DepartmentId.listOfValuesModel.queryDescriptor}"
      model="#{bindings.DepartmentId.listOfValuesModel.queryModel}"
      queryListener="#{LOVUtilBean.queryListener.processQuery}"
      resultComponentId="::t1"
      saveQueryMode="hidden"
      modeChangeVisible="false" maxColumns="4"
      rows="1" id="q1"/>
             
    <af:table value="#{bindings.DepartmentId.listOfValuesModel.
                       tableModel.collectionModel}"
              var="row" rowBandingInterval="1"                        
              rowSelection="single" columnStretching="last"
              width="700px" id="t1">
      <af:column headerText="#{bindings.DepartmentId.listOfValuesModel.
                               itemDescriptors[0].name}"
                 id="c1" sortable="true"
                 sortProperty="#{bindings.DepartmentId.listOfValuesModel.
                                 itemDescriptors[0].name}"
                 align="left" width="80">
        <af:outputText value="#{row.DepartmentId}" id="ot1"></af:outputText>
      </af:column>
     
      <af:column headerText="#{bindings.DepartmentId.listOfValuesModel.
                               itemDescriptors[1].name}"
                 id="c2">
        <af:outputText value="#{row.DepartmentName}" id="ot2"/>
      </af:column>
    </af:table>
  </af:panelGroupLayout>
</f:facet>


The facet contains the standard Query+Table form, which is based on the LOV's model. Actually, we can fill free designing the content of the facet. The only requirement is that we should have one query component and one table component within the facet. But, even this restriction is based on the listener's implementation provided in this post. If you want to avoid the requirement, you can rewrite the listeners as you wish.

So, let's have a look at the listeners.
We're going to use the internal framework listener as the query listener of the Query component:
public QueryListener getQueryListener() 
{
  return queryListener;
}
private QueryListener queryListener = new InternalLOVQueryListener();
Handling the returnPopupEvent after selecting the value in the search dialog:  
public void lovSearchListener(ReturnPopupEvent returnPopupEvent)
{
  UIXInputPopup lovComponent =
    (UIXInputPopup) returnPopupEvent.getSource();

  //Looking for the table component within 
  //seacrhContent facet
  RichTable table = findTable(lovComponent);

  RowKeySet keySet = table.getSelectedRowKeys();


  if (keySet != null && keySet.size() > 0)
  {

    ListOfValuesModel model = lovComponent.getModel();
    Object newVal = model.getValueFromSelection(keySet);
    Object oldVal = lovComponent.getValue();

    //Have we selected anything new?
    //If yes, set it as a value of the LOV and update the model
    if (!ObjectUtils.equal(oldVal, newVal))
    {
      lovComponent.setValue(newVal);
      lovComponent.processUpdates(FacesContext.getCurrentInstance());
    }

  }
}

Handling the launchPopupEvent just before the search dialog is going to be rendered:
public void lovPopupListener(LaunchPopupEvent launchPopupEvent)
{
  UIXInputPopup lovComponent =
    (UIXInputPopup) launchPopupEvent.getSource();
  ListOfValuesModel model = lovComponent.getModel();

  if (model != null)
  {
    //Resetting the query component. 
    //So each time whenever the dialog is rendered the query component 
                  //is in its initial state
    QueryModel queryModel = model.getQueryModel();
    QueryDescriptor queryDesc = model.getQueryDescriptor();
    if ((queryModel != null) && (queryDesc != null))
    {
      queryModel.reset(queryDesc);
      //Looking for the query component within 
      //seacrhContent facet
      RichQuery query = findQuery(lovComponent);
      if (query != null)
        query.refresh(FacesContext.getCurrentInstance());
    }


    //If the LOV component has the searchFacet, then the framework fires this
    //event even in case of leaving the LOV component by TAB pressing. 
    //So, we have to check whether the dialog is really need to be launched.
    //And in case of exact match, we don't need any search dialog.
    Object oldVal = lovComponent.getValue();
    if (!ObjectUtils.equal(String.valueOf(oldVal),
                           launchPopupEvent.getSubmittedValue()))
    {
      List<Object> autoComplete =
        model.autoCompleteValue(launchPopupEvent.getSubmittedValue());
      
      //Do we have an exact match?
      if (autoComplete != null && autoComplete.size() == 1)
      {
        Object autoCompletedValue = autoComplete.get(0);

        Object newVal = model.getValueFromSelection(autoCompletedValue);
        lovComponent.setValue(newVal);
        lovComponent.processUpdates(FacesContext.getCurrentInstance());
        
        //We don't need to launch the dialog anymore
        launchPopupEvent.setLaunchPopup(false);
      }

    }

  }
}


The important thing is that these listeners are generic, they don't depend on any particular use-case and they don't depend on any particular LOV component. These methods can be gathered in some utility bean and used across the entire application.
The result of our work looks like this:
 
The sample application for this post can be downloaded here. It requires JDeveloper R2.

That's it! 

2 comments:

  1. Hi,
    Thanks for this helpful post you shared.
    But I'm having error on :
    import oracle.adfinternal.view.faces.renderkit.rich.simpleInputListOfValuesRenderBase.InternalLOVQueryListener;
    it says : Illegal Internal package import.

    ReplyDelete
  2. this solved my problem :
    https://blogs.oracle.com/jdevotnharvest/entry/internal_package_import_errors_and_how_to_switch_them_off

    Thanks again

    ReplyDelete

Post Comment