Upgrading ExtJS in old ASP.NET application from 2.3 to 6 - javascript

Looking for some assistance. TLDR version: we have an ASP.NET web app that leverages ExtJS 2.3 and we are looking to upgrade to the current ExtJS version. Trying to get my head around what we’re in for.
Now for the details. I will preface by saying that I am not an expert in ExtJS nor .NET development. In fact, I’m a novice pretty much across the board when it comes to web development, so please excuse any poor explanations or misuse of terms on my part. My team is developing a web app on a “custom” framework that was developed a number of years ago at our company. It’s based on some re-runnable code generation tools that take xml templates and spit out the necessary code files. Our project is an ASP.NET MVP application that uses .aspx pages and NHibernate for ORM. Our UI is created from ExtJS—the controls are defined in each page’s .js file and then “assembled” in the .aspx page. The codebehind contains web methods that leverage the presenter of the C# code. I’ve included a snippet to demonstrate what I’m talking about below.
.aspx page:
<%# Page Language="C#" AutoEventWireup="true" CodeBehind="Entity.aspx.cs" Inherits="View.Example.EntityView" MasterPageFile="~/MasterPages/Content.Master" %>
<asp:Content ID="Content1" runat="server">
<script language="javascript" type="text/javascript" src="~/Scripts/ext-2.2.1/ext-all.js"></script>
<script language="javascript" type="text/javascript" src="<%=ResolveUrl("~/Scripts/Factory/Example/Entity.js")%>"></script>
<script language="javascript" type="text/javascript">
var localConfig = new panelConfig();
localConfig.applyExtendedConfig('default_page');
localConfig.addItem(new Ext.grid.GridPanel(pageConfigs.default_page_ManageEntity));
localConfig.addItem(
new Ext.form.Hidden({
id: 'ManageEntityGrid_Rows'
}));
var default_page = localConfig.createExt();
default_page.on('render', default_page_OnShow, default_page, { single: true });
</script>
</asp:Content>
.js file:
var get_manageEntity_columns = function() {
var columns = [
{ header: "Name"
,id: 'ManageEntity-col-Name'
, dataIndex: 'Name'
, sortable: true
},
{ id: 'ManageEntity-col-ActiveFlag'
, header: 'Active Flag'
, dataIndex: 'ActiveFlag'
, hidden: true
,tags: []
, sortable: true
},
{ id: 'ManageEntity-col-CreatedTimestamp'
, header: 'Created Timestamp'
, dataIndex: 'CreatedTimestamp'
, hidden: true
,tags: []
, renderer : formattedDateTime
, sortable: true
},
{ id: 'ManageEntity-col-Id'
, header: 'Entity ID'
, dataIndex: 'Id'
, hidden: true
,tags: []
, sortable: true
}
];
return columns;
}
var get_grid_reader_manageEntity = function(custom_fields) {
var fields = [
{ name: 'ActiveFlag', mapping: 'ActiveFlag' },
{ name: 'CreatedTimestamp', mapping: 'CreatedTimestamp' },
{ name: 'Id', mapping: 'Id' },
{ name: 'Name', mapping: 'Name' }
];
if (custom_fields) {
fields = fields.concat(custom_fields);
}
return new Ext.data.JsonReader({
root: 'Results',
totalProperty: 'Total',
id: 'Id'
}, fields);
}
var get_grid_datastore_manageEntity = function() {
var store = new Ext.data.Store({
proxy: new Ext.data.PageMethodProxy({
pageMethod: 'GetManageEntity'
}),
reader: get_grid_reader_manageEntity()
, remoteSort: true
});
store.loadOrReload = function() {
if (store.rapidLoaded)
store.reload();
else
{
store.rapidLoaded = true;
store.load({ params: { start: 0, limit: gPageSize }
});
}
}
get_grid_datastore_manageEntity = function() { return store; };
return store;
}
var pageConfigs = {
default_page_ManageEntity: {
store: get_grid_datastore_manageEntity(),
columns: get_manageEntity_columns(),
viewConfig: {
forceFit: true
},
sm: get_manageEntity_sm(),
layout:'fit',
frame: true,
id: 'ManageEntity',
plugins: [
grid_filters_manageEntity
],
iconCls: 'icon-grid',
loadMask: true,
stripeRows: true,
bbar: get_grid_paging_toolbar_manageEntity(),
listeners: {
rowcontextmenu: show_grid_menu_manageEntity
,bodyscroll: function() {
var menu = get_grid_menu_manageEntity();
if (menu.isVisible()) menu.hide();
}
,headerClick: function() {
this.getStore().on('beforeload', this.saveState, this, { single: true });
}
,render: function(){
var grid = this;
Ext.onReady(function() {
add_applied_filters(grid);
var grid_state = Ext.state.Manager.get('ManageEntity') || {};
if (!grid_state.default_filter_applied) {
var filters = grid_filters_manageEntity;
var activeflag_filter = filters.getFilter("ActiveFlag");
activeflag_filter.setValue(["", new Array("1")]);
activeflag_filter.setActive(true);
grid.on('beforestatesave', function(grid, state) { state.default_filter_applied = true; });
}
grid.getStore().load({ params: { start: 0, limit: gPageSize }
});
});
}
}
}}
.aspx.cs file:
[WebMethod()]
public static ExtJSGridData GetManageEntity(PageProxyArgs args)
{
var watch = new Stopwatch();
watch.Start();
try
{
var data = new ExtJSGridData();
var criteria = GetManageEntityQuery(args);
criteria.SetFirstResult(args.Start).SetMaxResults(args.Limit);
data.Results = GetDataManageEntity(args.RecordId, criteria);
criteria.SetFirstResult(0).SetMaxResults(RowSelection.NoValue);
criteria.ClearOrders();
data.Total = criteria.SetProjection(Projections.CountDistinct("Id")).UniqueResult<int>();
data.UserUiStateSaved = UserUiStateHelper.SaveUserUiStateInTransaction(args.UserUiState);
watch.Stop();
PageLogHelper.CurrentLog.ServerTime = watch.ElapsedMilliseconds;
return data;
}
catch (Exception ex)
{
LogManager.GetLogger((MethodBase.GetCurrentMethod().DeclaringType)).Error(ex);
ErrorHandler.LogError(ex);
throw;
}
}
private static IList GetDataManageEntity(int id, ICriteria criteria)
{
var list = criteria.List<Model.BusinessObjects.Entity>();
var jsonList = Model.BusinessObjects.Entity.ToJSON(list);
return jsonList;
}
private static ICriteria GetManageEntityQuery(PageProxyArgs args)
{
ICriteria criteria = StaticPresenter.GetEntity();
var helper = new GridFilterHelper(criteria, args, _dManageEntityLookupSortInfo);
helper.ApplyFilterMap(EntityJSON.GetGridFilterMap(criteria, args.Filters));
MapManageEntityFilters(args.Filters, criteria);
helper.ApplyFilters();
if (args.SortInfo == null || string.IsNullOrEmpty(args.SortInfo.FieldName))
return criteria;
IList<IProjection> sortMap = StaticPresenter.GetSortMap_ManageEntity(args.SortInfo.FieldName, args.RecordId, args.ExtraParams, criteria);
if (sortMap == null)
sortMap = EntityJSON.GetSortMap(args.SortInfo.FieldName, criteria);
helper.ApplySort(sortMap);
return criteria;
}
So, here is where the question comes in. As mentioned, the version of ExtJS we’re using is 2.3, and we’re looking to upgrade to the current version. I’ve done some initial homework of googling and looking through the sencha documentation, but there are some things which I’m unclear on and would like to get addressed before I start getting hands on with this effort. I’ve tried to outline my specific questions below.
First and foremost: Is the way our application is built even possible with ExtJS 6? By this, I mean leveraging the ExtJS API to define controls in the .js file and then create a UI on top of a .NET C# backbone. Based on the change notes and questions from other users, it’s pretty apparent that there have been massive (understatement) changes between 2.3 and 6. I guess what I’m getting at is that based on what I’ve read it seems you can now build your entire app, including the model and view (and controller?) in ExtJS. Is this a requirement, or can we still lay ExtJS controls on top of our .NET C# model and view?
As a follow up, I’ve been seeing references about Sencha CMD to create and build the app etc. Is cmd going to be required no matter what? Or can we simply reference the ext js library like we’re currently doing?
Assuming the answer to question 1 is yes it’s possible, the next obvious question becomes: how much work is this going to be? Let’s get the “a lot” answer out of the way—I know. What I do know is that we will have to update all of our templates to use the new API syntax (new Ext… to Ext.create() etc). I’m okay with this. What I’m trying to figure out is what I don’t know. Assuming I update all of the syntax, would our application work? Or are there other things I need to change/fix in order to get it working?
Related to question 2: based on my reading it looks like the way data stores for controls has changed and they now use the model defined in ExtJS. Is this a requirement? As described earlier, we’re currently using web methods in the aspx.cs file. Am I going to need to duplicate our C# model in ExtJS?
Lastly, I see this asked a lot but I can’t seem to find a definitive answer. Classic vs modern? The answer I typically see is that modern is aimed more towards touch screens and modern browsers, while classic is more geared toward desktop users. I’ve also read in places that modern has fewer controls available. Our web app is running in a local environment and will not be going to mobile in the future, which leads me to think classic might be the right choice? I guess I’m just wondering technically what the difference is.
I’m sure there are things I don’t even know I’m missing. Any and all feedback is welcome.

It is possible, but you will have to do a lot handwriting. Just three weeks ago I had to leverage a 3.4 ASP to 6.2.1
You can either set the variables to a global variable and on start add these to the mainView ViewModel or load them right away onBeforeLaunch.
Then code your app and build it using Sencha CMD. At the end add all together in your ASP stuff.
About how much work ... depends a lot on how structured your code is, how easy it will be to rewrite the code.
Let's pretend it is written in the same style all over the application, then it will be relatively easy.

Related

Creation of a global, persistent object into Extjs

Good day all.
I'm into a big project that uses EXTjs (i guess it's 4.0), the project is huge and have several years behind.
I'm not into Extjs so I'm trying to learn what to do and how to do it, and my new task is to create a persistent, global object, available into the whole application in which I need to store some information that are used in different parts of the project (let's say for example that the user can set a particular property of this object to "true" while doing some actions and this "true" it will be used into another viewcontroller to enable some functions, things like this).
so, I've created a new file called userJsonMainModel.js :
Ext.define('Tac3.userJsonMainModel', {
extend: 'Ext.data.Model',
constructor: function() {
var userJsonMainModel = this;
userJsonMainModel.callParent(arguments);
userJsonMainModel.data.tmp = {};
},
testProperty:{foo:"bar"},
testMethod: function (){
console.log("testFunction called");
}
});
and in Application.js :
requires: [
...
'Tac.userJsonMainModel'
],
stores: ['Countries', 'Kpis', 'Dimensions'],
autoCreateViewport: false,
init: function() {
var controller = this
Ext.tip.QuickTipManager.init();
Ext.setGlyphFontFamily('FontAwesome');
var userJsonMainModel = controller.createUserJsonMainModel();
console.log("into init: ", this.userJsonMainModel.testProperty);
...
createUserJsonMainModel: function() {
var controller = this;
controller.userJsonMainModel = Ext.create('Tac3.userJsonMainModel', {
controller: controller
});
console.log("check if the jsonmainmodel exist ",controller.userJsonMainModel.testProperty);
},
this is actually working, now the second step is to access the same object from another view (or its viewcontroller), this is what I've done into a a viewController:
Ext.define('Tac3.view.udesign.UdesignController', {
extend: 'Ext.app.ViewController',
alias: 'controller.udesign',
init: function(view) {
...
console.log("into init: ", this.userJsonMainModel.testProperty);
}
and this is actually throwing a:
Uncaught TypeError: Cannot read property 'testProperty' of undefined
I was pretty sure the objects defined into application.js would be globally accessible, but I guess I'm wrong, or doing something in a wrong way.
since I've found quite no examples on this topic (which is probably because it is not a standard way to do this), I'd like to ask what I'm doing wrong?
Just define a class and require it in your application:
Ext.define('MyApp.Globals', {
singleton: true,
foo: 100,
bar: 'baz'
});

Angular with Kendo, Using Grid Values Asynchronously

Ok I'm pretty sure I know exactly what I need to do here but I'm not sure how to do it. Basically I have a grid that I want to make a key column bind to an array of key/values, which I've done before with kendo (not using Angular) and I know that when I'm creating my key/value array asynchronously then that needs to complete before I can get them show-up with kendo, which I have done using promises before.
So here I have the same issue only angular is also involved. I need to fetch and format an array of data into the format in which a kendo grid column can digest it, so no problem here is my controller code:
var realm = kendo.data.Model.define({
id: 'realmID',
fields: {
realmID: { editable: false, nullable: true }
realmType: { type: 'string', validation: { required: true } }
}
})
var ds1 = kendoHelpers.dataSourceFactory('realms', realm, 'realmID')
var realmType = kendo.data.Model.define({
id: 'realmTypeID',
fields: {
realmTypeID: { editable: false, nullable: true },
name: { type: 'string', validation: { required: true } }
}
})
var ds2 = kendoHelpers.dataSourceFactory('realms/types', realmType, 'realmTypeID')
$scope.mainGridOptions = {
dataSource: ds1,
editable: true,
navigatable: true,
autoBind:false,
toolbar: [
{ name: "create" },
{ name: 'save' },
{ name: 'cancel' }
],
columns: [
{ field: 'realmID', title: 'ID' }
{ field: 'realmTypeID', title: 'Realm Type', editor: realmTypesDDL, values: $scope.realmTypeValues },
{ command: "destroy" }
]
}
$scope.secondGridOptions = {
dataSource: ds2,
editable: true,
navigatable: true,
toolbar: [
{ name: "create" },
{ name: 'save' },
{ name: 'cancel' }
],
columns: [
{ field: 'realmTypeID', title: 'ID' },
{ field: 'name', title: 'Name' }
{ command: "destroy" }
]
}
ds2.fetch(function () {
$scope.realmTypeValues = [{ text: 'Test', value: "24bc2e62-f761-4e70-804c-bc36fdeced3d" }];
//this.data().map(function (v, i) {
// $scope.realmTypeValues.push({ text: v.name, value: v.realmTypeID})
//});
//$scope.mainGridOptions.ds1.read()
});
function realmTypesDDL(container, options) {
$('<input />')
.appendTo(container)
.kendoDropDownList({
dataSource: ds2,
dataTextField: 'name',
dataValueField: 'realmTypeID'
});
}
I made this dataSourceFatory helper method above to return me a basic CRUD kendo dataSource that uses transport and also injects an authorization header which is working fine so don't get hung up on that, ultimately I'm going to be using this data in another grid as well as for reference values for the main grid, but I've hard coded some values that I can use to test with in the ds2.fetch callback.
My HTML is pretty plain:
<div>
<h2>Realms</h2>
<kendo-grid options="mainGridOptions"></kendo-grid>
<h2>Realm Types</h2>
<kendo-grid options="secondGridOptions"></kendo-grid>
</div>
This all works fine and well except I am only seeing the GUID of the realmTypeID in the grid, I click it and the editor is populated correctly so that's good but I want the text value to be displayed instead of the GUID. I'm sure the issue is that the array of values is empty whenever angular is binding to the grid options. My questions are:
How do I either delay this bind operation or manually rebind it after the fetch call?
Is there a better way to handle a situation like this? I try not to expend finite resources for no reason (IE making server calls when unnecessary)
Note: When I move the creation of the text/value array to happen before the grid options, I get the desired behavior I am after
EDIT A work around is to not use the directive to create the grid and instead defer the grid creation until the callback of whatever data your column is dependent on, I was hoping for a more elegant solution but this is better than nothing. So your HTML becomes something like
<h2>Realms</h2>
<div id="realms"></div>
<h2>Realm Types</h2>
<kendo-grid options="secondGridOptions"></kendo-grid>
Then you can create the grid in the fetch callback for example:
ds2.fetch(function () {this.data().map(function (v, i) {
$scope.realmTypeValues.push({ text: v.name, value: v.realmTypeID})
});
$('#realms').kendoGrid($scope.mainGridOptions);
$scope.mainGridOptions.dataSource.fetch()
});
But this doesn't feel very angularish so I'm really hoping for a better solution!
Ok...well I think I hacked this enough and without another suggestion I'm going to go forward with this approach. I'm just going to move the binding logic to the requestEnd event of the second grid so that the values array can be populated right before the binding even. I'm also reworking the values array in this method. It is a bit weird though, I think there is some kendo black magic going on with this array because I can't just set it to a new empty array without it breaking completely...which is why I'm poping everything out prior to repopulating the array. That way when something is deleted or edited in the second grid, the DDL in the first grid is updated in the callback.
function requestEnd(e) {
for (var i = $scope.realmTypeValues.length; i >= 0; i--) $scope.realmTypeValues.pop();
var data;
if (e.type == "read")
data = e.response;
else
data = e.sender.data();
data.map(function (v, i) { $scope.realmTypeValues.push({ text: v.name, value: v.realmTypeID }); });
if ($('#realms').data('kendoGrid') == undefined) {
$('#realms').kendoGrid($scope.mainGridOptions);
}
else
$('#realms').data('kendoGrid').columns[4].values = $scope.realmTypeValues;
}
ds2.bind('requestEnd', requestEnd);
So I'm going to accept my own answer unless anyone has a better approach!

JS Object strange behaviour when trying access Loopback related model query

I am working with the Loopback Framework, doing a web project.
But I think that the question that I am exposing here has less to do with this, but with general Javascript / Node.JS knowledge.
At one part of the code, I am doing:
roleMapping.find({
where: {
principalType: 'USER',
principalId: context.principals[0].id
},
include: 'role'
}, function(err, roles){
console.log(roles[0]);
for (var i in roles)
{
if (roles[i].role.name === 'teamLeader' &&
roles[i].groupId === context.modelId)
{
cb(null,true);
}else {
cb(null,false);
}
}
});
Ok with this, but it fails when trying to compare roles[i].role.name.
So, I went logging what the roles[i] object contained.
{ groupId: 1,
id: 3,
principalType: 'USER',
principalId: 1,
roleId: 2,
role:
{ id: 2,
name: 'teamLeader',
description: 'The leader(s) of a team',
created: null,
modified: null } }
Ok, nothing wrong, but it still fails, so I tried to print just the role property. And to my surprise:
{ [Function]
update: [Function],
destroy: [Function],
create: [Function],
build: [Function],
_targetClass: 'Role' }
So, the role property seems to be some sort of function? But how it was been correctly printed before?
Eventually, lost in my frustration I tried var role = JSON.parse(JSON.stringify(roles[i]));
And then I could access every property of the object normally, but this is not clean nor normal.
This blew my mind for the first time in years of JS programming (sort of amateurish though), and I would be pleased if someone could clarify this to me. Thanks
EDIT: It seems that it is specific to this Framework, so I'm changing title to help community.
I just found issue 1425 which links to the following docs:
With Node.js API, you need to call toJSON() to convert the returned model instance with related items into a plain JSON object
Please note the relation properties […] points to a JavaScript function for the relation method.
So it seems you have to use
for (var i=0; i<roles.length; i++) {
var x = roles[i].toJSON();
cb(null, x.role.name === 'teamLeader'
&& x.groupId === context.modelId);
}

Variable in javascript file not accessible in object property

I have the following problem. I have a JS-file which has a handful of variables. Those I initialize in a function:
var currentYear;
var previousYear;
var urlQuarterDates;
var urlHalfYear;
var urlYear;
var urlMonth;
var urlProposalsSentAndReceived; //= '/Marketing/ListProposalsSentAndReceived';
var urlProposalsResponsibleMonth;
function initTabReportProposalsMonth(_currentYear, _previousYear, _urlViewProposal,
_urlQuarterDates, _urlHalfYear, _urlYear, _urlMonth, _urlProposalsSentAndReceived,
_urlProposalsResponsibleMonth) {
currentYear = _currentYear;
previousYear = _previousYear;
urlQuarterDates = _urlQuarterDates;
urlHalfYear = _urlHalfYear;
urlYear = _urlYear;
urlMonth = _urlMonth;
urlProposalsSentAndReceived = _urlProposalsSentAndReceived;
urlProposalsResponsibleMonth = _urlProposalsResponsibleMonth;
}
I have defined an event handler in the same JS-file:
function onPeriodSelect(combo, rec, i) {
var conn = new Ext.data.Connection();
var params = { };
switch(rec.get('myId'))
{
case _currentQuarter1:
conn.url = urlQuarterDates;
params.y = currentYear;
params.index = 1;
break;
}
reload(); //
}
The variables urlQuarterDates and currentYear are readily accessible. So far, so good...
I also have an ExtJs Grid with a data store which is declared inline:
var gridSentAndReceived = new Ext.grid.GridPanel({
title: 'Totaal',
autoHeight: true,
autoWidth: true,
store: new Ext.data.Store({
id: 'idStoreSentAndReceived',
proxy: new Ext.data.HttpProxy({ url: urlProposalsSentAndReceived,
timeout: 1800000 }),
reader: new Ext.data.JsonReader(
{
root: 'rows'
},
[
{ name: 'Status' },
{ name: 'nrOfProposals' },
{ name: 'TotalRevenueHardware' },
{ name: 'TotalRevenueYearly' },
{ name: 'TotalRevenueHours' }
]),
remoteSort: false
}),
frame: true,
iconCls: 'icon-grid',
columns: [
...
],
viewConfig: {
forceFit: true
}
});
The reload() function calls the load of the store of gridSentAndReceived. This generates an exception: the url is not defined at all. If I initialize the url right at its declaration (which is currently commented out' it works fine. When I browse using the debugger it shows that urlProposalsSentAndReceived is initialized. Still, it claims there is no URL.
This seems to be a scope problem, since variables are accessible from the event handler but obviously not elsewhere. Anybody knows how to fix it? The URLs are created using server tags and those cannot be put in JS files. I wouldn't enjoy putting them directly in the JS file as a text string. Is there a possible solution?
Update
I have tried a few more things but nothing works.
I have tried:
'beforeload': function (store, options) {
store.proxy.setUrl('/Marketing/ListProposalsSentAndReceived');
}
but even that didn't work. Still got the same exception. I really have no clue why that failed though, I took the code from the ExtJs Documentation under 'api'.
Now I have no choice but hardcoding the urls in my js-file though I'd very much prefer to use servertags and add them dynamically. Hopefully, one day, I'll find a solution rather than getting runtime errors when I change the location of a controller action.
This is not a scope issue. At the time you run your code urlProposalsSentAndReceived is not defined. If you set that variable via an event handler, the value is always set after gridSentAndReceived is initialized.

Different behavior of ExtJS application in debug and minified mode

I'm facing an strange behavior in an web application I'm currently working on. The interface is totally built with ExtJs 4. The application has two modes, Debug and Production.
In Debug mode, when user click a menu item, I load synchronously, the dependent Javascripts file. Something like this:
for ( var i = 0; i < config.dependency.length; i++ ) {
var element = document.createElement('script');
element.onload = callback;
element.onreadystatechange = function () {
if ( element.readyState == 'loaded' || element.readyState == 'complete' ) {
callback();
}
};
}
One of the module's javascript file is like this:
module.js
Ext.define('Company.view.system.Module', {
extend: 'Company.view.abstract.Panel',
alias: 'widget.system.module',
/**
* These "_items" are built into the Object in `Company.view.abstract.Panel`
* using: Ext.create(this._items[i].clazz, conf);
*
* I'm just omitting the whole business logic there
*/
_items: [{
flex: 2,
clazz: 'Company.view.system.module.Form'
}, {
flex: 5,
clazz: 'Company.view.system.module.HtmlEditor'
}]
});
Ext.define('Company.view.system.module.Form', {
extend: 'Company.view.abstract.Form',
alias: 'widget.system.module.form',
items: [{
xtype: 'fieldset',
title: 'Module Grid',
items: [{
xtype: 'system.module.grid'
}]
}]
});
Ext.define('Company.view.system.module.Grid', {
extend: 'Company.view.abstract.Grid',
alias: 'widget.system.module.grid',
columns: [{
...
}],
afterLayout: function() {
this.callParent(arguments);
alert('after layout');
}
});
Ext.define('Company.view.system.module.HtmlEditor', {
extend: 'Company.view.abstract.HtmlEditor',
alias: 'widget.system.module.htmleditor',
...
});
Everything's working as expected in Debug mode. But in Production mode, I load the whole Modules as a single generated-minified-obfuscated javascript file (modules.js) at start up (i.e. before the ViewPort is rendered), instead of loading dependent modules after menu click event.
In Production mode, every Items, which is instantiated using "xtype" (Company.view.system.module.Grid in the above sample), is not rendered at all!
Screenshots of Debug mode and Production Mode.
What do you think is going on here which I'm missing?
Update:
The problem lies with the Height of the GridPanel, as It was computed at runtime, from the browser's screen size. Hence when the component being instantiated, the Height was not computed yet! Silly mistake :)
I would try moving the system.module.grid definition before it's being used in the system.module.form.
I would advise from having two different versions. What's your reason for that?

Categories

Resources