JavaScript Extension Development API for Oracle Visual Builder Cloud Service - Classic Applications

Source: pages.dt/js/api/Page.js

define([
    'core/js/api/Listenable',
    'core/js/api/ListenerBuilder',
    'core/js/api/utils/RandomUtils',
    'core/js/api/utils/StringUtils',
    'core/js/router/PageNavigationRouterStateObserver',
    'core.dt/js/api/Utils',
    'core.dt/js/api/application/ApplicationSupport',
    'customcode.dt/js/CustomCodePageIntegration',
    'layout/js/api/LayoutConstants',
    'pagedesigner.dt/js/DesignerUtils',
    'pagedesigner.dt/js/api/grid/GridUtils',
    'pages.dt/js/ViewFactory',
    'pages.dt/js/api/Navigation',
    'pages.dt/js/api/PageType',
    'pages.dt/js/api/ViewGeneratorSupport',
    'translations/js/api/I18n',
    'viewmodel/js/api/context/ContextualData',
    'viewmodel.dt/js/api/ViewModelDefinition'
], function (
        Listenable,
        ListenerBuilder,
        RandomUtils,
        StringUtils,
        pageNavigationRouterStateObserver,
        CoreUtils,
        ApplicationSupport,
        CustomCodePageIntegration,
        LayoutConstants,
        DesignerUtils,
        GridUtils,
        ViewFactory,
        navigation,
        PageType,
        ViewGeneratorSupport,
        I18n,
        ContextualData,
        ViewModelDefinition
        ) {

    'use strict';

    /**
     * Represents the Application's page object.
     *
     * <p>{@link pages.dt/js/api/Page Page} is one of the essential
     * structural elements of the ABCS applications. The application consists
     * of set of pages. Every {@link pages.dt/js/api/Page Page} holds created UI
     * components in a tree structure - every page object has a top level
     * {@link pages.dt/js/api/View View} as a root for the whole component hierarchy
     * in the page.</p>
     *
     * <p>Every {@link pages.dt/js/api/Page Page} has defined {@link pages.dt/js/api/PageType PageType}.
     * Types can be stated for free designing purposes, for CRUD scenarios etc.
     * If the {@link pages.dt/js/api/Page page} is of {@link pages.dt/js/api/PageType.CREATE PageType.CREATE},
     * {@link pages.dt/js/api/PageType.EDIT PageType.EDIT} or
     * {@link pages.dt/js/api/PageType.DETAILS PageType.DETAILS} type, the page
     * is always bound to the specific {@link entity/js/api/Entity entity} then.</p>
     *
     * @AbcsExtension stable
     * @version 16.3.5
     *
     * @exports pages.dt/js/api/Page
     * @private
     * @constructor
     *
     * @param {string} id the page unique ID
     * @param {string|Translatable} displayName the page displayName
     * @param {boolean} isModalPage flag whether the page is modal page or not
     * @param {PageType} type type of the page
     * @param {object} viewModelDefinition
     * @param {object} viewDefinition
     * @param {Pages} Pages
     *
     * @see {@link pages.dt/js/api/Pages Pages} module for management of existing {@link pages.dt/js/api/Page page}s
     * @see {@link pages.dt/js/api/PageType PageType} for description of supported page's types
     * @see {@link pages.dt/js/api/View View} as the representation of UI components within page structure
     */
    var Page = function (id, displayName, isModalPage, type, viewModelDefinition, viewDefinition, Pages) {
        AbcsLib.checkThis(this);
        Listenable.apply(this, arguments);

        var self = this;

        self._Pages = Pages; //need to pass & store Pages module since the constructor is called sync from within Pages constructor (require cycle)

        self._id = id;

        self.__updateFromDefinition(displayName, isModalPage, type, viewModelDefinition, viewDefinition);

        self._refreshPageTask = CoreUtils.createTask(function (views) {
            self.refresh(false, views); //refresh the page DOM and model
        }, 0);

        self._refreshPageTaskLazy = CoreUtils.createTask(function () {
            self.refresh(); //refresh the page DOM and model
        }, 500);

        self._storePageTask = CoreUtils.createTask(function (pageDefinitionChange) {
            self.fireEvent(Page.__EVENT_PAGE_CHANGED, self, pageDefinitionChange); //cause to be stored to backend
        }, 0);

        self.errors2notes = {
            dom: {},
            model: {},
            binding: {},
            misc: {}
        };
    };
    AbcsLib.mixin(Page, Listenable);

    Page._LOGGER = Logger.get('pages.dt/js/api/Page');

    Page._GLOBAL_VIEW_MODEL_VERSION = 1;

    //It's not deprecated to listen on the pageChanged event dirrectly.
    //  You should rather listen on Pages module.
    Page.__EVENT_PAGE_CHANGED = 'pageChanged';
    Page.EVENT_PAGE_UPDATED = 'pageUpdated';

    Page.prototype.saveChanges = function (pageDefinitionChanged) {
        var hasPageDefChanged = arguments.length === 0 || pageDefinitionChanged === true;
        this._storePageTask.schedule(hasPageDefChanged);
    };

    Page.prototype._refreshAndStore = function (view) {
        if (this.isActive()) {
            this._refreshPageTask.schedule(view); //recreate the page is it is active (visible)
        }
        this._storePageTask.schedule(true);
    };

    /**
     * Gets ID of the {@link pages.dt/js/api/Page Page}.
     *
     * <p>This ID is unique across the whole ABCS application. So the ID
     * as such can be used for referencing, navigation and similar
     * purposes.</p>
     *
     * @AbcsExtension stable
     * @version 16.3.5
     *
     * @returns {string} page's ID
     *
     * @see {@link pages.dt/js/api/Pages Pages} module for management of existing {@link pages.dt/js/api/Page page}s
     *
     * @example <caption>Gets ID of the page and uses it for navigation purposes</caption>
     * // we assume that you have the page instance referenced by 'page'
     * var pageId = page.getId();
     * // navigate to the particular page
     * Abcs.Pages().navigateToPage(pageId);
     */
    Page.prototype.getId = function () {
        AbcsLib.checkParameterCount(arguments, 0);
        return this._id;
    };

    /**
     * Gets display name of the {@link pages.dt/js/api/Page Page}.
     *
     * <p>Page's display names needn't be unique. This naming is usually
     * used for Designer purposes to address {@link pages.dt/js/api/Page Page}
     * meaningfully to users.</p>
     *
     * @AbcsExtension stable
     * @version 16.3.5
     *
     * @returns {Translatable} page's display name
     *
     * @example <caption>Gets display name of the active page</caption>
     * // gets the active page
     * var activePage = Navigation.getActivePage();
     * // will yield display name
     * activePage.getDisplayName();
     */
    Page.prototype.getDisplayName = function () {
        AbcsLib.checkParameterCount(arguments, 0);
        return this._displayName;
    };

    /**
     * Sets the page display name.
     *
     * @param {String|Translatable} displayName
     */
    Page.prototype.setDisplayName = function (displayName) {
        var wasTranslatable = I18n.isTranslatable(this._displayName);
        this._displayName = wasTranslatable ? this._displayName :
                I18n.createTranslatable(I18n.key(I18n.Type.Page, this._id, I18n.Suffix.DisplayName));
        this._displayName.store(displayName.toString());
        // refresh the viewModel instance since the display name is used in the PageViewModel
        this._viewModelDefinition.refreshArchetype();
        this.saveChanges(!wasTranslatable);
    };

    /**
     * Gets the flag whether the page is a 'modal page' or not.
     *
     * @returns {boolean}
     */
    Page.prototype.isModalPage = function () {
        return this._isModalPage;
    };

    /**
     * Gets type of the {@link pages.dt/js/api/Page Page} (like LANDING, CREATE, EDIT, DETAIL).
     *
     * <p>{@link pages.dt/js/api/PageType.CREATE PageType.CREATE},
     * {@link pages.dt/js/api/PageType.EDIT PageType.EDIT},
     * {@link pages.dt/js/api/PageType.DETAILS PageType.DETAILS}
     * are the types of pages always bound to a specific BO record.
     * {@link pages.dt/js/api/PageType.LANDING PageType.LANDING}
     * type represents initially blank pages used for free designing.</p>
     *
     * @AbcsExtension stable
     * @version 16.3.5
     *
     * @returns {pages.dt/js/api/PageType} type of the page
     *
     * @see {@link pages.dt/js/api/PageType PageType} for enumeration of all possible page types
     *
     * @example <caption>Gets type of the active page</caption>
     * // gets the active page
     * var activePage = Navigation.getActivePage();
     * // will yield page's type
     * activePage.getType();
     */
    Page.prototype.getType = function () {
        AbcsLib.checkParameterCount(arguments, 0);
        return this._type;
    };

     /**
     * Sets the type of the page (like LANDING, CREATE, EDIT, DETAIL).
     *
     * @param {PageType} type of the page
     */
    Page.prototype.setType = function (type) {
        this._type = type;
    };

    /**
     * Sets the flag whether the page is a 'modal page' or not.
     *
     * @param {boolean} isModalPage
     */
    Page.prototype.setIsModalPage = function (isModalPage) {
        this._isModalPage = isModalPage;
        this.saveChanges(true);
    };

    /**
     * Gets the information whether the page is home (initial) page.
     * @returns {Boolean}
     */
    Page.prototype.isHomepage = function () {
        return this._Pages.getHomepageId() === this.getId();
    };

    /**
     * Returns true if the page is currently shown in the application content area.
     * @returns {Boolean}
     */
    Page.prototype.isActive = function () {
        return navigation.getActivePage() === this;
    };

    /**
     * Gets a ViewFactory which can create new Views in this page.
     *
     * @returns {ViewFactory}
     */
    Page.prototype.getViewFactory = function () {
        return this._viewFactory;
    };

    /**
     * Gets the root View object of the page.
     *
     * @returns {View}
     */
    Page.prototype.getView = function () {
        return this._view;
    };

    /**
     * Gets the page view model definition.
     *
     * @returns {ViewModelDefinition} the description of the view model
     */
    Page.prototype.getViewModelDefinition = function () {
        return this._viewModelDefinition;
    };

    /**
     * Gets an instance of the page view model.
     *
     * @param {boolean} contextWillBeInitializedLater - TODO David add description
     */
    Page.prototype.getViewModel = function (/*contextWillBeInitializedLater*/) {
        // XXX: this is DT implementation of the method; in RT it should just
        // load static JS file which represents the view model
        return this._viewModelDefinition.getViewModel(/*contextWillBeInitializedLater*/);
    };

    Page.prototype.getViewModelCode = function () {
        return this._viewModelDefinition.getViewModelCode();
    };

    /**
     * Finds a View object by its ID.
     *
     * The IDs of the view objects are propagated to the top-most wrapping DOM elements.
     * One may then go through the page DOM tree, get a particular DOM node and
     * find the corresponding view object by passing its ID to this method.
     *
     * @param {type} viewId
     * @returns {undefined}
     */
    Page.prototype.findView = function (viewId) {
        return this._viewIdToViewMap[viewId];
    };

    Page.prototype.findSameView = function (view) {
        var result = null;
        if (view === this.findView(view.getId())) {
            result = view;
        }
        return result;
    };

    /**
     * Finds all {@link View}s on the page with the given {@name archetypeType}.
     *
     * <p>
     * Iterates through the all {@link View}s on the page and collects all that
     * are using archetype type given by the parameter.
     * </p>
     *
     * @param {ArchetypeType} archetypeType if ommited the method will return all views w/ any archetype
     * @returns {View[]}
     */
    Page.prototype.findViews = function (archetypeType) {
        return this._findViews(this.getView(), archetypeType);
    };

    Page.prototype._findViews = function (view, archetypeType) {
        var self = this;
        var views = [];

        // Iterates over all child views
        if (view.hasChildren()) {
            view.getChildren().forEach(function (childView) {

                // And for each of them recursively collects results
                var childViews = self._findViews(childView, archetypeType);
                childViews.forEach(function (child) {
                    views.push(child);
                });

            });
        }

        var archetype = view.getArchetype();
        if (archetype && (!archetypeType || (archetype.getType() === archetypeType))) {
            views.push(view);
        }

        return views;
    };

    Page.prototype.getDefinition = function () {
        var def = {};
        //page id
        def.id = this.getId();

        //page display name
        var dnValue = this.getDisplayName();
        def.displayName = I18n.isTranslatable(dnValue) ? dnValue.toJSON() : dnValue;

        //page is modal page flag
        def.isModalPage = this.isModalPage();

        //page type
        def.type = this.getType();

        //page view model
        def.viewModel = this.getViewModelDefinition().getDefinition();

        //views structure
        def.view = this.getView().getDefinition();

        return def;
    };

    Page.PAGE_CUSTOM_MODEL_INIT_LIMIT = 5 * 1000; //ms

    /**
     * Replaces the content of the <section> element in the main
     * application template by content of the navigated page.
     *
     * Called upon navigation to this page.
     *
     * @param {ContextualData} contextualData
     * @param {Boolean} returningToPreviousPage
     * @param {View[]} views
     */
    Page.prototype._createDynamicPage = function (contextualData, returningToPreviousPage, views) {
        if (ApplicationSupport.isInGenerationMode()) {
            throw new Error('can\'t call Page._createDynamicPage() in static app generation mode!');
        }

        Page._LOGGER.trace('_createDynamicPage( ' + JSON.stringify(contextualData) + ') called on ' + this.getId());

        var self = this;

        //registers all used ad-hoc web components from the page
        return CustomCodePageIntegration.prepareCustomComponents(self).then(function (errors) {

            self._updateErrorNotes(self.errors2notes.model, errors);

            //check for modified page model. In this case the whole page needs to be rebound.
            var currentPageModelInstanceVersion = self.getViewModelDefinition().getModelInstanceVersion();

            //check if we've previously bound a different version of the page model se we can't incrementally rebind
            //the modified views, we need to rebind the whole page.
            //
            //TBD we may make it more clever later - for example if somethhing is added to the viewmodel
            //there's no need to rebind the already bound views etc... some sort of true incremental update
            //
            //The intended meaning of the model version is that it should be incremented
            //whenever an incompatible change is done to the model
            //
            var forcePageModelRebind = self._lastBoundPageModelInstanceVersion !== currentPageModelInstanceVersion;

            self._lastBoundPageModelInstanceVersion = currentPageModelInstanceVersion;

            var viewsToRender;
            //render whole page or individual views
            if (forcePageModelRebind) {
                Page._LOGGER.trace('Page view model version has changed to ' + currentPageModelInstanceVersion + ' from ' + self._lastBoundPageModelInstanceVersion + ' => forcing page view model rebind to the whole page.');
                self._rebuildPageDOM();
            } else {
                var existingViews = self._filterOutDeletedViews(views);
                var viewsWithOneDOMElement = Page._filterToOneDOMElementViews(existingViews);
                viewsToRender = Page._filterToParents(viewsWithOneDOMElement);
                if (viewsToRender && viewsToRender.length) {
                    for (var i = 0; i < viewsToRender.length; i++) {
                        var view = viewsToRender[i];
                        var viewId = view.getId();
                        if (viewId === LayoutConstants.PAGE_TOP_VIEW_ID) {
                            //page top view regeneration
                            viewsToRender = null;
                            self._rebuildPageDOM();
                            break;
                        } else {
                            var $viewElement = $('[data-view-id="' + viewId + '"]');
                            if (!AbcsLib.Test.isUnitTestsEnv() && $viewElement.length === 0) {
                                viewsToRender = null;
                                self._rebuildPageDOM();
                                break;
                            } else {
                                self._rebuildViewDOM(view, $viewElement);
                            }
                        }
                    }
                } else {
                    self._rebuildPageDOM();
                }
            }

            var dtModel = self.getViewModel();

            //if self function had been called from the Page.activated and contextualData
            //have been passed, lets initialize the view model
            self._initializeGeneratedViewModel(dtModel, contextualData, returningToPreviousPage);

            //apply knockout binding to the modified views

            var viewsToRebind = self._prepareViewsToRebind(viewsToRender, forcePageModelRebind);

            self._applyBindings(viewsToRebind, dtModel);

            //fire page changes event
            self.fireEvent(Page.EVENT_PAGE_UPDATED, self);

            Page._LOGGER.trace('\'' + self.getId() + '\' dynamically created.');

            return dtModel;
        });

    };

    Page.prototype._updateErrorNotes = function(error2note, errors) {
        require(['base/js/ui/UiUtils'], function (UiUtils) {

            //add new notes
            errors.forEach(function (error) {
                var noteHandle = error2note[error];
                if (!noteHandle) {
                    //new error => show note
                    noteHandle = UiUtils.showErrorNote(error);
                    error2note[error] = noteHandle;
                }
            });

            //remove old ones
            var keysToRemove = [];
            for (var key in error2note) {
                if (errors.indexOf(key) === -1) {
                    //removed
                    keysToRemove.push(key);
                }
            }
            keysToRemove.forEach(function(keyToRemove) {
                var noteHandle = error2note[keyToRemove];
                delete error2note[keyToRemove];
                noteHandle.close();
            });

        });
    };

    Page.prototype._showErrorNote = function (msg) {
        require(['base/js/ui/UiUtils'], function (UiUtils) {
            UiUtils.showErrorNote(msg);
        });
    };

    /**
     * @TODO - nowhere used - is this method required?
     */
    Page.prototype._closeErrorNote = function () {
        require(['base/js/ui/UiUtils'], function (UiUtils) {
            UiUtils.closeAllErrorNotes();
        });
    };

    //apply page model binding to all modified views
    Page.prototype._applyBindings = function (viewsToRender, customDTModel) {
        var self = this;

        var errors = [];
        viewsToRender.forEach(function (view) {
            var viewId = view.getId();
            var viewDOMElement = document.querySelector('[data-view-id="' + viewId + '"]');

            if (!viewDOMElement && AbcsLib.Test.isUnitTestsEnv()) {
                return ; //ignore
            }

            try {
                ko.applyBindings(customDTModel, viewDOMElement);

                Page._LOGGER.trace('Re-bound view model for view ' + viewId);
            } catch (e) {
                //binding error - can be either internal error or caused custom model being live edited
                errors.push('Can\'t bind view: ' + e.message);

                Page._LOGGER.exception(e, 'Failed to bind view model');
            }
        });
        self._updateErrorNotes(self.errors2notes.binding, errors);
    };

    Page.prototype._prepareViewsToRebind = function (viewsToRender, forcePageModelRebind) {
        var result = null;
        if (!viewsToRender || !viewsToRender.length || forcePageModelRebind) {
            result = [this.getView()];
        } else {
            result = viewsToRender;
        }
        return result;
    };

    Page.prototype._filterOutDeletedViews = function (views) {
        var result = [];
        if (views) {
            if (this._deletedViews && this._deletedViews.length) {
                for (var i = 0; i < views.length; i++) {
                    var view = views[i];
                    if (this._deletedViews.indexOf(view) === -1) {
                        result.push(view);
                    }
                }
                this._deletedViews = [];
            } else {
                result = views;
            }
        }
        return result;
    };

    Page._filterToOneDOMElementViews = function (views) {
        var result = [];
        if (views && views.length) {
            for (var i = 0; i < views.length; i++) {
                var view = views[i];
                var viewElements = document.querySelectorAll('[data-view-id="' + view.getId() + '"]');
                var viewElementsLength = viewElements.length;
                if (viewElementsLength > 0) {
                    if (viewElementsLength === 1) {
                        if (result.indexOf(view) === -1) {
                            result.push(view);
                        }
                    } else if (viewElementsLength > 1) {
                        // more elements probably generated by ko.foreach() in parent view template
                        var parent = Page._findParentWithOneElement(view);
                        if (parent && result.indexOf(parent) === -1) {
                            result.push(parent);
                        }
                    }
                }
            }
        }
        return result;
    };

    Page._findParentWithOneElement = function (view) {
        var result = null;
        var parent = view.getParent();
        if (parent) {
            var parentElements = document.querySelectorAll('[data-view-id="' + parent.getId() + '"]');
            if (parentElements && parentElements.length > 1) {
                result = Page._findParentWithOneElement(parent);
            } else {
                result = parent;
            }
        }
        return result;
    };

    Page._filterToParents = function (views) {
        var viewsToRender = [];
        if (views) {
            viewsToRender = views.slice();
            for (var i = 0; i < viewsToRender.length; i++) {
                var view = viewsToRender[i];
                for (var j = 0; j < viewsToRender.length; j++) {
                    var possibleParent = viewsToRender[j];
                    if (view.isChildOf(possibleParent)) {
                        viewsToRender.splice(i, 1);
                        i--;
                        break;
                    }
                }
            }
        }
        return viewsToRender;
    };

    Page.prototype._rebuildPageDOM = function () {
        var contentSelector = '#' + LayoutConstants.APP_CONTENT_ID;
        var $content = $(contentSelector);
        if (!AbcsLib.Test.isUnitTestsEnv() && $content.length === 0) {
            throw new Error('FATAL: Can\'t find the \'' + contentSelector + '\' element where the page DOM should be generated!');
        }

        var prevContent = $content.children()[0];
        if (prevContent) {
            setTimeout(function() {
                ko.cleanNode(prevContent);
            }, 10);
        }

        // empty the content
        while ($content[0].firstChild) {
            $content[0].removeChild($content[0].firstChild);
        }
        //tabindex set to make the page content focusable - see BUFP-9351
        var $pageElement = $('<div id="' + this.getId() + '" tabindex="-1"></div>');
        $content.append($pageElement);
        var pageMode = this._Pages.getMode();
        var $element = ViewGeneratorSupport.buildElementInMode(this.getView(), pageMode, this);
        $pageElement.append($element);

        Page._LOGGER.trace('Whole page ' + this.getId() + ' DOM rebuilt.');
    };

    Page.prototype._rebuildViewDOM = function (view, $viewElement) {
        var $container = $viewElement.parent();
        var pageMode = this._Pages.getMode();
        var $element = ViewGeneratorSupport.buildElementInMode(view, pageMode, this);
        var $elementToReplace = $viewElement;
        var $previousSibling;
        var $nextSibling;
        if ($container.is(DesignerUtils.DESIGN_WRAPPER_SELECTOR)) {
            var $wrapper = $container;
            $container = $wrapper.parent();
            $elementToReplace = $wrapper;
        }
        GridUtils.moveLayoutClasses($elementToReplace[0], $element);
        $previousSibling = $elementToReplace.prev();
        $nextSibling = $elementToReplace.next();

        ko.cleanNode($elementToReplace[0]);

        $elementToReplace.remove();
        if ($previousSibling && $previousSibling.length) {
            $previousSibling.after($element);
        } else if ($nextSibling && $nextSibling.length) {
            $nextSibling.before($element);
        } else {
            $container.append($element);
        }

        Page._LOGGER.trace('View ' + view.getId() + ' of page ' + this.getId() + ' has been rebuilt.');
    };

    /**
     * Called by the infrastructure when this page becomes active (shown).
     *
     * @param {ContextualData} contextualData
     * @param {Boolean} returningToPreviousPage
     * @return {Promise}
     */
    Page.prototype.activated = function (contextualData, returningToPreviousPage) {
        var self = this;

        try {
            return self._createDynamicPage(contextualData, returningToPreviousPage);
        } catch (error) {
            //do not allow the page binding and code errors to break the designer
            Page._LOGGER.exception(error);

            return Promise.reject(error);
        }
    };

    /**
     * Called by the infrastructure when this page is going to be deactivated (navigated
     * to different page).
     *
     * @return {Promise} promise which waits for finished deactivation
     */
    Page.prototype.deactivated = function () {
        // there is no special page's deactivation opperation so far
        Page._LOGGER.debug('End of the page\'s deactivation callback.');
        return Promise.resolve();
    };

    // this is RT method hence it should stay in RT version of Page; and NOT for example in
    // ViewModelDefinition in viewmodel.dt module (where I had it before):
    /**
     *
     * @param {type} pageModel
     * @param {ContextualData} contextualData
     * @param {Boolean} returningToPreviousPage
     */
    Page.prototype._initializeGeneratedViewModel = function (pageModel, contextualData, returningToPreviousPage) {
        if (contextualData) {
            // #BUFP-2423 - when returning to previous page which does not exist
            // (because of browser reload or opening Breeze app via bookmark)
            // the page model needs to be created first:
            if (returningToPreviousPage && !pageModel.stateData) {
                returningToPreviousPage = false;
            }

            // #BUFP-6896 - support for recurrent page openning
            var previousPageState = pageNavigationRouterStateObserver.findPreviousPageState(
                    this._id,
                    pageModel[navigation.PAGE_VIEWMODEL_VERSION_PROPERTY],
                    returningToPreviousPage);
            if (previousPageState !== undefined) {
                contextualData = ContextualData.fromJSON(previousPageState.contextualData);
                returningToPreviousPage = false;
                pageNavigationRouterStateObserver.popState();
            }

            if (returningToPreviousPage) {
                //backward navigation (returning to previous page)
                pageModel.update(contextualData);
                pageNavigationRouterStateObserver.popState();
            } else {
                //forward navigation
                pageModel.create(contextualData);
                pageModel[navigation.PAGE_VIEWMODEL_VERSION_PROPERTY] = Page._GLOBAL_VIEW_MODEL_VERSION++;
            }
        }
    };

    Page.prototype.refresh = function (lazy, views) {
        if (!lazy) {
            if (navigation.getActivePage() !== this) {
                return;
            }
            //synchronous refresh
            try {
                return this._createDynamicPage(null, null, views);
            } catch (error) {
                //do not allow the page binding and code errors to break the designer
                Page._LOGGER.exception(error);
            }
        } else {
            //lazy page refresh
            this._refreshPageTaskLazy.schedule();
        }
    };

    Page.prototype.createViewId = function (viewType) {
        AbcsLib.checkDefined(viewType, 'viewType');
        var hash = Math.abs(StringUtils.hash(viewType + RandomUtils.randomString(10))) % 100000;
        var newViewId = Page._composeViewId(viewType, hash);
        while (this.findView(newViewId)) {
            hash++;
            newViewId = Page._composeViewId(viewType, hash);
        }
        return newViewId;
    };

    Page.prototype.__updateFromDefinition = function (displayName, isModalPage, type, viewModelDefinition, viewDefinition) {
        var self = this;
        var oldModelDefinition = self._viewModelDefinition ? self._viewModelDefinition.getDefinition() : {};
        if (!I18n.isTranslatable(displayName)) {
            Page._LOGGER.debug('displayName was not a translatable, was \'' + displayName + '\'');
        }
        self._displayName = displayName ? displayName : self._id;
        self._isModalPage = isModalPage !== undefined ? isModalPage : false;
        self._type = type !== undefined ? type : PageType.LANDING;
        self._viewIdToViewMap = {};

        self._viewFactory = new ViewFactory();

        self._deletedViews = [];
        self._viewFactory.addListener(new ListenerBuilder(ViewFactory.EVENT_VIEW_CHANGED, function (view) {
            self._handleViewChangedEvent(view);
        }).event(ViewFactory.EVENT_VIEW_CHANGED_SILENTLY, function () {
            self.saveChanges(true);
        }).event(ViewFactory.EVENT_VIEW_CREATED, function (view) {
            self._viewIdToViewMap[view.getId()] = view;
        }).event(ViewFactory.EVENT_VIEW_REMOVED, function (view) {
            delete self._viewIdToViewMap[view.getId()];
            self._deletedViews.push(view);
        }).event(ViewFactory.EVENT_VIEW_ADDED, function (view) {
            self._viewIdToViewMap[view.getId()] = view;
            self._refreshAndStore(view.getParent());
        }).build());

        if (!self._viewModelDefinition || !AbcsLib.equals(JSON.parse(AbcsLib.stringify(viewModelDefinition)), JSON.parse(AbcsLib.stringify(oldModelDefinition)))) {
            self._viewModelDefinition = new ViewModelDefinition(self, viewModelDefinition);
        }

        // Passing ViewModelDefinition to be able to restore original links between View-Archetype
        if (viewDefinition) {
            self._view = self._viewFactory.createViewFromDefinition(viewDefinition, self._viewModelDefinition);
        }
    };

    Page.prototype._handleViewChangedEvent = function (view) {
        if (!view.isBeingDeleted()) {
            var viewToRerender = Page._findTopMostViewToRerender(view);
            this._refreshAndStore(viewToRerender);
        }
    };

    Page._findTopMostViewToRerender = function (view) {
        var viewToRefreshAndStore = view;
        while (viewToRefreshAndStore.isRerenderParent()) {
            var parent = viewToRefreshAndStore.getParent();
            if (parent) {
                viewToRefreshAndStore = parent;
            } else {
                break;
            }
        }
        return viewToRefreshAndStore;
    };

    Page._composeViewId = function (viewType, hash) {
        return viewType + '-' + hash;
    };

    return Page;

});