This is a migrated thread and some comments may be shown as answers.

how to use Custom Data-binding directive with grouping

5 Answers 1408 Views
General Discussions
This is a migrated thread and some comments may be shown as answers.
Chau
Top achievements
Rank 1
Chau asked on 19 Jul 2018, 01:51 AM

I have implemented a grid with remote binding directive subscribing to a service to get data from server, and it is working great with the grid having sort, filter properties defined.

Now, I need to work on another grid which has a group defined, and I keep getting the error of 'Cannot convert undefined or null' at the line where translateDataSourceResultGroups is called.

@Injectable()
export class CodeSelectionsServiceClient extends BehaviorSubject<GridDataResult> {
 
    public isLoading = true;
    private BASE_URL = "api/BaseConfig/Get?type=codeselections_get&id=";
 
    constructor(private http: HttpClient) { super(null); }
 
    public query(state: any, config_id: any): void {
        //alert('timestart:' + new Date());
        var url = this.BASE_URL + config_id + "&override_combo=dep_0004&override_level=dep";
 
        this.fetch(state, url)
            .subscribe(data => {
                super.next(data);
 
                this.isLoading = false;
            } );
    }
 
public fetch(state: any, url: string): Observable<GridDataResult> {
        const queryStr = `${toODataString(state)}`;  
 
        const hasGroups = state.group && state.group.length;
 
        return this.http
            .get(`${url}&${queryStr}`) // Send the state to the server
            .pipe(
            map(<CodeSelectionsGridDataResult>({ value, datacount }) => {
                console.log(value);
                return (<GridDataResult>{
                    data: hasGroups ? translateDataSourceResultGroups(value) : value,
                    total: parseInt(datacount, 10)
                })
            }));
 
}
//class defined on server
public class CodeSelectionsGridDataResult
    {
        public int datacount { get; set; }
        public List<CodeSelection> value { get; set; }
 
    }

 

If I don't send in the state.group info, then the fetch method does not call translateDataSourceResultGroups and the grid is binding OK with the list<CodeSelection> returned.

This is how my grid state is defined when I pass in the group info:

public state: DataStateChangeEvent = {
        skip: 0,
        take: 50,     //controls grid paging settings
        filter: {
            logic: 'and',
            filters: [{ field: 'isSelected', operator: 'eq', value: true }]
        },
        //group: [],
        group: [{ field: 'CodeId' }],
        sort: []
    }

 

Could you tell me what I may have missed here?  

I'm attaching the error displayed in debug mode on Chrome. The data returned (for testing) is a list of only one record as highlighted on the screenshot.

Thanks!

 

5 Answers, 1 is accepted

Sort by
0
Dimiter Topalov
Telerik team
answered on 20 Jul 2018, 01:47 PM
Hello Chau,

Can you share further details about the server-side processing of the data? The translateDataSourceResultGroups() function is used only when the data is returned after being processed via the ToDataSourceResult() helper method as described in the following section of our documentation (where most of the provided code seems to originate from):

https://www.telerik.com/kendo-angular-ui/components/dataquery/mvc-integration/

However, the following line suggests that some other backend is used:

const queryStr = `${toODataString(state)}`;

... as the approach outlined in the MVC integration article requires the query string to be created with the toDataSourceRequestString() instead.

The server-side grouped data has a different structure (array of arrays) and thus a JavaScript error may be caused when you call the translateDataSourceResultGroups() function with an array containing a single object only.

I hope this helps.

Regards,
Dimiter Topalov
Progress Telerik
Try our brand new, jQuery-free Angular components built from ground-up which deliver the business app essential building blocks - a grid component, data visualization (charts) and form elements.
0
Chau
Top achievements
Rank 1
answered on 20 Jul 2018, 08:41 PM

Hi Dimiter,

Thanks for your reply.

I think you kinda answered to my question, i.e. what data structure would be returned by the server when there is a group criteria.

Currently for my testing, my API Controller returns the same list of records (only one array, not array of arrays) whether the criteria has group or not. That seems to be the problem causing the javascript error.

I have this plunker calling the Northwind service, and the same error happened there.

https://next.plnkr.co/edit/WNyqBJMA0LkHwP4b

Would you have a sample of the grouped data structure that is returned by the server, so I can  build a similar structure. For example, with group criteria: [{ field: 'UnitPrice'}], what would the data structure be like?

Thanks so much,

Chau

0
Chau
Top achievements
Rank 1
answered on 24 Jul 2018, 05:21 AM

Hi Dimiter,

I spent some more time today trying to pass :

const queryStr = `${toDataSourceRequestString(state)}`;

and return an object with (array of array) structure from the server, and the exception still occurs when calling translateDataSourceResultGroups.

Here's the flow of my code:

1. from myapp.html, there is a tag <app-codeselections>:

<app-codeselections>
     <div class='k-overlay' *ngIf="codeselectionsLoading" style="opacity:.5;"></div>
</app-codeselections>

2. In codeselections.component.ts:

import { CodeSelectionsServiceClient } from '../PricingService/pricing.service';
@Component({
    selector: 'app-codeselections',
    templateUrl: './Apps_Root/Apps/PricingConfig/ConfigOverrides/codeselections.component.html',
    styles: [`
    .grid-wrapper {
      position: relative;
    }
    >>> .k-window-actions.k-dialog-actions {
        display: none;
      }
    `]
})
export class CodeSelectionsComponent implements OnInit {
@ViewChild('mycodeselectionsgrid') mycodeselectionsgrid;
    //used for the code selections main grid
    view: GridDataResult;
    
    //controls the code selections main grid sort state
    public state: DataStateChangeEvent = {
        skip: 0,
        take: 50,     //controls grid paging settings
        filter: {
            logic: 'and',
            filters: [{ field: 'isSelected', operator: 'eq', value: true }]
        },
        group: [{ field: 'CodeId' }],
        sort: []
}
...
}

3. In codeselections.component.html:

<kendo-grid #mycodeselectionsgrid codeselectionsBinding
                            [pricingConfigObj]="this.pricingConfigObj"
                            [config_id]="this.pricingConfigObj.dlConfigurationSelected.key"
                            [override_combo]="this.pricingConfigObj.selectedOverride_Combo"
                            [override_level]="this.pricingConfigObj.selectedOverride_Level"
                            [height]="750"
                            [resizable]="true"
                            [pageSize]="state.take"
                            [skip]="state.skip"
                            [pageable]="true"
                            [sort]="state.sort"
                            [sortable]="true"
                            [filter]="state.filter"
                            [filterable]="true"
                            [group]="state.group"
                            >
                    <ng-template kendoGridToolbarTemplate>
                        <table class="k-grid-ignore-click">
                            <tr>
                                <td style="padding-top:0px; padding-bottom:0px;">
                                    <input type="checkbox" id="chkAllCodes" (change)="selectAllCodes($event)" style="margin-left:4px;vertical-align:middle;" />
                                    <label style="font-weight:normal;font-size:12px;vertical-align:text-top;" for="chkAllCodes">Select Filtered {{getTitleName('')}}</label>
                                </td>
                            </tr>
                        </table>
                    </ng-template>
                    <kendo-grid-column field="isSelected" [width]="120" [style]="{'text-align': 'center'}" [filterable]="true" filter="boolean" title="">
                        <ng-template kendoGridCellTemplate let-dataItem>
                            <input type="checkbox" [checked]="dataItem.isSelected" (click)="selectCodeSelection($event, dataItem.CodeId)" [(ngModel)]="dataItem.isSelected" />
                        </ng-template>
                    </kendo-grid-column>
                    <kendo-grid-column field="Entity_Cd" width="150">
                    </kendo-grid-column>
                    <kendo-grid-column field="CodeId" title="{{getTitleName('Id')}}" width="180">
                        <ng-template kendoGridGroupHeaderTemplate let-group let-field="field" let-value="value">
                            <strong>{{getTitleName('Id')}}</strong>: {{value}}
                              <input type="checkbox" name="chkCodeIdAll_{{value}}" id="chkCodeIdAll_{{value}}" (change)="selectGroupedCodeIdAll($event, value)"
                                               [checked]="getAllChildrenChecked(value)" />
                        </ng-template>
                    </kendo-grid-column>
                    <kendo-grid-column field="CodeDesc" width="200" title="{{getTitleName('Description')}}">
                    </kendo-grid-column>
                    <kendo-grid-column field="CurrentOverrideCombo" width="180" title="Current Override Combo">
                    </kendo-grid-column>
                    <kendo-grid-column field="CurrentOverrideDesc" width="200" title="Current Override Description">
                    </kendo-grid-column>
 
 </kendo-grid>

4. In codeselections-remotebinding.directive.ts:

import { Directive, OnInit, OnDestroy, Input } from '@angular/core';
import { DataBindingDirective, GridComponent } from '@progress/kendo-angular-grid';
import { Subscription } from 'rxjs/Subscription';
 
import { CodeSelectionsServiceClient } from '../PricingService/pricing.service';
 
 
@Directive({
    selector: '[codeselectionsBinding]'
    //,
    //exportAs: 'bindingDirective'
})
 
export class CodeSelectionsBindingDirective extends DataBindingDirective implements OnInit, OnDestroy {
 
    private serviceSubscription: Subscription;
    @Input() pricingConfigObj: any;
    @Input() config_id: number;
    @Input() override_combo: string;
    @Input() override_level: string;
 
    constructor(private codeselectionsserviceclient: CodeSelectionsServiceClient, grid: GridComponent) {
        super(grid);
    }
 
 
 
    public ngOnInit(): void {
 
        this.serviceSubscription = this.codeselectionsserviceclient.subscribe((result) => {
            this.grid.data = result;
            this.notifyDataChange();
        });
 
        super.ngOnInit();
 
        this.rebind();
 
    }
 
    public ngOnDestroy(): void {
        if (this.serviceSubscription) {
            this.serviceSubscription.unsubscribe();
        }
 
        super.ngOnDestroy();
    }
 
    public rebind(): void {
       
        this.codeselectionsserviceclient.isLoading = true;
 
        this.codeselectionsserviceclient.query(this.state, this.config_id, this.override_combo, this.override_level);
    }
 
}

5. In pricing.service.ts:

@Injectable()
export class CodeSelectionsServiceClient extends BehaviorSubject<GridDataResult> {
 
    public isLoading = true;
    private BASE_URL = "api/BaseConfig/Get?type=codeselections_get&id=";
 
 
    constructor(private http: HttpClient) { super(null); }
 
 
    public query(state: any, config_id: any, overridecombo: any, overridelevel: any): void {
 
        var url = this.BASE_URL + config_id + "&override_combo=" +overridecombo + "&override_level=" + overridelevel;
 
        this.fetch(state, url)
            .subscribe(data => {
                super.next(data);
 
                this.isLoading = false;
                //alert('time end:' + new Date());
                //var datatest = JSON.stringify(data);
                //alert('audittrail datatest: ' + datatest);
            }
            );
    }
 
    public fetch(state: any, url: string): Observable<GridDataResult> {
        //const queryStr = `${toODataString(state)}`;  
        const queryStr = `${toDataSourceRequestString(state)}`; 
 
        const hasGroups = state.group && state.group.length;
 
        return this.http
            .get(`${url}&${queryStr}`) // Send the state to the server
            .pipe(
            map(<CodeSelectionsGridDataResult>({ value, datacount, groupedvalue }) => {
                console.log(value);
                console.log(groupedvalue);
                return (<GridDataResult>{
                    data: hasGroups ? translateDataSourceResultGroups(groupedvalue) : value,
                    total: parseInt(datacount, 10)
                })
            }));
 
 
 
    }

6. In BasicConfigController.cs (API Controller returning data from server):

[RoutePrefix("api/BaseConfig")]
    public class BaseConfigController : ApiController
    {
 
        //[HttpGet]
        public HttpResponseMessage Get(ConfigActionType type, int id, string entitycd = "", string override_combo="", string override_level="")
        {
 
            using (DBSession session = new DBSession(ConnString))
            {
                try
                {
                    switch (type)
                    {
                         case ConfigActionType.codeselections_get:
                            {
                                 var querystring = HttpContext.Current.Request.QueryString;
            
                                 var retobj = CodeSelectionsHelper.Process(session, sessionuser.UserId,orgid, resultid, entitycd,
                                                                                             overridecombo, overridelevel, querystring.ToString());
                                 
                                return Request.CreateResponse<CodeSelectionsGridDataResult>(HttpStatusCode.OK, retobj);
                            }
 
 
                        default:
                            break;
 
                    }
               }
               catch(Exception ex)
               {
                   ...
               }
           }
 
  }

7. Object CodeSelectionsGridDataResult returned by BasicConfigController can contain either a groupedvalue (=array of array) or a value (array of items):

public class CodeSelectionsGridDataResult
    {
        public int datacount { get; set; }
        public List<CodeSelection> value { get; set; }
        public List<GroupedCode> groupedvalue { get; set; }
 
    }
 
public class GroupedCode
    {
        public string fieldname { get; set; }
        public string fieldvalue { get; set; }
        public List<CodeSelection> items { get; set; }
    }
 
public class CodeSelection
    {
        public bool isSelected { get; set; }
        public string CodeId { get; set; }
        public string CodeDesc { get; set; }
 
        public string CodeType { get; set; }
        public string Entity_Cd { get; set; }
 
        public string CurrentOverrideDesc { get; set; }
        public string CurrentOverrideCombo { get; set; }
 
        public CodeSelection() { }
 
    }

8. Attached are screenshots of data returned by the service on server side.

Many thanks for helping me resolve the parameter to be passed to the javascript function translateDataSourceResultGroups().

Let me know if you need additional info.

 

 

 

 

 

 

0
Dimiter Topalov
Telerik team
answered on 24 Jul 2018, 01:34 PM
Hi Chau,

There seems to be some misunderstanding and I am sorry if was not clear enough in my previous response. The approach demonstrated in the previously linked article and demo:

https://www.telerik.com/kendo-angular-ui/components/dataquery/mvc-integration/

... relies on using the Telerik UI for ASP.NET Core/Telerik UI for ASP.NET MVC DataSourceRequest model binded and ToDataSourceResult() helper method that processed the data in accordance with the incoming request.

The Data Query translateDataSourceResultGroups() function is only required to process the response when the data is grouped on the server via the ToDataSourceResult() method as in such cases the resulting structure is specific and needs further processing on the client.

When the ToDataSourceResult() method is not used to process the data on the server, you do not need to call the translateDataSourceResultGroups() function on the client. The data structure that the Grid expects when the data is grouped is a collection of type GroupResult:

https://www.telerik.com/kendo-angular-ui/components/dataquery/api/GroupResult/

As long the object returned from the server has a data property that is an array of GroupResults, and a total property indicating the total count of the items, the Grid can be directly bound to it.

For example, the result from processing the Grid data in the following grouping example:

https://www.telerik.com/kendo-angular-ui/components/grid/grouping/



https://plnkr.co/edit/67oCI4bf3fNte5j8mw8f?p=preview

The object the Grid is bound to has a data property that contains an array of GroupResult objects. Each of them has properties aggregates, field, items, and value.

To sum up, you can use either:

1) The available built-in DataSourceRequest and ToDataSourceResult() helpers like in the integration article, alongside the translateDataSourceResultGroups() function for processing the grouped data and the toDataSourceRequestString() function for processing the incoming Grid State and creating the proper query string the DataSourceRequest model binder will properly parse on the server.

2) Use custom logic both on the client and on the server to create a proper request from the current Grid state, process the data on the server in accordance with the incoming state, and return the resulting data to the client data service. You can further process the response on the client (if necessary) before the Grid can be bound to it. Using the translateDataSourceResultGroups() function in this case is not only not necessary, but will cause the discussed error as it is meant to process specific data structures, created by the ToDataSourceResult() method only.

The final structure the Grid expects when there is grouping applied, is the one observed in the screenshot above.

I hope this helps.

Regards,
Dimiter Topalov
Progress Telerik
Try our brand new, jQuery-free Angular components built from ground-up which deliver the business app essential building blocks - a grid component, data visualization (charts) and form elements.
0
Chau
Top achievements
Rank 1
answered on 27 Jul 2018, 09:29 PM

THANK YOU, Dimiter for your clear explanation.

I removed the call to 'translateDataSourceResultGroups' in my client service, and fixed my grouped result structure to have these properties: field, value, items.  The grid is now binding OK with grouped results and showing the expected groups!

Thanks again!

Tags
General Discussions
Asked by
Chau
Top achievements
Rank 1
Answers by
Dimiter Topalov
Telerik team
Chau
Top achievements
Rank 1
Share this question
or