Implementing the JobClient Application
We can now turn our attention to implementing a client application that makes remote method calls on the remote object hosted by the JobServer application. It s worth repeating that one of the great things about .NET Remoting is how unobtrusive it is. .NET s remoting capabilities just seem to exist in the background, without requiring a class implementer to write a large amount of extra glue code to benefit from the .NET Remoting architecture. Let s now detail the basic tasks that you must complete to enable a client application for remoting.
Choosing a Client Application Domain
We had several choices for hosting the JobServerImpl instance as a remote object. We have the same choices for implementing the client application.
We ve chosen to implement the JobClient application as a Windows Forms application by using C#. The application is straightforward, consisting of a main form containing a ListView control. The ListView control displays a column for each JobInfo struct member: JobID, Description, User, and Status.
The form also contains three buttons that allow the user to perform the following actions:
Create a new job.
Assign a job.
Complete a job.
The remainder of this discussion assumes that you ve created a new Microsoft Visual Studio .NET C# Windows Application project. After creating the project, add a System.Windows.Forms.ListView control and three System.Windows.Forms.Button controls to the Form1 form so that it resembles Figure 3-1.
Figure 3-1. The JobClient application s main form
The JobClient application interacts with an instance of the JobServerImpl class that we developed in the previous section. Therefore, you need to add a reference to the JobServerLib.dll assembly so that the client can use the IJobServer interface, the JobServerImpl class, the JobInfo struct, and the JobEventArgs class.
Because the Form1 class will interact with the JobServerImpl class instance, add a JobServerImpl type member and a method named GetIJobServer to the Form1 class in the Form1.cs file:
using JobServerLib; public class Form1 : System.Windows.Forms.Form { // This member holds a reference to the IJobServer interface // on the remote object. private IjobServer m_IJobServer; private IJobServer GetIJobServer() { return (IJobServer)new JobServerImpl(); } }
Although the JobServerImpl type is remotable because it derives from MarshalByRefObject, the JobServerImpl instance created by the GetIJobServer method is local to the JobClient application s application domain. To make JobServerImpl remote, we need to configure .NET Remoting services, which we ll do later in this section after we implement the client application logic. For now, however, we ll develop the entire client application by using a local instance of the JobServerImpl class. As we mentioned at the beginning of this section, doing so offers a number of benefits, one of which is allowing us to quickly develop the sample application without dealing with .NET Remoting issues.
The following code listing shows the Form1 constructor:
public Form1() { // // Required for Windows Form Designer support // InitializeComponent(); // Get a reference to the remote object. m_IJobServer = GetIJobServer(); // Subscribe to the JobEvent. m_IJobServer.JobEvent += new JobEventHandler(this.MyJobEventHandler); }
The last statement in this listing subscribes to the JobEvent. We ll unsubscribe from the JobEvent when the user terminates the application. The following listing shows the Form.Close event handler:
private void OnClosed(object sender, System.EventArgs e) { // Make sure we unsubscribe from the JobEvent. m_IJobServer.JobEvent -= new JobEventHandler(this.MyJobEventHandler); }
Recall from the previous section that the JobServerImpl instance raises the IJobServer.JobEvent whenever a client creates a new job or changes a job s status to Assigned or Complete. The following code listing shows the implementation for the MyJobEventHandler method:
public void MyJobEventHandler(object sender, JobEventArgs args) { switch(args.Reason) { case JobEventArgs.ReasonCode.NEW: AddJobToListView(args.Job); break; case JobEventArgs.ReasonCode.CHANGE: UpdateJobInListView(args.Job); break; } }
The MyJobEventHandler method uses two helper methods, which we ll discuss shortly. Based on the value of the JobEventArgs instance s Reason property, the method either adds the job information conveyed in the JobEventArgs instance to the list view or updates an existing job in the list view.
CAUTION
Declaring the MyJobEventHandler event handler method with private (nonpublic) access will result in the runtime throwing a System.Runtime.Serialization.SerializationException exception when the client application subscribes to the JobEvent. The associated error message states, Serialization will not deserialize delegates to nonpublic methods. This makes sense from a security perspective because otherwise code could circumvent the method s declared nonpublic accessibility level.
Note that the callback will occur on a thread different from the thread that created the Form1 control. Because of the threading constraints of controls, most methods on controls aren t thread-safe, and invoking a Control.xxxxx method from a thread other than the creating thread might result in undefined behavior, such as deadlocks. Fortunately, the System.Windows.Forms.Control type provides several methods (such as the Invoke method) that allow noncreating threads to cause the creating thread to call methods on a control instance. The Invoke method takes two parameters: the instance of the delegate to invoke, and an array of object instances to pass as parameters to the target method.
The AddJobToListView method uses the ListView.Invoke method to call the ListView.Items.Add method on the creating thread. Before using the ListView.Invoke method, you must define a delegate for the method you want to invoke. The following code shows how to define a delegate for the ListView.Items.Add method:
// Need a delegate to the ListView.Items.Add method. delegate ListViewItem dlgtListViewItemsAdd(ListViewItem lvItem);
The AddJobToListView method uses Invoke to add job information to the list view, as the following listing shows:
// Add job to the list view. void AddJobToListView(JobInfo ji) { // Create a delegate targeting the listView1.Items.Add method. dlgtListViewItemsAdd lvadd = new dlgtListViewItemsAdd( listView1.Items.Add ); // Package the JobInfo data in a ListViewItem instance. ListViewItem lvItem = new ListViewItem(new string[] { ji.m_nID.ToString(), ji.m_sDescription, ji.m_sAssignedUser, ji.m_sStatus } ); // Use Invoke to add the ListViewItem to the list view. listView1.Invoke( lvadd, new object[]{lvItem}); }
The implementation of the UpdateJobInListView method follows the same model as the AddJobToListView method to invoke the GetEnumerator method of the ListView.Items collection class. The following code implements the UpdateJobInListView method:
// Update job in list view. void UpdateJobInListView(JobInfo ji) { IEnumerator ie = (IEnumerator)listView1.Invoke(new dlgtItemsGetEnumerator(listView1.Items.GetEnumerator)); while( ie.MoveNext() ) { // Find the job in the list view matching this JobInfo. ListViewItem lvItem = (ListViewItem)ie.Current; if ( ! lvItem.Text.Equals(ji.m_nID.ToString()) ) { continue; } // Found it. Now go through the ListViewItem's subitems // and update accordingly. IEnumerator ieSub = lvItem.SubItems.GetEnumerator(); ieSub.MoveNext(); // Skip JobID. // Update the description. ieSub.MoveNext(); if ( ((ListViewItem.ListViewSubItem)ieSub.Current).Text != ji.m_sDescription ) { ((ListViewItem.ListViewSubItem)ieSub.Current).Text = ji.m_sDescription; } // Update the assigned user. ieSub.MoveNext(); if ( ((ListViewItem.ListViewSubItem)ieSub.Current).Text != ji.m_sAssignedUser ) { ((ListViewItem.ListViewSubItem)ieSub.Current).Text = ji.m_sAssignedUser; } // Update the status. ieSub.MoveNext(); if ( ((ListViewItem.ListViewSubItem)ieSub.Current).Text != ji.m_sStatus ) { ((ListViewItem.ListViewSubItem)ieSub.Current).Text = ji.m_sStatus; } } // End while }
The UpdateJobInListView method enumerates over the ListView.Items collection, searching for a ListViewItem that matches the JobInfo type s job identifier field. When the method finds a match, it enumerates over and updates the subitems for the ListViewItem. Each subitem corresponds to a column in the details view.
So far, we ve created an instance of the JobServerImpl class and saved a reference to its IJobServer interface. We ve added event-handling code for the JobEvent. We ve also looked at how using the ListView.Invoke method allows us to update the ListView control from a thread other than the thread that created the control instance. We must complete the following tasks:
Obtain a collection of all current jobs to populate the ListView when the form loads.
Implement the Button.Click event handlers to allow the user to create, assign, and complete jobs.
Before implementing the Button.Click handlers, it s useful to introduce a couple of helper functions. The following code implements a method named GetSelectedJob that returns a JobInfo instance corresponding to the currently selected ListViewItem:
private JobInfo GetSelectedJobInfo() { JobInfo ji = new JobInfo(); // Which job is selected? IEnumerator ie = listView1.SelectedItems.GetEnumerator(); while( ie.MoveNext() ) { // Our list view does not allow multiple selections, so we // should have no more than one job selected. ji = ConvertListViewItemToJobInfo( (ListViewItem)ie.Current ); } return ji; }
This method in turn utilizes another method named ConvertListViewItemToJobInfo, which takes a ListViewItem instance and returns a JobInfo instance based on the values of the ListViewItem subitems:
private JobInfo ConvertListViewItemToJobInfo(ListViewItem lvItem) { JobInfo ji = new JobInfo(); IEnumerator ieSub = lvItem.SubItems.GetEnumerator(); ieSub.MoveNext(); ji.m_nID = Convert.ToInt32( ((ListViewItem.ListViewSubItem)ieSub.Current).Text); ieSub.MoveNext(); ji.m_sDescription = ((ListViewItem.ListViewSubItem)ieSub.Current).Text; ieSub.MoveNext(); ji.m_sAssignedUser = ((ListViewItem.ListViewSubItem)ieSub.Current).Text; ieSub.MoveNext(); ji.m_sStatus = ((ListViewItem.ListViewSubItem)ieSub.Current).Text; return ji; }
With the helper functions in place, we can implement the handler methods for the Assign, Complete, and Create Button.Click events:
private void buttonAssign_Click(object sender, System.EventArgs e) { // Which job is selected? JobInfo ji = GetSelectedJobInfo(); m_IJobServer.UpdateJobState(ji.m_nID, System.Environment.MachineName, "Assigned"); } private void buttonComplete_Click(object sender, System.EventArgs e) { // Which job is selected? JobInfo ji = GetSelectedJobInfo(); m_IJobServer.UpdateJobState(ji.m_nID, System.Environment.MachineName, "Completed"); }
The Assign button and Complete button Click event handlers simply get the selected job and call IJobServer.UpdateJobState. Calling the IJobServer.UpdateJobState method elicits two results:
The JobServerImpl instance sets the job state information for the specified job ID to Assigned or Completed.
The JobServerImpl instance raises the JobEvent.
Finally, the following listing shows the implementation for the Create New Job button:
private void buttonCreate_Click(object sender, System.EventArgs e) { // Show Create New Job form. FormCreateJob frm = new FormCreateJob(); if ( frm.ShowDialog(this) == DialogResult.OK ) { // Create the job on the server. string s = frm.JobDescription; if ( s.Length > 0 ) { m_IJobServer.CreateJob(frm.JobDescription); } } }
The buttonCreate_Click method displays another form named FormCreateJob, which asks the user to enter a description for the new job. After the user closes the form, the buttonCreate_Click method obtains the description entered by the user (if any). Assuming the user enters a description, the code calls the IJobServer.CreateJob method on the JobServerImpl instance, which creates a new job and raises the JobEvent.
You can implement the FormCreateJob form by adding a new form to the project. Figure 3-2 shows the FormCreateJob form.
Figure 3-2. The JobClient application s Create New Job form
The following code listing shows the additional code necessary to implement the FormCreateJob class:
public class FormCreateJob : System.Windows.Forms.Form { private System.Windows.Forms.Button button1; private System.Windows.Forms.Button button2; private System.Windows.Forms.TextBox textBox1; private System.Windows.Forms.Label label1; private string m_sDescription; public string JobDescription { get{ return m_sDescription; } } private void button1_Click(object sender, System.EventArgs e) { m_sDescription = textBox1.Text; } private void button2_Click(object sender, System.EventArgs e) { this.Hide(); } }
Obtaining the Server s Metadata
Now you must obtain the metadata describing the remote type you want to use. As we mentioned in the Implementing the JobServer Application section, the metadata is needed for two main reasons: to enable the client code that references the remote object type to compile, and to enable the .NET Framework to generate a proxy class that the client uses to interact with the remote object. This sample references the JobServerLib assembly and thus uses the JobServerImpl type s implementation. We ll discuss other ways of obtaining suitable metadata for the remote object later in this chapter in the sections Exposing the JobServerImpl Class as a Web Service and Metadata Dependency Issues.
At this point, you should be able to compile and run the application and test it by creating, assigning, and completing several new jobs. Figure 3-3 shows how the JobClient application looks after the user creates a few jobs.
Figure 3-3. The JobClient application s appearance after the user creates some jobs
Let s recap what we ve accomplished so far. We ve implemented the JobClient application as a C# Windows Forms application. If you run the sample application, the GetIJobServer method actually creates the JobServerImpl instance in the client application s application domain; therefore, the instance isn t remote. You can see this in the debugger, as shown in Figure 3-4.
Figure 3-4. The JobServerImpl instance is local to the client application domain.
Recall from Chapter 2 that clients interact with instances of remote objects through a proxy object. Currently, the m_IJobServer member references an instance of the JobServerLib.JobServerImpl class. Because the JobServerImpl class derives from System.MarshalByRefObject, instances of it are remotable. However, this particular instance isn t remote because the application hasn t yet configured the .NET Remoting infrastructure. You can tell that it s not remote because the m_IJobServer member doesn t reference a proxy. In contrast, Figure 3-5 shows how the Watch window would look if the instance were remote.
Figure 3-5. The JobServerImpl instance is remote to the client application domain.
You can easily see in Figure 3-5 how the m_IJobServer member references an instance of the System.Runtime.Remoting.Proxies.__TransparentProxy type. As discussed in Chapter 2, this type implements the transparent proxy, which forwards calls to the underlying type instance derived from RealProxy. This latter type instance can then make a method call on the remote object instance.
So far, we haven t dealt with any remoting-specific code for the JobClient application. Let s change that.
Configuring the JobClient Application for .NET Remoting
The second task necessary to enable remote object communication is to configure the client application for .NET Remoting. Configuration consists largely of registering a communications channel appropriate for the remote object and registering the remote object s type. You configure an application for .NET Remoting either programmatically or by using configuration files.
Programmatic Configuration
The .NET Framework provides the RemotingConfiguration class in the System.Runtime.Remoting namespace for configuring an application to use .NET Remoting. As discussed in the Implementing the JobServer Application section, this class provides various methods to allow programmatic configuration of the Remoting infrastructure. Let s look at the client-specific configuration methods that this class exposes.
You can configure the JobClient application for .NET Remoting by modifying the GetIJobServer method, as the following listing shows:
private IJobServer GetIJobServer() { // // Register a channel. HttpChannel channel = new HttpChannel(0); ChannelServices.RegisterChannel(channel); // // Register the JobServerImpl type as a WKO. WellKnownClientTypeEntry remotetype = new WellKnownClientTypeEntry(typeof(JobServerImpl), "http://localhost:4000/JobURI"); RemotingConfiguration.RegisterWellKnownClientType(remotetype); return (IJobServer)new JobServerImpl(); }
In this listing, you create a new instance of the HttpChannel class, passing the value of 0 to the constructor. A value of 0 causes the channel to pick any available port and begin listening for incoming connection requests. If you use the default constructor (no parameters), the channel won t listen on a port and can only make outgoing calls to the remote object. Because the JobClient application subscribes to the JobServerImpl instance s JobEvent event, it needs to register a channel capable of receiving the callback when the JobServerImpl instance raises the JobEvent event. If you need to have the callback occur on a specific port, you can specify that port number instead of 0. When the constructor returns, the application is actively listening for incoming connections on either the specified port or an available port.
NOTE
The mscorlib.dll assembly defines most of the commonly used .NET Remoting types. However, another assembly named System.Run time.Remoting.dll defines some other types, such as HttpChannel in the System.Runtime.Remoting.Channels.Http namespace.
After creating an instance of HttpChannel, you register the instance with ChannelServices via its RegisterChannel method. This method takes the IChannel interface from the HttpChannel instance and adds the interface to its internal data structure of registered channels for the client application s application domain. Once registered with the .NET Remoting infrastructure, the channel transports .NET Remoting messages between the client and the server. The channel also receives callbacks from the server on the listening port.
NOTE
You can register more than one channel if the channel names are unique. For example, you can expose a single remote object by using HttpChannel and TcpChannel at the same time. This way, a single host can expose the object to clients across firewalls (by using HttpChannel) and to .NET clients inside a firewall (by using the better-performing TcpChannel).
The next step is to configure the .NET Remoting infrastructure to treat the JobServerImpl type as a remote object that resides outside the JobClient application s application domain. The RemotingConfiguration class provides the RegisterWellKnownClientType method for configuring well-known objects on the client side. This method has two overloads. The one used in this sample takes an instance of a System.Runtime.Remoting.WellKnownClientTypeEntry class. You create an instance of this class by specifying the type of the remote object in this case, typeof(JobServerImpl) and the object s well-known URL.
NOTE
The RemotingConfiguration class provides an overloaded form of the RegisterWellKnownClientType method that takes two arguments, a System.Type instance specifying the type of the remote object, and a string specifying the URL, as shown here:
RemotingConfiguration.RegisterWellKnownClientType( typeof(JobServerImpl), http://localhost:4000/JobURI );
This form of the RegisterWellKnownClientType method uses these arguments to create an instance of WellKnownClientType, which it then passes to the other form of RegisterWellKnownClientType.
At this point, you ve configured the JobClient application for remoting. The application can now connect to the JobServerImpl object hosted by the JobServer application without requiring any further changes in the original application code. It s not absolutely necessary to register the remote object s type with the RemotingConfiguration class. You need to do so only if you want to use the new keyword to instantiate the remote type. The other option is to use the Activator.GetObject method, which we ll look at later in the Remoting the IJobServer Interface section.
Configuration File
.NET Remoting offers a second method for configuring an application for .NET Remoting that uses configuration files. We introduced using configuration files earlier, in the section Implementing the JobServer Application. You use different tags when configuring a client application for .NET Remoting. The following XML code shows the configuration file for the sample JobClient application:
<configuration> <system.runtime.remoting> <application name="JobClient"> <client> <wellknown type="JobServerLib.JobServerImpl, JobServerLib" url="http://localhost:4000/JobURI" /> </client> <channels> <channel ref="http" port="0" /> </channels> </application> </system.runtime.remoting> </configuration>
The client configuration file contains a <client> element, which you use to specify any remote objects used by the client application. In the previous listing, the <client> element contains a child element named <wellknown>, which you use to indicate any well-known server-activated objects this client uses. You can see how the <wellknown> element s attributes map closely to the parameters passed to the WellKnownClientTypeEntry constructor call that appeared in the earlier example showing programmatic configuration. The <client> element can also contain an <activated> element for specifying client-activated objects, which we ll discuss later in the Extending the Sample with Client Activated Objects section.
The elements for channel configuration are the same as those for the server configuration file. You specify the HTTP channel by using the ref property along with a port value of 0 to allow the .NET Remoting infrastructure to pick any available port. The following code listing shows how you can use the Job Client.exe.config file to configure .NET Remoting by replacing the implementation of the GetIJobServer method with a call to the RemotingConfiguration.Configure method:
private IJobServer GetIJobServer() { RemotingConfiguration.Configure( @"..\..\JobClient.exe.config" ); return (IJobServer)new JobServerImpl(); }