ComplexTypes in RIA Services
Our team has just released WCF RIA Services V1.0 SP1 Beta and WCF RIA Services Toolkit October 2010! You can also access these download links from http://silverlight.net/riaservices. In addition to lots of bug fixes, the release also includes several updates which address some of the top pain points reported by our users. I’d like to use this post to talk about just one of these : the new support we’ve added for complex types. You can download a sample application for complex types here. We’ll be exploring this sample in detail below.
To summarize briefly, the feature adds support for non-Entity complex types (CTs) to the framework. Previously the only structured types we supported and code-genned were Entities. Now you can define types like Currency or Address and use those as Entity properties or parameters into or return values from DomainService methods. Some of the feature highlights:
- Codegen – full codegen support for CTs giving you corresponding client proxies for your types. The codegen is just as rich as it is for entities (partial extensibility methods, validation metadata, etc.). Codegenned CTs derive from a new ComplexObject base class.
- Metadata support – as with entities, you can apply additional metadata on the server via buddy classes to your ComplexTypes, and that metadata will flow through the system. Similarly, we also infer DAL metadata for ComplexTypes (e.g. inferring StringLengthAttribute for members based on your EF model). CTs participate in the metadata pipeline the same way entities do.
- Deep Validation – validation is performed as CT members are modified in the same way that it is for entity members. In addition, validation errors for nested CT members propagate up the containment hierarchy. Deep submit time validation is performed on both tiers for entity CT members.
- Change tracking – entity CT members participate in entity change tracking and the rest of the Accept/RejectChanges pipeline. As nested CT members are changed, those changes are reported up hierarchically causing the parent entity to become dirty. If changes are rejected on the entity, all nested CT changes are reverted.
- Edit Sessions – entity CT members participate in entity edit sessions. If a BeginEdit is performed on an entity, the state snapshot for the entity includes all nested CT state recursively. Similarly if Cancel/EndEdit are performed, the changes are applied recursively. Additionally, ComplexObject itself implements IEditableObject, so you get full edit session support for CTs not hosted by entities.
- ComplexType Parameters – CTs (or collections of CTs) can be passed as parameters to Invoke operations, returned from them, etc. They can also be passed as custom update method parameters.
Lets take a quick walkthrough of the sample app and see some of the above items in action. The below content is also included in the ComplexTypesSample.doc included with the sample. The sample demonstrates use of the new ComplexTypes feature in a variety of scenarios including:
- Use of EntityFramework mapped complex types in the data model, both as nested Entity members as well as mapped stored procedure return types
- Use of POCO complex types as parameters to DomainService invoke operations
- Use of complex types as return values from DomainService invoke operations
The sample app was created using the Silverlight Business Application Template. The Order Management page allows you to select a customer, at which time the orders for that customer are asynchronously loaded in a master/details view:
The Customer Management page shows a master/detail view of Customers, and also displays the order history for the selected customer. These results are loaded on demand as customers are selected by executing an Invoke operation mapped to an EF stored procedure:
ComplexTypes in the EntityFramework Data Model
Opening the Northwind.edmx file in the server project Models folder you can see how several EF complex types have been mapped in the model. The EF model browser shows the two types:
The Address CT is shared by both the Customer Entity as well as the Order entity, where the address details for each are mapped to the CT. Creation of a new complex type can be done in the EF designer simply by selecting one or more properties of an entity and selecting “Refactor into new Complex Type” context menu option. After this is done the set of properties will now be exposed by the entity as a single CT property:
You can then use the Mapping Details window to map the database columns to your CT properties:
In a similar way, you can also map a sproc return Type to a CT. In this sample I mapped the CustOrdHistory sproc to a new CT Type CustomerOrderHistoryResult:
This sproc is used in the sample app exposed by the CustomerManagement service GetCustomerOrderHistory operation.
ComplexType usage in RIA Services
The generated Northwind ObjectContext for the above model is used in this sample by both the CustomerManagement and OrderManagement DomainServices. RIA Services recognizes these types as ComplexTypes and uses this information during codegen to generate the corresponding proxy Types, members and methods. For example, in the client project an Address class has been generated that derives from ComplexObject:
The Order and Customer entities have corresponding ComplexObject properties generated. You’ll also notice that the metadata pipeline for CTs behaves the same way it does for entities. For example, you can apply additional metadata via a “buddy class”. In addition, some of the EF model metadata also flows to the client. Below you can see both a Max Length model attribute as well as a buddy class DisplayAttribute being carried to the client codegen:
Standard Create/Update/Delete Functionality
The Order.ShipAddress and Customer.Address CT properties participate in updates as you’d expect. The client detects modifications, performs cross tier validation, etc. On the server, an updated entity with CT member updates is attached normally.
ComplexType Parameters in DomainService Operations
ComplexTypes can be passed as parameters to Invoke and Custom Update operations, and can be returned from Invoke operations. In the sample, the CustomerOrderHistory sproc is exposed in the CustomerManagement service via an Invoke operation and it returns a collection of CTs:
This DomainService method results in a CustomerOrerHistoryResult ComplexObject being generated on the client, along with a corresponding Invoke operation on the CustomerManagement DomainContext:
As an example of POCO CTs that aren’t mapped in the EntityFramework model, this sample has two examples. First the UserRegistrationService RegistrationData class has been updated to be a CT:
These are actually a product changes that we’ll be making – to make our BAT use the ComplexTyeps feature. The BAT itself was forced to employ several workarounds due to the lack of ComplexType support in the past. On the client this is codegenned as a CT deriving from ComplexObject. The full IEditableObject and Validation behavior provided by the ComplexObject base class can be seen in the UI:
Similarly the LoginInfo Type used on the login screen has also been made a ComplexObject. These modifications to the standard Business Application Template (BAT) to use ComplexTypes will be made in the actual product soon. This will allow us to remove several workarounds the BAT currently employs.
ComplexType UI Binding
In general the binding story for CT properties is straight forward – in your xaml you simply use dotted paths to reference the nested CT properties. Since ComplexObject implements the right set of binding interfaces (INotifyPropertyChanged, IEditableObject, INotifyDataErrorInfo), you get rich UI functionality. Below the DataForm xaml and resulting UI are shown for the bound members of Order.ShipAddress:
Since DataForm wasn’t designed to do deep change tracking on nested instances, it doesn’t work properly in update scenarios where you’re modifying nested CT properties. The underlying issue is that the DataForm only tracks top level properties to determine if the current instance has been modified in the session. This will result in the commit/cancel buttons not behaving properly. This sample works around this by configuring the DataForm in a certain way (AutoEdit="False" AutoCommit="True" CommandButtonsVisibility="Edit, Cancel"). You can also create your own update UI bound to your CTs and control the entire experience yourself.
The sample also demonstrates the validation behavior of ComplexObject in nesting scenarios. In the below scenario, the Order.ShipAddress.PostalCode member has been set to an invalid value. You see the error reported on the control itself since Address instance implements INDEI which the control is bound to. In addition, the error summary control is bound to INDEI on the Order instance – this demonstrates that nested CT instances report their validation attributes up to their parent.
Above you saw client side validation occurring as properties were set. As with entities, all this validation also runs server side. In addition to validation that runs on both tiers, you can also have server side only validation (declarative or imperative). In the sample, the UpdateCustomer method the Customer.Address.Country member is validated:
Notice that on the server the dotted path to the invalid member is specified. This validation error will be sent back to the client, and the error will be applied down the hierarchy, ultimately registering on the Customer.Address.Country member which will cause the UI to display the server error:
Since ComplexObject implements IEO, when bound to a DataForm or other control providing edit sessions based on this interface, changes can be committed/cancelled easily. Below, the current Order has been put into edit mode and a property has been changed. Hitting the Cancel button will cancel the edit and the value will be reverted. To commit the edit, navigate to another Order (again working around the issue that the DataForm Commit button doesn’t work for nested CT edits).
Once one or more edits have been made, the Save/Reject buttons will become enabled. You can set a breakpoint on the server to see that the nested CT instance is sent as part of the changeset and will be populated in both the current and original instances passed to your update method.
As with entities, the RoundtripOriginalAttribute governs which properties are roundtripped to the server as part of the original object. You must either mark the members of your ComplexType as ConcurrencyMode=Fixed in your EF model (in which case the FX will infer RTO for you), or via the buddy class:
In addition to setting the concurrency mode on the individual members of the ComplexType, you must also apply RoundtripOriginalAttribute on the Order.ShipAddress property itself. See the Order and Customer buddy metadata classes in the sample.
In summary, if your RIA Services application development has been hampered in the past by lack of this support and you’ve been forced to implement ugly workarounds, let us know how the feature addresses your scenarios and give us feedback!