Delphi 11 10 XE8 XE7 XE Seattle Berlin Tokyo Rio Firemonkey, Delphi Android, Delphi IOS

Generate Cross Platform Dynamic Forms At Runtime From JSON In Delphi 10.2.1 Tokyo

| Delphi 11 10 XE8 XE7 XE Seattle Berlin Tokyo Rio Firemonkey Delphi Android Delphi IOS

| Delphi 11 10 XE8 XE7 XE Seattle Berlin Tokyo Rio Firemonkey Delphi Android Delphi IOSThe Hospitality Survey Client project is part of the Hospitality Survey App template for Delphi 10.2.1 Tokyo that Embarcadero has released through their GetIt platform. The Hospitality Survey App consists of four different projects. In this blog post I will cover the dynamic form generator that is built into the Hospitality Survey Client project. Also keep in mind that the client can be deployed to Android, iOS, macOS, and Windows with a single code base and a single responsive UI.

Basically how it works is on the server there is a database table which contains the list of survey questions to be asked to patrons from each tenant (in this case restaurant). The /survey/ end point in RAD Server is called from TBackendEndpoint (which is basically a TRESTClient) in the Survey App Client. The RAD Server end point returns the contents of the Questions table as the FireDAC JSON format. You can customize the questions using the Hospitality Survey Editor. The Client saves out the FireDAC JSON to a surveys.json file which is then loaded into an TFDMemTable. The GenerateSurvey() function (see below) loops through the records in the TFDMemTable and creates a TFrame for each record. Each record corresponds to a question in the database and you can see the different fields in a record below:

The Type column determines which TFrame is loaded for that record. The built in types are: rating, yesno, edit, options. You can add your own row types as well by modifying the GenerateSurvey() procedure. You can see the units below for each of the dynamic TFrames including a header frame and a complete button frame for submitting the form.

The GenerateSurvey() procedure itself is pretty simple. It loops through the TFDMemTable dataset and checks the type field to see which TFrame to load and populate for that record. The options field is a JSON array that is used to populate the values for the yesno type and options type. The ID field is used to mark the TFrame with the specific question it was created from (FrameItem.Tag := BindSourceDBForm.DataSet.FieldByName(‘ID’).AsInteger;) so that the value field can be filled out with the answer from the user.

//
procedure TMainForm.GenerateSurvey(Sender: TObject);
var
  FrameItem: TFrame;
  FieldType: String;
  FieldCategory: Integer;
  JSONArray: TJSONArray;
  I: Integer;
begin
  FrameItem := TSurveyHeaderFrame.Create(TFMXObject(Sender));
  FrameItem.Parent := TFMXObject(Sender);
  FrameItem.Name := 'FSurveyHeader';
  FrameItem.Align := TAlignLayout.Top;
  FrameItem.Position.Y := 0;
 
  BindSourceDBForm.DataSet.First;
  while not BindSourceDBForm.DataSet.Eof do
  begin
    FieldCategory := BindSourceDBForm.DataSet.FieldByName('category').AsInteger;
 
      FieldType := BindSourceDBForm.DataSet.FieldByName('type').AsString;
      if FieldType = 'edit' then
      begin
        FrameItem := TEditFrame.Create(TFMXObject(Sender));
        FrameItem.Parent := TFMXObject(Sender);
        TEditFrame(FrameItem).QuestionText.Text :=
          BindSourceDBForm.DataSet.FieldByName('title').AsString;
      end;
      if FieldType = 'yesno' then
      begin
        FrameItem := TYesNoFrame.Create(TFMXObject(Sender));
        FrameItem.Parent := TFMXObject(Sender);
        TYesNoFrame(FrameItem).QuestionText.Text := BindSourceDBForm.DataSet.FieldByName('title').AsString;
        JSONArray := TJSONObject.ParseJSONValue(BindSourceDBForm.DataSet.FieldByName('options').AsString) as TJSONArray;
        for I := 0 to JSONArray.Count - 1 do
        begin
          case I of
            0:
              begin
                TYesNoFrame(FrameItem).ValueSpeedButton1.Text := JSONArray.Items[I].Value;
                TYesNoFrame(FrameItem).ValueSpeedButton1.GroupName := BindSourceDBForm.DataSet.FieldByName('name').AsString;
              end;
            1:
              begin
                TYesNoFrame(FrameItem).ValueSpeedButton2.Text := JSONArray.Items[I].Value;
                TYesNoFrame(FrameItem).ValueSpeedButton2.GroupName := BindSourceDBForm.DataSet.FieldByName('name').AsString;
              end;
          end;
        end;
        JSONArray.Free;
      end;
      if FieldType = 'rating' then
      begin
        FrameItem := TRatingBarFrame.Create(TFMXObject(Sender));
        FrameItem.Parent := TFMXObject(Sender);
        TRatingBarFrame(FrameItem).QuestionText.Text := BindSourceDBForm.DataSet.FieldByName('title').AsString;
      end;
      if FieldType = 'options' then
      begin
        FrameItem := TOptionsFrame.Create(TFMXObject(Sender));
        FrameItem.Parent := TFMXObject(Sender);
        TOptionsFrame(FrameItem).QuestionText.Text := BindSourceDBForm.DataSet.FieldByName('title').AsString;
        JSONArray := TJSONObject.ParseJSONValue(BindSourceDBForm.DataSet.FieldByName('options').AsString) as TJSONArray;
        TOptionsFrame(FrameItem).ValueComboBox.Items.BeginUpdate;
        for I := 0 to JSONArray.Count - 1 do
        begin
          TOptionsFrame(FrameItem).ValueComboBox.Items.Add(JSONArray.Items[I].Value);
        end;
        TOptionsFrame(FrameItem).ValueComboBox.Items.EndUpdate;
        JSONArray.Free;
      end;
      FrameItem.Name := 'F' + BindSourceDBForm.DataSet.FieldByName('ID').AsString;
      FrameItem.Align := TAlignLayout.Top;
      FrameItem.Tag := BindSourceDBForm.DataSet.FieldByName('ID').AsInteger;
      FrameItem.Position.Y := BindSourceDBForm.DataSet.FieldByName('ID').AsInteger * 100;
 
    BindSourceDBForm.DataSet.Next;
    Application.ProcessMessages;
  end;
 
  FrameItem := TCompleteFrame.Create(TFMXObject(Sender));
  FrameItem.Parent := TFMXObject(Sender);
  FrameItem.Name := 'FComplete';
  FrameItem.Align := TAlignLayout.Top;
  FrameItem.Position.Y := 1000000;
  TCompleteFrame(FrameItem).CompleteButton.OnClick := CompleteClick;
 
end;

The selected option in each TFrame gets sent back to the TFDMemTable via the UpdateValueByID() procedure as you can see below. In the below code the Self.Tag field corresponds to the ID field of the question.

//
procedure TOptionsFrame.ValueComboBoxChange(Sender: TObject);
begin
  MainForm.UpdateValueByID(Self.Tag,ValueComboBox.Items[ValueComboBox.ItemIndex]);
end;

The UpdateValueByID() procedure uses the Locate() procedure on the DataSet to find the correct record and then update the value field.

//
procedure TMainForm.UpdateValueByID(ID: Integer; const Value: string);
begin
  if BindSourceDBForm.DataSet.Locate('ID', VarArrayOf([ID]), []) = True then
  begin
    BindSourceDBForm.DataSet.Edit;
    BindSourceDBForm.DataSet.FieldByName('value').AsString := Value;
    BindSourceDBForm.DataSet.Post;
  end;
end;

Once the survey has been completed by the user then the entire contents of the TFDMemTable are saved out to the FireDAC JSON format and uploaded back to the server via a POST from a TBackendEndpoint component to the /survey/complete endpoint. In the case of the Hospitality Survey App all of the uploaded records are saved for each collected survey. This allows the survey questions to be created, removed, and changed without affecting any of the existing surveys that have already been collected.

Want to know more? Check out the full Deep Dive into the Hospitality Survey App template for Delphi 10.2.1 Tokyo.

Exit mobile version