From: Hamatoma Date: Thu, 5 Nov 2020 22:05:05 +0000 (+0100) Subject: module user works X-Git-Url: https://gitweb.hamatoma.de/?a=commitdiff_plain;h=85d1ce97ee1be6436df0f29991d8d58d5d52215e;p=flutter_bones.git module user works --- diff --git a/lib/app.dart b/lib/app.dart index 12883f7..b40f123 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -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 = { '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 { visualDensity: VisualDensity.adaptivePlatformDensity, ), initialRoute: '/user/list', + //initialRoute: '/async', onGenerateRoute: _getRoute, ); } @@ -50,6 +52,9 @@ Route _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; diff --git a/lib/src/model/column_model.dart b/lib/src/model/column_model.dart index b994677..69ecb4c 100644 --- a/lib/src/model/column_model.dart +++ b/lib/src/model/column_model.dart @@ -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 = diff --git a/lib/src/model/combo_base_model.dart b/lib/src/model/combo_base_model.dart index f7a6c26..7e4880b 100644 --- a/lib/src/model/combo_base_model.dart +++ b/lib/src/model/combo_base_model.dart @@ -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() { diff --git a/lib/src/model/combobox_model.dart b/lib/src/model/combobox_model.dart index 6a27d4c..da2a948 100644 --- a/lib/src/model/combobox_model.dart +++ b/lib/src/model/combobox_model.dart @@ -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); diff --git a/lib/src/model/db_reference_model.dart b/lib/src/model/db_reference_model.dart index 48137b5..2032c70 100644 --- a/lib/src/model/db_reference_model.dart +++ b/lib/src/model/db_reference_model.dart @@ -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; diff --git a/lib/src/model/field_model.dart b/lib/src/model/field_model.dart index 66a7bb2..6e2a19e 100644 --- a/lib/src/model/field_model.dart +++ b/lib/src/model/field_model.dart @@ -14,9 +14,10 @@ abstract class FieldModel extends WidgetModel { DataType dataType; List options; FilterType filterType; + dynamic _value; + dynamic defaultValue; final Map 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 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 diff --git a/lib/src/model/model_base.dart b/lib/src/model/model_base.dart index 7756b60..947c039 100644 --- a/lib/src/model/model_base.dart +++ b/lib/src/model/model_base.dart @@ -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 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 parseOptions(String key, Map map) { diff --git a/lib/src/model/standard/user_model.dart b/lib/src/model/standard/user_model.dart index 98c68b7..01bdc64 100644 --- a/lib/src/model/standard/user_model.dart +++ b/lib/src/model/standard/user_model.dart @@ -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", } ] } diff --git a/lib/src/model/text_field_model.dart b/lib/src/model/text_field_model.dart index d893f31..3c623db 100644 --- a/lib/src/model/text_field_model.dart +++ b/lib/src/model/text_field_model.dart @@ -27,9 +27,10 @@ class TextFieldModel extends FieldModel { String toolTip, DataType dataType, List 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) { diff --git a/lib/src/model/widget_model.dart b/lib/src/model/widget_model.dart index 269964b..c4fcce7 100644 --- a/lib/src/model/widget_model.dart +++ b/lib/src/model/widget_model.dart @@ -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); diff --git a/lib/src/page/application_data.dart b/lib/src/page/application_data.dart index dd28cc6..9dda8ed 100644 --- a/lib/src/page/application_data.dart +++ b/lib/src/page/application_data.dart @@ -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 index 0000000..b50c8d8 --- /dev/null +++ b/lib/src/page/async_example_page.dart @@ -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 { + AsyncExamplePageState(this.pageData); + + final ApplicationData pageData; + + Future 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 wait({int millisec = 50}) async { + await Future.delayed(Duration(milliseconds: millisec)); + return true; +} diff --git a/lib/src/page/user/user_list_page.dart b/lib/src/page/user/user_list_page.dart index 3ff9de2..863fc82 100644 --- a/lib/src/page/user/user_list_page.dart +++ b/lib/src/page/user/user_list_page.dart @@ -54,13 +54,12 @@ class UserListPageState extends State { 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), diff --git a/lib/src/persistence/persistence_cache.dart b/lib/src/persistence/persistence_cache.dart index b78c0a1..90de6be 100644 --- a/lib/src/persistence/persistence_cache.dart +++ b/lib/src/persistence/persistence_cache.dart @@ -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 = []; final map = {}; - 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 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 = {}; params.forEach((element) { final keyValue = element.split('='); params2[keyValue[0]] = keyValue[1]; }); - final entry = CacheEntry( - key, - CacheEntryType.combobox, - [ - hasUndef ? ['-'] : [], - hasUndef ? [null] : [] - ], + final texts = hasUndef ? ['-'] : []; + final values = hasUndef ? [null] : []; + + 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 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, [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; } diff --git a/lib/src/private/bsettings.dart b/lib/src/private/bsettings.dart index 4879c64..7236b4e 100644 --- a/lib/src/private/bsettings.dart +++ b/lib/src/private/bsettings.dart @@ -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); } diff --git a/lib/src/widget/page_controller_bones.dart b/lib/src/widget/page_controller_bones.dart index a100337..bdb87f4 100644 --- a/lib/src/widget/page_controller_bones.dart +++ b/lib/src/widget/page_controller_bones.dart @@ -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 globalKey; final String pageName; PageModel page; + Future waitForCompletion; final ApplicationData applicationData; State parent; final BuildContext context; Iterable listRows; final textControllers = {}; final comboboxDataMap = {}; + final asyncCompletedModels = []; + int refreshCounter = 0; PageControllerBones(this.globalKey, this.parent, this.moduleModel, this.pageName, this.context, this.applicationData, [this.redrawCallback]); + Future 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 completeModelsAsync() { + final completer = Completer(); + 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) diff --git a/lib/src/widget/view.dart b/lib/src/widget/view.dart index e297f35..f6e1324 100644 --- a/lib/src/widget/view.dart +++ b/lib/src/widget/view.dart @@ -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 buttonList( - List buttonModels, CallbackControllerBones controller) { + List buttonModels, PageControllerBones controller) { final rc = []; 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( + future: controller.waitForCompletion, + builder: (BuildContext context, AsyncSnapshot 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: [ + 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 /// with an [initialValue]. Widget comboboxByType( - FieldModel model, CallbackControllerBones controller, initialValue) { + FieldModel model, PageControllerBones controller, initialValue) { ComboboxData comboboxData = (model as ComboBaseModel).data; final items = >[]; 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 modelsToWidgets( - List models, CallbackControllerBones controller) { + List models, PageControllerBones controller) { final rc = []; 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); diff --git a/lib/src/widget/widget_list.dart b/lib/src/widget/widget_list.dart index 434367a..897787b 100644 --- a/lib/src/widget/widget_list.dart +++ b/lib/src/widget/widget_list.dart @@ -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); - } - } - } } diff --git a/test/model/model_test.dart b/test/model/model_test.dart index e4fdbd2..bbfc055 100644 --- a/test/model/model_test.dart +++ b/test/model/model_test.dart @@ -130,6 +130,7 @@ void main() { 'z', DataType.int, [], + 33, logger)); page.addField(TextFieldModel.direct( null, @@ -140,6 +141,7 @@ void main() { 'z', DataType.int, [], + 44, logger)); page.buttonByName('unknown'); page.fieldByName('nothing'); diff --git a/test/persistence_cache_test.dart b/test/persistence_cache_test.dart index c05a1f4..fc35221 100644 --- a/test/persistence_cache_test.dart +++ b/test/persistence_cache_test.dart @@ -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)); }); }); } -