Composite data objects are very common and their support will significantly reduce the amount of code and will make the application simpler.
So, what are composite objects? Traditionally, a data object is an object with a set of properties that return and modify internal data. It can include data of primitive types such as int, bool, strings, that have to be displayed in grid cells. Composite objects may contain references to other, more complex objects, in addition to properties returning primitive types. Let’s review a general example of a client’s order to demonstrate new grid features for working with composite objects. An order contains price, quantity date and references to the product and to the client.
A chart of classes describing order - product – client system is provided below.
In a class view this system can be expressed as follows:
C# | Copy |
---|---|
//Class client public class Client { private readonly string _mail; private readonly string _name; public Client(string name, string mail) { _name = name; _mail = mail; } //public properties public string Name { get { return _name; } } public string Mail { get { return _mail; } } } //Class product public class Product { private readonly string _description; private readonly string _name; public Product(string name, string description) { _name = name; _description = description; } //public properties public string Name { get { return _name; } } public string Description { get { return _description; } } } //Class order public class Order { private readonly Client _client; private readonly Product _product; private readonly long _quantity; private readonly DateTime _timeStamp; private readonly decimal _unitPrice; public Order(decimal unitPrice, long quantity, Product product, Client client) { _timeStamp = DateTime.Now; _unitPrice = unitPrice; _quantity = quantity; _product = product; _client = client; } //public properties public DateTime TimeStamp { get { return _timeStamp; } } public decimal UnitPrice { get { return _unitPrice; } } public long Quantity { get { return _quantity; } } public Product Product { get { return _product; } } public Client Client { get { return _client; } } } |
Let’s display a list of orders consisting of date, price, quantity, client name, contact information and product description in the grid. If the grid doesn’t support composite data, we have to create an intermediate class or a container that assembles this data together. Inserting an Order object directly to the grid enables displaying time, price and order quantity, but not client’s name and product name.
An illustration of this statement is shown below.
The above-shown data presentation doesn’t suit us because instead of client’s name and product name it displays only types of objects that contain this information. Creating additional classes and wrappers might make the software structure more complicated. However, now we have a better solution. We shall specify that Order data object is a composite object and that objects returned by Product and Client properties should be considered an addition to the order object and should get all needed information from these objects.
C# | Copy |
---|---|
public class Order { ... [CompositeField] public Product Product { get { return _product; } } [CompositeField] public Client Client { get { return _client; } } } |
An illustration of using composite objects is provided below.
After a magical transformation we see all needed data in the grid. How did it happen and how does it work? As we have already mentioned in [faa05983-e24f-423f-bcf2-b4e5ab970478], the grid supports working with various data types of different nature including arbitrary classes, arrays or dictionaries. It is achieved thanks to IDataAccessor interface that hides data details from the grid and provides a unified interface for working with data. IDataAccessor is actually a container of IDataField object that enables receiving data and setting it to a corresponding data object. The grid never works with data directly but only via an accessor that is created every time when data is added to the grid, i.e. DataObjectAccessor for objects of arbitrary classes and EnumerableDataAccessor for IEnumerable collections. If the object is a composite object (i.e. if it has at lease one property with CompositeField attibute), CompositeObjectAccessor is selected. The purpose of this accessor is to allocate data fields of the object returned by composite object property and to add these fields to existing composite object fields. Therefore, the grid sees the composite object as a single entity with expanded set of data fields. In this case the grid can display not only data of object contained in a Row, but also data of other objects that it refers to.
Now let’s see what happens if composite objects have fields with the same identifiers. By default, field identifier is its property name. In our example the Order object has two composite fields: Client and Product. Each of the returned objects has Name property. It creates an ambiguous situation when the grid cannot decide, which data should be displayed in Name columns. To solve this problem, you can identify field identifiers using FieldAttribute property.
C# | Copy |
---|---|
public class Product { ... [Field("ProductName")] public string Name { get { return _name; } } } |
Without ambiguity the grid knows exactly, which data should be displayed. Client’s name has been added to the figure below.
Composite objects and event-driven model
Let’s review behavior of composite objects with event-driven model. This model is defined by classes based on INotifyPropertyChanged interface. Grid subscribes to data object events. When the grid receives a notification, it provides thread synchronization, sorting, filtering or Row grouping containing a data object. This ensures fully automated grid operation. What happens if data in the object returned by a composite property also changes. As it has been mentioned above, the grid doesn’t work with data objects directly but uses IDataAccessor. CompositeObjectAccessor works with composite objects adding new data fields from objects returned by composite properties. CompositeObjectAccessor also receives notifications from such objects (if they implement INotifyPropertyChanged) and forwards these notifications to the grid. This approach is totally transparent for the grid and makes the grid totally independent of data, its nature and its internal structure.
C# | Copy |
---|---|
public class Client : INotifyPropertyChanged { ... private int _rating; ... public int Rating { get { return _rating; } set { _rating = value; if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs("Rating")); } } } public event PropertyChangedEventHandler PropertyChanged; } |
Therefore, if internal data of Product or Client object changes, the grid gets a notification from the composite objects and performs sorting, filtering, grouping, etc.
Memory consumption
Composite object concept is very simple and very capable. Without composite object support developers often have to create intermediate classes or containers that usually duplicate data object information. It increases memory consumption and makes the code more complicated as part of the code will just synchronize data between business logic and intermediate classes. With composite object memory consumption of the grid slightly increases (by approximately 10-15%), however, overall savings might be significant, especially if business logic is shared by multiple grids.
Other types of composite objects.
Suggested composite object model is an extremely powerful and convenient tool. Composite object properties may return data of different types including objects of arbitrary classes, object containers or dictionaries.
C# | Copy |
---|---|
public class SomeClass { private readonly string _name; readonly IDictionary<string, object> _dynamicFields = new Dictionary<string, object>(); public SomeClass(string name) { _name = name; } public string Name { get { return _name; } } [CompositeField] public IDictionary<string, object> DynamicFields { get { return _dynamicFields; } } } //Create object SomeClass dataObject = new SomeClass("Object 1"); dataObject.DynamicFields.Add("Field1", true); dataObject.DynamicFields.Add("Field2", 12345); //Initialize header grid.Headers.Add(new Header()); grid.Headers[0].Add(new Column("Name")); grid.Headers[0].Add(new Column("Field1")); grid.Headers[0].Add(new Column("Field2")); //Add object grid.Rows.Add(dataObject); |
An illustration of using composite objects containing dictionaries is provided below: