Often, we want to have detail that will contain records which don't exist in DataBase, for this aim we can create "Virtual Detail".
In our development cases we usually create detail which load data from external services. If you want to create this detail you should complete three main steps.
- Create virtual object for entities.
- Create service which will prepare data collection.
- Create virtual detail.
- Insert virtual detail into page.
Create POCO object for entities.
You should create virtual object that won't store in DB, but we will display this one in our detail.
Figure 1. POCO object
Create service which will prepare data collection.
In this tutorial I will get data from external WCF service by ProxyClient, you can use another way for this purpose.
In general case you should write configuration service which will prepare data for our detail. My one looks like following:
namespace Terrasoft.Configuration.UsrLocationService { using System; using System.Linq; using System.IO; using System.Text; using System.Net; using System.Web; using System.Collections.Generic; using System.Runtime.Serialization; using System.Reflection; using System.ServiceModel; using System.ServiceModel.Activation; using System.ServiceModel.Web; using Terrasoft.Common; using Terrasoft.Core; using Terrasoft.Core.Factories; using Terrasoft.Core.DB; using Terrasoft.Core.Entities; using ErrorInfo = Terrasoft.Nui.ServiceModel.DataContract.ErrorInfo; using SelectQueryResponse = Terrasoft.Nui.ServiceModel.DataContract.SelectQueryResponse; using DataValueType = Terrasoft.Nui.ServiceModel.DataContract.DataValueType; using EntityCollection = Terrasoft.Nui.ServiceModel.DataContract.EntityCollection; using Newtonsoft.Json; using System.Globalization; [ServiceContract] [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Required)] public class UsrLocationService { [OperationContract] [WebInvoke(Method = "POST", UriTemplate = "GetLocations", RequestFormat = WebMessageFormat.Json, ResponseFormat = WebMessageFormat.Json)] public string GetLocations(LocationRequest request) { UsrBaseLocationServiceHelper helper = ClassFactory.Get<UsrBaseLocationServiceHelper>(); var client = helper.ProxyClient; var result = client.GetLocations( string.IsNullOrEmpty(request.AccountName) ? null : request.AccountName, string.IsNullOrEmpty(request.AccountNumber) ? null :request.AccountNumber, string.IsNullOrEmpty(request.Location) ? null : request.Location, null, string.IsNullOrEmpty(request.CityName) ? null : request.CityName, string.IsNullOrEmpty(request.StateName) ? null : request.StateName, string.IsNullOrEmpty(request.Zip) ? null : request.Zip, string.IsNullOrEmpty(request.Phone) ? null : request.Phone, null, null, Convert.ToInt32(request.ExcludeDeactivated), Convert.ToInt32(request.ExcludeUnassigned) ); var response = new UsrLocationResponse(); response.Message = result.ErrorMessage; foreach(var entity in result.Locations) { response.Locations.Add( new Dictionary<string, object>() { { "Id", Guid.NewGuid() }, { "AccountID", entity.AccountID.ToString() }, { "AccountName", entity.AccountName }, { "Address", entity.Address }, { "Country", entity.Country }, { "City", entity.City }, { "GeneralNbr", entity.GeneralNbr }, { "EMail", entity.EMail } } ); } var res = new SelectQueryResponse { RowsAffected = response.Locations.Count, Success = (response.Message == "CallCenterServices.Success." ? true : false), Rows = response.Locations, RowConfig = RowConfig, ErrorInfo = new ErrorInfo { Message = response.Message } }; return JsonConvert.SerializeObject(res); } public Dictionary<string, object> RowConfig = new Dictionary<string, object> { {"Id", new { dataValueType = DataValueType.Guid }}, {"AccountID", new { dataValueType = DataValueType.Text }}, {"AccountName", new { dataValueType = DataValueType.Text }}, {"Address", new { dataValueType = DataValueType.Text }}, {"Country", new { dataValueType = DataValueType.Text }}, {"City", new { dataValueType = DataValueType.Text }}, {"GeneralNbr", new { dataValueType = DataValueType.Text }}, {"EMail", new { dataValueType = DataValueType.Text }} }; } [DataContract] public class LocationRequest { [DataMember] public string AccountName { get; set; } [DataMember] public string AccountNumber { get; set; } [DataMember] public string Location { get; set; } [DataMember] public string CityName { get; set; } [DataMember] public string StateName { get; set; } [DataMember] public string Zip { get; set; } [DataMember] public string Phone { get; set; } [DataMember] public bool ExcludeDeactivated { get; set; } [DataMember] public bool ExcludeUnassigned { get; set; } } [DataContract] public class UsrLocationResponse { public UsrLocationResponse() { Locations = new EntityCollection(); } [DataMember] public string Message { get; set; } [DataMember] public EntityCollection Locations { get; set; } } }
This service should return object which contains EntityCollection.
Inside GetLocations methods after calling proxyсlient you can see how we prepare data:
var response = new UsrLocationResponse(); response.Message = result.ErrorMessage; foreach(var entity in result.Locations) { response.Locations.Add( new Dictionary<string, object>() { { "Id", Guid.NewGuid() }, { "AccountID", entity.AccountID.ToString() }, { "AccountName", entity.AccountName }, { "Address", entity.Address }, { "Country", entity.Country }, { "City", entity.City }, { "GeneralNbr", entity.GeneralNbr }, { "EMail", entity.EMail } } ); } var res = new SelectQueryResponse { RowsAffected = response.Locations.Count, Success = (response.Message == "CallCenterServices.Success." ? true : false), Rows = response.Locations, RowConfig = RowConfig, ErrorInfo = new ErrorInfo { Message = response.Message } };
Important! Columns should be same in virtual object from first step, foreach statment and RowConfig collection.
Create virtual detail.
Code of virtual detail is pretty simple. You should just copy it and change few things:
define("UsrLocationSearchResultDetail", ["UsrLocationSearchResultDetailResources"], function(resources) { return { entitySchemaName: "LocationServicePoco", methods: { sortColumn: this.Terrasoft.emptyFn, loadGridData: this.Terrasoft.emptyFn, init: function() { this.callParent(arguments); this.set("IsGridEmpty", true); this.set("IsGridDataLoaded", true); this.loadSearchItems(); }, loadSearchItems: function() { this.set("MaskId", Terrasoft.Mask.show({timeout: 0})); this.set("IsGridEmpty", true); this.set("IsGridLoading", true); this.set("IsGridDataLoaded", false); var serviceConfig = { serviceName: "UsrLocationService", methodName: "GetLocations", timeout: 120000, data: {/*Some input params*/} }; this.callService(serviceConfig, function(responseJson) { this.set("IsGridLoading", false); this.set("IsGridDataLoaded", true); if (!this.Ext.isEmpty(responseJson)) { var response = this.Ext.decode(responseJson); if (response.success) { if (response.rowsAffected > 0) { this.set("IsGridEmpty", false); var esq = this.Ext.create("Terrasoft.EntitySchemaQuery", { rootSchemaName: "LocationServicePoco" }); esq.parseResponse(response, function(result) { if (result.success) { var gridData = this.getGridData(); gridData.clear(); gridData.loadAll(result.collection); Terrasoft.Mask.hide(this.get("MaskId")); } else { this.showInformationDialog(result.errorInfo); Terrasoft.Mask.hide(this.get("MaskId")); } }, this); } } else { this.showInformationDialog(response.errorInfo.message); Terrasoft.Mask.hide(this.get("MaskId")); } } }, this); } }, diff: /**SCHEMA_DIFF*/[ { "operation": "merge", "name": "DataGrid", "values": { "type": this.Terrasoft.GridType.LISTED, "listedConfig": { "name": "DataGridListedConfig", "items": [ { "name": "AccountNumberListedGridColumn", "bindTo": "AccountID", "caption": "Account number", "type": Terrasoft.GridCellType.TEXT, "position": {"column": 1, "colSpan": 2} }, { "name": "AccountListedGridColumn", "bindTo": "AccountName", "caption": "Account", "type": Terrasoft.GridCellType.TEXT, "position": {"column": 3, "colSpan": 3} }, { "name": "AddressListedGridColumn", "bindTo": "Address", "caption": "Address", "type": Terrasoft.GridCellType.TEXT, "position": {"column": 6, "colSpan": 4} }, { "name": "CountryListedGridColumn", "bindTo": "Country", "caption": "Country", "type": Terrasoft.GridCellType.TEXT, "position": {"column": 10, "colSpan": 4} }, { "name": "CityListedGridColumn", "bindTo": "City", "caption": "City", "type": Terrasoft.GridCellType.TEXT, "position": {"column": 14, "colSpan": 4} }, { "name": "PhoneListedGridColumn", "bindTo": "GeneralNbr", "caption": "Phone", "type": Terrasoft.GridCellType.TEXT, "position": {"column": 18, "colSpan": 3} }, { "name": "EmailListedGridColumn", "bindTo": "EMail", "caption": "Email", "type": Terrasoft.GridCellType.TEXT, "position": {"column": 21, "colSpan": 3} } ] }, "activeRowActions": [], "activeRowAction": {"bindTo": "onActiveRowAction"}, "tiledConfig": { "name": "DataGridTiledConfig", "grid": {"columns": 24, "rows": 1}, "items": [] } } }/*, { "operation": "remove", "name": "ToolsButton" }*/ ]/**SCHEMA_DIFF*/ }; });
- You should change entitySchemaName (schema name of your virtual object).
- Change parameters of web service calling (service name, method name, timeout and input parameters in data object).
- Inside callback of callService function you should change entitySchemaName in ESQ (write schema name of your virtual object).
- Change "listedConfig" inside "DataGrid" element of "diff" section. This column config should be same to config in service from step 2.
Insert virtual detail into page.
Just insert created detail into your page.
define("UsrLocationSearchPage", [], function() { return { entitySchemaName: "BaseEntity", details: /**SCHEMA_DETAILS*/{ "UsrLocationSearchResultDetail": { "schemaName": "UsrLocationSearchResultDetail" } }/**SCHEMA_DETAILS*/, diff: /**SCHEMA_DIFF*/[ { "operation": "insert", "name": "UsrLocationSearchResultDetail", "values": { "itemType": 2, "markerValue": "added-detail" }, "parentName": "SearchResultContainer", "propertyName": "items", "index": 0 } ]/**SCHEMA_DIFF*/ }; });
Could you please send us the screen shot of the data that will be displayed once this function will be called ?
Hi Tatiana Rogova,
I followed your guide step by step and receive these error and don't know what going on. How can I find what problem with this? Thanks
Hi Tatiana Rogova,
I don't know why file js of virtual detail wasn't downloaded after I add it to pre-configuration page. Please help.
Toàn Mai
Hi Toan Mai,
Could you please provide a bit more detailed information on your issue? Please send us your code so we could check the request precisely. Look forward to hearing from you!
Regards,
Anastasiia
Anastasiia Markina,
Hi Anastasiia, Had a follow up question on the virtual detail -
The sort, sort by and filter features are not available by default on a virtual detail. UI notice that the sort & search on a real detail (One with an object in the database) happens on the server side and the detail just loads the fresh data it receives from the server.
Given this, Is there a way to enable/implement the sort & filter functionality for virtual details as well?
One way would be to override those functions on the detail schema and implement a custom sort & search purely on the client side using JS. But this is practically re-inventing the wheel. Is there a better way to implement the sort & filter? Thanks in advance!
M Shrikanth,
If you want your custom detail to have sorting, searching, and filtering functions, it's better to use the detail inheritance from the base "BaseGridDetailV2" schema instead.
Thus, you will be able to customize it according to your needs, inherit necessary functionality (like filtering and sorting) and remove unnecessary ones.
Regards,
Anastasiia