New approach of development of FireMonkey control “Control — Model — Presentation”. Part 2. TEdit with autocomplete

Пример работы AutoComplete в TEditWe will continue a subject of the review of new approach on division of a control into the model and presentation described here. And in this article we will consider practical uses of this approach on the example autocompletion of input in TEdit.

Demo project: Sample for XE8Sample for XE10


The first, we need to decide, where we will keep a list of suggested words for substitution. We will keep in the TEdit model. But class of model TCustomEditModel doesn’t have a special property/field for it like a SuggestionList. Therefore the natural desire consists in expansion of a class of model and addition in the class of this field. But we will keep from this rush, and we will look that the model has for us something another bypassing creation of a new class.

The model allows to store data of any types without inheritance

Let’s look at a basic class of model TDataModel. It is a successor of the class TMessageSender, which provides functionality on sending messages. TDataModel adds in additional the mechanism for storing data of any types with using RTTI TValue. It means that we can keep any our data in TDataModel without creating separate class of model.

I will not consider about all features of TValue. The main idea of TValue stores data of any kind of type. About another details of TValue you can look at (XE7 Doc wiki).

So we can place in model our data, then:

  1. Data will be automatically available to us in our representation.
  2. Creation of a separate class of model isn’t required.

Below a sample of setting string, array and handler value in TEdit.

uses
  System.Rtti;

//...
type
  TStringArray = TArray<string>;
var
  ArrayList: TStringArray;
  EventHandler: TNotifyEvent;
begin
  { Setting/Getting string value }
  Edit1.Model.Data['string_value'] := TValue.From<string>('My string value');
  if Edit1.Model.Data['string_value'].IsType<string> then
    ShowMessage(Edit1.Model.Data['string_value'].AsString);

  { Removing data }
  Edit1.Model.Data['string_value'] := TValue.Empty;

  { Setting/Getting value of array type }
  ArrayList := ['Apple', 'ARC', 'Auto', 'Allday', 'Alltime'];
  Edit1.Model.Data['array_value'] := TValue.From<TStringArray>(ArrayList);
  if Edit1.Model.Data['array_value'].IsType<TStringArray> then
    ArrayList := Edit1.Model.Data['array_value'].AsType<TStringArray>;

  { Setting/Getting event handler }
  Edit1.Model.Data['event_handler_value'] := TValue.From<TNotifyEvent>(EventHandler);
  if Edit1.Model.Data['event_handler_value'].IsType<TNotifyEvent> then
    EventHandler := Edit1.Model.Data['event_handler_value'].AsType<TNotifyEvent>();
end;

Data – it’s a property with index, which is introduced on TDataModel level. It provides a way for setting and getting value by specified key (Key – Value). Key is register the dependent.

TValue.From<T> – wraps data of type T in TValue.

If you would like to remove your data from model, enough set TValue.Empty for you value key.

Pay attention that in an example above, I added check of value on the expected value type. In this example it isn’t necessary as I know that I set and at once I take the same value. However it will be required in the future to be sure that value of the necessary type lies.

When we set value in Model, Model sends message into presentation with next codes:

  • MM_DATA_CHANGED – data changed.
  • MM_GETDATA – Request on getting data from model.

And value of type:

TDataRecord = TPair<string, TValue>;

When the first value – key name, the second – value.

Now we will return to our example.

For our example we will store the offered words in a type of an string array TArray<string>. We will name key for this list as “suggestion_list“. Next code shows, how we set our list of words into base TEdit model.:

var
  SuggestionList: TArray<string>;
begin
  SuggestionList := ['Apple', 'Arc', 'Auto', 'Ask', 'Allday', 'Alltime', 'Orange', 'Pineapple'];
  Edit1.Model.Data['suggestion_list'] := TValue.From<TArray<string>>(SuggestionList);

Now we know how presented control transfers data of any types. Let’s look at creation of our representation.

Creating and registration of new presentation based on TStyledEdit

The better to create separated file for our new presentation. Further it will be possible easily  to reuse it in other projects, by simple adding of the file to your project. We will call the file: FMX.Edit.Autocomplete.pas

Creates new presentation. We choose TStyledEdit as a base class for our presentation. TStyledEidt – is a default styled presentation for TEdit. It is used by default in all TEdit. Choose TStyledAutocompleteEdit for name of our new presentation. Prefix “Styled” means, that this presentation uses firemonkey style and not a native. The name of representation can be any and as doesn’t influence further use in TEdit.

We will at once create a class the proxy for access to our representation. As we remember from 1 part of article, in factory we register not representations, and their proxy. Therefore in the beginning we create a proxy for our presentation.

For versions before XE8 (included)

We create a class the proxy of presentation, we call it “TStyledAutocompleteEditProxy“. Override CreateReceiver method and returns instance of presentation.  In fact this method also realizes communication of a proxy with presentation.

uses
  FMX.Edit.Style, FMX.Controls.Presentation;

type

  TStyledAutocompleteEdit = class(TStyledEdit)
  end;

  TStyledAutocompleteEditProxy = class(TPresentationProxy)
  protected
    function CreateReceiver: TObject; override;
  end;

implementation

{ TStyledAutocompleteEditProxy }

function TStyledAutocompleteEditProxy.CreateReceiver: TObject;
begin
  Result := TStyledAutocompleteEdit.Create(nil);
end;

end.

For versions since XE10 (included)

uses
  FMX.Edit.Style, FMX.Controls.Presentation, FMX.Presentation.Style;
 
type
 
  TStyledAutocompleteEdit = class(TStyledEdit)
  end;
 
  TStyledAutocompleteEditProxy = TStyledPresentationProxy(TStyledAutoCompleteEdit);
 
implementation
 
end.

On this moment, we have create new presentation based on styled presentation of TEdit and have created presentation proxy for accessing to our presentation.

Now we need to make a registration of our presentation for automatically using in our TEdit. In first part I told about presentation proxy factory and about process of loading presentation by TPresentedControl. So now i’m only publish a code of registration.

There are two way to do it.

  1. Replace default TEdit style presentation on our. In this case all TEdit controls in application will use our presentation instead of default.
  2. Separately to register our representation. In this case everything TEdit in application will continue to use standard representation by default. To connect ours, it will be necessary to specify them the name of our representation.

Replacing a presentation TEdit on our by default

If we go in the first way, we delete from factory a standard proxy presentation of TEdit in the beginning, and then we register with the same name of a proxy with our presentation. The name of the stylized representation of TEdit – “Edit-Style“.

uses
  FMX.Presentation.Factory;

initialization
  TPresentationProxyFactory.Current.Unregister('Edit-style');
  TPresentationProxyFactory.Current.Register('Edit-style', TStyledAutocompleteEditProxy);
finalization
  TPresentationProxyFactory.Current.Unregister('Edit-style');
  TPresentationProxyFactory.Current.Register('Edit-style', TStyledEditProxy);
end.

Now if you start application, all TEdit fields will use our representation. You can check it by getting class name of TEdit.PresentationProxy. It should be a “StyledAutocompleteEditProxy“.

Registration of additional presentation of TEdit

If we go in the second way, in the beginning we select a name for our representation, for example, of “AutocompleteEdit-style”. And then we register its proxy. The name of our presentation can be any:

uses
  FMX.Presentation.Factory;

initialization
  TPresentationProxyFactory.Current.Register('AutocompleteEdit-style', TStyledAutocompleteEditProxy);
finalization
 TPresentationProxyFactory.Current.Unregister('AutocompleteEdit-style');
end.

Now we need to specify for our TEdit that they used our representation instead of the standard. For this purpose all presented control have a special event:

TPresentedControl.OnPresentationNameChoosing(Sender: TObject; var PresenterName: string);

which is invoked before search of presentation in factory by control. PresentationName contains a name of presentation, which will be requested from factory. By means of this event, you can replace the standard name on the name of your representation. In our case “AutocompleteEdit-style” need to be returned.

procedure TForm8.Edit1PresentationNameChoosing(Sender: TObject; var PresenterName: string);
begin
 PresenterName := 'AutocompleteEdit-style';
end;

Implementation of AutoComplete

Now we will be engaged in implementation of the AutoComplete. We will add to ours presentation a field for storage of the list of words with FSuggestions.

We will use TPopup control with TListBox inside for creating drop down list. Add field for keeping these controls and add creating code into presentation:

uses
  FMX.Edit.Style, FMX.Controls.Presentation, FMX.Controls.Model, FMX.Presentation.Messages,
  FMX.Controls, FMX.ListBox, System.Classes, System.Types;

type

  TStyledAutocompleteEdit = class(TStyledEdit)
  private
    FSuggestions: TArray<string>;
    FPopup: TPopup;
    FListBox: TListBox;
    FDropDownCount: Integer;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
  end;

implementation

{ TStyledAutocompleteEdit }

constructor TStyledAutocompleteEdit.Create(AOwner: TComponent);
begin
  inherited;
  FPopup := TPopup.Create(nil);
  FPopup.Parent := Self;
  FPopup.PlacementTarget := Self;
  FPopup.Placement := TPlacement.Bottom;
  FPopup.Width := Width;
  FListBox := TListBox.Create(nil);
  FListBox.Parent := FPopup;
  FListBox.Align := TAlignLayout.Client;
  FDropDownCount := 5;
end;

destructor TStyledAutocompleteEdit.Destroy;
begin
  FPopup := nil;
  FListBox := nil;
  inherited;
end;

FDropDownCount – contains count of the words shown in the dropdown list without scrolling.

PlacementTarget – specifies control concerning which there will be a dropdown list.

Placement – specifies placement of drop down list  concerning PlacementTarget.

So far in this example of FDropDownCount it will be rigidly set in the constructor. Homework, add storing this property through the TEdit model.

Pay attention that in a destructor we don’t delete FPopup and FListBox. These components will be automatically removed during removal of representation as they are attached to presentation. All objects of structure of components of a form are removed recursively. Therefore our kontrola, will also be automatically removed. Also for correct working off of ARC on mobile platforms, we don’t forget to nullify the link to them.

General algorithm of substitution

When user have entered new char in TEdit, we filter common words dictionary and select those words, which begin with text in TEdit. If after filtering we have a words, we fill drop down list these words and show popup, otherwise we hide Popup.

If the user selects the word from the dropdown list, we will insert it into the current TEdit.

Extracting suggestion words from model in Presentation

As in the last section we marked, in case of change of data in model, the model sends the message with the MM_DATA_CHANGED code in case of change of data in model through Data. We catch this message in presentation to pull out the list of words for substitution.

type

  TStyledAutocompleteEdit = class(TStyledEdit)
  private
    FSuggestions: TArray<string>;
    FPopup: TPopup;
    FListBox: TListBox;
    FDropDownCount: Integer;
  protected
    procedure MMDataChanged(var AMessage: TDispatchMessageWithValue<TDataRecord>); message MM_DATA_CHANGED;

//....

implementation

procedure TStyledAutocompleteEdit.MMDataChanged(var AMessage: TDispatchMessageWithValue<TDataRecord>);
var
  Data: TDataRecord;
begin
  Data := AMessage.Value;
  if Data.Value.IsType < TArray < string >> and (Data.Key = 'suggestion_list') then
    FSuggestions := AMessage.Value.Value.AsType<TArray<string>>;
end;

Filling of the dropdown list with words

The method of filling of the list with our words will be such:

procedure TStyledAutocompleteEdit.RebuildSuggestionList;
var
  Word: string;
begin
  FListBox.Clear;
  FListBox.BeginUpdate;
  try
    for Word in FSuggestions do
      if Word.ToLower.StartsWith(Model.Text.ToLower) then
        FListBox.Items.Add(Word);
  finally
    FListBox.EndUpdate;
  end;
end;

We touch all words in our dictionary and compare each word with current text in TEdit. Those words which begin with the entered text are included into TListBox. In style representation we have access to the TEdit model.

Note: You can try to make different criteria of word selection in the dictionary: for example, to make selection on last entered words into TEdit, but not in all text.

Calculation of drop down list size

After filling drop down list, we have to calculate height of drop down list based on number of words and FDropDownCount:

procedure TStyledAutocompleteEdit.RecalculatePopupHeight;
begin
  FPopup.Height := FListBox.ListItems[0].Height * Min(FDropDownCount, FListBox.Items.Count) + FListBox.BorderHeight;
  FPopup.PopupFormSize := TSizeF.Create(FPopup.Width, FPopup.Height);
end;

If TEdit changes Width and drop down list is opened, we need to fit width of drop down list to TEdit. When TEdit changes size, it sends message with code PM_SET_SIZE. Catch it and adjust width of popup to TEdit:

type

  TStyledAutocompleteEdit = class(TStyledEdit)
  private
//...
  protected
    procedure PMSetSize(var AMessage: TDispatchMessageWithValue<TSizeF>); message PM_SET_SIZE;

//....

implementation

procedure TStyledAutocompleteEdit.PMSetSize(var AMessage: TDispatchMessageWithValue<TSizeF>);
begin
  inherited;
  FPopup.Width := Width;
end;

We track text entering and update the list of words

Catch a moment of changes text in TEdit through virtual method of presentation DOChangeTrackig.

procedure TStyledAutocompleteEdit.DoChangeTracking;

  function HasSuggestion: Boolean;
  var
    I: Integer;
  begin
    I := 0;
    Result := False;
    while not Result and (I < Length(FSuggestions)) do
    begin
      Result := FSuggestions[I].ToLower.StartsWith(Model.Text.ToLower);
      if not Result then
        Inc(I)
      else
        Exit(Result);
    end;
  end;

  function IndexOfSuggestion: Integer;
  var
    Found: Boolean;
    I: Integer;
  begin
    Found := False;
    I := 0;
    Result := -1;
    while not Found and (I < FListBox.Count) do
    begin
      Found := FListBox.Items[I].ToLower.StartsWith(Model.Text.ToLower);
      if not Found then
        Inc(I)
      else
        Exit(I);
    end;
  end;

begin
  inherited;
  if HasSuggestion then
  begin
    RebuildSuggestionList;
    RecalculatePopupHeight;
    Index := IndexOfSuggestion;
    if Model.Text.IsEmpty then
      FListBox.ItemIndex := -1
    else
      FListBox.ItemIndex := Index;
    FPopup.IsOpen := True;
  end
  else
    FPopup.IsOpen := False;
end;

Navigation in dropdown list by keypad

Add control of choosing word from DropDown list by keyboard.

procedure TStyledAutocompleteEdit.KeyDown(var Key: Word; var KeyChar: Char; Shift: TShiftState);
begin
  inherited;
  case Key of
    vkReturn:
      if FListBox.Selected <> nil then
      begin
        Model.Text := FListBox.Selected.Text;
        Edit.GoToTextEnd;
        FPopup.IsOpen := False;
      end;
    vkEscape:
      FPopup.IsOpen := False;
    vkDown:
      if FListBox.Selected <> nil then
        FListBox.ItemIndex := Min(FListBox.Count - 1, FListBox.ItemIndex + 1);
    vkUp:
      if FListBox.Selected <> nil then
        FListBox.ItemIndex := Max(0, FListBox.ItemIndex - 1);
  end;
end;

Everything is ready.

Now we start application and look at result.

Important! Current sample doesn’t implement choosing words by mouse in drop down list. It’s your home work. For do it, you need to set event handler on TListBox.OnClick.

Conclusion

We have developed new presentation for TEdit, which is based on default TEdit styled presentation and adds new function on auto-complete. Now, if you would like you add supporting Auto-Complete function in any project, enough to add file with new presentation in your project.

Demo project: Sample for XE8Sample for XE10

6 thoughts on “New approach of development of FireMonkey control “Control — Model — Presentation”. Part 2. TEdit with autocomplete

  1. john

    Thank you for the great articles on autocomplete. I’m a little confused on how I can get a notification of which item was eventually selected. Are you able to provide some direction to accomplish this?

    Thanks!

    Reply
    1. Yaroslav BrovinYaroslav Brovin Post author

      Hello John,

      Thank you for response.

      • If you mean selecting word in dropdown list by Mouse, then the best way is using FListBox.OnItemClick. Just write you event handler.
      • If you talk about selecting item by keyboard, look at the TStyledAutocompleteEdit.KeyDown

      Thank you

      Reply
      1. Marcel Almeida

        Hi,

        Excellent post, but can not implement the OnItemClick event to use the mouse.

        I tried to create it following the idea KeyDown

        protected
        ItemClick procedure (const Sender: TCustomListBox ; const Item: TListBoxItem ) ; override ;

        Could you give me a hand .

        Thank you very much.

        Reply
  2. john

    I forgot to add I would like to be able to drop down the entire list, how can this be achieved. Basically how does one access the TStyledAutocompleteEdit object assigned to the edit control? Basically I’d like to be able to do something like Edit1.PopUp.IsOpen:=true when the arrow button i put inside edit button is clicked.

    Reply
    1. Yaroslav BrovinYaroslav Brovin Post author

      The best way is send message from TEdit to presentation, catch it in presentation and make FPopup.IsOpen := True.
      You can write helper method for TEdit, which will do it. For Example:

      const
        PM_DROP_DOWN = PM_EDIT_USER + 1;
      
      type
      
        TEditHelpers = class helper for TEdit
        public
          procedure DropDown;
        end;
      
      implementation
      
      procedure TEditHelpers.DropDown;
      begin
        if HasPresentationProxy then
          PresentationProxy.SendMessage(PM_DROP_DOWN);
      end;
      

      After it, we catch custom message PM_DROP_DOWN:

      type
      
        TStyledAutocompleteEdit = class(TStyledEdit)
        // .....
        protected 
          procedure PMDropDown(var AMessage: TDispatchMessage); message PM_DROP_DOWN;
        // ....
        end;
      
      implementation
      
      // ......
      
      procedure TStyledAutocompleteEdit.PMDropDown(var AMessage: TDispatchMessage);
      begin
        FPopup.IsOpen := True;
      end;
      
      Reply
  3. john

    Hi Thank you for your replies.
    #1. I was wanting some sort of notification of exactly which item was selected vs just knowing the text. This will be useful for retrieving some associated data.

    #2. I think i’ve found a simpler way to drop down the popup. I exposed a public method in the “styled” class and then call it through the edit control’s “Presenation” object cast to the TStyledAutoCompleteEdit in my desired event, i.e.

    TStyledAutoCompleteEdit(edit.Presentation).MyNewMethod;

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *