In-Depth
Silverlight 3 Enables Data-Driven App Dev
The new DataForm control and enhanced support for data-driven applications make it possible for developers to deliver line-of-business applications to any user with a Silverlight-enabled Web browser.
While Silverlight 1 and 2 concentrated on helping developers deliver rich Internet applications (RIAs), coders building line-of-business (LOB) applications had little to cheer about. The limited support for data binding and data validation complicated basic data management tasks in LOB applications. That's about to change.
As announced in March at the MIX09 conference in Las Vegas, Silverlight 3 adds a host of data-oriented features that go a long way toward providing critical support for LOB development. I'll show you how to build a Silverlight 3 client that accesses a Web service to retrieve and update data. My initial version will be a form that displays a single entity. But by leveraging the new data binding and validation features in Silverlight 3, I'll illustrate how I can migrate my solution to a full-featured master-detail page in just a few minutes.
If you want to try out the new features in Silverlight 3 yourself, you can download the beta here. There are two caveats that apply. First: Don't install the beta on a computer where you want to continue creating Silverlight 2 applications -- installing the beta converts Visual Studio to a Silverlight 3 development tool. Second: This is still a beta release and no "go-live" license is available. Any applications you develop at this point aren't supported by Microsoft.
A Sample Application
OK, enough warnings. For this article, I created a simple Silverlight application that interacts with a Web service on my server. The application pulls down a collection of objects representing Customers entities in the Northwind Database and displays them in a DataForm .
To create this application yourself, after installing the Silverlight 3 beta package, select File | New Project in Visual Studio and choose Silverlight under the language of your choice (I chose Visual Basic). From the available Templates, select Silverlight Application, enter a name for the project (I used DataDrivenSL) and click the OK button. In the New Silverlight Application dialog that appears, change the project type to ASP.NET Web Site before clicking OK. I got the solution you see in Figure 1: an ASP.NET Web Site and a Silverlight app.
[Click on image for larger view.] |
Figure 1. The Web site in a Silverlight solution has an HTML page and an .ASPX page, both of which host the Silverlight application that's part of the same solution. |
In the Web site, I added a service with two methods: one that returns Customer objects (GetCustomers) and one that, when passed a single Customer object, updates the database with the values on the object (UpdateCustomer). I made the .ASPX page my start page. In the Silverlight app, I knocked together a simple user interface in the MainPage.xaml file, using a Button and the new DataForm control inside a StackPanel:
<Grid x:Name="LayoutRoot" Background="White"> <Button Name="Get" Content="Get Customer Data" Click="ButtonGet_Click"></Button> <StackPanel Canvas.Left="10"> <dataControls:DataForm> </dataControls:DataForm> </StackPanel></Grid>
In the code file for MainPage.XAML, to retrieve the Customer entity objects from my Web service, I created an EndPoint object for my service's URL, a variable to hold a reference to my service and a BasicHttpBinding object. In the Click event for the Button, I used those objects to retrieve a collection of Customer entities by calling the GetCustomers method on my service:
Private WithEvents nwd As _ NorthwindData.NorthwindDataSoapClientDim bind As New System.ServiceModel.BasicHttpBindingDim ep As New System.ServiceModel.EndpointAddress( _…URL for Web Service…)Private Sub ButtonGet_Click( _ ByVal sender As System.Object, _ ByVal e As System.Windows.RoutedEventArgs) nwd = New NorthwindData.NorthwindDataSoapClient( _ bind, ep) nwd.GetCustomersAsync()End Sub
Binding to Local Objects
In order to take full advantage of Silverlight's new features, you need an object in your Silverlight application that you can decorate with data-related Attributes. To support that, I created a LocalCustomer object that duplicated the properties from the Customer object I want to display in my user interface. For this example, I defined just three properties: CustomerId, ContactName, CompanyName. The start of the class, with the CompanyName property, looks like this:
Public Class LocalCustomer Private _CompanyName As String Private _CustomerId As String Private _ContactName As String Public Property CompanyName() As String Get Return _CompanyName End Get Set(ByVal value As String) _CompanyName = value End Set End Property
In order to handle the conversion from the Customer object (returned from my service) to the LocalCustomer object (used in my Silverlight project), I would normally create a conversion class. Alas, the necessary Attribute for conversion classes isn't yet supported for Silverlight 3. Instead, I added code to the event that fires after the service returns the Customer objects. That code creates a LocalCustomer object for each Customer object and attaches it to an ObservableCollection. Once the collection is built, I used the DataForm's CurrentItem property to have it display the first entity in the collection:
Private custs As New System.Collections.ObjectModel. _ ObservableCollection(Of LocalCustomer)Private Sub nwd_GetCustomersCompleted( _ ByVal sender As Object, ByVal e As _ NorthwindData.GetCustomersCompletedEventArgs) _ Handles nwd.GetCustomersCompleted Dim lc As LocalCustomer For Each cust As NorthwindData.Customer In e.Result lc = New LocalCustomer lc.CompanyName = cust.CompanyName lc.CustomerId = cust.CustomerId lc.ContactName = cust.ContactName custs.Add(lc) Next MyDataForm.CurrentItem = custs(0)End Sub
To make the whole collection available to the DataForm, I first create a property in MainPage that returns the ObservableCollection of LocalCustomers. Then in MainPage's Loaded event, I set the DataContext to the XAML's class:
Public ReadOnly Property Customers() As _ System.Collections.ObjectModel. _ ObservableCollection(Of LocalCustomer) Get If custs IsNot Nothing Then Return custs Else Return Nothing End If End GetEnd PropertyPrivate Sub MainPage_Loaded(ByVal sender As Object, _ ByVal e As System.Windows.RoutedEventArgs) _ Handles Me.Loaded nwd = New NorthwindData.NorthwindDataSoapClient( _ bind, ep) nwd.GetCustomersAsync() Me.DataContext = MeEnd Sub
With those changes in place, in the .XAML file, I can bind the DataForm to the collection returned by my new property using the DataForm's ItemsSource attribute:
<dataControls:DataForm ItemsSource="{Binding Customers}" Name="MyDataForm">
The solution is very basic at this stage and doesn't, for instance, support validating the data or calling the method on the service that handles updates. Silverlight 3 provides two ways to extend this basic solution to include those activities: extending the DataForm and extending the local object.
Extending the DataForm
You can alter the DataForm's behavior either by setting properties from code or, as I'll do here, by declaratively adding attributes to the DataForm's element. For instance, by default, the DataForm initially shows each Customer in display mode. If you set the DataForm's AutoEdit property to True, the form will automatically display each entity in edit mode when the user moves to it. AutoCommit, on the other hand, is True by default, allowing the user to save changes just by viewing the next entity in the collection. If you would prefer the user to explicitly click the auto-generated Save button to commit changes, you just need to set the AutoCommit attribute to false.
You can also control what activities the user can perform with the DataForm. The CanUserDeleteItems and CanUserAddItems attributes allow you off turn off some of the CRUD activities supported by the DataForm. Extending the DataForm with these options would result in a tag that looks like this:
<dataControls:DataForm ItemsSource="{Binding Customers}" Name="MyDataForm" AutoCommit="False" AutoEdit="True" CanUserDeleteItems="False">
The DataForm is a templated control, with separate DataTemplates for inserting, displaying, and updating data. By default, the DataForm adds a control to every template for every property on the entity. Setting the DataForm's AutoGenerateFields attribute to False suppresses that behavior, allowing you to design your own layouts for the templates. The DataForm also fires a multitude of events both before and after critical activities (such as when records are added or during validation, for example), allowing you to add code to validate data or control updates.
You'll have some design decisions to make, though. Many of the items that you can control by extending the DataForm can also be controlled by extending the local object. In many cases, the best practice is to use the features of the local object to control data-related activities and limit your DataForm changes to controlling workflow. By extending the local object, your data-related changes can "follow the object" to whatever control is handling updates.
Extending the Local Object
For LOB developers, the real power in Silverlight's data enhancements is the ability to extend the local object. If, for instance, you want to take command of changes being made to the object, you can have the local object's class implement the IEditableObject interface:
Public Class LocalCustomer Implements System.ComponentModel.IEditableObject
Implementing this interface adds three methods to your object that are fired when your object is placed in edit mode (BeginEdit) and when it leaves edit mode (EndEdit and CancelEdit). For instance, rather than use some event in the DataForm to perform updates, I can use the EndEdit method in the LocalObject to persist my changes back to the database. This example converts my LocalCustomer back to a Customer object and passes it to the UpdateCustomer method on my service to have any changes saved to the database:
Public Sub EndEdit() _cust = CustomerConverter.ConvertBack(Me) nwd.UpdateCustomerAsync(_cust)End Sub
While I used code to give the local object the ability to update itself, I can implement many of my data validation activities declaratively, by decorating the local object's properties.
For instance, on the CustomerId property, I want to prevent the property from being updated so I can use the Bindable Attribute to specify that the property only supports one-way databinding (the default is two-way). I can also use the Display Attribute to control the order that the fields appear on the page, specify the text for the field's label, and provide a descriptive ToolTip. I can also use the Required Attribute to specify that the property must be provided with a value and what error message to display if it's omitted. With all of these Attributes in place, the property looks like this:
<System.ComponentModel.Bindable(True, _ ComponentModel.BindingDirection.OneWay)> _<System.ComponentModel.DataAnnotations.Display( _ Name:="Id", Order:=0, Description:= _ "Five character customer identifier")> _<System.ComponentModel.DataAnnotations.Required( _ Errormessage:="Customer Id is required")> _Public Property CustomerId() As String
The DataForm will take all of these decorations into account when working with the property. More importantly, if I change my form and tie the local object to a different user interface control, these decorations will be used by that control also -- they "follow the object" to whatever control the property is used with.
The validation DataAnnotations like Required are useful, but they only handle basic validation tasks. For more extensive validation, you need to write code. You can put the code in the property itself. To communicate error conditions back to the Silverlight user interface, you just need to throw an exception, as this version of the ContactName does:
Public Property ContactName() As String Get Return _ContactName End Get Set(ByVal value As String) If value.Length < 5 Then Throw New Exception( _ "Contact name must be"+ _ "at least 5 characters long") End If _ContactName = value End SetEnd Property
The resulting error is automatically displayed in the user interface regardless of which control is bound to the object (see Figure 2).
[Click on image for larger view.] |
Figure 2. When a local object throws an exception the associated message is automatically displayed at the bottom of the page and inside the DataForm. |
You can also consolidate your validation code into a separate class and associate it either with the local object or with individual properties on the local object. This example attaches the ValidateCompanyName method on a class called CompanyValidator to the CompanyName property on my local object:
<System.ComponentModel.DataAnnotations.CustomValidation( _ GetType(CompanyValidator), "ValidateCompanyName", _ ErrorMessage:="Invalid company name")> _Public Property CompanyName() As String
The related validator class with the ValidateCompanyName method would look like this:
Public Class CompanyValidator Public Shared Function ValidateCompanyName( _ ByVal Name As String) As Boolean Dim ValidCName As Boolean …CompanyName validation code… Return ValidCName End FunctionEnd Class
Creating a Master-Detail Page
The real power in modifying the local object becomes apparent when I convert my original design to a master-detail page (Figure 3). The changes required are minimal. I add a DataGrid to my page and bind its ItemsSource attribute to the property that returns my collection of LocalCustomer objects. I then bind my DataForm's CurrentItem property to the SelectedItem property of the DataGrid (and remove the DataForm's binding through the ItemsSource attribute). The resulting XAML for my master-detail page looks like this:
[Click on image for larger view.] |
Figure 3. In this master-detail page, as the user selects a new item in the grid at the top of the page, the item is displayed in the DataForm. |
<data:DataGrid ItemsSource="{Binding Customers}" Name="MyDataGrid"></data:DataGrid><dataControls:DataForm Name="MyDataForm" CurrentItem="{Binding SelectedItem, ElementName=MyDataGrid}"></dataControls:DataForm>
In real life, I would use templates in the DataGrid to have the grid display fewer properties than the DataForm. But this is where the changes that I've made to the LocalCustomer class pay off: Because my data-related code and annotations are tied to my LocalCustomer class, I can turn on editing in the DataGrid, confident that my data management code will follow the object to the DataGrid.
While Silverlight 3 provides significant improvements needed to craft LOB applications, it is not a complete solution. A complete solution would provide, for instance, better control over how many objects are returned to the client and the ability to attach Attributes to the data transfer objects created at the server to eliminate the need to convert to a local object in the Silverlight client. That, however, goes beyond Silverlight and into the domain of .NET RIA Services, which integrates Silverlight with the server-side capabilities of ASP.NET (see "RIA .NET Services, for more about the framework released with the Silverlight 3 beta).
.NET RIA Services |
When the first Silverlight community technology preview launched to much fanfare in December 2006, it carried with it a lot of promise. Despite being a somewhat limited Web media playback platform, Silverlight grabbed the attention of .NET developers keen on moving their application logic over the network to diverse client platforms. Easing Development With Silverlight 3, Microsoft seems determined to win over those business developers for good. As part of the current Silverlight 3 beta, Microsoft has released .NET RIA Services, a framework that pulls together elements of ASP.NET and Silverlight to ease development of data-centric RIA applications. The RIA Services pattern lets developers write application logic to run on the mid-tier that, in a Microsoft statement, "controls access to data for queries, changes and custom operations." Bob Baker, president of MicroApplications Inc., has been working in the Silverlight 3 early adopter program. He says .NET RIA Services will change the face of Silverlight development. "This release will approach application development ease for quick line-of-business apps that rivals what we used to be able to do with Microsoft Access -- before macro security and digital signatures got in the way," he writes in an e-mail exchange. He adds that .NET RIA Services promises to eliminate what he calls "that dual, object class library thing we've been doing for a year or so." Adding Critical Capabilities The new framework integrates client-side Silverlight components with the ASP.NET mid-tier to add critical capabilities for business applications, including data validation, authentication and roles. Brad Abrams, product unit manager of the Application Framework team at Microsoft, says RIA .NET Services grew out of the work Microsoft did on LINQ. "With RIA Services we're extending this pattern by offering a prescriptive model for exposing your domain logic over LINQ," Abrams writes. "We think of domain logic as that part of your application that is very specific to your domain -- that is the particular business problem you are solving. By following this pattern we are able to provide a number of features that help you focus on your domain logic rather than the plumbing in your code." At press time, Microsoft .NET RIA Services was in a March '09 Preview. It can be downloaded for free. -- Michael Desmond |
But, while you're waiting for .NET RIA Services, Silverlight 3 gives you the ability to create true data-driven, line-of-business applications. And all your user needs to access your application is a Web browser.
About the Author
Peter Vogel is a system architect and principal in PH&V Information Services. PH&V provides full-stack consulting from UX design through object modeling to database design. Peter tweets about his VSM columns with the hashtag #vogelarticles. His blog posts on user experience design can be found at http://blog.learningtree.com/tag/ui/.