Нюансы MVVM в Ext JS при разработке компонентов

    Всем привет. Прошло немало времени с момента выхода Ext JS 5, где представили возможность разработки приложений с использованием паттерна MVVM. За это время я успел столкнуться с некоторыми трудностями, о которых хотел бы рассказать.

    Начну с того, что в Ext JS 4 (а предварительно в Sencha Touch) при создании компонентов их конфигурационные свойства объявлялись в объекте config, для каждого из которых автоматически создавался свой getter и setter. Несмотря на то, что вручную писать все обработчики могло быть несколько утомительно, это был стандартный подход.

    В пятой же версии Ext JS используя MVVM можно было легко избавиться от доброй части рутины: удалить конфигурационные свойства и их обработчики, а вместо этого привязаться к нужному свойству или формуле ViewModel'и. Кода становилось значительно меньше, а читаемость — лучше.

    Но меня беспокоил вопрос инкапсуляции. Что если в процессе разработки часть функциональности я захочу вынести в отдельный компонент для повторного использования? Нужно ли при этом создавать собственную ViewModel? Как изменять состояние компонента: обращаться напрямую к его ViewModel'и или всё-таки стоит использовать конфигурационные свойства и их публичные setter'ы?

    Мысли об этом и других вопросах, а также примеры с напильником — под катом.

    Часть 1. Используем ViewModel


    Давайте попробуем создать, к примеру, таблицу каких-нибудь пользователей. Чтобы она могла добавлять и удалять записи, но при необходимости переходить в режим только чтения. Также я хочу, чтобы кнопка удаления содержала имя выделенного пользователя.

    Пример 1. Стандартный подход


    Как бы мы это сделали без использования MVVM?



    Посмотреть в Sencha Fiddle

    Fiddle.view.UsersGrid
    Ext.define('Fiddle.view.UsersGrid', {
        extend: 'Ext.grid.Panel',
        xtype: 'usersgrid',
        
        config: {
            /**
            @cfg {Boolean} Read only mode
            */
            readOnly: null
        },
        
        defaultListenerScope: true,
        
        tbar: [{
            text: 'Add',
            itemId: 'addButton'
        }, {
            text: 'Remove',
            itemId: 'removeButton'
        }],
        
        columns: [{
            dataIndex: 'id',
            header: 'id'
        }, {
            dataIndex: 'name',
            header: 'name'
        }],
        
        listeners: {
            selectionchange: 'grid_selectionchange'
        },
        
        updateReadOnly: function (readOnly) {        
            this.down('#addButton').setDisabled(readOnly);
            this.down('#removeButton').setDisabled(readOnly);
        },
               
        grid_selectionchange: function (self, selected) {
            var rec = selected[0];
            if (rec) {
            	this.down('#removeButton').setText('Remove ' + rec.get('name'));
            }
        }
    });
    


    Установка режима Read only
    readOnlyButton_click: function (self) {
        this.down('usersgrid').setReadOnly(self.pressed);
    }
    


    Довольно многословно, зато понятно: вся логика работы компонента находится внутри. Нужно оговориться, что можно использовать ViewController'ы, и это тоже будет считаться частью компонента, но в примерах я обойдусь без них.

    Пример 2. Добавляем MVVM


    Уберём обработчики кода и заменим их привязками (bind).

    Посмотреть в Sencha Fiddle

    Fiddle.view.UsersGrid
    Ext.define('Fiddle.view.UsersGrid', {
        extend: 'Ext.grid.Panel',
        xtype: 'usersgrid',
        
        reference: 'usersgrid',    
        
        viewModel: {
            data: {
                readOnly: false
            }
        },
        
        tbar: [{
            text: 'Add',
            itemId: 'addButton',
            bind: {
                disabled: '{readOnly}'
            }
        }, {
            text: 'Remove',
            itemId: 'removeButton',
            bind: {
                disabled: '{readOnly}',
                text: 'Remove {usersgrid.selection.name}'
            }
        }],
        
        columns: [{
            dataIndex: 'id',
            header: 'id'
        }, {
            dataIndex: 'name',
            header: 'name'
        }]
    });
    


    Установка режима Read only
    readOnlyButton_click: function (self) {
        this.down('usersgrid').getViewModel().set('readOnly', self.pressed);
    }
    


    Выглядит значительно лучше, правда? Особенно, если представить, что входных параметров кроме readOnly может быть гораздо больше — тогда разница будет колоссальной.

    Сравнивая эти примеры, напрашиваются некоторые вопросы:

    Вопрос 1. Где мы должны были создавать ViewModel? Можно ли было описать её во внешнем контейнере?

    — С одной стороны, можно, но тогда мы получаем сильную связанность: каждый раз, когда мы переносим этот компонент в другое место, мы будем обязаны не забыть добавить свойство readOnly во ViewModel'и нового контейнера. Так легко ошибиться и вообще родительский контейнер не должен знать о внутренностях компонентов, которые в него добавляются.

    Вопрос 2. Что такое reference? Почему мы прописали его внутри компонента?

    — Reference — это аналог id компонента во ViewModel'и. Мы прописали его потому что для кнопки Remove стоит привязка к имени выделенного пользователя, а без указания reference это работать не будет.

    Вопрос 3. А правильно ли так делать? Что если я захочу добавить два экземпляра в один контейнер — у них будет один reference?

    — Да, и это конечно же неправильно. Нужно подумать, как это решить.

    Вопрос 4. Правильно ли обращаться к ViewModel'и компонента извне?

    — Вообще, работать оно будет, но это опять обращение к внутренностям компонента. Меня, по идее, не должно интересовать, есть у него ViewModel или нет. Если я хочу изменить его состояние, то я должен вызвать соответствующий setter как это и было когда-то задумано.

    Вопрос 5. Можно ли всё-таки использовать конфигурационные свойства, и при этом привязываться к их значениям? Ведь в документации для этого случая есть свойство publishes?

    — Можно и это хорошая идея. Кроме, конечно, проблемы с явным указанием reference в привязке. Установка режима readOnly в данном случае будет такой же, как и в Примере 1 — через публичный setter:

    Пример 3. Fiddle.view.UsersGrid
    Ext.define('Fiddle.view.UsersGrid', {
        extend: 'Ext.grid.Panel',
        xtype: 'usersgrid',
        
        reference: 'usersgrid',
        
        viewModel: {
            
        },
        
        config: {
            readOnly: false
        },
        
        publishes: ['readOnly'],
        
        tbar: [{
            text: 'Add',
            itemId: 'addButton',
            bind: {
                disabled: '{usersgrid.readOnly}'
            }
        }, {
            text: 'Remove',
            itemId: 'removeButton',
            bind: {
                disabled: '{usersgrid.readOnly}',
                text: 'Remove {usersgrid.selection.name}'
            }
        }],
        
        columns: [{
            dataIndex: 'id',
            header: 'id'
        }, {
            dataIndex: 'name',
            header: 'name'
        }]
    });
    


    Посмотреть в Sencha Fiddle

    Кое-что ещё


    Это касается последнего вопроса. Если мы привяжемся из внешнего контейнера на свойство внутреннего компонента (например, на выделенную строку таблицы) — привязка работать не будет (пруф). Это случается как только у внутреннего компонента появляется своя ViewModel — изменения свойств публикуются только в неё (а если точнее, то в первую по иерархии). На официальном форуме этот вопрос поднимался несколько раз — и пока тишина, есть лишь только зарегистрированный реквест (EXTJS-15503). Т.е, если взглянуть на картинку из КДПВ с этой точки зрения, то получается вот что:



    Т.е. контейнер 1 может привязаться ко всем внутренним компонентам, кроме контейнера 2. Тот в свою очередь так же, кроме контейнера 3. Потому что все компоненты публикуют изменения свойств только в первую по иерархии ViewModel, начиная со своей.

    Слишком много информации? Попробуем разобраться.




    Часть 2. За работу!




    ПРЕДУПРЕЖДЕНИЕ. Решения, описанные ниже, носят статус экспериментальных. Используйте их с осторожностью, потому что обратная совместимость гарантируется не во всех случаях. Замечания, исправления и другая помощь приветствуются. Поехали!

    Итак, для начала я бы хотел сформулировать своё видение разработки компонентов с MVVM:

    1. Для изменения состояния компонента использовать конфигурационные свойства и их публичные setter'ы.
    2. Иметь возможность привязываться к собственным конфигурационным свойствам (внутри компонента).
    3. Иметь возможность привязываться к свойствам компонента снаружи вне зависимости, есть у него своя ViewModel или нет.
    4. Не задумываться об уникальности имён внутри иерархии данных ViewModel'ей.


    Фикс №1. Публикуем изменения вверх


    Начнём с чего-нибудь попроще, например, с пункта 3. Здесь дело в классе-примеси Ext.mixin.Bindable и его методе publishState. Если заглянуть внутрь, то мы увидим, что изменения публикуются во ViewModel, которая находится первой по иерархии. Давайте сделаем так, чтобы родительская ViewModel об этом тоже знала:

    publishState: function (property, value) {
        var me = this,
            vm = me.lookupViewModel(),
            parentVm = me.lookupViewModel(true),
            path = me.viewModelKey;
    
        if (path && property && parentVm) {
            path += '.' + property;
            parentVm.set(path, value);
        }
    
        Ext.mixin.Bindable.prototype.publishState.apply(me, arguments);
    }
    


    До После

    Демо на Sencha Fiddle.

    Фикс №2. Привязываемся к собственным конфигурационным свойствам


    Касаемо пункта 2. Кажется несправедливым, что снаружи есть возможность привязаться к свойствам компонента, а изнутри — нет. Вернее, с указанием reference
    — можно, но раз мы решили, что это не очень красивый вариант, то как минимум вручную можем сделать лучше:

    Fiddle.view.UsersGrid
    Ext.define('Fiddle.view.UsersGrid', {
        extend: 'Ext.grid.Panel',
        xtype: 'usersgrid',
        
        viewModel: {
            data: {
                readOnly: false,
                selection: null
            }
        },
        
        config: {
            readOnly: false
        },
        
        tbar: [{
            text: 'Add',
            itemId: 'addButton',
            bind: {
                disabled: '{readOnly}'
            }
        }, {
            text: 'Remove',
            itemId: 'removeButton',
            bind: {
                disabled: '{readOnly}',
                text: 'Remove {selection.name}'
            }
        }],
        
        // ...
        
        updateReadOnly: function (readOnly) {
            this.getViewModel().set('readOnly', readOnly);
        },
        
        updateSelection: function (selection) {
            this.getViewModel().set('selection', selection);
        }
    });
    


    Демо на Sencha Fiddle

    Выглядит лучше, правда? Снаружи привязываемся с указанием reference, а изнутри — без. Теперь каким бы он ни был, код компонента не меняется. Более того, теперь мы можем добавить два компонента в один контейнер, дать им свои названия reference
    — и всё будет работать!

    Автоматизируем? Добавим к предыдущему методу publishState:

    if (property && vm && vm.getView() == me) {
        vm.set(property, value);
    }
    

    Вот и всё. Оцените, насколько лаконичными стали привязки к своим конфигурационным свойствам:

    Fiddle.view.UsersGrid
    Ext.define('Fiddle.view.UsersGrid', {
        extend: 'Ext.grid.Panel',
        xtype: 'usersgrid',
        
        viewModel: {
            
        },
        
        config: {
            readOnly: false
        },
        
        publishes: ['readOnly'],
        
        tbar: [{
            text: 'Add',
            itemId: 'addButton',
            bind: {
                disabled: '{readOnly}'
            }
        }, {
            text: 'Remove',
            itemId: 'removeButton',
            bind: {
                disabled: '{readOnly}',
                text: 'Remove {selection.name}'
            }
        }],
        
        columns: [{
            dataIndex: 'id',
            header: 'id'
        }, {
            dataIndex: 'name',
            header: 'name'
        }]
    });
    


    Ext.ux.mixin.Bindable
    /* global Ext */
    
    /**
     * An override to notify parent ViewModel about current component's published properties changes
     * and to make own ViewModel contain current component's published properties values.
     */
    Ext.define('Ext.ux.mixin.Bindable', {
        initBindable: function () {
            var me = this;
            Ext.mixin.Bindable.prototype.initBindable.apply(me, arguments);
            me.publishInitialState();
        },
    
        /**
        Notifying both own and parent ViewModels about state changes
        */
        publishState: function (property, value) {
            var me = this,
                vm = me.lookupViewModel(),
                parentVm = me.lookupViewModel(true),
                path = me.viewModelKey;
    
            if (path && property && parentVm) {
                path += '.' + property;
                parentVm.set(path, value);
            }
    
            Ext.mixin.Bindable.prototype.publishState.apply(me, arguments);
    
            if (property && vm && vm.getView() == me) {
                vm.set(property, value);
            }
        },
    
        /**
        Publish initial state
        */
        publishInitialState: function () {
            var me = this,
                state = me.publishedState || (me.publishedState = {}),
                publishes = me.getPublishes(),
                name;
    
            for (name in publishes) {
                if (state[name] === undefined) {
                    me.publishState(name, me[name]);
                }
            }
        }
    }, function () {
        Ext.Array.each([Ext.Component, Ext.Widget], function (Class) {
            Class.prototype.initBindable = Ext.ux.mixin.Bindable.prototype.initBindable;
            Class.prototype.publishState = Ext.ux.mixin.Bindable.prototype.publishState;
            Class.mixin([Ext.ux.mixin.Bindable]);
        });
    });
    


    Демо на Sencha Fiddle.

    Фикс №3. Разделяем ViewModel'и компонентов


    Самое сложное: пункт 4. Для чистоты эксперимента предыдущие фиксы не используем. Дано: два вложенных компонента с одинаковым конфигурационным свойвтвом — color. Каждый использует ViewModel для привязки к этому значению. Требуется: привязать свойство внутреннего компонента к свойству внешнего. Попробуем?

    Fiddle.view.OuterContainer
    Ext.define('Fiddle.view.OuterContainer', {
        // ...    
        viewModel: {
            data: {
                color: null
            }
        },
        
        config: {
            color: null
        },
        
        items: [{
            xtype: 'textfield',
            fieldLabel: 'Enter color',
            listeners: {
                change: 'colorField_change'
            }
        }, {
            xtype: 'displayfield',
            fieldLabel: 'Color',
            bind: '{color}'
        }, {
            xtype: 'innercontainer',
            bind: {
                color: '{color}'
            }
        }],
        
        colorField_change: function (field, value) {
            this.setColor(value);
        },
        
        updateColor: function (color) {
            this.getViewModel().set('color', color);
        }
    })
    


    Fiddle.view.InnerContainer
    Ext.define('Fiddle.view.InnerContainer', {
        // ...
        viewModel: {
            data: {
                color: null
            }
        },
        
        config: {
            color: null
        },
        
        items: [{
            xtype: 'displayfield',
            fieldLabel: 'Color',
            bind: '{color}'
        }],
        
        updateColor: function (color) {
            this.getViewModel().set('color', color);
        }
    })
    


    Демо на Sencha Fiddle.



    Выглядит просто, но не работает. Почему? Потому что если внимательно приглядеться, то следующие формы записи абсолютно идентичны:

    Вариант 1.
    Ext.define('Fiddle.view.OuterContainer', {
        // ...   
        viewModel: {
            data: {
                color: null
            }
        },
        
        items: [{
            xtype: 'innercontainer',
            bind: {
                color: '{color}'
            }
        }]
        // ...
    })
    

    Ext.define('Fiddle.view.InnerContainer', {
        // ...
        viewModel: {
            data: {
                color: null
            }
        },
        
        config: {
            color: null
        },
        
        items: [{
            xtype: 'displayfield',
            fieldLabel: 'Color',
            bind: '{color}'
        }]
        // ...
    })
    



    Вариант 2.
    Ext.define('Fiddle.view.OuterContainer', {
        // ...   
        viewModel: {
            data: {
                color: null
            }
        },
        
        items: [{
            xtype: 'innercontainer'        
        }]
        // ...
    })
    

    Ext.define('Fiddle.view.InnerContainer', {
        // ...
        viewModel: {
            data: {
                color: null
            }
        },
        
        config: {
            color: null
        },
    
        bind: {
            color: '{color}'
        },
        
        items: [{
            xtype: 'displayfield',
            fieldLabel: 'Color',
            bind: '{color}'
        }]
        // ...
    })
    



    Внимание, вопрос! К свойству color чьей ViewModel'и мы биндимся во внутреннем контейнере? Как ни странно, в обоих случаях — к внутренней. При этом, судя по документации и картинке из шапки, данные ViewModel'и внешнего контейнера являются прототипом для данных ViewModel'и внутреннего. А т.к. у последнего переопределено значение color, то при изменении значения у прототипа, у наследника оно остаётся старым (null). Т.е. в принципе, глюка нет — так и должно быть.

    Как можно выйти из ситуации? Самое очевидное — убрать color из внутренней ViewModel'и. Тогда нам также придётся убрать обработчик updateColor. И конфигурационное свойство — тоже в топку! Будем надеяться, что в родительском контейнере всегда будет ViewModel со свойством color.

    Или нет? Надежда — это не то, с чем мы имеем дело. Другой вариант — это переназвать все конфигурационные свойства (и поля ViewModel'и) так, чтобы не было дублирования (в теории): outerContainerColor и innerContainerColor. Но это тоже ненадёжно. В больших проектах столько имён, да и вообще не очень красиво получается.

    Вот было бы здорово, описывая внешний контейнер, указывать привязку как-нибудь так:

    Ext.define('Fiddle.view.OuterContainer', {
        viewModel: {
            data: {
                color: null
            }
        },
        
        items: [{
            xtype: 'innercontainer',
            bind: {
                color: '{outercontainer.color}' // с префиксом
            }
        }]
    })
    


    Не буду томить, это тоже можно сделать:

    Ext.ux.app.SplitViewModel + Ext.ux.app.bind.Template
    /**
    An override to split ViewModels data by their instances
    */
    Ext.define('Ext.ux.app.SplitViewModel', {
        override: 'Ext.app.ViewModel',
    
        config: {
            /**
            @cfg {String}
            ViewModel name
            */
            name: undefined,
    
            /**
            @cfg {String}
            @private
            name + sequential identifer
            */
            uniqueName: undefined,
    
            /**
            @cfg {String}
            @private
            uniqueName + nameDelimiter
            */
            prefix: undefined
        },
    
        nameDelimiter: '|',
        expressionRe: /^(?:\{[!]?(?:(\d+)|([a-z_][\w\-\.|]*))\})$/i,
        uniqueNameRe: /-\d+$/,
    
        privates: {
            applyData: function (newData, data) {
                newData = this.getPrefixedData(newData);
                data = this.getPrefixedData(data);
    
                return this.callParent([newData, data]);
            },
    
            applyLinks: function (links) {
                links = this.getPrefixedData(links);
                return this.callParent([links]);
            },
    
            applyFormulas: function (formulas) {
                formulas = this.getPrefixedData(formulas);
                return this.callParent([formulas]);
            },
    
            bindExpression: function (path, callback, scope, options) {
                path = this.getPrefixedPath(path);
                return this.callParent([path, callback, scope, options]);
            }
        },
    
        bind: function (descriptor, callback, scope, options) {
            if (Ext.isString(descriptor)) {
                descriptor = this.getPrefixedDescriptor(descriptor);
            }
            return this.callParent([descriptor, callback, scope, options]);
        },
    
        linkTo: function (key, reference) {
            key = this.getPrefixedPath(key);
            return this.callParent([key, reference]);
        },
    
        get: function (path) {
            path = this.getPrefixedPath(path);
            return this.callParent([path]);
        },
    
        set: function (path, value) {
            if (Ext.isString(path)) {
                path = this.getPrefixedPath(path);
            }
            else if (Ext.isObject(path)) {
                path = this.getPrefixedData(path);
            }
            this.callParent([path, value]);
        },
    
        applyName: function (name) {
            name = name || this.type || 'viewmodel';
            return name;
        },
    
        applyUniqueName: function (id) {
            id = id || Ext.id(null, this.getName() + '-');
            return id;
        },
    
        applyPrefix: function (prefix) {
            prefix = prefix || this.getUniqueName() + this.nameDelimiter;
            return prefix;
        },
    
        /**
        Apply a prefix to property names
        */
        getPrefixedData: function (data) {
            var name, newName, value,
                result = {};
    
            if (!data) {
                return null;
            }
    
            for (name in data) {
                value = data[name];
                newName = this.getPrefixedPath(name);
                result[newName] = value;
            }
    
            return result;
        },
    
        /**
        Get a descriptor with a prefix
        */
        getPrefixedDescriptor: function (descriptor) {
            var descriptorParts = this.expressionRe.exec(descriptor);
    
            if (!descriptorParts) {
                return descriptor;
            }
    
            var path = descriptorParts[2]; // '{foo}' -> 'foo'
            descriptor = descriptor.replace(path, this.getPrefixedPath(path));
    
            return descriptor;
        },
    
        /**
        Get a path with a correct prefix
    
        Examples:
    
            foo.bar -> viewmodel-123|foo.bar
            viewmodel|foo.bar -> viewmodel-123|foo.bar
            viewmodel-123|foo.bar -> viewmodel-123|foo.bar (no change)
    
        */
        getPrefixedPath: function (path) {
            var nameDelimiterPos = path.lastIndexOf(this.nameDelimiter),
                hasName = nameDelimiterPos != -1,
                name,
                isUnique,
                vmUniqueName,
                vm;
    
            if (hasName) {
                // bind to a ViewModel by name: viewmodel|foo.bar
                name = path.substring(0, nameDelimiterPos + this.nameDelimiter.length - 1);
                isUnique = this.uniqueNameRe.test(name);
    
                if (!isUnique) {
                    // replace name by uniqueName: viewmodel-123|foo.bar
                    vm = this.findViewModelByName(name);
                    if (vm) {
                        vmUniqueName = vm.getUniqueName();
                        path = vmUniqueName + path.substring(nameDelimiterPos);
                    }
                    else {
                        Ext.log({ level: 'warn' }, 'Cannot find a ViewModel instance by a specifed name/type: ' + name);
                    }
                }
            }
            else {
                // bind to this ViewModel: foo.bar -> viewmodel-123|foo.bar
                path = this.getPrefix() + path;
            }
    
            return path;
        },
    
        /**
        Find a ViewModel by name up by hierarchy
        @param {String} name ViewModel's name
        @param {Boolean} skipThis Pass true to ignore this instance
        */
        findViewModelByName: function (name, skipThis) {
            var result,
                vm = skipThis ? this.getParent() : this;
    
            while (vm) {
                if (vm.getName() == name) {
                    return vm;
                }
                vm = vm.getParent();
            }
    
            return null;
        }
    });
    
    /**
    This override replaces tokenRe to match a token with nameDelimiter
    */
    Ext.define('Ext.ux.app.bind.Template', {
        override: 'Ext.app.bind.Template',
        
        tokenRe: /\{[!]?(?:(?:(\d+)|([a-z_][\w\-\.|]*))(?::([a-z_\.]+)(?:\(([^\)]*?)?\))?)?)\}/gi
    });
    



    Теперь так и пишем (только вместо точки другой символ, т.к. она зарезервирована):

    Ext.define('Fiddle.view.OuterContainer', {
        viewModel: {
            name: 'outercontainer',
            data: {
                color: null
            }
        },
        
        items: [{
            xtype: 'innercontainer',
            bind: {
                color: '{outercontainer|color}'
            }
        }]
    })
    

    Демо на Sencha Fiddle.



    Т.е. мы прописали более конкретный bind с указанием имени ViewModel'и. При вынесении кода ViewModel'и в отдельный файл, имя можно не указывать — оно возьмётся из alias. Всё, больше никаких изменений не требуется. На свою ViewModel можно привязываться по старинке без префикса. Его мы указываем для вложенных компонентов, у которых есть (или может появиться) своя ViewModel.

    Под капотом этого расширения к полям ViewModel'и добавляется префикс, состоящий из её имени (name или alias) и уникального id (как для компонентов). Затем, в момент инициализации компонентов, он добавляется к названиям всех привязок.

    Что это даёт?


    Данные ViewModel'ей будут разделены по иерархии. В привязках будет конкретно видно, на свойство чьей ViewModel'и они ссылаются. Теперь можно не беспокоиться за дублирование свойств внутри иерархии ViewModel'ей. Можно писать повторно используемые компоненты без оглядки на родительский контейнер. В связке с предыдущими фиксами в сложных компонентах объём кода сокращается радикально.

    Последний пример с фиксами №№1-3

    Но на этом этапе частично теряется обратная совместимость. Т.е. если вы, разрабатывая компоненты, полагались на присутствие каких-то свойств во ViewModel'и родительского компонента, то последний фикс вам всё сломает: необходимо будет добавить в привязку префикс, соответствующий имени/alias'у родительской ViewModel'и.

    Итого


    Исходный код расширений лежит на GitHub, добро пожаловать:
    github.com/alexeysolonets/extjs-mvvm-extensions

    Они применены у нас в нескольких проектах — полёт более чем нормальный. Помимо того, что кода пишем меньше, появилось более чёткое понимание, как связаны компоненты — всё стало кристально ясно, голова уже не болит и перхоть исчезла.

    Для себя есть один вопрос: оставить последнее расширение в виде глобального, которое действует на все ViewModel'и (override), или вынести как класс, от которого наследоваться? Второе решение вроде более демократично, но не внесёт ли оно большей путаницы? В общем, пока этот вопрос открытый.

    Какие у вас были нюансы при разработке c MVVM? Обсудим?

    Похожие публикации

    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 4

      0
      оставить последнее расширение в виде глобального, которое действует на все ViewModel'и (override), или вынести как класс, от которого наследоваться?

      Плагин, который делает override для объекта, на котором вызван.
        0
        Вариант. Правда, плагины поддерживаются только компонентами, т.е. можно попробовать пойти путём mixin'ов.
        0
        Хотел посмотреть примеры, а они умерли… Можно как-то их восстановить?
          0
          да, какие-то непонятки происходят, я проверю, спасибо, что сказали

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое