Skip to main content

Data List Objects

DataListObject is a special type of data object, where its data is stored as an observable collection of rows, instead of being stored directly within each of its data properties. A data list object represents a data table that is typically bound to a data grid UI component.

Each data property of a data list object can be viewed as a column in that data table, which is responsible for converting, formatting and validating values, maintaining metadata, such as the label, editability and visibility, as well as for any other functions for handling the data in that column.

You should construct and register data list objects just like the regular data objects, and implement the Initialize method to add data properties and initialize any actions. Unlike regular data objects though, data lists usually don't have child objects, but they can be added as a child to another parent data object.

Row collection

The collection of rows in a data list object is stored as a special observable collection of DataRow classes. You can get a read-only observable collection of rows as an IList by calling the GetData() method, and also get the number of rows and columns in the data list via the RowCount and ColumnCount properties respectively, as shown below.

IList<DataRow> rowData = dataListObject.GetData(); // get a readonly observable collection of rows
int rowCount = dataListObject.RowCount; // get the count of rows w/o creating a readonly row collection
int columnCount = dataListObject.ColumnCount; // get the count of columns based on the number of properties

The DataRow class that is used for storing rows in a data list object contains the actual data for the row, as well as a selection indicator and metadata by column, which specifies if the value in each particular column is editable, modified, valid, etc.

Accessing row values

DataRow does not allow you to get the row's values directly. Instead, you should use the GetValue methods on the corresponding data properties of your data list object, and pass your data row as an argument, in addition to the desired value format. This allows reusing the value conversion logic encapsulated in your data properties for both regular data objects and data list objects.

The following example illustrates how to get both typed and untyped values of a row in a salesOrderList object using various methods and formats, including static utility methods on the DataRow class.

SalesOrderListObject salesOrderList = ServiceProvider.GetService<SalesOrderListObject>();
await salesOrderList.ReadAsync(null); // populate data
DataRow firstRow = salesOrderList.GetData()[0];

// get internal value as an object
object salesOrderId = salesOrderList.SalesOrderIdProperty.GetValue(ValueFormat.Internal, firstRow);
salesOrderId = DataRow.GetValue(firstRow, "SalesOrderId"); // same, using a property name

// get a string value converted to a display string format
string statusString = salesOrderList.StatusProperty.GetStringValue(ValueFormat.DisplayString, firstRow);
statusString = DataRow.GetString(firstRow, SalesOrderListObject.Status); // using a constant property name

// using the typed method on generic data properties to get internal value as an integer
int salesOrderIdInt = salesOrderList.SalesOrderIdProperty.GetValue(firstRow);
tip

If you only have a reference to a data row, you can always get its parent data list object using the List property. Then, to get the row values, you can access the data list properties either directly (by casting it to a concrete class), or by the property name. Alternatively, you can use DataRow's static utility methods to access values by the property name, as shown above.

In addition, DataRow class extends DynamicObject, and implements getting internal values using the property name as follows.

dynamic dynamicRow = firstRow;
object salesOrderId = dynamicRow.SalesOrderId; // get value from a dynamic property by name

Updating row values

Similar to accessing row values, you have to use the data property's SetValueAsync method to update the value of that property in a specific DataRow by passing the row as an extra argument, as follows.

SalesOrderListObject salesOrderList = ServiceProvider.GetService<SalesOrderListObject>();
DataRow row = new DataRow(salesOrderList);

await salesOrderList.SalesOrderIdProperty.SetValueAsync(1, row);

This will use the data property's value conversion logic to convert it to the proper internal format or data type, and possibly look it up in the associated lookup table.

warning

While data properties also have a synchronous method SetValue, it is important to set the value with the async method and await the result, since enum properties may need to initialize their lookup table using a remote call during the conversion. For the same reason, you should avoid setting row values via the DynamicObject interface, as it would be synchronous.

When you set the value of a data property for a specific row like that, it will fire a property change event and will include that row in the event arguments, which you can access via the Row field. This allows the listeners to get or set values of other properties in the same row, such as when using computed values in a data list object.

Computed values in lists

The computed value expression that you define for your property should have an extra parameter of type DataRow at the end, which you should use to get other values from the same row, in order to compute your value. For example, if you have a child list of line items for a sales order, you can set the computed LineTotal using the values of Price, Discount and Quantity from the same row, as follows.

LineItemListObject.cs
Expression<Func<LineItemListObject, DataRow, decimal>> xLineTotal = (lst, row) => GetLineTotal(
lst.UnitPriceProperty.GetValue(ValueFormat.Internal, row),
lst.PriceDiscountProperty.GetValue(ValueFormat.Internal, row),
lst.OrderQuantityProperty.GetValue(ValueFormat.Internal, row)
);
LineTotalProperty.SetComputedValue(xLineTotal, this);

Copying data from rows

Another option to set the data row's values is to copy them from another DataRow by calling the CopyFromAsync method on the DataRow, or the UpdateRow method on the DataListObject.

A typical use case for this is when you need to provide row-level editing in your data grid, and the user can either cancel the edits or update the entire row. To support this, you want to copy the current row into a new row for editing, and then copy the edited values back to the original row, if they pass validation and the user doesn't cancel.

DataListObject even provides a special member EditRow for these purposes, so copying the rows would look as follows.

async Task StartEditing()
{
DataRow selectedRow = listObject.SelectedRows[0];
// create an EditRow, and initialize it from the currently selected row
listObject.EditRow = new DataRow(selectedRow);
await listObject.EditRow.CopyFromAsync(selectedRow);
}

async Task FinishEditing()
{
// update the original row from the current EditRow
await listObject.UpdateRow(listObject.EditRow.OriginalRow, listObject.EditRow);
listObject.EditRow = null;
}
warning

While DataRow also provides a synchronous version of this method CopyFrom, you still want to stick with the async version.

Adding/removing rows

In addition to the async method UpdateRow above that replaces a row, DataListObject provides async methods InsertAsync and RemoveRows to insert a row at the specified index or to remove the specified data rows, as illustrated below.

DataRow newRow = new DataRow(dataListObject);
await dataListObject.InsertAsync(0, newRow, false); // pass true to suppress notifications
await dataListObject.RemoveRows(new[] { newRow }, false); // pass true to suppress notifications

tip

If you need to call a service operation when inserting or deleting a row to persist the change to the database, then you can override these methods, and call the operation there.

DataListObject also has synchronous methods Insert and RemoveAt to insert or remove a row at the specified index. To remove all rows you can also call the Clear method. You can use these methods for local modification of the data list, but they are not suitable for calling service operations.

Collection events

DataListObject implements the INotifyCollectionChanged, which means that you can subscribe to its CollectionChanged event, and get notified of the updates to its row collection.

You can also manually trigger a collection changed event by calling the FireCollectionChange method to notify the listeners, such as the bound data grid. If any listener code needs to know if a collection change event is currently in progress, they can check the data list object's CollectionChangeFiring flag.

Some methods, such as InsertAsync and RemoveRows allow you to suppress the collection notification, to prevent other listeners from interfering with the caller's logic. In this case, they can manually call the FireCollectionChange later, if desired.

Modification tracking

Updating values in a DataRow will mark the corresponding properties in the row as modified. You can check if anything in the row is modified using the IsModified method. Or you can check if a specific data property is modified in the given row using the GetModified method, as follows.

DataRow row = dataListObject.GetData()[0];

// check if any value in the row is modified
bool? modified = row.IsModified();

// check if MyProperty value is modified in the row
modified = dataListObject.MyProperty.GetModified(row);

If you remove any rows though, the DataListObject itself will be marked as modified, such that its IsModified method or Modified property returns true.

You can also call the standard SetModified method to set the modification state on the data list, or also on all its rows when you pass the recursive argument as true. This will also trigger the standard INotifyPropertyChanged event for the Modified property.

tip

As with regular data objects, you can disable modification tracking on a data list object by setting its TrackModifications flag.

Row-level editing

To find out if a data property of a data list object is editable for a specific row, you can call the GetEditable(row) method on that property for the specified row. By default, it would return the Editable state of the entire property (column), which is also based on the editability of its parent data object.

However, even if a property is editable, you can make it not editable for a specific row by calling the SetEditable method for that row, as follows.

DataRow row = myListObject.GetData()[0];
myListObject.MyDataProperty.SetEditable(false, row);

This may fire a property change event for PropertyChange.Editable for that row. You can always manually reset it back to true, as needed. Alternatively, you can set up computed editability for that property on a row level by adding a DataRow to your expression, as illustrated below.

// make discount editable only for products that are on sale
Expression<Func<LineItemListObject, DataRow, bool>> xDiscountEditable = (lst, row) =>
IsOnSale(lst.ProductProperty.GetValue(ValueFormat.Internal, row));
DiscountProperty.SetComputedEditable(xDiscountEditable, this);
note

In a similar manner, you can get or set a row-level Editing flag for a property by calling the GetEditing(row) or SetEditing(value, row) methods respectively. The latter will also fire a corresponding property change event for that row, which allows you to run certain logic, such as validating the value when the user stops editing it.

Validating row values

When your data list object is bound to an editable data grid with row-level editing, each value that you edit in the row will be automatically validated when the user stops editing it, i.e. the Editing flag gets set to false. Any validation errors for that property will be stored in that row. You can also manually trigger validation of that property for that row, and then retrieve the validation errors, as follows.

myProperty.Validate(row); // manually validate the row's value of myProperty
ErrorList errors = myProperty.GetValidationErrors(row); // get row's validation errors for myProperty

The bound editable data grid would perform such validations, and prevent updating the row, if there are any validation errors. You can also reset validation errors for each row by calling the method ResetAllValidation on your data list object.

warning

Nevertheless, calling Validate on the DataListObject would not automatically validate its rows, in order to avoid performance issues with non-editable list objects. Nor does the GetValidationErrors method return row validation errors by default. However, you can always override those methods in your specific data list to validate each row and return all row errors as needed.

The functions for validating values in data rows are the same as for validating regular data property values, since they accept a row as the last argument, and must pass it to the AddValidationError method of the data property, as shown below.

public static void ValidateDecimal(DataProperty dp, object value, DataRow row)
{
if (dp != null && !dp.IsValueNull(value, ValueFormat.Internal) && !(value is decimal))
dp.AddValidationError(row, Messages.Validation_DecimalFormat, dp.Label);
}

Row-specific enums

When you have an EnumProperty in an editable data list object, the cell editor for that property would provide a selection from a list of possible values associated with that property, which typically comes from a LookupTable. When that selection list is the same for all rows, and the lookup table comes from a static global cache, then this works the same as in regular data objects.

However, sometimes you may need to display different selection lists in each row based on other values in the same row. In this case, you can set up cascading selection from other properties in the data list object the same way as you do for regular objects. As described in the above link, you can do it in one of the following two ways.

  1. Set up cascading client-side filtering of a globally cached static lookup table by a specific attribute using SetCascadingProperty.
  2. If the complete list is too large to be cached on the client side, you can set up a LocalCacheLoader for the property, and call the SetCacheLoaderParameters to provide values from other data properties in the same row. The local cache loader will then populate a local cache from a remote service and will store it in the current data row for that property.

Populating data

When your data list object is a child of a parent data object, and the Read service operation for the parent object also returns the data for your data list as a nested collection, then your child list object will be automatically populated whenever you call ReadAsync on the parent object, provided that the names of the properties in the nested collection match the names of the list object's properties.

Otherwise, if you need to populate the data in your data list object using a separate service call, then you can override the DoReadAsync method, call the service in there, and populate the list from the result using the FromDataContractAsync method. For example, reading and populating a list of line items on a sales order object from a service call would look as follows.

LineItemListObject.cs
protected override async Task<ErrorList> DoReadAsync(object options, CancellationToken token = default)
{
using (var s = ServiceProvider.CreateScope())
{
var salesOrderId = (int)(Parent as SalesOrderObject).SalesOrderIdProperty.TransportValue;
var salesOrderService = s.ServiceProvider.GetService<ISalesOrderService>();

var output = await salesOrderService.ReadLineItemsAsync(salesOrderId, token);
await FromDataContractAsync(output?.Result, options, token);
return output.Messages;
}
}
note

You can also pass the FromDataContractAsync method a CrudOptions parameter configured to preserve the current row selection if desired.

DTO conversion

If your data properties don't fully line up with the properties of the returned DTOs, then you can override the FromDataContractAsync method, construct a List<DataRow> manually, and pass it to the data.ReplaceData() method.

In order to populate each row from the corresponding item in your DTO, you can leverage the version of the FromDataContractAsync method that takes a row argument, and then set the values of any custom properties, as follows.

public override void FromDataContract(object dataContract, object options)
{
IList<DataRow> rows = new List<DataRow>();
foreach (DTOItem dtoItem in (IEnumerable<DTOItem>)dto)
{
DataRow row = new DataRow(this);
rows.Add(row);
await FromDataContractAsync(dtoItem, options, row, token);
// set custom properties
await MyProperty.SetValueAsync(dtoItem.CustomValue, row, token);
}
data.ReplaceData(rows);
}
note

In your overridden FromDataContractAsync method, you may also need to restore the selection, set your list object as not modified, and perform other actions as needed.

Similarly, when you need to send the entire list to a service operation, you can convert the data from your data list object to a DTO using the standard ToDataContract method, provided that the property names in your list object and DTO match up. Otherwise, you can override it and populate the DTO list manually, leveraging the ToDataContractProperties method for each data row.

Criteria object

When a data list object is a primary object on your view, rather than a child list, you typically want to provide some filter criteria for the service operation, so that you don't retrieve the entire list, but only the relevant records. Such criteria are also usually specified by the users on the screen, and validated before running the search and retrieving the results.

To hold the values for the search criteria, DataListObject has a special member CriteriaObject, which you can set to your custom data object, as shown below, and then bind its data properties to the fields on your search criteria panel.

salesOrderList.CriteriaObject = ServiceProvider.GetService<SalesOrderCriteria>();

The custom data object needs to inherit from the CriteriaObject base class and add data properties for each filter criteria as needed. For any field, you can use an OperatorProperty to give the users extra flexibility when specifying the criteria. In this case, you will need to define additional properties that hold the actual criteria value(s), as follows.

public class SalesOrderCriteria : CriteriaObject
{
protected override void Initialize()
{
// add filter criteria by order date with operator and two value properties for the range
OrderDateOperatorProperty = new OperatorProperty(this, OrderDateOperator)
{
EnumType = "operators",
};
OrderDateProperty = new DateProperty(this, OrderDate);
OrderDate2Property = new DateProperty(this, OrderDate2);
}
}
note

If you use the OperatorProperty, your service operation should accept and implement the specified filter operators, such as by using query operators supported by Xomega Framework services.

With the CriteriaObject being part of the base class DataListObject, view models in Xomega Framework will automatically validate its values before running the search.

When implementing the DoReadAsync method for retrieving the filtered data using a service operation, you can convert the data of your CriteriaObject to the input DTO for the operation, and pass it to the service call, as follows.

protected override async Task<ErrorList> DoReadAsync(object options, CancellationToken token = default)
{
SalesOrderCriteriaDTO criteria = CriteriaObject?.ToDataContract<SalesOrderCriteriaDTO>(options);
using (var s = ServiceProvider.CreateScope())
{
var salesOrderService = s.ServiceProvider.GetService<ISalesOrderService>();
var output = await salesOrderService.ReadListAsync(criteria, token);
await FromDataContractAsync(output?.Result, options, token);
return output.Messages;
}
}

Applied criteria

Once the search successfully runs, and the data list object is populated with the results, it will extract the actual criteria that were used for the search from the CriteriaObject, and will store it in a separate property AppliedCriteria, which is a list of structures of type FieldCriteriaSetting.

You can use it to display a summary of the applied criteria on the screen, and refresh it whenever it changes, since changing AppliedCriteria will fire a regular INotifyPropertyChanged event. FieldCriteriaSetting structure stores the field, operator and the value(s) separately, so that you could style them on the screen individually.

note

Even though your CriteriaObject may have many properties, the AppliedCriteria will have only the criteria that have values, which creates a nice and short summary. You also don't want to generate that summary directly from the CriteriaObject, since it will be different when the user has changed the criteria without having applied them yet, which could be misleading.

Client filtering

If your data grid bound to your data list object needs to perform additional filtering on the client side by one or more columns, then DataListObject provides a handy utility method PropertyValueMatches, which checks if the value of a specified data property in a given row matches the specified criteria value using the supplied operator.

tip

You may need to translate the data grid's client-side filter criteria to a number of operator/value clauses, and combine the results of the calls to PropertyValueMatches for each clause, to determine whether each row matches those filter criteria.

Resetting data

To reset the data in your data list object, as well as the AppliedCriteria, you can call the ResetData method. If you also want to clear the values in the CriteriaObject, then you should call ResetData on that object too, as follows.

myListObject.ResetData();
myListObject.CriteriaObject.ResetData();
note

This is what the Reset action usually does on the standard Xomega Framework search forms.

Sorting rows

DataListObject allows you to sort its rows locally using standard or custom sort criteria. In order to sort the data with a custom row comparison function, you can pass it to the Sort method, as follows.

Comparison<DataRow> compareRows = (r1, r2) => CompareRows(r1, r2); // custom comparison function
myListObject.Sort(compareRows);

Sort criteria

To help you sort a data list object, Xomega Framework provides a class ListSortCriteria, which is an ordered list of ListSortField objects. Each of those defines a sort order by a specific data property in the list object, as well as the sort direction.

You can construct it and store it in the SortCriteria property of your list object, which is intended to keep track of the currently applied sort criteria, and it will be used whenever you call the Sort method without parameters, as follows.

var sortField = new ListSortField()
{
PropertyName = "MyProperty",
SortDirection = ListSortDirection.Ascending
};
list.SortCriteria = new ListSortCriteria() { sortField };
list.Sort();

If you want to sort a data list object using temporary sort criteria, without storing it in the SortCriteria, then you can do it using a custom comparison function, as follows.

var sortCriteria = new ListSortCriteria() { ... };
myListObject.Sort(sortCriteria.Compare);

If the internal values of your sort properties at each row implement IComparable, then it will be used for comparing the rows. This allows you to compare typed properties, such as numeric or date/time, using the underlying data type. In all other cases, it will compare the values converted to the DisplayString format, which is how the user sees them on the screen.

note

Null values will be sorted first in ascending order, and last in descending order.

Row selection

DataListObject supports tracking row selection by having a Selected property on the DataRow objects. You can control whether you can select multiple rows or just a single row by setting the RowSelectionMode property to one of the following strings.

  • DataListObject.SelectionModeSingle - at most one row can be selected. Selecting any row will clear the previous selection.
  • DataListObject.SelectionModeMultiple - multiple rows can be selected.
note

Any other value will not be handled by the DataListObject, but the bound data grid can interpret it in a custom way. For example, a blank value of the RowSelectionMode can mean that selection is not allowed, while other values may indicate a more detailed mode, such as contiguous selection only.

The DataListObject allows you to get either selected rows or the indexes of the selected rows, as well as to check if the row is selected using an index, as illustrated below.

DataRow row = dataListObject.GetData()[0];

// check if given row is selected
bool selected = row.Selected; // check the row
selected = dataListObject.IsRowSelected(0); // check by index

// get selected rows
List<DataRow> selRows = dataListObject.SelectedRows;

// get indexes of selected rows
List<int> selIndexes = dataListObject.SelectedRowIndexes;

Selecting rows

DataListObject provides several different ways to select (or deselect) a single row or multiple rows, depending on your RowSelectionMode setting, as demonstrated by the following examples.

IList<DataRow> rows = dataListObject.GetData();

// select the first row, and clear selection of any other row(s)
dataListObject.SelectRow(0);
dataListObject.SelectRow(rows[0]);

// select the second row, and clear any other row, if selection mode is single
dataListObject.SetRowSelected(rows[1], true);

// toggles selection of the current row. May clear other rows in single selection mode
dataListObject.ToggleSelection(rows[1]);

// select rows at indexes #2 through #4, and clear selection of any other rows
bool success = dataListObject.SelectRows(2, 4, true); // returns false, if selection mode is single
dataListObject.SelectedRows = new List<DataRow>() { rows[2], rows[3], rows[4] };

// add selection of row #5 to the current selection, if selection mode is multiple.
// otherwise, select row #5 and clear other selected rows
dataListObject.SelectRows(5, 5, false);

// select all rows, if selection mode is multiple
dataListObject.SelectAllRows();

// clear any row selection
dataListObject.ClearSelectedRows();
note

The bound data grid that allows selection would typically call some of these methods when the user makes a selection, in order to synchronize the grid selection with the data list object.

Selection events

Data list objects provide a standard SelectionChanged event, which allows you to listen for any changes in the row selection. The bound data grid may listen to the selection changes to highlight the selected rows.

This event allows you to write platform-independent UI logic based on the selection change. For example, upon selecting a row in a master data grid, you may trigger an update of the details object to read and show the details for the selected row.

tip

This event also enables using SelectedRows or SelectedRowIndexes in the expressions for computed properties, such as to set enabling conditions for certain actions when a row is selected (e.g. the SelectAction), or to show a calculated summary for the selected rows.

If you need to trigger the SelectionChanged manually, you can call the FireSelectionChanged with no arguments, as follows.

dataListObject.FireSelectionChanged();

Preserving selection

When refreshing the data in a data list object from the backend, it is often desirable to preserve the currently selected row, if it still exists in the new dataset. To support this, the data list object's FromDataContractAsync method handles options passed as CrudOptions with a PreserveSelection parameter set. You would typically pass it through the ReadAsync method as follows.

var options = new DataObject.CrudOptions { PreserveSelection = true };
var errors = await dataListObject.ReadAsync(options, token);

In order to allow the DataListObject to find the currently selected row in the new data set, one or more of its data properties should have the IsKey flag set to true, as follows.

public partial class SalesOrderList : DataListObject
{
protected override void Initialize()
{
SalesOrderIdProperty = new IntegerKeyProperty(this, SalesOrderId)
{
Editable = false,
IsKey = true
};
...
}
}

By default, the DataListObject will use the same comparison mechanism for the key values, as is used by the sort criteria to find the selected rows in the new list.

tip

If that doesn't work for you, you can always override the method SameEntity on the DataListObject as needed.

Select action

For search views that are intended for searching some entities, and then selecting one or more of such entities, DataListObject defines a standard SelectAction, which is configured to be enabled only when one or more rows are selected, as follows.

Expression<Func<DataListObject, bool>> xSel = lst => lst.SelectedRows.Any();
SelectAction.SetComputedEnabled(xSel, this);

This allows you to bind the SelectAction to a Select button on your search screen, and have it disabled when there is no selection.