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';
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));
visualDensity: VisualDensity.adaptivePlatformDensity,
),
initialRoute: '/user/list',
+ //initialRoute: '/async',
onGenerateRoute: _getRoute,
);
}
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;
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 =
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() {
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);
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;
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)
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;
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
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) {
{
'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",
},
]
},
"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",
}
]
}
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) {
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 {
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);
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 {
final AppBar Function(String title) appBarBuilder;
final Drawer Function(dynamic context) drawerBuilder;
final Persistence persistence;
+ PersistenceCache persistenceCache;
String currentUser;
int currentRoleId;
this.persistence, this.logger) {
currentUser = 'Gast';
currentRoleId = 100;
+ persistenceCache = PersistenceCache(persistence, logger);
}
/// Enforces a redraw of the caller of the current page.
--- /dev/null
+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;
+}
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),
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 {
/// 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() {
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.
/// 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;
}
/// 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('.');
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;
}
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',
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);
}
+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';
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';
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;
}
/// 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]
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(';');
});
}
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);
});
}
+ /// 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());
}
/// Returns a [WidgetList] filled with widgets
+ @deprecated
WidgetList filterSet({@required String pageName}) {
final rc = WidgetList('${page.fullName()}.widgets', moduleModel.logger);
moduleModel
}
}
+ /// 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)
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';
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,
/// 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));
}
/// 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(
/// 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:
/// 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) {
/// 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) {
/// 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);
/// 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);
/// 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);
/// 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);
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';
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);
- }
- }
- }
}
'z',
DataType.int,
<String>[],
+ 33,
logger));
page.addField(TextFieldModel.direct(
null,
'z',
DataType.int,
<String>[],
+ 44,
logger));
page.buttonByName('unknown');
page.fieldByName('nothing');
void main() async {
final logger = MemoryLogger(LEVEL_FINE);
+ Persistence persistence;
PersistenceCache cache;
setUpAll(() {
final configuration = BaseConfiguration({
'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));
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));
});
});
}
-