Skip to main content

3.7 Fields from related objects

In this step, we will demonstrate how to show fields from related objects on a details screen using techniques that we've learned so far.

Overview of updates

The customer-related fields, highlighted in yellow on the Sales Order details screen below, are just the internal IDs of the related customer and address objects that are stored on the sales order object, and were added to our screen by default.

Customer TODO

What we need to do is to move them into a child panel Customer, and display the relevant customer's fields instead of the IDs. In the next step, we will also show how to enable the selection of a customer ID using a lookup form.

Grouping into the Customer tab

Let's make the necessary model updates, to group the customer fields into a separate tab.

Defining child data object

Similar to the steps that we did before, we will define a new data object SalesOrderCustomerObject and will add it as a child to the SalesOrderObject under the name customer as follows.

sales_order.xom
  <xfk:data-objects>
<xfk:data-object class="SalesOrderObject">
<xfk:add-child name="customer" class="SalesOrderCustomerObject"/>
<xfk:add-child name="detail" class="SalesOrderDetailList"/>
...
</xfk:data-object>
...
<xfk:data-object class="SalesOrderCustomerObject"/>
</xfk:data-objects>

Now let's define a customer info structure for this data object, which will be used for reading customer information as part of the sales order, including additional customer fields that we want to display on the sales order details, such as the customer account number and territory.

If you remember from the search screen, a customer object consists of a store and/or a person, so we will add a store name and a person name parameters to our structure. We'll also add the corresponding hidden parameters store id and person id, which we will need later to read address details.

The resulting structure should look as shown below.

  <structs>
<struct name="customer info" object="customer">
<param name="customer id"/>
<param name="store id"/>
<param name="store name" type="string" required="false"/>
<param name="person id"/>
<param name="person name" type="string" required="false"/>
<param name="account number"/>
<param name="territory id"/>
<param name="bill to address id" type="address"/>
<param name="ship to address id" type="address"/>
<config>
<xfk:add-to-object class="SalesOrderCustomerObject"/>
</config>
</struct>
...
</structs>
note

We used object="customer" to set customer as the base object for our structure. Therefore, any parameters that don't have a matching field on the customer object must be qualified with a type.

Similarly, we'll define a customer update structure for updating customer information on the sales order. It will have just a few customer-related fields that can be saved on the order, as shown below.

    <struct name="customer info" object="customer">[...]
<struct name="customer update" object="customer">
<param name="customer id"/>
<param name="bill to address id" type="address"/>
<param name="ship to address id" type="address"/>
<config>
<xfk:add-to-object class="SalesOrderCustomerObject"/>
</config>
</struct>

Now we can configure our SalesOrderCustomerObject data object to have those internal ID fields hidden on the screen, and to set a proper label for the territory id parameter, as follows.

    <xfk:data-object class="SalesOrderCustomerObject">
<ui:display>
<ui:fields>
<ui:field param="customer id" hidden="true"/>
<ui:field param="person id" hidden="true"/>
<ui:field param="store id" hidden="true"/>
<ui:field param="territory id" label="Territory"/>
</ui:fields>
</ui:display>
</xfk:data-object>

Refactoring CRUD operations

Next, we will replace those internal ID parameters (i.e. customer id, bill to address id, and ship to address id) on the output of the read operation, and the input for the create and update operations with references to the customer info and customer update structures respectively.

While we're at it, we will also set the type for the ship date parameter to be just the date without the time component in all those operations.

Below is a snippet that highlights all these changes.

    <object name="sales order">
<operations>
<operation name="read" type="read">
<input>[...]
<output>
...
<struct name="customer" ref="customer info"/>
<param name="customer id"/>
<param name="bill to address id"/>
<param name="ship to address id"/>
<param name="ship date" type="date"/>
...
</output>
</operation>
<operation name="create" type="create">
<input arg="data">
...
<struct name="customer" ref="customer update"/>
<param name="customer id"/>
<param name="bill to address id"/>
<param name="ship to address id"/>
<param name="ship date" type="date"/>
...
...
</input>
<output>[...]
</operation>
<operation name="update" type="update">
<input>
<param name="sales order id"/>
<struct name="data">
...
<struct name="customer" ref="customer update"/>
<param name="customer id"/>
<param name="bill to address id"/>
<param name="ship to address id"/>
<param name="ship date" type="date"/>
...
</struct>
</input>
<output>[...]
</operation>
</operations>
</object>
note

As usual, we had to add the structure references using the same name customer that we used for our child data object earlier.

Custom service implementation

Now that we have updated the model, let's provide a custom implementation for the services to support our updates.

Customized Person entity

To make writing custom service code easier, let's first create a custom partial class for the Person entity, to expose a FullName property to display a person as a string.

As we did before, we can simply set the extend="true" attribute on the edm:customize element of the person object's configuration, as shown below.

person.xom
    <object name="person">
<fields>[...]
<config>
<sql:table name="Person.Person"/>
<edm:customize extend="true"/>
</config>
</object>

After you build the model, open up the PersonExtended.cs file nested under the Entities/Person/Person.cs file of the AdventureWorks.Services.Entities project, and add a new property FullName, as follows.

PersonExtended.cs
    public partial class Person
{
public string FullName => $"{LastName}, {FirstName}";
}

Custom Get for the output

Now let's open the SalesOrderServiceExtended.cs file, and add a GetCustomerInfo method to populate our customer info structure from the SalesOrder object, as follows.

SalesOrderServiceExtended.cs
public partial class SalesOrderService
{
...
protected static CustomerInfo GetCustomerInfo(SalesOrder obj) => new()
{
CustomerId = obj.CustomerId,
AccountNumber = obj.CustomerObject?.AccountNumber,
PersonId = obj.CustomerObject?.PersonObject?.BusinessEntityId,
PersonName = obj.CustomerObject?.PersonObject?.FullName,
StoreId = obj.CustomerObject?.StoreObject?.BusinessEntityId,
StoreName = obj.CustomerObject?.StoreObject?.Name,
TerritoryId = obj.CustomerObject?.TerritoryObject?.TerritoryId,
BillToAddressId = obj.BillToAddressId,
ShipToAddressId = obj.ShipToAddressId,
};
}

As you see, the structure parameters are just populated from the various related objects' fields.

note

Notice how we used our new custom FullName property, which allowed us to easily use a null propagation operator.

tip

You can also use our new FullName property in other places where we had to manually construct it in the LINQ queries. This will allow you to define this format in just one place.

However, you may want to watch the performance of any such LINQ queries, since Entity Framework will no longer be able to translate this property directly to SQL, which means that it will have to always read the entire Person entity and evaluate the FullName on the service side after the query has been executed.

To populate the customer structure in the read results, we'll call this method in the custom code of the generated ReadAsync method of the SalesOrderService, as follows.

SalesOrderService.cs
    public partial class SalesOrderService : BaseService, ISalesOrderService
{
public virtual async Task<Output<SalesOrder_ReadOutput>>
ReadAsync(int _salesOrderId, CancellationToken token = default)
{
...
// CUSTOM_CODE_START: populate the Customer output structure of Read operation below
// TODO: res.Customer = ???; // CUSTOM_CODE_END
res.Customer = GetCustomerInfo(obj); // CUSTOM_CODE_END
...
}
}

Custom Update for the input

Just like we did before, we are going to add a validation message for the customer to the Resources.resx file in the AdventureWorks.Services.Entities project, and then run the custom tool on the nested Messages.tt to generate message constants.

NameValueComment
CustomerRequiredCustomer information is required for order {0}.{0}=Order ID

Next, we will add UpdateCustomer method to the extended SalesOrderService service, to update the customer and address objects on the SalesOrder object from the provided CustomerUpdate structure, as follows.

SalesOrderServiceExtended.cs
public partial class SalesOrderService
{
...
protected async Task UpdateCustomer(SalesOrder obj, CustomerUpdate _data)
{
if (_data == null)
{
currentErrors.AddValidationError(Messages.CustomerRequired, obj.SalesOrderId);
return;
}
obj.CustomerObject = await ctx.FindEntityAsync<Customer>(currentErrors, _data.CustomerId);
obj.BillToAddressObject = await ctx.FindEntityAsync<Address>(currentErrors, _data.BillToAddressId);
obj.ShipToAddressObject = await ctx.FindEntityAsync<Address>(currentErrors, _data.ShipToAddressId);
}
}
note

Notice how we use a generated constant for the validation error message that we added to the resources.

After that, we can update both CreateAsync and UpdateAsync methods in the generated SalesOrderService class to handle the new CustomerUpdate structure, as follows.

SalesOrderService.cs
public partial class SalesOrderService : BaseService, ISalesOrderService
{
public virtual async Task<Output<SalesOrder_CreateOutput>>
CreateAsync(SalesOrder_CreateInput _data, CancellationToken token = default)
{
...
// CUSTOM_CODE_START: use the Customer input parameter of Create operation below
// TODO: ??? = _data.Customer; // CUSTOM_CODE_END
await UpdateCustomer(obj, _data.Customer); // CUSTOM_CODE_END
...
}

public virtual async Task<Output<SalesOrder_UpdateOutput>>
UpdateAsync(int _salesOrderId, SalesOrder_UpdateInput_Data _data, CancellationToken token = default)
{
...
// CUSTOM_CODE_START: use the Customer input parameter of Update operation below
// TODO: ??? = _data.Customer; // CUSTOM_CODE_END
await UpdateCustomer(obj, _data.Customer); // CUSTOM_CODE_END
...
}
}

Reviewing the results

Let's run the application to review our changes. The following screenshot shows what the Sales Order details screen will look like now.

Customer result

Notice that now we have a child tab Customer with the customer information and that the Ship Date has no time component anymore.

Since the internal customer ID is no longer visible on the screen, and hence not editable either, we will need to provide another way to select a customer for a sales order, which we will do in the next step.