Updating Server Data Using .NET Remoting
Problem
You want to update a data source using .NET remoting and use the remote application from your client application.
Solution
Use System.MarshalByRefObject to create a remoteable class.
The server-side code that registers the remoteable class for remote activation contains one event handler and one configuration file:
Start Server Button.Click
Registers the remoteable class RemoteClass for remote activation.
Server-side configuration file
Contains parameters used to register the class and the channel on the server so that the class can be activated from another application domain.
The remoteable class code contains two methods :
LoadOrders( )
Creates and returns a DataSet containing the Orders and Order Details tables from Northwind and a DataRelation between those tables.
UpdateOrders( )
Takes a DataSet argument containing the changes made to the DataSet created by the LoadOrders( ) method, creates two DataAdapter objects with CommandBuilder -generated update logic for each, and uses the DataAdapter objects to update the Orders and Order Details tables in Northwind.
The client-side code contains two event handlers and one configuration file:
Form.Load
Sets up the example by calling the LoadOrders( ) method in the remote object to populate a DataSet . The default view of the Orders table is bound to the data grid on the form.
Update Button.Click
Calls the UpdateOrders( ) method in the remote object passing a DataSet containing changes made to the DataSet since the form was loaded or since the last time the UpdateOrders( ) method was called.
Client-side configuration file
Contains parameters used to register the remote class and channel on the client so that the remote class can be instantiated by the client.
The C# server-side code that registers the remoteable class for activation is shown in Example 4-27.
Example 4-27. File: NorthwindServerCSMainForm.cs
// Namespaces, variables, and constants using System; using System.Configuration; using System.Windows.Forms; using System.Runtime.Remoting; using System.Runtime.Remoting.Channels; using System.Runtime.Remoting.Channels.Tcp; using ADOCookbookCS.NorthwindRemoteCS; private bool isStarted = false; // . . . private void startServerButton_Click(object sender, System.EventArgs e) { if (!isStarted) { // config file RemotingConfiguration.Configure( "NorthwindServerCS.exe.config"); serverResultTextBox.Text += "Remote server started." + Environment.NewLine + " ApplicationName = " + RemotingConfiguration.ApplicationName + Environment.NewLine + " ApplicationId = " + RemotingConfiguration.ApplicationId + Environment.NewLine + " ProcessId = " + RemotingConfiguration.ProcessId; isStarted = true; } else { serverResultTextBox.Text += "Remote Server already started." + Environment.NewLine; } }
Example 4-28. File: NorthwindServerCSNorthwindServerCS.exe.config
The C# remoteable class code is shown in Example 4-29.
Example 4-29. File: NorthwindRemoteCSNorthwindRemoteCS.cs
// Namespaces, variables, and constants using System; using System.Data; using System.Data.SqlClient; // . . . namespace ADOCookbookCS.NorthwindRemoteCS { ///
/// Summary description for NorthwindRemote ///
public class RemoteClass : MarshalByRefObject { public const String SQL_CONNECTIONSTRING = "Data Source=(local);Integrated security=SSPI;" + "Initial Catalog=Northwind;"; // Table name constants private const String ORDERS_TABLE = "Orders"; private const String ORDERDETAILS_TABLE = "OrderDetails"; // Relation name constants private const String ORDERS_ORDERDETAILS_RELATION = "Orders_OrderDetails_Relation"; // Field name constants private const String ORDERID_FIELD = "OrderID"; public DataSet LoadOrders( ) { DataSet ds = new DataSet( ); SqlDataAdapter da; // Fill the Order table and add it to the DataSet. da = new SqlDataAdapter("SELECT * FROM Orders", SQL_CONNECTIONSTRING); DataTable orderTable = new DataTable(ORDERS_TABLE); da.FillSchema(orderTable, SchemaType.Source); da.Fill(orderTable); ds.Tables.Add(orderTable); // Fill the OrderDetails table and add it to the DataSet. da = new SqlDataAdapter("SELECT * FROM [Order Details]", SQL_CONNECTIONSTRING); DataTable orderDetailTable = new DataTable(ORDERDETAILS_TABLE); da.FillSchema(orderDetailTable, SchemaType.Source); da.Fill(orderDetailTable); ds.Tables.Add(orderDetailTable); // Create a relation between the tables. ds.Relations.Add(ORDERS_ORDERDETAILS_RELATION, ds.Tables[ORDERS_TABLE].Columns[ORDERID_FIELD], ds.Tables[ORDERDETAILS_TABLE]. Columns[ORDERID_FIELD], true); return ds; } public bool UpdateOrders(DataSet ds) { // Create the DataAdapters for order and order details // tables. SqlDataAdapter daOrders = new SqlDataAdapter("SELECT * FROM Orders", SQL_CONNECTIONSTRING); SqlDataAdapter daOrderDetails = new SqlDataAdapter("SELECT * FROM [Order Details]", SQL_CONNECTIONSTRING); // Use CommandBuilder to generate update logic. SqlCommandBuilder cbOrders = new SqlCommandBuilder(daOrders); SqlCommandBuilder cbOrderDetails = new SqlCommandBuilder(daOrderDetails); // Update parent and child records. daOrderDetails.Update( ds.Tables[ORDERDETAILS_TABLE].Select(null, null, DataViewRowState.Deleted)); daOrders.Update(ds.Tables[ORDERS_TABLE].Select( null, null, DataViewRowState.Deleted)); daOrders.Update(ds.Tables[ORDERS_TABLE].Select(null, null, DataViewRowState.ModifiedCurrent)); daOrders.Update(ds.Tables[ORDERS_TABLE].Select(null, null, DataViewRowState.Added)); daOrderDetails.Update( ds.Tables[ORDERDETAILS_TABLE].Select(null, null, DataViewRowState.ModifiedCurrent)); daOrderDetails.Update( ds.Tables[ORDERDETAILS_TABLE].Select(null, null, DataViewRowState.Added)); return true; } } }
The C# client-side code that activates the remoteable class remotely is shown in Examples Example 4-30 and Example 4-31.
Example 4-30. File: RemotingForm.cs
// Namespaces, variables, and constants using System; using System.Windows.Forms; using System.Runtime.Remoting; using System.Runtime.Remoting.Channels; using System.Runtime.Remoting.Channels.Tcp; using System.Data; using ADOCookbookCS.NorthwindRemoteCS; // Table name constants private const String ORDERS_TABLE = "Orders"; private RemoteClass rs; private DataSet ds; // . . . private void RemotingForm_Load(object sender, System.EventArgs e) { Cursor.Current = Cursors.WaitCursor; RemotingConfiguration.Configure("RemotingForm.exe.config"); rs = new RemoteClass( ); // Load the DataSet containing orders and order details. ds = new DataSet( ); ds = rs.LoadOrders( ); // Bind the default view of the orders table to the grid. dataGrid.DataSource = ds.Tables[ORDERS_TABLE].DefaultView; Cursor.Current = Cursors.Default; } private void updateButton_Click(object sender, System.EventArgs e) { Cursor.Current = Cursors.WaitCursor; // Get the changes to the data. DataSet dsChanges = ds.GetChanges( ); // Update the changes to the order and order detail informtation. if (dsChanges != null) rs.UpdateOrders(dsChanges); Cursor.Current = Cursors.Default; }
Example 4-31. File: RemotingForm.exe.config
Discussion
A remoteable class, unlike a conventional class, can be used by clients running outside of application domain of the remoteable class. All that is required to make a class remoteable is to derive it from System.MarshalByRefObject .
When a client creates an instance of a remote class, a proxy is created in the client's application domain instead of an actual object. The proxy acts exactly like the object, but is actually a reference to the object. This proxy communicates with the remote class through a channel connecting the two application domains.
|
Before the class can be instantiated remotely, a server process must register it so that it can be activated from another application domain. This can be done either by calling the RegisterActivatedServiceType( ) or RegisterWellKnownServiceType( ) of the RemotingConfiguration class with the appropriate parameters or by calling the Configure( ) method of the RemotingConfiguration class with the name of the configuration file as an argument. The solution uses the Configure( ) method with the code:
RemotingConfiguration.Configure("NorthwindServerCS.exe.config");
For a well known ( server-activated ) class, the server registration code would be:
RemotingConfiguration.RegisterWellKnownServiceType( typeof(ADOCookbookCS.NorthwindRemoteCS.RemoteClass), "RemoteClass", WellKnownObjectMode.SingleCall);
For an activated (client-activated) class, the server registration code would be:
RemotingConfiguration.RegisterActivatedServiceType(typeof(RemoteClass));
A server channel also must be created and registered when one of the two methods, for a well known or an activated class, is used. When a configuration file is used, the channel information is specified within the file. The server channel can be either one that accepts TCP connections from the client or one that accepts HTTP connections from the client. The code to register a TCP channel that listens on port 1234 is:
TcpServerChannel channel = new TcpServerChannel(1234); ChannelServices.RegisterChannel(channel);
The code to register a HTTP channel that listens on port 1234 is:
HttpServerChannel channel = new HttpServerChannel (1234); ChannelServices.RegisterChannel(channel);
To create an instance of the remote class, the client must first register either a TCP or HTTP client channel by creating an instance of either the TcpClientChannel or HttpClientChannel class and using the RegisterChannel( ) method of the ChannelServices class to register the channel. The code is similar to the code for registering a server channel.
Next, if the client wants to be able to instantiate the remote object using the new operator, the client must register the remote class in the local application domain using the RegisterWellKnownClientType( ) or the RegisterActivatedClientType( ) method of the RemotingConfiguration class with the appropriate parameters or by calling the Configure( ) method of the RemotingConfiguration class. The solution uses the Configure( ) method with the code:
RemotingConfiguration.Configure("RemotingForm.exe.config");
For a well known class, the client registration code would be:
RemotingConfiguration.RegisterWellKnownClientType( typeof(RemoteClass), "tcp://localhost:1234/RemoteClass");
For an activated class, the client registration code would be:
RemotingConfiguration.RegisterActivatedClientType( typeof(RemoteClass), "tcp://localhost:1234");
Once the client and server have performed the necessary registration, the remote class can be instantiated by the client using the new operator as shown here:
rs = new RemoteClass( );
As mentioned earlier, this creates a proxy for the remote object on client that you can use in exactly the same way as if the object were local.
In the solution, the GetChanges( ) method is called on the client-side DataSet to create a copy of the DataSet containing only the changes. This DataSet is passed to the UpdateOrders( ) method of the remote object instead of to the entire DataSet to minimize the required bandwidth across a potentially slow connection.
The UpdateOrders( ) method updates the database with the changes using method calls with different subsets of the DataSet as arguments. This technique, used to avoid referential integrity problems, is discussed in more detail in Recipe 4.10.
The solution shows that there is very little difference between implementing the LoadOrders( ) and UpdateOrders( ) methods as a local class or a remoteable class.