]> gitweb.hamatoma.de Git - flutter_bones.git/commitdiff
module user works
authorHamatoma <author@hamatoma.de>
Thu, 5 Nov 2020 22:05:05 +0000 (23:05 +0100)
committerHamatoma <author@hamatoma.de>
Thu, 5 Nov 2020 22:05:05 +0000 (23:05 +0100)
20 files changed:
lib/app.dart
lib/src/model/column_model.dart
lib/src/model/combo_base_model.dart
lib/src/model/combobox_model.dart
lib/src/model/db_reference_model.dart
lib/src/model/field_model.dart
lib/src/model/model_base.dart
lib/src/model/standard/user_model.dart
lib/src/model/text_field_model.dart
lib/src/model/widget_model.dart
lib/src/page/application_data.dart
lib/src/page/async_example_page.dart [new file with mode: 0644]
lib/src/page/user/user_list_page.dart
lib/src/persistence/persistence_cache.dart
lib/src/private/bsettings.dart
lib/src/widget/page_controller_bones.dart
lib/src/widget/view.dart
lib/src/widget/widget_list.dart
test/model/model_test.dart
test/persistence_cache_test.dart

index 12883f7fba8780b0b8f0eba11637787a138c4dd4..b40f1234ff9469f74fdc24ac351c8c18e11c6051 100644 (file)
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
 
 import 'src/helper/settings.dart';
 import 'src/page/demo_page.dart';
+import 'src/page/async_example_page.dart';
 import 'src/page/login_page.dart';
 import 'src/page/role/role_create_page.dart';
 import 'src/page/role/role_list_page.dart';
@@ -15,12 +16,12 @@ import 'src/private/bsettings.dart';
 class BoneApp extends StatefulWidget {
   @override
   BoneAppState createState() {
-    BSettings();
+    final logger = MemoryLogger(LEVEL_FINE);
+    BSettings.create(logger);
     final mapWidgetData = <String, dynamic>{
       'form.card.padding': '16.0',
       'form.gap.field_button.height': '16.0',
     };
-    final logger = MemoryLogger();
     Settings(
         logger: logger,
         widgetConfiguration: BaseConfiguration(mapWidgetData, logger));
@@ -38,6 +39,7 @@ class BoneAppState extends State<BoneApp> {
         visualDensity: VisualDensity.adaptivePlatformDensity,
       ),
       initialRoute: '/user/list',
+      //initialRoute: '/async',
       onGenerateRoute: _getRoute,
     );
   }
@@ -50,6 +52,9 @@ Route<dynamic> _getRoute(RouteSettings settings) {
     case '/demo':
       page = DemoPage(BSettings.lastInstance.pageData);
       break;
+    case '/async':
+      page = AsyncExamplePage(BSettings.lastInstance.pageData);
+      break;
     case '/role/list':
       page = RoleListPage(BSettings.lastInstance.pageData);
       break;
index b9946776f120e1c127f0b8a108f0e4ee6c05c92c..69ecb4c511d1720d6728ee97252638e696919f30 100644 (file)
@@ -75,7 +75,7 @@ class ColumnModel extends ComboBaseModel {
     name = parseString('column', map, required: true);
     checkSuperfluousAttributes(
         map,
-        'column dataType foreignKey label listOption listType options rows size texts tooTip values widgetType'
+        'column dataType defaultValue foreignKey label listOption listType options rows size texts tooTip values widgetType'
             .split(' '));
     super.parse();
     dataType =
index f7a6c266bae5c4a908741b77427189cc8ecf05a8..7e4880b24ced9d2a52fab8cd3544ad8f449e8b53 100644 (file)
@@ -35,9 +35,22 @@ abstract class ComboBaseModel extends FieldModel {
       this.listOption,
       this.listType,
       WidgetModelType widgetType,
+      dynamic defaultValue,
       BaseLogger logger})
       : super.direct(section, page, map, widgetType, name, label, toolTip,
-            dataType, options, logger);
+            dataType, options, defaultValue, logger);
+
+  /// Transfers the list data from [texts] and [values] to [data].
+  /// May only called for comboboxes with [listType] = [ComboboxListType.explicite].
+  void completeSync() {
+    if (listType != null) {
+      if (listType != ComboboxListType.explicite) {
+        logger.error('wrong call of completeSync(): ${fullName()}');
+      } else {
+        data = ComboboxData(texts, values, WaitState.ready);
+      }
+    }
+  }
 
   /// Parses the map and stores the data in the instance.
   void parse() {
index 6a27d4cd216e2061da20a8ce554ca1048a309d1f..da2a9483a3054ee58d4cb6852147f328adeb9480 100644 (file)
@@ -24,7 +24,7 @@ class ComboboxModel extends ComboBaseModel {
   void parse() {
     checkSuperfluousAttributes(
         map,
-        'name label dataType filterType listOption listType options texts toolTip values widgetType'
+        'dataType defaultValue filterType label listOption listType name options texts toolTip values widgetType'
             .split(' '));
     super.parse();
     checkOptionsByRegExpr(options, regExprOptions);
index 48137b53867e09de5cd5cc1c5921ef6a38756686..2032c709c87651fb05913fc3e634d570462769dd 100644 (file)
@@ -69,7 +69,7 @@ class DbReferenceModel extends ComboBaseModel {
     toolTip ??= column.toolTip;
     filterType ??= column.filterType;
     options = parseOptions('options', map);
-    dataType ??= column.dataType;
+    dataType = column.dataType;
     texts ??= column.texts;
     values ??= column.values;
     listOption ??= column.listOption;
index 66a7bb2415b9d17084d61592960526523a0c7ec2..6e2a19e5d0a6abdf885aa6cd9849631f4a3ceaeb 100644 (file)
@@ -14,9 +14,10 @@ abstract class FieldModel extends WidgetModel {
   DataType dataType;
   List<String> options;
   FilterType filterType;
+  dynamic _value;
+  dynamic defaultValue;
 
   final Map<String, dynamic> map;
-  var _value;
 
   FieldModel(SectionModel section, PageModel page, this.map,
       WidgetModelType fieldModelType, BaseLogger logger)
@@ -26,22 +27,16 @@ abstract class FieldModel extends WidgetModel {
       SectionModel section,
       PageModel page,
       this.map,
-      WidgetModelType fieldModelType,
-      String name,
-      String label,
-      String toolTip,
-      DataType dataType,
-      List<String> options,
+      WidgetModelType widgetModelType,
+      this.name,
+      this.label,
+      this.toolTip,
+      this.dataType,
+      this.options,
+      this.defaultValue,
       BaseLogger logger,
-      [FilterType filterType])
-      : super(section, page, fieldModelType, logger) {
-    this.name = name;
-    this.label = label;
-    this.toolTip = toolTip;
-    this.options = options;
-    this.dataType = dataType;
-    this.filterType = filterType;
-  }
+      [this.filterType])
+      : super(section, page, widgetModelType, logger);
 
   get value => _value;
 
@@ -88,6 +83,17 @@ abstract class FieldModel extends WidgetModel {
           break;
       }
     }
+    final value = parseDynamic('defaultValue', map);
+    defaultValue =
+        value is String ? StringHelper.fromString(value, dataType) : value;
+  }
+
+  /// Gets the [value] from a [row] like a db record:
+  /// Key of the entry in [row] is the [name].
+  void valueFromRow(Map row) {
+    if (row.containsKey(name)) {
+      value = row[name];
+    }
   }
 
   @override
index 7756b6047ae0e5fbad756f250ab43a02abd8383c..947c0390423f31c9b115eb087ee3dede7795db70 100644 (file)
@@ -93,6 +93,21 @@ abstract class ModelBase {
     return rc;
   }
 
+  /// Fetches an entry from a map addressed by a [key].
+  /// An error is logged if [required] is true and the map does not contain the key.
+  dynamic parseDynamic(String key, Map<String, dynamic> map,
+      {bool required = false}) {
+    dynamic rc;
+    if (!map.containsKey(key)) {
+      if (required) {
+        logger.error('missing int attribute "$key" in ${fullName()}');
+      }
+    } else {
+      rc = map[key];
+    }
+    return rc;
+  }
+
   /// Fetches an entry from a map addressed by a [key].
   /// An error is logged if [required] is true and the map does not contain the key.
   List<String> parseOptions(String key, Map<String, dynamic> map) {
index 98c68b76bbc0c7f3df208ee793ecc3e6c25db33e..01bdc64ad75403cc6e2e5bffad869aee58f8e73f 100644 (file)
@@ -46,13 +46,12 @@ class UserModel extends ModuleModel {
           {
             'column': 'user_role',
             'dataType': 'reference',
-            'label': 'Role',
+            'label': 'Rolle',
             'foreignKey': 'role.role_id role_name',
-            //'listType': 'explicite',
-            //'texts': ';-;Administrator;Benutzer;Gast;Verwalter',
-            //'values': ';;1;2;3;4',
             "listType": "dbColumn",
             "listOption": "all.role.list;role_name role_id;:role_name=%",
+            "options": "undef",
+            "defaultValue": "4",
           },
         ]
       },
@@ -110,7 +109,7 @@ class UserModel extends ModuleModel {
                 "filterType": "equals",
                 "column": "user_role",
                 "toolTip":
-                    "Filter bezüglich der Rolle der anzuzeigenden Einträge. '-' bedeutet keine Einschränkung"
+                    "Filter bezüglich der Rolle der anzuzeigenden Einträge. '-' bedeutet keine Einschränkung",
               }
             ]
           }
index d893f31bd761157b7a2fc05ee9d878620aca191c..3c623dbff99b562fddcee3de6179ad6833b2da1e 100644 (file)
@@ -27,9 +27,10 @@ class TextFieldModel extends FieldModel {
       String toolTip,
       DataType dataType,
       List<String> options,
+      dynamic defaultValue,
       BaseLogger logger)
       : super.direct(section, page, null, WidgetModelType.textField, name,
-            label, toolTip, dataType, options, logger);
+            label, toolTip, dataType, options, defaultValue, logger);
 
   /// Dumps the internal structure into a [stringBuffer]
   StringBuffer dump(StringBuffer stringBuffer) {
index 269964b068607dec5e1ef0f64e58cb1cf11315c4..c4fcce772f399fa8ac5a757c11d930381cce8020 100644 (file)
@@ -3,6 +3,7 @@ import 'package:dart_bones/dart_bones.dart';
 import 'model_base.dart';
 import 'page_model.dart';
 import 'section_model.dart';
+import 'combo_base_model.dart';
 
 /// A base class for items inside a page: SectionModel ButtonModel TextModel...
 abstract class WidgetModel extends ModelBase {
@@ -17,6 +18,13 @@ abstract class WidgetModel extends ModelBase {
     this.id = ++lastId;
   }
 
+  /// Returns whether the model must be completed asynchronously e.g. by
+  /// [Persistence].
+  bool get mustCompletedAsync =>
+      this is ComboBaseModel &&
+      ((this as ComboBaseModel).listType == ComboboxListType.dbColumn ||
+          (this as ComboBaseModel).listType == ComboboxListType.configuration);
+
   /// Dumps the internal structure into a [stringBuffer]
   StringBuffer dump(StringBuffer stringBuffer);
 
index dd28cc6245fd7c9309e5851ecc483c6db4b07aaf..9dda8ed4dd5795527100123c9c91d5fa8e3af87a 100644 (file)
@@ -1,8 +1,10 @@
 import 'package:dart_bones/dart_bones.dart';
 import 'package:flutter/material.dart';
-import 'package:flutter_bones/flutter_bones.dart';
-import 'package:flutter_bones/src/widget/callback_controller_bones.dart';
-import 'package:flutter_bones/src/widget/page_controller_bones.dart';
+
+import '../widget/callback_controller_bones.dart';
+import '../widget/page_controller_bones.dart';
+import '../persistence/persistence_cache.dart';
+import '../persistence/persistence.dart';
 
 /// Data class for storing parameter to build a page.
 class ApplicationData {
@@ -13,6 +15,7 @@ class ApplicationData {
   final AppBar Function(String title) appBarBuilder;
   final Drawer Function(dynamic context) drawerBuilder;
   final Persistence persistence;
+  PersistenceCache persistenceCache;
   String currentUser;
   int currentRoleId;
 
@@ -33,6 +36,7 @@ class ApplicationData {
       this.persistence, this.logger) {
     currentUser = 'Gast';
     currentRoleId = 100;
+    persistenceCache = PersistenceCache(persistence, logger);
   }
 
   /// Enforces a redraw of the caller of the current page.
diff --git a/lib/src/page/async_example_page.dart b/lib/src/page/async_example_page.dart
new file mode 100644 (file)
index 0000000..b50c8d8
--- /dev/null
@@ -0,0 +1,58 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_bones/flutter_bones.dart';
+
+class AsyncExamplePage extends StatefulWidget {
+  final ApplicationData pageData;
+
+  AsyncExamplePage(this.pageData, {Key key}) : super(key: key);
+
+  @override
+  AsyncExamplePageState createState() {
+    // AsyncExamplePageState.setPageData(pageData);
+    final rc = AsyncExamplePageState(pageData);
+
+    return rc;
+  }
+}
+
+class AsyncExamplePageState extends State<AsyncExamplePage> {
+  AsyncExamplePageState(this.pageData);
+
+  final ApplicationData pageData;
+
+  Future<bool> waiting;
+
+  @override
+  void initState() {
+    super.initState();
+    waiting = wait(millisec: 5000);
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+        appBar: pageData.appBarBuilder('Demo'),
+        drawer: pageData.drawerBuilder(context),
+        body: FutureBuilder(
+          future: waiting,
+          builder: (context, snapshot) {
+            if (snapshot.connectionState == ConnectionState.done) {
+              if (snapshot.hasData) {
+                return Text('Data found');
+              } else if (snapshot.hasError) {
+                return Text(snapshot.error);
+              } else {
+                return Text('missing data...');
+              }
+            } else {
+              return Text('missing data (2)...');
+            }
+          },
+        ));
+  }
+}
+
+Future<bool> wait({int millisec = 50}) async {
+  await Future.delayed(Duration(milliseconds: millisec));
+  return true;
+}
index 3ff9de216330ac492b72291d29491785c7a34e02..863fc82713e7b2c0d389a57503a8bd74bdf708b6 100644 (file)
@@ -54,13 +54,12 @@ class UserListPageState extends State<UserListPage> {
     controller.initialize();
     controller.buildWidgetList();
     controller.buildRows();
-    controller.widgetList
-        .waitForCompletion(controller)
-        .then((result) => setState(() => null));
+    controller.completeAsync();
   }
 
   @override
   Widget build(BuildContext context) {
+    controller.buildHandler(context);
     return Scaffold(
       appBar: applicationData.appBarBuilder('Benutzer'),
       drawer: applicationData.drawerBuilder(context),
index b78c0a1e88b5a42080258ea0d094c350164da88b..90de6beb64f159c2dbee4977fceb86f75c1168aa 100644 (file)
@@ -6,11 +6,11 @@ import 'package:flutter_bones/src/model/combo_base_model.dart';
 class CacheEntry {
   final String key;
   final CacheEntryType entryType;
-  final List list;
-  bool ready;
+  final ComboboxData comboboxData;
+  final data;
   bool oneTime;
-  CacheEntry(this.key, this.entryType, this.list,
-      {this.ready = false, this.oneTime = false});
+  CacheEntry(this.key, this.entryType,
+      {this.comboboxData, this.data, this.oneTime = false});
 }
 
 enum CacheEntryType {
@@ -22,34 +22,17 @@ enum CacheEntryType {
 /// A cache for objects coming from a persistence layer.
 /// Uses the least recently used algorithm to avoid overflow.
 class PersistenceCache {
-  static PersistenceCache _instance;
   static final regExpCombobox =
       RegExp(r'^\w+\.\w+\.\w+;\w+ \w+;( ?:\w+=\S*)*$');
   static final regExpRecord = RegExp(r'^\w+\.\w+\.\w+;( ?:\w+=\S*)+$');
   static int nextRecordNo = 0;
   final leastReasentlyUsed = <CacheEntry>[];
   final map = <String, CacheEntry>{};
-  final maxEntries;
-  final ApplicationData application;
-  BaseLogger logger;
+  final int maxEntries;
+  final BaseLogger logger;
+  final Persistence persistence;
 
-  factory PersistenceCache() {
-    return _instance;
-  }
-
-  /// True constructor (of the singleton instance).
-  /// [application]: offers all needed data: logger, persistence...
-  /// [maxEntries]: the maximal entries of the cache. If a new entry exceeds
-  /// that number the least recently used entry is removed before.
-  factory PersistenceCache.create(ApplicationData application,
-      {int maxEntries = 256}) {
-    _instance = PersistenceCache.internal(application, maxEntries);
-    return _instance;
-  }
-
-  PersistenceCache.internal(this.application, this.maxEntries) {
-    this.logger = this.application.logger;
-  }
+  PersistenceCache(this.persistence, this.logger, {this.maxEntries = 128});
 
   /// Returns a unique key for using record().
   String buildRecordKeyPrefix() {
@@ -57,6 +40,12 @@ class PersistenceCache {
     return rc;
   }
 
+  /// Removes all entries from the cache.
+  void clear() {
+    map.clear();
+    leastReasentlyUsed.clear();
+  }
+
   /// Returns the data (text, values) of a combobox specified by [key].
   /// If the data are unavailable (e.g. a asynchronous request is running) the
   /// result is null.
@@ -66,48 +55,50 @@ class PersistenceCache {
   /// If [hasUndef] is true the first combobox list entry is '-' with the value null.
   /// If [oneTime] is true the result is only used one time, the LRU algoritm is
   /// not used.
-  ComboboxData combobox(String key,
-      {bool hasUndef = false, bool oneTime = false}) {
+  Future<ComboboxData> comboboxAsync(String key,
+      {bool hasUndef = false, bool oneTime = false}) async {
     ComboboxData rc;
     if (regExpCombobox.firstMatch(key) == null) {
       logger.error('wrong key syntax: $key');
     } else if (map.containsKey(key)) {
       final entry = updateLRU(key);
-      if (entry.ready) {
-        rc = ComboboxData(entry.list[0], entry.list[1]);
-      }
+      rc = entry.comboboxData;
     } else {
-      final parts = key.split(';');
-      final idModuleName = parts[0].split('.');
-      final nameValue = parts[1].split(' ');
-      final params = parts[2].split(' ');
+      final sourceColumnsParams = key.split(';');
+      final idModuleName = sourceColumnsParams[0].split('.');
+      final nameValue = sourceColumnsParams[1].split(' ');
+      final params = sourceColumnsParams[2].split(' ');
       final params2 = <String, dynamic>{};
       params.forEach((element) {
         final keyValue = element.split('=');
         params2[keyValue[0]] = keyValue[1];
       });
-      final entry = CacheEntry(
-          key,
-          CacheEntryType.combobox,
-          [
-            hasUndef ? ['-'] : <String>[],
-            hasUndef ? <dynamic>[null] : <dynamic>[]
-          ],
+      final texts = hasUndef ? ['-'] : <String>[];
+      final values = hasUndef ? <dynamic>[null] : <dynamic>[];
+
+      final rows = await persistence.list(
+          module: idModuleName[1], sqlName: idModuleName[2], params: params2);
+      final entry = CacheEntry(key, CacheEntryType.combobox,
+          comboboxData: rc = ComboboxData(texts, values, WaitState.ready),
           oneTime: oneTime);
-      insert(entry);
-      application.persistence
-          .list(
-              module: idModuleName[1],
-              sqlName: idModuleName[2],
-              params: params2)
-          .then((rows) {
-        rows.forEach((row) {
-          entry.list[0].add(row[nameValue[0]]);
-          entry.list[1].add(row[nameValue[1]]);
-        });
-        entry.ready = true;
-        rc = ComboboxData(entry.list[0], entry.list[1]);
+      rows.forEach((row) {
+        texts.add(row[nameValue[0]]);
+        values.add(row[nameValue[1]]);
       });
+      insert(entry);
+    }
+    return rc;
+  }
+
+  /// Returns null or a [ComboboxData] instance specified by [key].
+  /// This is a synchronous method.
+  ComboboxData comboboxFromCache(String key) {
+    ComboboxData rc;
+    if (map.containsKey(key)) {
+      final entry = map[key];
+      if (entry.entryType == CacheEntryType.combobox) {
+        rc = entry.comboboxData;
+      }
     }
     return rc;
   }
@@ -146,15 +137,13 @@ class PersistenceCache {
   /// e.g. "x99.role.by_name:role_name=eve :excluded=22"
   /// If [oneTime] is true the result is only used one time, the LRU algoritm is
   /// not used.
-  Map record(String key, {bool oneTime = true}) {
+  Future<Map> recordAsync(String key, {bool oneTime = true}) async {
     Map rc;
     if (regExpRecord.firstMatch(key) == null) {
       logger.error('wrong key syntax: $key');
     } else if (map.containsKey(key)) {
       final entry = updateLRU(key);
-      if (entry.ready) {
-        rc = entry.list[0];
-      }
+      rc = entry.data;
     } else {
       final parts = key.split(';');
       final idModuleName = parts[0].split('.');
@@ -164,18 +153,15 @@ class PersistenceCache {
         final keyValue = element.split('=');
         params2[keyValue[0]] = keyValue[1];
       });
-      final entry =
-          CacheEntry(key, CacheEntryType.record, <Map>[null], oneTime: oneTime);
-      insert(entry);
-      application.persistence
-          .recordByParameter(
-              module: idModuleName[1],
-              sqlName: idModuleName[2],
-              parameters: params2)
-          .then((row) {
-        entry.list[0] = row;
-        entry.ready = true;
-      });
+      final map = await persistence.recordByParameter(
+          module: idModuleName[1],
+          sqlName: idModuleName[2],
+          parameters: params2);
+      if (map != null && map.isNotEmpty) {
+        final entry = CacheEntry(key, CacheEntryType.record,
+            data: rc = map, oneTime: oneTime);
+        insert(entry);
+      }
     }
     return rc;
   }
index 4879c64cf86f536248a6222dcd6f3b079c2e5cba..7236b4e90597da7453ea75a10f614a6018907bc7 100644 (file)
@@ -11,7 +11,7 @@ class BSettings {
   static BSettings lastInstance;
 
   /// Returns the singleton of BSetting.
-  factory BSettings() {
+  factory BSettings.create(BaseLogger logger) {
     final map = {
       'form.card.padding': '16.0',
       'form.gap.field_button.height': '16.0',
@@ -23,13 +23,16 @@ class BSettings {
         port: 58011,
         host: 'localhost',
         logger: logger);
-    final pageData = ApplicationData(BaseConfiguration(map, logger),
+    final applicationData = ApplicationData(BaseConfiguration(map, logger),
         BAppBar.builder, BonesDrawer.builder, persistence, logger);
     final rc = BSettings.internal(
-        BaseConfiguration(map, logger), pageData, persistence, logger);
+        BaseConfiguration(map, logger), applicationData, persistence, logger);
     return lastInstance = rc;
   }
 
+  factory BSettings() {
+    return lastInstance;
+  }
   BSettings.internal(
       this.configuration, this.pageData, this.persistence, this.logger);
 }
index a100337536ff05ded4d835eb7d9604d7535019a1..bdb87f4f2ece39a455a276d8282d508289428e63 100644 (file)
@@ -1,6 +1,9 @@
+import 'dart:async';
+
+import 'package:dart_bones/dart_bones.dart';
 import 'package:flutter/material.dart';
-import 'package:flutter_bones/flutter_bones.dart';
 
+import '../helper/helper_async.dart';
 import '../helper/string_helper.dart';
 import '../model/column_model.dart';
 import '../model/combo_base_model.dart';
@@ -8,6 +11,7 @@ import '../model/field_model.dart';
 import '../model/model_types.dart';
 import '../model/module_model.dart';
 import '../model/page_model.dart';
+import '../model/widget_model.dart';
 import '../page/application_data.dart';
 import 'callback_controller_bones.dart';
 import 'view.dart';
@@ -22,17 +26,38 @@ class PageControllerBones implements CallbackControllerBones {
   final GlobalKey<FormState> globalKey;
   final String pageName;
   PageModel page;
+  Future<bool> waitForCompletion;
   final ApplicationData applicationData;
   State parent;
   final BuildContext context;
   Iterable listRows;
   final textControllers = <String, TextEditingController>{};
   final comboboxDataMap = <String, ComboboxData>{};
+  final asyncCompletedModels = <ComboBaseModel>[];
+  int refreshCounter = 0;
 
   PageControllerBones(this.globalKey, this.parent, this.moduleModel,
       this.pageName, this.context, this.applicationData,
       [this.redrawCallback]);
 
+  Future<ComboboxData> buildComboboxDataFromPersistence(
+      ComboBaseModel model) async {
+    final rc = await applicationData.persistenceCache
+        .comboboxAsync(model.listOption, hasUndef: model.hasOption('undef'));
+    return rc;
+  }
+
+  /// This method should be called in each stateful widget of a page in the
+  /// build() method.
+  void buildHandler(BuildContext context) {
+    if (asyncCompletedModels.isNotEmpty && ++refreshCounter == 1) {
+      wait(millisec: 2000).then((any) => reload());
+    }
+    if (refreshCounter > 1) {
+      moduleModel.logger.log('buildHandler(): $refreshCounter', LEVEL_FINE);
+    }
+  }
+
   @override
   buildRows() {
     final persistence = applicationData.persistence;
@@ -72,33 +97,22 @@ class PageControllerBones implements CallbackControllerBones {
   }
 
   /// Prepares the widgetList: builds the widgets of the page.
-  /// [initialRow] is null or a map with the field values, e.g. { 'role_name': 'admin' ...}
+  /// [initialRow] is null or a map with the field values,
+  /// e.g. { 'role_name': 'admin', ...}
   void buildWidgetList([Map initialRow]) {
     widgetList.clear();
+    final view = View(moduleModel.logger);
     page.fields.forEach((model) {
+      if (initialRow != null && model is FieldModel) {
+        model.valueFromRow(initialRow);
+      }
       final value = initialRow == null ? null : initialRow[model.name];
-      completeModelByPersistence(model);
-      widgetList.addWidget(model.name,
-          View(moduleModel.logger).modelToWidget(model, this, value));
+      completeModels(model);
+      widgetList.addWidget(model.name, view.modelToWidget(model, this, value));
     });
   }
 
-  /// Completes database based components to the model, e.g. the list for
-  /// comboboxes.
-  void completeModelByPersistence(WidgetModel model) {
-    if (model is ComboBaseModel && model.listType != null) {
-      if (model.data == null) {
-        if (model.listType == ComboboxListType.explicite) {
-          model.data = ComboBaseModel.createByType(
-              model.dataType, model.texts, model.values);
-          model.data.waitState = WaitState.ready;
-        } else {
-          model.data = comboboxData(model.name);
-        }
-      }
-    }
-  }
-
+  @deprecated
   ComboboxData comboboxData(String name) {
     ComboboxData rc = comboboxDataMap.containsKey(name)
         ? comboboxDataMap[name]
@@ -127,6 +141,7 @@ class PageControllerBones implements CallbackControllerBones {
     return rc;
   }
 
+  @deprecated
   void comboboxDataDb(ComboBaseModel model, ComboboxData data) {
     // example: xxx.role.list;role_displayname role_id;:role_name=% :excluded=0'
     final keyColumnsParams = model.listOption.split(';');
@@ -140,12 +155,13 @@ class PageControllerBones implements CallbackControllerBones {
       });
     }
     applicationData.persistence
-        .list(module: nameModuleSql[1], sqlName: nameModuleSql[2], params: params)
+        .list(
+            module: nameModuleSql[1], sqlName: nameModuleSql[2], params: params)
         .then((rows) {
       rows.forEach((row) {
         data.texts.add(row[cols[0]]);
         var value = row[cols[1]];
-        if (value is String){
+        if (value is String) {
           value = StringHelper.fromString(value, model.dataType);
         }
         data.addValue(value);
@@ -159,6 +175,50 @@ class PageControllerBones implements CallbackControllerBones {
     });
   }
 
+  /// Completes the widgets asynchronously if needed.
+  void completeAsync() {
+    waitForCompletion = completeModelsAsync();
+  }
+
+  /// Completes models synchronously if possible or initializes the asynchronous
+  /// completion.
+  void completeModels(WidgetModel model) {
+    if (model is ComboBaseModel && model.listOption != null) {
+      model.data =
+          applicationData.persistenceCache.comboboxFromCache(model.listOption);
+      if (model.data == null) {
+        if (model.mustCompletedAsync) {
+          asyncCompletedModels.add(model);
+        } else {
+          model.completeSync();
+        }
+      }
+    }
+  }
+
+  Future<bool> completeModelsAsync() {
+    final completer = Completer<bool>();
+    Future.forEach(asyncCompletedModels, (ComboBaseModel model) async {
+      switch (model.listType) {
+        case ComboboxListType.dbColumn:
+          final data = await buildComboboxDataFromPersistence(model);
+          model.data = data;
+          break;
+        default:
+          moduleModel.logger.error(
+              'unexpected model in completeModelsAsync: ${model.fullName()}');
+          break;
+      }
+    }).then((any) {
+      completer.complete(true);
+      moduleModel.logger.log('completeModelsAsync() ready', LEVEL_FINE);
+    }).catchError((error) {
+      completer.completeError(error);
+    });
+
+    return completer.future;
+  }
+
   @override
   void dispose() {
     textControllers.values.forEach((controller) => controller.dispose());
@@ -180,6 +240,7 @@ class PageControllerBones implements CallbackControllerBones {
   }
 
   /// Returns a [WidgetList] filled with widgets
+  @deprecated
   WidgetList filterSet({@required String pageName}) {
     final rc = WidgetList('${page.fullName()}.widgets', moduleModel.logger);
     moduleModel
@@ -391,6 +452,12 @@ class PageControllerBones implements CallbackControllerBones {
     }
   }
 
+  /// Reloads the current page: the widgets will be constructed again.
+  void reload() {
+    final route = '/${moduleModel.name}/${page.name}';
+    Navigator.pushNamed(context, route);
+  }
+
   @override
   Widget searchButton() {
     final rc = View(moduleModel.logger)
index e297f35365892d86b94554a61cf67f676f1996f8..f6e13240fa0f990e0debcfa41e18a0cdec918ba9 100644 (file)
@@ -12,7 +12,7 @@ import '../model/text_model.dart';
 import '../model/widget_model.dart';
 import '../model/button_model.dart';
 import '../model/combo_base_model.dart';
-import 'callback_controller_bones.dart';
+import 'page_controller_bones.dart';
 import 'checkbox_list_tile_bone.dart';
 import 'dropdown_button_form_bone.dart';
 import 'raised_button_bone.dart';
@@ -37,7 +37,7 @@ class View {
   View.internal(this.logger);
 
   /// Creates a button from the [controller].
-  Widget button(ButtonModel model, CallbackControllerBones controller) {
+  Widget button(ButtonModel model, PageControllerBones controller) {
     Widget rc;
     rc = RaisedButtonBone(
       model.name,
@@ -52,7 +52,7 @@ class View {
 
   /// Creates a list of buttons from a list of [controllers].
   List<Widget> buttonList(
-      List<ButtonModel> buttonModels, CallbackControllerBones controller) {
+      List<ButtonModel> buttonModels, PageControllerBones controller) {
     final rc = <Widget>[];
     for (var model in buttonModels) {
       rc.add(button(model, controller));
@@ -61,7 +61,7 @@ class View {
   }
 
   /// Creates a checkbox from the [model] using the controller with the value [initialValue].
-  Widget checkbox(FieldModel model, CallbackControllerBones controller,
+  Widget checkbox(FieldModel model, PageControllerBones controller,
       [bool initialValue]) {
     final tristate = model.hasOption('undef');
     final rc = toolTip(
@@ -76,7 +76,40 @@ class View {
 
   /// Creates a combobox via the [controller] with an [initialValue].
   Widget combobox(
-      FieldModel model, CallbackControllerBones controller, initialValue) {
+      ComboBaseModel model, PageControllerBones controller, initialValue) {
+    Widget rc;
+    if (model.data != null && model.data.waitState == WaitState.ready) {
+      rc = comboboxRaw(model, controller, initialValue);
+    } else {
+      rc = FutureBuilder<bool>(
+        future: controller.waitForCompletion,
+        builder: (BuildContext context, AsyncSnapshot<bool> snapshot) {
+          if (snapshot.hasData) {
+            return comboboxRaw(model, controller, initialValue);
+          } else if (snapshot.hasError) {
+            return errorMessage(snapshot.error);
+          } else {
+            return Container(
+              alignment: Alignment.topCenter,
+              child: Row(children: <Widget>[
+                SizedBox(
+                  child: CircularProgressIndicator(),
+                  width: 60,
+                  height: 60,
+                ),
+                Text('Warten auf Vervollständigung von ${model.label}')
+              ]),
+            );
+          }
+        },
+      );
+    }
+    return rc;
+  }
+
+  /// Creates a combobox via the [controller] with an [initialValue].
+  Widget comboboxRaw(
+      FieldModel model, PageControllerBones controller, initialValue) {
     Widget rc;
     switch (model.dataType) {
       case DataType.bool:
@@ -104,7 +137,7 @@ class View {
   /// Creates a combobox via the [controller] depending on the type <T>
   /// with an [initialValue].
   Widget comboboxByType<T>(
-      FieldModel model, CallbackControllerBones controller, initialValue) {
+      FieldModel model, PageControllerBones controller, initialValue) {
     ComboboxData comboboxData = (model as ComboBaseModel).data;
     final items = <DropdownMenuItem<T>>[];
     if (comboboxData == null || comboboxData.texts.length == 0) {
@@ -132,7 +165,7 @@ class View {
 
   /// Creates a widget related to a [model] of type [DbReferenceModel]
   /// via the [controller] with an [initialValue].
-  Widget dbReference(DbReferenceModel model, CallbackControllerBones controller,
+  Widget dbReference(DbReferenceModel model, PageControllerBones controller,
       dynamic initialValue) {
     var rc;
     if (model.dataType == DataType.bool) {
@@ -170,7 +203,7 @@ class View {
 
   /// Converts a list of [models] into a list of [Widget]s.
   List<Widget> modelsToWidgets(
-      List<WidgetModel> models, CallbackControllerBones controller) {
+      List<WidgetModel> models, PageControllerBones controller) {
     final rc = <Widget>[];
     for (var model in models) {
       final widget = modelToWidget(model, controller);
@@ -183,9 +216,12 @@ class View {
 
   /// Converts a [model] into a [Widget] under control of [controller]
   /// with the [initialValue].
-  Widget modelToWidget(WidgetModel model, CallbackControllerBones controller,
+  Widget modelToWidget(WidgetModel model, PageControllerBones controller,
       [dynamic initialValue]) {
     Widget rc;
+    if (model is FieldModel && initialValue == null) {
+      initialValue = model.value ?? model.defaultValue;
+    }
     switch (model?.widgetModelType) {
       case WidgetModelType.textField:
         rc = textField(model, controller, initialValue);
@@ -233,7 +269,7 @@ class View {
   /// Returns a form with the properties given by the [model]
   /// [formKey] identifies the form. Used for form validation and saving.
   Form simpleForm(
-      {SectionModel model, CallbackControllerBones controller, Key formKey}) {
+      {SectionModel model, PageControllerBones controller, Key formKey}) {
     assert(formKey != null);
     final padding =
         widgetConfiguration.asFloat('form.card.padding', defaultValue: 16.0);
@@ -265,7 +301,7 @@ class View {
 
   /// Creates a form text field from the [model].
   Widget textField(
-      FieldModel model, CallbackControllerBones controller, initialValue) {
+      FieldModel model, PageControllerBones controller, initialValue) {
     final value =
         initialValue == null ? null : StringHelper.asString(initialValue);
     final textController = controller.textController(model.name);
index 434367a64f00cdc1696fe7f32dec7b8bff11e751..897787be72b9cdf7c0b3c62ee6ec5a2406d4166a 100644 (file)
@@ -1,7 +1,6 @@
 import 'package:dart_bones/dart_bones.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bones/flutter_bones.dart';
-import 'package:flutter_bones/src/model/combo_base_model.dart';
 
 import '../helper/string_helper.dart';
 import '../model/model_types.dart';
@@ -97,30 +96,4 @@ class WidgetList {
     widgets.clear();
     widgetMap.clear();
   }
-
-  /// Checks in a loop all open requests for completion.
-  /// If
-  Future waitForCompletion(PageControllerBones controller) async {
-    var again = true;
-    ComboboxData data;
-    // 30 sec:
-    int maxCount = 50 * 30;
-    while (again && maxCount > 0) {
-      again = false;
-      for (var model in waitCandidates) {
-        if (model is ComboBaseModel) {
-          data = controller.comboboxData(model.listOption);
-          if (data != null && data.waitState == WaitState.ready) {
-            model.data = data;
-            waitCandidates.remove(model);
-          } else {
-            again = true;
-          }
-        }
-      }
-      if (again) {
-        await wait(millisec: 20);
-      }
-    }
-  }
 }
index e4fdbd2ac11103120731fedc31bb7a86317eca6c..bbfc055e43fa16b8d2ece6a5fc24a5b40ad5d383 100644 (file)
@@ -130,6 +130,7 @@ void main() {
           'z',
           DataType.int,
           <String>[],
+          33,
           logger));
       page.addField(TextFieldModel.direct(
           null,
@@ -140,6 +141,7 @@ void main() {
           'z',
           DataType.int,
           <String>[],
+          44,
           logger));
       page.buttonByName('unknown');
       page.fieldByName('nothing');
index c05a1f42eada96a84b1ab1d5825e4fe008eaf052..fc352213cfbc8fb30525aff0977382048cc932a9 100644 (file)
@@ -11,6 +11,7 @@ import 'package:flutter_test/flutter_test.dart';
 
 void main() async {
   final logger = MemoryLogger(LEVEL_FINE);
+  Persistence persistence;
   PersistenceCache cache;
   setUpAll(() {
     final configuration = BaseConfiguration({
@@ -22,26 +23,19 @@ void main() async {
         'version': '1.0.0',
       }
     }, logger);
-    final persistence = RestPersistence.fromConfig(configuration, logger);
-    final appData =
-        ApplicationData(configuration, null, null, persistence, logger);
-    cache = PersistenceCache.create(appData, maxEntries: 2);
+    persistence = RestPersistence.fromConfig(configuration, logger);
+    cache = PersistenceCache(persistence, logger, maxEntries: 2);
   });
   group('basics', () {
     test('combobox', () async {
       logger.clear();
-      final cache2 = PersistenceCache();
+      final cache2 = PersistenceCache(persistence, logger);
       expect(cache2, isNotNull);
+      cache.clear();
       const key = 'id1.role.list;role_name role_id;:role_name=%';
       const key2 = 'id2.role.list;role_name role_id;:role_name=%';
-      var data = cache.combobox(key, hasUndef: true);
-      var data2 = cache.combobox(key2, hasUndef: false);
-      expect(data, isNull);
-      expect(data2, isNull);
-      await wait(millisec: 200);
-      data = cache.combobox(key);
-      await wait(millisec: 200);
-      data2 = cache.combobox(key2);
+      var data = await cache.comboboxAsync(key, hasUndef: true);
+      var data2 = await cache.comboboxAsync(key2, hasUndef: false);
       expect(logger.errors.length, equals(0));
       expect(data, isNotNull);
       expect(data.texts.length, greaterThan(4));
@@ -52,39 +46,51 @@ void main() async {
       expect(data.texts.length, equals(data2.valuesLength + 1));
       expect(cache.leastReasentlyUsed.length, equals(2));
       const key3 = 'id3.role.list;role_name role_id;:role_name=%';
-      var data3 = cache.combobox(key3, hasUndef: false);
-      expect(data3, isNull);
+      var data3 = await cache.comboboxAsync(key3, hasUndef: false);
+      expect(data3, isNotNull);
       expect(cache.leastReasentlyUsed.length, equals(2));
       expect(cache.map.containsKey(key), isFalse);
       expect(cache.map.containsKey(key2), isTrue);
       expect(cache.deleteEntry(key3), isTrue);
     });
-    test('error', () {
+    test('error', () async {
       logger.clear();
+      cache.clear();
       const key = 'id1.role.list;role_name+role_id;:role_name=%';
-      expect(cache.combobox(key), isNull);
+      final data = await cache.comboboxAsync(key);
+      expect(data, isNull);
       expect(logger.errors.length, equals(1));
-      expect(logger.contains('wrong key syntax: id1.role.list;role_name+role_id;:role_name=%'), isTrue);
+      expect(
+          logger.contains(
+              'wrong key syntax: id1.role.list;role_name+role_id;:role_name=%'),
+          isTrue);
     });
     test('record', () async {
       logger.clear();
-      final cache2 = PersistenceCache();
-      expect(cache2, isNotNull);
+      final cache = PersistenceCache(persistence, logger);
+      cache.clear();
+      expect(cache, isNotNull);
       const key = 'id1.role.by_role_name;:role_name=Administrator :excluded=0';
       const key2 = 'id2.role.by_role_name;:role_name=Admistrator :excluded=1';
-      var data = cache.record(key, oneTime: true);
-      var data2 = cache.record(key2, oneTime: false);
-      expect(data, isNull);
-      expect(data2, isNull);
-      await wait(millisec: 200);
-      data = cache.record(key);
-      data2 = cache.record(key2);
-      await wait(millisec: 200);
+      final data = await cache.recordAsync(key);
+      final data2 = await cache.recordAsync(key2);
       expect(logger.errors.length, equals(0));
       expect(data, isNotNull);
       expect(data.containsKey('role_id'), isTrue);
-      expect(data2.isEmpty, isTrue);
+      expect(data2, isNull);
+    });
+    test('comboboxFromCache', () async {
+      logger.clear();
+      final cache = PersistenceCache(persistence, logger);
+      cache.clear();
+      expect(cache, isNotNull);
+      const key = 'id1.role.list;role_name role_id;:role_name=%';
+      var data = await cache.comboboxAsync(key, hasUndef: true);
+      final data2 = cache.comboboxFromCache(key);
+      expect(logger.errors.length, equals(0));
+      expect(data, isNotNull);
+      expect(data2, isNotNull);
+      expect(data2, equals(data));
     });
   });
 }
-