Skip to main content

3.10 Standard contextual selection

In this section we will use a similar technique to set up contextual selection for the Credit Card Id on the Payment tab, but we'll use Xomega Framework support to minimize the custom code needed for standard cases.

Overview of updates

Instead of showing a text field to enter ID of a stored credit card, we will display a dropdown list with the credit cards stored for the current customer's person. We will move both the credit card selection and the Credit Card Approval Code fields to a child panel Credit Card under the Payment tab.

Credit card ID

When the user selects a credit card, we'll display additional credit card details in the same Credit Card child panel.

Defining contextual enumeration

We will start by defining a contextual dynamic enumeration that returns a list of credit cards with their details for a specific person.

Person Credit Card subobject

The list of credit cards of a specific persons is defined in the Sales.PersonCreditCard database table, which should normally be a subobject of the person aggregate object.

However that table does not have a cascade delete on its foreign key to the parent Person.Person table, so it was imported into the model as a separate person credit card entity that has a compound key defined by a fieldset with the same name, as follows.

person_credit_card.xom
  <fieldsets>
<fieldset name="person credit card">
<field name="business entity id" type="person" required="true">[...]
<field name="credit card id" type="credit card" required="true">[...]
</fieldset>
</fieldsets>
<objects>
<object name="person credit card">
<fields>
<fieldset ref="person credit card" key="supplied" required="true"/>
<field name="modified date" type="date time" required="true">[...]
</fields>
...
</object>
</objects>

In order to make it a subobject of a person object, we'll move its object element to the person.xom file under a subobjects element. We will also rename it to be just credit card, since it's name is automatically qualified with the parent object's name, and will change the key field to be just the credit card id from the above fieldset, since the business entity id will be already inherited from the parent person object, as shown below.

person.xom
  <types>
<type name="person" base="business entity"/>
</types>
<objects>
<object name="person">
<fields>
<field name="business entity id" type="person" key="supplied" required="true">
...
</fields>
...
<subobjects>
<object name="credit card">
<fields>
<field name="credit card id" type="credit card" key="reference" required="true">[...]
<field name="modified date" type="date time" required="true">[...]
</fields>
<config>
<sql:table name="Sales.PersonCreditCard">
<sql:parent-foreign-key delete="no action"/>
</sql:table>
</config>
...
</object>
</subobjects>
</object>
</objects>
note

Since the credit card id field references a separate credit card object, we need to set the key attribute to reference. Also, if we want to keep the delete action for the parent foreign key, instead of using the default cascade action, then we'll need to set it on the sql:parent-foreign-key element.

At this point we can delete the person_credit_card.xom file from the Sales folder, since there will be nothing useful left in it. Also, in order to rebuild the entity classes using the new structure, we will run a Clean command on the EF Domain Objects generator under the Data Layer folder, followed by the Generate command on that generator.

Configuring credit card types

Before we add our enumeration, let's open the credit_card.xom file, and update the card number field to use a new logical type credit card number, instead of the string25 that was created by the import process. In order to make credit card a selection, we will also update the key type credit card to be based on integer enumeration, as illustrated below.

credit_card.xom
  <types>
<type name="credit card" base="integer key"/>
<type name="credit card" base="integer enumeration"/>
<type name="credit card number" base="string" size="25"/>
</types>
<objects>
<object name="credit card">
<fields>
...
<field name="card number" type="string25" required="true">
<field name="card number" type="credit card number" required="true">
...
</fields>
</object>
</objects>

Adding contextual enumeration

Similar to what we did before, we will make sure that Read Enum Operation generator is configured to generate a read enum on subobjects only, and then will run that generator on the person.xom file.

Next, instead of the default modified date output parameter, we will add output parameters for a number of credit card object's fields that we want to show when selecting a credit card, using their respective types, as illustrated below.

person.xom
<object name="person">
...
<subobjects>
<object name="credit card">
...
<operation name="read enum">
<input>
<param name="business entity id" type="person" required="true"/>
</input>
<output list="true">
<param name="credit card id"/>
<param name="description" type="string"/>
<param name="modified date"/>
<param name="person name" type="name" required="true"/>
<param name="card type" type="name" required="true"/>
<param name="card number" type="credit card number" required="true"/>
<param name="exp month" type="tiny int" required="true"/>
<param name="exp year" type="small int" required="true"/>
</output>
<config>
<rest:method verb="GET" uri-template="person/{business entity id}/credit-card/enum"/>
<xfk:enum-cache enum-name="person credit card" id-param="credit card id"
desc-param="description"/>
</config>
<doc>[...]
</operation>
...
</object>
</subobjects>
</object>

Custom service implementation

To provide custom service implementation for our output parameters, let's build the model project and open the generated PersonService class. We'll update our CreditCard_ReadEnumAsync operation to read all the credit card parameters, where we will return the credit card description as the card type, and the last four digits of the card number, as follows.

PersonService.cs
public partial class PersonService : BaseService, IPersonService
{
...
public virtual async Task<Output<ICollection<PersonCreditCard_ReadEnumOutput>>>
CreditCard_ReadEnumAsync(int _businessEntityId, CancellationToken token = default)
{
...
var qry = from obj in src
select new PersonCreditCard_ReadEnumOutput() {
CreditCardId = obj.CreditCardId,
// CUSTOM_CODE_START: set the Description output parameter of CreditCard_ReadEnum operation below
// TODO: Description = obj.???, // CUSTOM_CODE_END
Description = obj.CreditCardObject.CardType + "-*" +
obj.CreditCardObject.CardNumber.Substring(
obj.CreditCardObject.CardNumber.Length - 4), // CUSTOM_CODE_END
// CUSTOM_CODE_START: set the PersonName output parameter of CreditCard_ReadEnum operation below
// TODO: PersonName = obj.???, // CUSTOM_CODE_END
PersonName = obj.PersonObject.LastName + ", " + obj.PersonObject.FirstName, // CUSTOM_CODE_END
// CUSTOM_CODE_START: set the CardType output parameter of CreditCard_ReadEnum operation below
// TODO: CardType = obj.???, // CUSTOM_CODE_END
CardType = obj.CreditCardObject.CardType, // CUSTOM_CODE_END
// CUSTOM_CODE_START: set the CardNumber output parameter of CreditCard_ReadEnum operation below
// TODO: CardNumber = obj.???, // CUSTOM_CODE_END
CardNumber = obj.CreditCardObject.CardNumber, // CUSTOM_CODE_END
// CUSTOM_CODE_START: set the ExpMonth output parameter of CreditCard_ReadEnum operation below
// TODO: ExpMonth = obj.???, // CUSTOM_CODE_END
ExpMonth = obj.CreditCardObject.ExpMonth, // CUSTOM_CODE_END
// CUSTOM_CODE_START: set the ExpYear output parameter of CreditCard_ReadEnum operation below
// TODO: ExpYear = obj.???, // CUSTOM_CODE_END
ExpYear = obj.CreditCardObject.ExpYear // CUSTOM_CODE_END
};
...
}
}

In order to make sure that your inline customizations are preserved if you run the Clean command on the model, you can add a svc:customize config element to the person object, and set the preserve-on-clean="true" attribute, as follows.

person.xom
    <config>
<sql:table name="Person.Person"/>
<edm:customize extend="true"/>
<svc:customize preserve-on-clean="true"/>
</config>

Credit Card grouping object

To group credit card fields into a child panel, let's define a new data object CreditCardPaymentObject, and add it as a child of the SalesOrderPaymentObject using credit card as the name, as shown below.

sales_order.xom
  <xfk:data-objects>
<xfk:data-object class="CreditCardPaymentObject"/>
...
<xfk:data-object class="SalesOrderPaymentObject">
<xfk:add-child name="credit card" class="CreditCardPaymentObject"/>
<ui:display>[...]
</xfk:data-object>
</xfk:data-objects>

Next, we will add a generic credit card info structure for this object to add fields to it, as shown below.

  <struct name="credit card info" object="credit card">
<param name="credit card id"/>
<param name="card number"/>
<param name="expiration" type="string"/>
<config>
<xfk:add-to-object class="CreditCardPaymentObject"/>
</config>
<usage generic="true"/>
</struct>

Now we can configure our data object to make credit card non-key fields read-only, and to set a proper label for the credit card id, as follows.

  <xfk:data-object class="CreditCardPaymentObject">
<ui:display>
<ui:fields>
<ui:field param="credit card id" label="Credit Card"/>
<ui:field param="card number" editable="false"/>
<ui:field param="expiration" editable="false"/>
</ui:fields>
</ui:display>
</xfk:data-object>

Updating operation structures

Next we will move the credit card id and credit card approval code parameters from the payment info and payment update structures to a new structure sales order credit card, and will update both payment structures to use a reference to this new structure instead, as follows.

    <struct name="payment info" object="sales order">
...
<param name="credit card id"/>
<param name="credit card approval code"/>
<struct name="credit card" ref="sales order credit card"/>
...
</struct>
<struct name="payment update" object="sales order">
...
<param name="credit card id"/>
<param name="credit card approval code"/>
<struct name="credit card" ref="sales order credit card"/>
...
</struct>
<struct name="sales order credit card" object="sales order">
<param name="credit card id" required="true"/>
<param name="credit card approval code"/>
<config>
<xfk:add-to-object class="CreditCardPaymentObject"/>
</config>
</struct>
note

This new structure is different from the credit card info structure that we defined, since it's used specifically for the operations. It must be referenced using the same name credit card, as the one that we used for our child data object earlier.

Refactoring custom service code

Let's build the model project, and refactor our custom service code in the SalesOrderServiceExtended.cs to use the new structure, as follows.

SalesOrderServiceExtended.cs
public partial class SalesOrderService
{
protected static PaymentInfo GetPaymentInfo(SalesOrder obj) => new()
{
...
CreditCard = new SalesOrderCreditCard {
CreditCardId = obj.CreditCardObject?.CreditCardId ?? 0,
CreditCardApprovalCode = obj.CreditCardApprovalCode,
},
CurrencyRate = obj.CurrencyRateObject?.RateString
};

protected async Task UpdatePayment(SalesOrder obj, PaymentUpdate pmt, CancellationToken token)
{
...
obj.CreditCardApprovalCode = pmt.CreditCardApprovalCode;
obj.CreditCardApprovalCode = pmt.CreditCard.CreditCardApprovalCode;
obj.CreditCardObject = await ctx.FindEntityAsync<CreditCard>(currentErrors, token, pmt.CreditCardId);
obj.CreditCardObject = await ctx.FindEntityAsync<CreditCard>(currentErrors, token,
pmt.CreditCard.CreditCardId);
}
...
}

Contextual UI selection

Now we need to populate a list of person's credit cards for the selected customer on the sales order.

Since the customer and payment are sibling child objects on the sales order data object, we will implement this logic in their common parent object SalesOrderObject, so we'll need to customize it as follows.

sales_order.xom
    <xfk:data-object class="SalesOrderObject" customize="true">[...]

Let's build the model project and open up the generated SalesOrderObjectCustomized.cs file. We'll set up the local lookup cache loader for the CreditCardIdProperty using PersonCreditCardReadEnumCacheLoader generated for our enumeration.

Then, instead of manually listening to the updates of the PersonIdProperty and then refreshing the cache loader and updating the CreditCardIdProperty, we'll just call the SetCacheLoaderParameters method with the generated constant for the input parameter name and the source property, and Xomega Framework will automatically handle all the rest.

The following snippet illustrates this logic.

SalesOrderObjectCustomized.cs
using AdventureWorks.Services.Common;
using AdventureWorks.Services.Common.Enumerations;

public class SalesOrderObjectCustomized : SalesOrderObject
{
...
// perform post initialization
protected override void OnInitialized()
{
base.OnInitialized();
var ccProp = PaymentObject.CreditCardObject.CreditCardIdProperty;
ccProp.LocalCacheLoader = new PersonCreditCardReadEnumCacheLoader(ServiceProvider);
ccProp.SetCacheLoaderParameters(PersonCreditCard.Parameters.BusinessEntityId,
CustomerObject.PersonIdProperty);
}
}
tip

If a list of possible values in your property depends on values of multiple other properties, then you can call SetCacheLoaderParameters for each such property/input parameter, and the framework will update the list whenever either of those properties changes.

Populating credit card on selection

Now, in order to display the credit card details whenever a credit card is selected on the screen, we'll need to populate the read-only properties in the customized CreditCardPaymentObject. So let's add customize="true" to it in the model, and build the model project to generate the customization file.

sales_order.xom
    <xfk:data-object class="CreditCardPaymentObject" customize="true">[...]

We'll add a credit card change listener in the OnInitialized method, which will populate other properties from the credit card attributes. The expiration property will combine both expiration month and year, as shown below.

CreditCardPaymentObjectCustomized.cs
using AdventureWorks.Services.Common.Enumerations;

public class CreditCardPaymentObjectCustomized : CreditCardPaymentObject
{
...
// perform post initialization
protected override void OnInitialized()
{
base.OnInitialized();
CreditCardIdProperty.Change += OnCreditCardChanged;
}

private void OnCreditCardChanged(object sender, PropertyChangeEventArgs e)
{
if (e.Change.IncludesValue() && !Equals(e.OldValue, e.NewValue))
{
Header cc = CreditCardIdProperty.Value;
CardNumberProperty.SetValue(cc?[PersonCreditCard.Attributes.CardNumber]);
ExpirationProperty.SetValue(cc == null ? null : cc[PersonCreditCard.Attributes.ExpMonth]
+ "/" + cc[PersonCreditCard.Attributes.ExpYear]);
}
}
}

Reviewing the results

Let's run the application, and review our changes. The Payment tab should now look as shown below.

Credit card selection

As you see, we're displaying a list of person's saved credit cards using the credit card type and the last four digits of the card number now, and show specific credit card details when one is selected from the list.

If you change the customer on the Customer tab, the list of credit cards will be automatically refreshed, and the credit card fields will be blanked out until you select a new credit card.