Silverlight - Data - Guide
Silverlight - Data - Guide
Internet: [email protected]
Web site: http://www.componentone.com
Sales
E-mail: [email protected]
Telephone: 1.800.858.2739 or 1.412.681.4343 (Pittsburgh, PA USA Office)
Trademarks
The ComponentOne product name is a trademark and ComponentOne is a registered trademark of ComponentOne LLC. All
other trademarks used herein are the properties of their respective owners.
Warranty
ComponentOne warrants that the original CD (or diskettes) are free from defects in material and workmanship, assuming
normal use, for a period of 90 days from the date of purchase. If a defect occurs during this time, you may return the defective
CD (or disk) to ComponentOne, along with a dated proof of purchase, and ComponentOne will replace it at no charge. After
90 days, you can obtain a replacement for a defective CD (or disk) by sending it and a check for $25 (to cover postage and
handling) to ComponentOne.
Except for the express warranty of the original CD (or disks) set forth here, ComponentOne makes no other warranties, express
or implied. Every attempt has been made to ensure that the information contained in this manual is correct as of the time it was
written. We are not responsible for any errors or omissions. ComponentOne’s liability is limited to the amount you paid for the
product. ComponentOne is not liable for any special, consequential, or other damages for any reason.
iii
Data for Silverlight Overview
ComponentOne Data™ for Silverlight is an implementation of the standard DataSet, DataTable, and DataView
classes familiar to Windows Forms and ASP.NET developers. The following topics describe how you can use the
C1.Silverlight.Data assembly to implement data-centric Silverlight applications.
Introduction to C1.Silverlight.Data
The classes in the C1.Silverlight.Data assembly are a subset of those in the System.Data namespace. This means
that you can use your existing ADO.NET-based code in Silverlight applications. For example, the code below
shows how you can create and populate a DataTable object using C1.Silverlight.Data:
Create DataTable
DataTable dt = new DataTable();
1
Note: You must import the C1.Silverlight.Data.dll (contains the C1Data classes) and
System.Windows.Controls.Data.dll (contains Microsoft's DataGrid control) namespaces for the above code to
work.
The DefaultView property returns a DataView object associated with the table. Again, this is exactly the same
mechanism used in ADO.NET. The DataView object implements an IEnumerable interface that allows it to be
used as a data source for bindable controls and for LINQ queries.
Because the classes in the C1.Silverlight.Data assembly are a subset of those in the System.Data namespace, we
will not describe them in detail here. If you are not familiar with these classes, please check the descriptions of the
System.Data.DataSet and System.Data.DataTable classes on MSDN.
Data-centric Architecture
Silverlight can be used to build line-of-business and other data-centric applications. This type of application
typically involves the following steps:
1. Get the data from the server.
2. Display and edit the data on the client.
3. Submit the changes back to the server.
Steps 1 and 3 typically rely on Web services to transfer the data and traditional data access strategies to query and
update a database. Step 2 typically involves Silverlight data-bound controls.
Microsoft offers many tools that can be used to perform the server-side part of the job. The latest such tool is
ADO.NET Data Services, which provides Web-accessible endpoints for data models and integrates with
Silverlight through the ADO.NET Data Services for Silverlight library (System.Data.Services.Client.dll). A lot has
been written lately about this new technology (see for example "Data Services" in MSDN vol. 23, no. 10,
September 2008).
ADO.NET Data Services is a powerful new technology that is likely to become a standard for many types of data-
centric applications. However, it's not the only option. The traditional data ADO.NET classes (DataSet,
DataTable, DataAdapters, and so on) can also be used to retrieve and update data on the server. These classes
have been used by developers since .NET 1.0. They are solid, powerful, and easy to use. Furthermore, many
developers already have a considerable investment in the form of code that has been used and tested for a long
time.
ComponentOne Data for Silverlight is a set of classes that can be used by Silverlight clients to exchange data with
servers using traditional ADO.NET. The typical steps are as follows:
1. Get the data from the server.
a. Server populates a DataSet object the traditional way (typically using DataAdapter objects to retrieve
the data from a SqlServer database).
b. Server calls DataSet.WriteXml to serialize the DataSet into a stream and sends the stream to the
client.
c. Client uses the DataSet.ReadXml method to de-serialize the stream.
2. Display and edit the data on the client.
a. Client binds the tables in the DataSet to controls (possibly using LINQ queries).
b. User interacts with the controls to view, edit, add, and delete data items.
3. Submit the changes back to the server.
a. Client calls DataSet.GetChanges and DataSet.WriteXml to serialize the changes into a stream and
sends the stream to the server.
b. Server calls DataSet.ReadXml to de-serialize the changes, then uses DataAdapter objects to persist
the changes into the database.
2
The role of C1Data in this scenario is twofold. First, it provides a symmetric serialization mechanism that makes it
easy to transfer data between DataSet objects on the client and on the server. Second, it provides a familiar object
model to manipulate the data on the client.
Note: C1Data does not compete against ADO.NET Data Services. It allows you to leverage your knowledge
of ADO.NET and assets you may already have. You can transfer all that to Silverlight with minimal effort, and
migrate to ADO.NET Data Services gradually, if such a migration is desired.
C1Data is not a legacy technology. You can use it with LINQ, for example. In fact, C1Data enables LINQ
features that are available on desktop but not in Silverlight applications (partial anonymous classes).
The next sections describe the implementation of a simple application that performs the steps described above.
Despite its simplicity, the application shows how to perform the four CRUD operations (Create, Read, Update,
Delete) that are required of most data-centric applications.
3
Create the UI
The user interface will consist of two data grids (Categories and Products) and a few buttons used to add and delete
items and to commit the changes.
To create the user interface, open the MainPage.xaml file and in Source view copy the following XAML onto the
page:
<UserControl x:Class="MasterDetail.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:swc=
"clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data"
>
<Grid x:Name="LayoutRoot" Background="White" >
4
<swc:DataGrid x:Name="_gridProducts" Margin="5" Grid.Row="4" />
5
The image below shows the Solution Explorer window after this step is completed:
In addition to the database file, we need a mechanism to transfer data from and to the database. In this sample, we
will accomplish this using a utility class called SmartDataSet. SmartDataSet extends the regular ADO.NET
DataSet class and adds the following:
A ConnectionString property that specifies the database type and location.
A Load method that loads data from selected tables.
An Update method that saves changes back into the database.
The SmartDataSet is convenient but not necessary. It knows how to create and configure DataAdapter objects
used to load and save data, but you could also write standard ADO.NET code to accomplish the same thing.
Because it only uses standard ADO.NET techniques, we will not list it here.
To add the SmartDataSet.cs file to the project, complete the following:
1. Right-click the MasterDetailWeb node in the Solution Explorer, select Add | Existing Item.
2. In the Add Existing Item dialog box, locate the SmartDataSet.cs file in the C1.Silverlight distribution
package and click Add to add it to the project.
6
We could implement the server side as a generic handler class (ashx), as a Web Service (asmx), or as a Silverlight-
enabled WCF service (SVC). For this sample, any of the choices would do. We will choose a classic Web Service.
Follow these steps to create the service:
1. Right-click the MasterDetailWeb project.
2. Select Add | New Item.
3. In the Add New Item dialog box, select Web from the list of categories in the left pane.
4. In the right pane, select the Web Service template from the list of templates.
5. Name the new service "DataService.asmx".
The dialog box should look like the image below.
7
// Create DataSet with connection string
var ds = GetDataSet();
// Persist to stream
var ms = new System.IO.MemoryStream();
ds.WriteXml(ms, XmlWriteMode.WriteSchema);
8
foreach (DataTable dt in ds.Tables)
{
foreach (DataRow dr in dt.Rows)
{
switch (state)
{
case DataRowState.Added:
dr.SetAdded();
break;
case DataRowState.Modified:
dr.SetModified();
break;
case DataRowState.Deleted:
dr.Delete();
break;
}
}
}
// Make sure file is not read-only (source control often does this...)
FileAttributes att = File.GetAttributes(mdb);
if ((att & FileAttributes.ReadOnly) != 0)
{
att &= ~FileAttributes.ReadOnly;
File.SetAttributes(mdb, att);
}
9
The method starts by locating the database file, making sure it exists, and checking that it is not read-only
(or the updates would fail). Once that is done, it creates a new SmartDataSet, initializes its
ConnectionString property, and returns the newly created SmartDataSet to the caller.
10
using System.IO;
using C1.Silverlight.Data;
using MasterDetail.DataService;
11
{
Uri uri = System.Windows.Browser.HtmlPage.Document.DocumentUri;
string uriString = uri.AbsoluteUri;
int ls = uriString.LastIndexOf('/');
return new Uri(uriString.Substring(0, ls + 1) + relativeUri);
}
The GetDataService method instantiates and returns a new DataServiceSoapClient object. We don't use
the default constructor because that would refer to the development environment (http://localhost and so
on). It would work correctly on the development machine, but would break when the application is
deployed. Also, the default constructor uses a 65k buffer that might be too small for our data transfers. The
above GetDataService method implementation takes care of both issues.
The LoadData method above instantiates the service and invokes the GetDataAsync method. When the
method finishes executing, it invokes the svc_DataCompleted delegate. The delegate instantiates a
DataSet object, uses the ReadXml method to de-serialize the data provided by the server, then calls
BindData to bind the data to the controls on the page.
Note: This is one of the most important features of the C1.Silverlight.Data DataSet class. It uses the
same XML schema that ADO.NET DataSet objects use. This allows applications to serialize data on the
client and de-serialize it on the server, or vice-versa. The fact that the object models are very similar also
makes things substantially easier for the developer.
Note: If you have ever written WinForms or ASP.NET applications, this code should look familiar. This is one of
the strengths of C1.Silverlight.Data. It allows you to leverage your knowledge of ADO.NET in Silverlight
applications. You can use the DataSet object model to inspect and modify the tables available for data-binding,
including their schemas and data, as well as the data relations, keys, and so on.
The only slightly unusual statement is the one that creates a DataView object and specifies which columns should
be included in the view. This is an extension provided by the C1.Silverlight.Data implementation that is not
present in the original ADO.NET DataView class.
If you run the project now, you should see the data retrieved from the server:
12
Before we continue with the sample, a few comments are in order.
Although the code that binds the data to the controls looks familiar, what happens under the covers is quite
different from the traditional WinForms and ASP.NET scenarios. All data binding in WPF and Silverlight is done
through reflection. But the DataRowView objects exposed by DataView collections do not have properties that
match the columns in the underlying DataTable ("CategoryID", "CategoryName", "Description", and so on).
Note: The binding is possible because the DataView class dynamically creates anonymous types that derive from
DataRowView and expose additional properties that correspond to the table columns. Controls can then use
reflection to find these properties and bind to them.
The DataRow class also leverages this mechanism through its GetRowView method. This method builds a
wrapper object that exposes selected properties of the DataRow and can be used for data binding. You can use this
method to in your LINQ queries.
For example, code below builds a LINQ query that selects all categories that start with "C" and exposes their name
and description:
_gridCategories.ItemsSource =
from dr
in dtCategories.Rows
where ((string)dr["CategoryName"]).StartsWith("C")
select dr.GetRowView("CategoryName", "Description");
13
If you call GetRowView with no parameters, all columns are exposed.
14
// Load data, hook up event handlers
public Page()
{
InitializeComponent();
LoadData();
_gridCategories.SelectionChanged += _gridCategories_SelectionChanged;
_btnAdd.Click += _btnAdd_Click;
_btnRemove.Click += _btnRemove_Click;
}
The event handlers are simple, they look like regular ADO.NET code you would write on a WinForms
application:
// Add a new row
private void _btnAdd_Click(object sender, RoutedEventArgs e)
{
DataTable dt = _ds.Tables["Categories"];
DataRow newRow = dt.NewRow();
newRow["CategoryName"] = "New category";
newRow["Description"] = "This is a new category...";
dt.Rows.Add(newRow);
}
// Delete a row
private void _btnRemove_Click(object sender, RoutedEventArgs e)
{
DataRowView drv = _gridCategories.SelectedItem as DataRowView;
if (drv != null)
{
DataRow dr = drv.GetRow();
dr.Delete();
}
}
If you run the application now, you will be able to add, remove, and modify the items displayed on the grids.
Note: The DataSet we are using contains not only the tables you see, but also the DataRelation that connects
the two tables. That relation came from the MDB file and was downloaded from the server along with the data.
The relation has a ChildKeyConstraint property that specifies, among other things, that delete operations should
cascade through the tables. In other words, if you delete a category, all products that belong to that category will
also be automatically deleted.
_gridCategories.SelectionChanged += _gridCategories_SelectionChanged;
_btnAdd.Click += _btnAdd_Click;
_btnRemove.Click += _btnRemove_Click;
15
_btnCommit.Click += _btnCommit_Click;
}
And here is the implementation for the event handler:
// Commit changes to server
private void _btnCommit_Click(object sender, RoutedEventArgs e)
{
SaveData();
}
And here is the implementation for the SaveData method, which does the real work:
// Save data back into the database
void SaveData()
{
if (_ds != null)
{
// get changes of each type
byte[] dtAdded = GetChanges(DataRowState.Added);
byte[] dtModified = GetChanges(DataRowState.Modified);
byte[] dtDeleted = GetChanges(DataRowState.Deleted);
// Invoke service
var svc = new GetDataService();
svc.UpdateDataCompleted += svc_UpdateDataCompleted;
svc.UpdateDataAsync(dtAdded, dtModified, dtDeleted);
}
}
void svc_UpdateDataCompleted(object sender, UpdateDataCompletedEventArgs e)
{
if (!string.IsNullOrEmpty(e.Result))
{
throw new Exception("Error updating data on the server: " + e.Result);
}
_tbStatus.Text = "Changes accepted by server.";
_ds.AcceptChanges();
}
The method starts by calling the GetChanges method to build three byte arrays. Each one represents a DataSet
with the rows that have been added, modified, or deleted since the data was downloaded from the server. Then the
method invokes the Web service we implemented earlier and listens for the result. If any errors were detected while
updating the server, we throw an exception (real applications would deal with the error in a more elegant way).
The only piece still missing is the GetChanges method. Here it is:
byte[] GetChanges(DataRowState state)
{
DataSet ds = _ds.GetChanges(state);
if (ds != null)
{
MemoryStream ms = new MemoryStream();
ds.WriteXml(ms);
return ms.ToArray();
}
return null;
}
The method uses the DataSet.GetChanges method to obtain a new DataSet object containing only the rows that
have the DataRowState specified by the caller. This is the same method available on the ADO.NET DataSet
class.
16
The method then serializes the DataSet containing the changes into a MemoryStream, and returns the stream
contents as a byte array.
Try running the application now. Make some changes, then click the "Commit Changes" button to send the
changes to the server. If you stop the application and start it again, you should see that the changes were indeed
persisted to the database.
Note: The update may fail depending on the changes you make. For example, if you delete one of the existing
categories, all products that belong to that category will also be removed from the DataSet on the client. When
you try to apply these changes to the server, the transaction will likely fail because the database may contain
tables that still refer to the products you are trying to delete (the Orders table in this case). To test the
delete/commit action, try creating a category, committing the changes, then deleting the new category and
committing again. This will succeed because the new category won't have any products associated with it in the
database.
A real application would have to deal with this type of problem more intelligently. For example, we could have
loaded the Orders table as well, inspect the DataSet to detect whether the user is trying to delete an item that
he should not, and issue a warning. That is left as an exercise for the reader.
// Persist to stream
var ms = new MemoryStream();
using (var sw = new C1.C1Zip.C1ZStreamWriter(ms))
ds.WriteXml(sw, XmlWriteMode.WriteSchema);
//ds.WriteXml(ms, XmlWriteMode.WriteSchema);
17
Decompress the Data on the Client
To decompress the data on the client, we would modify the MasterDetail project by adding a reference to the
C1.Zip library (Silverlight version), then change the svc_GetDataCompleted method in the MainPage.xaml.cs
file as follows:
void svc_GetDataCompleted(object sender, GetDataCompletedEventArgs e)
{
// Handle errors
if (e.Error != null)
{
_tbStatus.Text = "Error downloading data...";
return;
}
Note: Without compression, the application transfers about 150k bytes of data on startup. After making these
small changes, the initial data transfer is reduced to less than 50k. The two small changes reduce the amount of
data transferred by about two-thirds. This reduces network traffic and the time it takes for the application to
initialize and show the data.
18
AppointmentMappingCollection mappings =
sched1.DataStorage.AppointmentStorage.Mappings;
mappings.IdMapping.MappingName = "Id";
mappings.Subject.MappingName = "Subject";
mappings.Body.MappingName = "Body";
mappings.End.MappingName = "End";
mappings.Start.MappingName = "Start";
mappings.Location.MappingName = "Location";
mappings.AppointmentProperties.MappingName = "Properties";
...
// set C1Scheduler data source to DataTable loaded from server
sched1.DataStorage.AppointmentStorage.DataSource = _dtAppointments;
When users add, remove or edit appointments, all changes are propagated to the underlying DataTable
automatically by C1Scheduler – no code required.
Conclusion
It is possible to build data-centric Silverlight applications today, using familiar tools and techniques. The
introduction of new data technologies does not mean ADO.NET developers have to throw out all their knowledge,
tools, and existing code. In most cases, these new technologies extend rather than replace the existing tools and
patterns.
C1.Silverlight.Data provides a Silverlight implementation of a substantial subset of the ADO.NET classes. This
allows developers to:
Leverage their knowledge of ADO.NET and the rich object model it provides, while at the same time
enjoying the benefits of new technologies such as LINQ, WCF, and so on.
Exchange relational data between client and server in a simple and efficient way;
Write code that uses similar classes and object models on client and server. Server and client use the same
language, similar classes, and similar object models.
19