2.2. Completing the Service
Our application is being
developed, in part, as a learning tool. To help us present the
completion of each individual project as clearly as possible, we are
going to separate each project into its own Visual Studio 2008 solution.
There is no technical need for doing this, but it does make it easier
to focus in on the Data Synchronization Services project first and then
the Smart Device client project second.
We begin our work on
the service by removing two files that came with the WCF Service project
template, as they are not needed for a Data Synchronization Service. We
mentioned at the time we added the project that we would be removing
them. So, we now delete the IService1.cs and Service1.cs files from the
project.
What we now are
interested in is the code in the CustCache.SyncContract.cs file. In
writing this file, the Configure Data Synchronization Wizard did three
things: It made one wrong assumption about our consuming client
application, it tried to help us transition the service from a WCF
Service to a Data Synchronization Service, and it had to specify a
default URL. To complete our service application, we need to address all
three.
First, the wrong
assumption: The wizard did not know that our client would be a Smart
Device client, and that a Smart Device client must use XML serialization
when passing objects to/from the service. We need to specify XML
serialization, and we do that by adding an [XmlSerializerFormat()] code attribute to the ICustCacheSyncContract definition that is located at the end of the CustCache.SyncContract.cs code file, as shown in Listing 1.
Listing 1. The Attributed ICustCacheSyncContract Class
[ServiceContractAttribute()]
[XmlSerializerFormat()]
public interface ICustCacheSyncContract
{
[OperationContract()]
SyncContext ApplyChanges(
SyncGroupMetadata groupMetadata,
DataSet dataSet,
SyncSession syncSession);
[OperationContract()]
SyncContext GetChanges(
SyncGroupMetadata groupMetadata,
SyncSession syncSession);
[OperationContract()]
SyncSchema GetSchema(
Collection<string> tableNames,
SyncSession syncSession);
[OperationContract()]
SyncServerInfo GetServerInfo(
SyncSession syncSession);
}
|
Second,
the generated code to help transition the service from a WCF Service to
a Data Synchronization Service: The project’s App.Config file contains
elements that are applicable to a WCF Service, not to a Data
Synchronization Service. At the top of the CustCache.SyncContract.cs
code file, as comments, are the suggested replacements that the
Configure Data Synchronization Wizard has provided for us. The
instructions in the comments tell us to uncomment two elements and
replace the corresponding elements in the App.Config file with the
uncommented elements.
As we do so, we’ll need to
make two changes. We will need to replace the default URL with our
service’s actual URL. We must do this, for localhost is meaningless within a Smart Device application (the wizard’s third miscalculation). In our case,
http://localhost:8080/CustCacheSyncService/
becomes
http://192.168.254.2:1824/CustCacheSyncService/
And because we must support Smart Device clients, we must use basic HTTP binding rather than Web Services HTTP binding. Thus,
becomes
binding="basicHttpBinding"
Once we do so, we have a valid App.Config file.
We also have completed the Data Synchronization Service. We can now turn our attention to our Smart Device client project.
2.3. Completing the Client
When we changed the binding specification of our service to basicHttpBinding,
it became a conventional Web service, something that .NET Compact
Framework applications have been communicating with for some time now.
And almost always, we enable that communication in our Smart Device
application by adding a Web reference to the Web service. To add a Web
reference, the service must be running, so we go to our service project
and run it.
Note
It is important that
your client program not be running at this time. If it is, you will be
unable to add a Web reference to your client project. If you are still
developing both projects within the same solution, running the service
will cause the client to be deployed. If that happens, you will need to
cancel that deployment.
Once the service is up and running, a default client program is automatically started, a portion of which is shown in Figure 7.
We are interested in the URL displayed on the second line in the window. It should be exactly what we specified in the baseAddress
element of the App.Config file, suffixed with “mex” (which indicates
the location for metadata exchange and which you need to ignore when
setting the reference). As we saw earlier, in our case this URL is
http://192.168.254.2:1824/CustCacheSyncService/.
Thus, we turn to
our client project and add a Web reference. In the Add Web Reference
dialog, we specify the URL exactly as we specified it in the service’s baseAddress
element, beginning with “http” and ending with the trailing “/”. Then
we click on the Go arrow, browsing to the service and receiving the
service description page, shown in Figure 8, in return.
We choose a
meaningful name for our Web reference and add it to our project. Once it
has been added, we expand the files in our Solution Explorer window, as
seen in Figure 9, to show the generated proxy code file, Reference.cs.
Like other files before it, this file also needs to be modified.
When the proxy code
within Reference.cs was generated, one class definition that we need was
written, as were several that we do not need. To make a long story as
short as we can: The methods of our Data Synchronization Service pass
parameters of data types that are defined in the Microsoft.Synchronization
namespaces. As an inevitable result of proxy code generation, these
classes are redefined as they are added to the proxy class definition;
specifically, they are redefined as carriers of data only. That is, all
method definitions are lost. Since we wish to use the Microsoft.Synchronization
versions and their defined methods, we must delete the generated
versions from the proxy code, now and anytime we regenerate the proxy
code!
The one class that we do not want to delete should be easy to find. It inherits from SoapHttpClientProtocol, should be the first class in the code file, and should have a name that ends with SyncService. So, open Reference.cs and look for the following line of code:
public partial class CustCacheSyncService
We delete all other classes and add the following using statement at the top of the file:
using Microsoft.Synchronization.Data;
We do not need to add references to the Microsoft.Synchronization
libraries, for the Configure Data Synchronization Wizard has already
done that for us. When it comes to dealing directly with Visual Studio,
its templates, and its wizards, we are done. What is left for us to do
is write the functionality of our application.
We’ll add just enough functionality so that we can walk through the following scenario.
Synchronize data with the server.
Make some nonconflicting changes at the device and on the server.
Synchronize data with the server.
Make some conflicting changes at the device and on the server.
Synchronize data with the server.
After each synchronization, examine results, data, and errors.
We begin by opening the form in design mode and then adding a full-screen docked DataGrid control plus a two-entry menu to it, as shown in Figure 10.
Then we turn to the form’s
code file. We’ll need to add the following: namespace declarations for
synchronization, for our proxy classes, and for our typed data set; a
routine to modify some customer rows; a routine to synchronize our
changes with the server; and code to display the results of that
synchronization.
We add the namespace declarations at the start of the code thusly:
using Microsoft.Synchronization.Data;
using SyncClnt.WSCustSync;
using SyncClnt.NorthwindDataSetTableAdapters;
namespace SyncClnt
{
public partial class FormData : Form
In the form’s Load event we create a typed data set and a table adapter for later use, like so:
private void FormData_Load(object sender,
EventArgs e)
{
dsetNorthwind = new NorthwindDataSet();
daptCust = new CustomersTableAdapter();
}
To modify some data at the
device, modifications that will then need to be synchronized with the
server, we iterate through the rows of the Customers data table. Every customer whose CustomerId starts with the letter A will have “** ” injected into the start of his CustomerName field. This code appears in Listing 2.
Listing 2. The Data Modification Routine
private void mitemModifyData_Click(object sender,
EventArgs e)
{
Cursor.Current = Cursors.WaitCursor;
foreach (NorthwindDataSet.CustomersRow rowCustomer
in dsetNorthwind.Customers)
{
if (rowCustomer.CustomerID.StartsWith("A"))
{
rowCustomer.CompanyName =
"** " + rowCustomer.CompanyName;
}
}
daptCust.Update(dsetNorthwind.Customers);
Cursor.Current = Cursors.Default;
}
|
Listing 3
shows the routine for doing the synchronization of our changes with the
server’s data store, via the service. It is well annotated. Any class
name that contains the word Cust was written by one of the wizards that we walked through. Any class name that contains the word Sync, but not the word Cust, is defined in one of the Microsoft.Synchronization libraries that the project references.
Listing 3. The Data Synchronization Routine
private SyncStatistics SyncCustomers()
{
SyncStatistics syncStats;
try
{ // Create a proxy object for the synch service.
// Use it to create a generic proxy object.
ServerSyncProviderProxy prxCustSync =
new ServerSyncProviderProxy(
new CustCacheSyncService());
// Create the sync agent
// for the Customer table.
// Assign the proxy object to it.
// Set it for bi-directional sync.
CustCacheSyncAgent syncAgent =
new CustCacheSyncAgent();
syncAgent.RemoteProvider = prxCustSync;
syncAgent.Customers.SyncDirection =
SyncDirection.Bidirectional;
// Sync changes with the server.
syncStats = syncAgent.Synchronize();
// The above call to syncAgent.Synchronize
// retrieved all Customer table changes
// that had were made at the server
// between the previous call and this
// call and placed them in our
// SQL Server CE Northwind database here
// on the device.
// We need to display that table to
// our user. We will use a data bound
// data table to do so.
daptCust.Fill(dsetNorthwind.Customers);
dgridCustomers.DataSource =
dsetNorthwind.Customers;
}
catch (Exception exSync)
{
MessageBox.Show(exSync.Message);
throw;
}
return syncStats;
}
|
Note
that the routine first creates a proxy object for our service and then
uses that proxy object in the creation of another, more generic, proxy
object. Then the SyncAgent is created and is passed a reference to the generic proxy object. Once any other SynchAgent properties have been set, the agent knows what needs to be synchronized and what service to use to do it. The agent’s Synchronize method then does the synchronization and returns the results in a SyncStats structure.
The SynchAgent
object is the keystone object of data synchronization. Although it is
located on the device, you want to think of it as sitting between the
server-side data store and the client-side SQL Server CE database.
Anything that your application needs to do at runtime to influence or
alter the behavior of a synchronization will be done by invoking methods
of, or setting properties of, or responding to events raised by the SynchAgent object or one of its contained objects.
For example, later in this
demo we will need to obtain information about rows that failed to
synchronize. We will do that by handling an event that is raised by an object that is contained within the SynchAgent object.
One last comment on our
synchronization code: Immediately after the synchronization, we load the
rows of the SQL Server CE’s Northwind Customers table into the typed data set’s Customers
table. Normally, this is not a good idea. The data set contains the
data that we attempted to upload to the server. After synchronization,
the SQL Server CE database contains the data that was received from the
server. If the synchronization of a row failed, the client-side version
will be in the data set table; the server-side version will be in the
SQL Server CE table. We might want access to both as we are reconciling
the differences.
In the menu
selection that initiates the synchronization, we do some UI setup, call
our synchronizing routine, and display the results. Listing 4 shows this code.
Listing 4. The Synchronization Menu Handler
private void mitemSynchronize_Click(object sender,
EventArgs e)
{
Cursor.Current = Cursors.WaitCursor;
SyncStatistics syncStats = SyncCustomers();
MessageBox.Show(
string.Format(
"{1} {2}.{0}" +
"{3} {4}.{0}" +
"{5} {6}.{0}" +
"{7} {8}.{0}",
Environment.NewLine,
"Rows sent - total",
syncStats.TotalChangesUploaded,
"Rows sent - succeeded",
syncStats.UploadChangesApplied,
"Rows received - total",
syncStats.TotalChangesDownloaded,
"Rows received - succeeded",
syncStats.DownloadChangesApplied));
Cursor.Current = Cursors.Default;
}
|
That’s it for adding code to our application. Now it’s time to run the application and see what happens.
We
start by running the application and selecting Synchronize from the
menu, twice if necessary, to ensure that the data on the device and on
the server are identical, as shown in Figure 11.
Now we use our data modification routine to make changes to our client version of the data; see Figure 12. And we use SQL Server’s Management Studio to make nonconflicting changes to the data on the server. We insert a few $s into the CompanyName column of the first two B customers, as shown in Figure 13.
Now we can synchronize. Once again we select our Synchronize menu selection. This time we receive the response shown in Figure 14.
From Figure 13, we can see that our four modified rows were sent to the server, modified at the server, and echoed back to us.
Why were our modified rows returned to us? For the same reason that so
many of you have written code that immediately retrieved newly
inserted/updated rows. Inserted/updated rows often contain values that
were supplied not
by the client but rather by the database engine. System-assigned primary
keys, GUID columns, default values, and trigger-generated values are
all examples of this. Rather than the client having to ask for the newly
inserted/modified row, the service returns the row to the client
automatically.
Also, we can see that
the rows that were added by some other client, in this case Management
Studio, are also delivered to our client, as we would expect.
Now let’s try
some conflicting updates. First, we set the data back to what it was
when we began our demo. Again we synchronize, twice if necessary, to
ensure that the data in both locations is identical. (Refer back to Figure 11.) As before, we modify data on the device by running the data modification routine. (Refer back to Figure 12.)
But this time, in Management Studio, we modify two of the same rows
that we are concurrently modifying on the device, as shown in Figure 15. When we synchronize the device, we receive the results shown in Figure 16.
Of
the four rows that were returned to our device, two are the two rows
that we sent to the server and were accepted at the server, including
any values that were added on the server as the row was being
inserted/updated. The other two are the two rows that we sent to the
server that were not accepted at the server. What is sent back to the
device is the row as it now exists on the server.
That is, we now have the server-side version of the row, which we can
present to the user to see whether she wants to resubmit the same
update.