How to Extend Customer List View adding Custom Field on Dynamics 365 Retails & Commerce POS
Pre-Requisite
1. Visual Studio 19
2. SQL Server
3. Scale Unit project from GitHub repository
( https://github.com/microsoft/Dynamics365Commerce.ScaleUnit ).
4. Verify you version and download as per match.
1. Download and Install SDK’s and runtime.
· sdk-2.1.513-windows-x64-installer
· runtime-2.1.17-windows-x64-installer
2. After installation of sdk and runtime.
3. Copy you scale unit project from download to Service Volume (k)
Development
1. Create MS Dynamics 365 FO project and add custom field on CustTable and Form.
2. Result
3. Add same custom field on ax.Custtable with script.
4. ALTER TABLE ax.CustTable Add RefNoExt nvarchar(510) Default ('').
5. Now link this field with the acutal field. Add Resource file with XML.
1. Visual Studio 19
2. SQL Server
3. Scale Unit project from GitHub repository
( https://github.com/microsoft/Dynamics365Commerce.ScaleUnit ).
4. Verify you version and download as per match.
1. Download and Install SDK’s and runtime.
· sdk-2.1.513-windows-x64-installer
· runtime-2.1.17-windows-x64-installer
2. After installation of sdk and runtime.
3. Copy you scale unit project from download to Service Volume (k)
Development
1. Create MS Dynamics 365 FO project and add custom field on CustTable and Form.
2. Result
3. Add same custom field on ax.Custtable with script.
4. ALTER TABLE ax.CustTable Add RefNoExt nvarchar(510) Default ('').
5. Now link this field with the acutal field. Add Resource file with XML.
XML Code
<RetailCdxSeedData Name="AX7" ChannelDBSchema="ext" ChannelDBMajorVersion="7">
<Subjobs>
<!--Adding additional columns to (existing) RetailTransactionTable and wants to pull it back to HQ.For upload subjobs, set the OverrideTarget property to "false", as ilustrated below. This will tell CDX to use the table defined by TargetTableName and TargetTableSchema as extension table on this subjob.-->
<Subjob Id="CUSTTABLE" TargetTableName ="CUSTTABLE" TargetTableSchema="ax" AxTableName="CUSTTABLE" IsUpload="false" OverrideTarget="false">
<ScheduledByJobs>
<ScheduledByJob>1010</ScheduledByJob>
</ScheduledByJobs>
<AxFields>
<!--If you notice the existing columns are not listed here in the <Field> tag, it's because the existing fields are already mapped in the main RetailCdxSeedData resource file, we only add the delta here.
-->
<Field Name="RefNoExt"/></AxFields>
</Subjob>
</Subjobs>
</RetailCdxSeedData>
XML Code
<RetailCdxSeedData Name="AX7" ChannelDBSchema="ext" ChannelDBMajorVersion="7">
<Subjobs>
<!--Adding additional columns to (existing) RetailTransactionTable and wants to pull it back to HQ.For upload subjobs, set the OverrideTarget property to "false", as ilustrated below. This will tell CDX to use the table defined by TargetTableName and TargetTableSchema as extension table on this subjob.-->
<Subjob Id="CUSTTABLE" TargetTableName ="CUSTTABLE" TargetTableSchema="ax" AxTableName="CUSTTABLE" IsUpload="false" OverrideTarget="false">
<ScheduledByJobs>
<ScheduledByJob>1010</ScheduledByJob>
</ScheduledByJobs>
<AxFields>
<!--If you notice the existing columns are not listed here in the <Field> tag, it's because the existing fields are already mapped in the main RetailCdxSeedData resource file, we only add the delta here.
-->
<Field Name="RefNoExt"/></AxFields>
</Subjob>
</Subjobs>
</RetailCdxSeedData>
6. Now add handler class
HandlerClass - Code
internal final class HandlerClass
{
[SubscribesTo(classstr(RetailCDXSeedDataBase), delegateStr(RetailCDXSeedDataBase, registerCDXSeedDataExtension))]
public static void RetailCDXSeedDataBase_registerSeedDataExtension(str orgSeedDataResource, List resource)
{
if (orgSeedDataResource == classStr(RetailCDXSeedDataAX7))
{
resource.addEnd(resourceStr(CustomColumnCustTable_AX7));
}
}
}
HandlerClass - Code
internal final class HandlerClass
{
[SubscribesTo(classstr(RetailCDXSeedDataBase), delegateStr(RetailCDXSeedDataBase, registerCDXSeedDataExtension))]
public static void RetailCDXSeedDataBase_registerSeedDataExtension(str orgSeedDataResource, List resource)
{
if (orgSeedDataResource == classStr(RetailCDXSeedDataAX7))
{
resource.addEnd(resourceStr(CustomColumnCustTable_AX7));
}
}
}
7. Now build the project and after that you have to run this job.
9. Now run full sync to update data from HQ to Channel. It will take some time to sync data.
10. Now you can see the data in table.
11. Open
Scale Unit Project from Service Volume (K) and build it. After successful build
add CustomCustomerSearchColumns.ts.
CustomCustomerSearchColumns.ts - Code
import {
ICustomerSearchColumn } from "PosApi/Extend/Views/SearchView";
import {
ICustomColumnsContext } from "PosApi/Extend/Views/CustomListColumns"; import { ProxyEntities } from "PosApi/Entities";
export default (context: ICustomColumnsContext): ICustomerSearchColumn[] => { return [
{
title: context.resources.getString("string_2"),
computeValue:
(row: ProxyEntities.GlobalCustomer): string => { return row.AccountNumber; },
ratio: 15,
collapseOrder: 5,
minWidth: 120
},
{
title: context.resources.getString("string_3"),
computeValue:
(row: ProxyEntities.GlobalCustomer): string => {
return row.FullName; },
ratio: 20,
collapseOrder: 4,
minWidth: 200
}, {
title: "Ref No Ext",
//title: context.resources.getString("string_4"), computeValue: (row: ProxyEntities.GlobalCustomer): string => {
return row.ExtensionProperties.filter(p
=> p.Key === "RefNoExt")[0].Value.StringValue as string;
},
ratio: 25,
collapseOrder: 1,
minWidth: 200
}, {
title: context.resources.getString("string_5"),
computeValue:
(row: ProxyEntities.GlobalCustomer): string => {
return row.Email; },
ratio: 20,
collapseOrder: 2,
minWidth: 200
}, {
title: context.resources.getString("string_7"),
computeValue:
(row: ProxyEntities.GlobalCustomer): string => {
return row.Phone;
},
ratio: 20,
collapseOrder: 3,
minWidth: 120
}
];
};
9. Now run full sync to update data from HQ to Channel. It will take some time to sync data.
10. Now you can see the data in table.
11. Open Scale Unit Project from Service Volume (K) and build it. After successful build add CustomCustomerSearchColumns.ts.
CustomCustomerSearchColumns.ts - Code
import {
ICustomerSearchColumn } from "PosApi/Extend/Views/SearchView";
import {
ICustomColumnsContext } from "PosApi/Extend/Views/CustomListColumns"; import { ProxyEntities } from "PosApi/Entities";
export default (context: ICustomColumnsContext): ICustomerSearchColumn[] => { return [
{
title: context.resources.getString("string_2"),
computeValue:
(row: ProxyEntities.GlobalCustomer): string => { return row.AccountNumber; },
ratio: 15,
collapseOrder: 5,
minWidth: 120
},
{
title: context.resources.getString("string_3"),
computeValue: (row: ProxyEntities.GlobalCustomer): string => {
return row.FullName; },
ratio: 20,
collapseOrder: 4,
minWidth: 200
}, {
title: "Ref No Ext",
//title: context.resources.getString("string_4"), computeValue: (row: ProxyEntities.GlobalCustomer): string => {
return row.ExtensionProperties.filter(p
=> p.Key === "RefNoExt")[0].Value.StringValue as string;
},
ratio: 25,
collapseOrder: 1,
minWidth: 200
}, {
title: context.resources.getString("string_5"),
computeValue: (row: ProxyEntities.GlobalCustomer): string => {
return row.Email; },
ratio: 20,
collapseOrder: 2,
minWidth: 200
}, {
title: context.resources.getString("string_7"),
computeValue: (row: ProxyEntities.GlobalCustomer): string => {
return row.Phone;
},
ratio: 20,
collapseOrder: 3,
minWidth: 120
}
];
};
12. Register class in manifest.json
ChannelDataServiceRequestTrigger.cs - Code
namespace Contoso.CommerceRuntime.Triggers
{
using Microsoft.Dynamics.Commerce.Runtime; using Microsoft.Dynamics.Commerce.Runtime.Data;
using Microsoft.Dynamics.Commerce.Runtime.DataModel;
using Microsoft.Dynamics.Commerce.Runtime.DataServices.Messages; using Microsoft.Dynamics.Commerce.Runtime.Messages;
using System;
using System.Collections.Generic; using System.Linq;
using System.Threading.Tasks;
public class ChannelDataServiceRequestTrigger :
IRequestTriggerAsync
{
public static readonly string PropertyKey = "ExtConfigurationParameters";
/// <summary>
/// Gets the supported requests for this trigger.
/// </summary>
public IEnumerable<Type> SupportedRequestTypes
{
get
{
return new Type[]
{
typeof(GetChannelConfigurationDataRequest), typeof(SearchCustomersDataRequest)
};
}
}
/// <summary>
/// Pre trigger code.
/// </summary>
/// <param name="request">The request.</param>
public Task OnExecuting(Request request)
{
// It's only stub to handle async signature
return Task.CompletedTask;
}
/// <summary>
/// Post request trigger
/// </summary>
/// <param name="request">request</param>
/// <param name="response">response</param>
public async Task
OnExecuted(Request request, Response response)
{
switch (request)
{
case GetChannelConfigurationDataRequest
originalRequest: var data = response as
SingleEntityDataServiceResponse<ChannelConfiguration>;
if (data !=
null && data.Entity != null && data.Entity.GetProperty(PropertyKey) ==
null)
{
// In this example, we just put the configuration parameters as part of
channelConfiguration property.
var configurationParameters = (await request.RequestContext.ExecuteAsync<EntityDataServiceResponse<RetailConfigurati onParameter>>(new GetConfigurationParametersDataRequest(originalRequest.ChannelId)).ConfigureAwai t(false)).ToList();
// The reason we need a lock here because of thread- safety.
// ChannelConfiguration is an object required in most crt request, and we cached in memory on
the underlying ChannelDataService.
// In case there is concurrent crt request, without lock here, it will modify against the same ChannelConfiguration and will result as 100% CPU usage in worst case.
// NOTE: both SetProperty and ExtensionProperties are not thread-safe.
// NOTE: same situation for DeviceConfiguration, in which it is also required in most crt request and is
cached in underlying DataService.
lock (data.Entity)
{
if (data.Entity.GetProperty(PropertyKey) ==
null)
{
data.Entity.SetProperty(PropertyKey,
configurationParameters);
}
}
}
break;
case SearchCustomersDataRequest getCustomerSearchResultsDataRequest:
var res = (EntityDataServiceResponse<GlobalCustomer>)response;
foreach (var
item in res)
{
string value = "";
var databaseContext =
new DatabaseContext(request.RequestContext);
ParameterSet configurationDataParameters = new ParameterSet
{
["@AccountNum"]
= item.AccountNumber
};
//Get tender type Id for tokenized tender line from database
var configurationDataSet = await databaseContext
.ExecuteQueryDataSetAsync("SELECT REFNOEXT from ax.CUSTTABLE where ACCOUNTNUM =
@AccountNum", configurationDataParameters);
if (configurationDataSet.Tables[0].Rows.Count >
0)
{
value
= configurationDataSet.Tables[0].Rows[0][0] as string;
}
item.ExtensionProperties.Add(new CommerceProperty()
{
Key = "RefNoExt", Value =
value
});
}
break; default:
throw new NotSupportedException($"Request '{request.GetType()}' is not supported.");
}
}
}
}
ChannelDataServiceRequestTrigger.cs - Code
namespace Contoso.CommerceRuntime.Triggers
{
using Microsoft.Dynamics.Commerce.Runtime; using Microsoft.Dynamics.Commerce.Runtime.Data;
using Microsoft.Dynamics.Commerce.Runtime.DataModel;
using Microsoft.Dynamics.Commerce.Runtime.DataServices.Messages; using Microsoft.Dynamics.Commerce.Runtime.Messages;
using System;
using System.Collections.Generic; using System.Linq;
using System.Threading.Tasks;
public class ChannelDataServiceRequestTrigger :
IRequestTriggerAsync
{
public static readonly string PropertyKey = "ExtConfigurationParameters";
/// <summary>
/// Gets the supported requests for this trigger.
/// </summary>
public IEnumerable<Type> SupportedRequestTypes
{
get
{
return new Type[]
{
typeof(GetChannelConfigurationDataRequest), typeof(SearchCustomersDataRequest)
};
}
}
/// <summary>
/// Pre trigger code.
/// </summary>
/// <param name="request">The request.</param>
public Task OnExecuting(Request request)
{
// It's only stub to handle async signature
return Task.CompletedTask;
}
/// <summary>
/// Post request trigger
/// </summary>
/// <param name="request">request</param>
/// <param name="response">response</param>
public async Task
OnExecuted(Request request, Response response)
{
switch (request)
{
case GetChannelConfigurationDataRequest
originalRequest: var data = response as
SingleEntityDataServiceResponse<ChannelConfiguration>;
if (data !=
null && data.Entity != null && data.Entity.GetProperty(PropertyKey) ==
null)
{
// In this example, we just put the configuration parameters as part of
channelConfiguration property.
var configurationParameters = (await request.RequestContext.ExecuteAsync<EntityDataServiceResponse<RetailConfigurati onParameter>>(new GetConfigurationParametersDataRequest(originalRequest.ChannelId)).ConfigureAwai t(false)).ToList();
// The reason we need a lock here because of thread-
// ChannelConfiguration is an object required in most crt request, and we cached in memory on the underlying ChannelDataService.
// In case there is concurrent crt request, without lock here, it will modify against the same ChannelConfiguration and will result as 100% CPU usage in worst case.
// NOTE: both SetProperty and ExtensionProperties are not thread-safe.
// NOTE: same situation for DeviceConfiguration, in which it is also required in most crt request and is cached in underlying DataService.
lock (data.Entity)
{
if (data.Entity.GetProperty(PropertyKey) ==
null)
{
data.Entity.SetProperty(PropertyKey,
configurationParameters);
}
}
}
break;
case SearchCustomersDataRequest getCustomerSearchResultsDataRequest:
var res = (EntityDataServiceResponse<GlobalCustomer>)response;
foreach (var
item in res)
{
string value = "";
var databaseContext =
new DatabaseContext(request.RequestContext);
ParameterSet configurationDataParameters = new ParameterSet
{
["@AccountNum"]
= item.AccountNumber
};
//Get tender type Id for tokenized tender line from database
var configurationDataSet = await databaseContext
.ExecuteQueryDataSetAsync("SELECT REFNOEXT from ax.CUSTTABLE where ACCOUNTNUM = @AccountNum", configurationDataParameters);
if (configurationDataSet.Tables[0].Rows.Count >
0)
{
value
= configurationDataSet.Tables[0].Rows[0][0]
}
item.ExtensionProperties.Add(new CommerceProperty()
{
Key = "RefNoExt", Value =
value
});
}
break; default:
throw new NotSupportedException($"Request '{request.GetType()}' is not supported.");
}
}
}
}
14. Rebuild the whole solution.
15. Once the build is compiled successfully Now copy the commerce runtime library. CommerceRuntime.dll
16. Open CommerceRuntime.Ext.config file and add the following line in the composition tag.
<add source="assembly" value="CommerceRuntime" />
17. Now deploy the extension on the dev machine by copying the code.
From:..\..\ScaleUnit\bin\Debug\netstandard2.0\CloudScaleUnitExtensionPackage\RetailClou dPOS\Code\Extensions”
To: “K:\RetailCloudPos\WebRoot\Extensions”
18. Now login to pos and go to setting to check package
16. Open CommerceRuntime.Ext.config file and add the following line in the composition tag.
<add source="assembly" value="CommerceRuntime" />
17. Now deploy the extension on the dev machine by copying the code.
From:..\..\ScaleUnit\bin\Debug\netstandard2.0\CloudScaleUnitExtensionPackage\RetailClou dPOS\Code\Extensions”
To: “K:\RetailCloudPos\WebRoot\Extensions”
18. Now login to pos and go to setting to check package
19. Now go to customer






























Comments
Post a Comment