Extending the Sample with Client-Activated Objects

Extending the Sample with Client-Activated Objects

So far, this chapter has demonstrated various methods of hosting server-activated objects and shown the client code necessary to interact with them. As discussed in Chapter 2, the .NET Framework offers a second form of remote objects: client-activated objects. Client-activated objects are “activated” on demand from the client, exist on a per-client and per-reference basis, and can maintain state between method calls.

To demonstrate client-activated objects, let’s extend the JobClient sample application by enabling users to add notes associated with a selected job. To support adding notes, we’ll add a class derived from MarshalByRefObject named JobNotes, which we’ll configure as a client-activated object. The stateful nature of client-activated objects will allow the notes to persist between method calls for as long as the JobNotes object instance remains alive.

The JobNotes Class

You implement a client-activated object in the same way that you implement a server-activated object: simply derive the class you want to be remotable from System.MarshalByRefObject. The way that the host application configures .NET Remoting determines whether a remote object is a client-activated object or a server-activated object.

The following code listing defines a new class named JobNotes that derives from System.MarshalByRefObject:

using System.Collections;

public class JobNotes : MarshalByRefObject
{
    private Hashtable m_HashJobID2Notes;

    public JobNotes()
    {
        m_HashJobID2Notes = new System.Collections.Hashtable();
    }

    public void AddNote(int id, string s)
    {
        // Defined later...
    }

    public ArrayList GetNotes(int id)
    {
        // Defined later...
    }

}

The JobNotes class allows clients to add textual notes for a specific job identifier. The class contains a System.Collections.Hashtable member that associates a given job identifier value to a System.Collections.ArrayList of strings that represent the notes for a particular job.

The AddNote method adds a note for the specified job ID, as shown in the following listing:

public void AddNote(int id, string s)
{
    // Look up notes list.
    ArrayList al = (ArrayList)m_HashJobID2Notes[id];
    if (al == null)
    {
        al = new ArrayList();
        m_HashJobID2Notes[id] = al;
    }

    // Insert a time stamp.
    s = s.Insert(0,Environment.NewLine);
    s = s.Insert(0,System.DateTime.Now.ToString());

    // Add s to the notes list.
    al.Add(s);
}

The following listing shows the implementation for the GetNotes method:

public ArrayList GetNotes(int id)
{
    // Look up notes for this job ID.
    ArrayList notes = (ArrayList)m_HashJobID2Notes[id];
    if (notes != null )
    {
        return notes;
    }
    return new ArrayList();
}

JobClient Application Changes

The client application needs a few code modifications to make use of the JobNotes class. This application needs to provide the user with the ability to add a new note for the currently selected job. This entails adding another button that, when clicked, will display a form prompting the user to enter a new note for the selected job. For this sample application, we’ll allow a user to add a note only if he or she is currently assigned to the selected job.

Let’s implement the user interface changes necessary to allow a user to enter a note for the currently selected job. You can start by designing a new form named FormAddNote that displays a list of the current notes for the job and allows the user to enter a new note to the list. Go ahead and create a new form that resembles the one shown in Figure 3-6.

figure 3-6 the formaddnote form user interface

Figure 3-6. The FormAddNote form user interface

Add a TextBox control named textBoxNotes that shows the current notes for the job. This TextBox should be read-only. Add another TextBox control named textBoxAddNote that accepts input from the user. Finally, add the obligatory OK and Cancel buttons.

To allow the client code to display the current notes for a job as well as obtain a new note for a job, the client code sets the Description property of the FormAddNote class before displaying the form and gets the value of the Description property after the user closes the form. The following code listing defines the Description property:

private string m_sDescription;

public string Description
{
    get
    { return m_sDescription; }

    set
    { m_sDescription = value; }
}

When the form loads, it populates the TextBox referenced by the textBoxNotes member with the value of the Description property. The following code implements this behavior in the Form.Load event handler:

private void FormAddNote_Load(object sender, System.EventArgs e)
{
    this.textBoxNotes.Text = m_sDescription;
}

You also need to add Button.Click event handlers for each of the buttons. The following code implements the handler for the Cancel button’s Click event:

private void button2_Click(object sender, System.EventArgs e)
{
    this.Hide();
}

The OK button Click event handler saves the text the user entered in textBoxAddNote to the m_sDescription member so that the client code can then retrieve the text that the user entered through the Description property:

private void button1_Click(object sender, System.EventArgs e)
{
    m_sDescription = this.textBoxAddNote.Text;
}

Because each instance of the JobClient application will have its own remote instance of the JobNotes client-activated object, you can add a new member variable of type JobNotes to the Form1 class and initialize it in the Form1 constructor.

To allow the user to add a note to a selected job, you can add a button named buttonAddNote to Form1 in Design view. The following code listing shows the implementation of the buttonAddNote_Click method:

private void buttonAddNote_Click(object sender, System.EventArgs e)
{
    // Instantiate a form for adding notes.
    FormAddNote frm = new FormAddNote();

    // Make sure the user is assigned to the selected job.
    JobInfo ji = GetSelectedJobInfo();
    if ( ji.m_sAssignedUser != System.Environment.MachineName )
    {
        MessageBox.Show("You are not assigned to that job");
        return;
    }
    
    // Get the notes for the currently selected job and
    // display them in the dialog box.
    ArrayList notes = m_JobNotes.GetNotes(ji.m_nID);
    IEnumerator ie = notes.GetEnumerator();
    while(ie.MoveNext())
    {
        frm.Description += (string)ie.Current;
        frm.Description += Environment.NewLine;
        frm.Description += Environment.NewLine;
    }

    // Display the form and obtain the new note.
    if ( frm.ShowDialog(this) == DialogResult.OK )
    {
        string s = frm.Description;
        if ( s.Length > 0 )
        {
            m_JobNotes.AddNote(ji.m_nID, frm.Description);
        }
    }
}

At this point, you should be able to run the client and test the functionality of the JobNotes class even though the JobNotes instance created by the JobClient application isn’t remote. Figure 3-7 shows the new form after a user has added some notes to a job.

figure 3-7 adding some notes to a job

Figure 3-7. Adding some notes to a job

Configuring the Client for .NET Remoting Client-Activated Objects

To make instances of the JobNotes type remote, you have to instruct the run­time to treat the JobNotes type as a client-activated object. Let’s do that now by configuring the JobClient application to consume the JobNotes type as a client-activated object.

Programmatic Configuration

The RemotingConfiguration class provides the RegisterActivatedClientType method to allow clients to register a type as client activated. You can add the following code to the Form1 constructor just before creating a new instance of JobNotes to instruct the runtime that the JobNotes type is a client-activated object:

ActivatedClientTypeEntry acte = 
        new ActivatedClientTypeEntry( typeof (JobNotes),
                                      "http://localhost:4000" );
RemotingConfiguration.RegisterActivatedClientType( acte );

You first instantiate the ActivatedClientTypeEntry type, passing two parameters to the constructor:

  • The remote object type

  • The URL of the endpoint where instances of the remote object type should be activated

You then pass the ActivatedClientTypeEntry instance to the RegisterActivatedClientType method, which registers the JobNotes type as a client-activated object. The RegisterActivatedClientType method is overloaded to also accept the same two parameters that the ActivatedClientTypeEntry constructor accepts.

Configuration File

As you saw earlier in the chapter, the alternative to programmatically configuring an application for .NET Remoting is to use a configuration file. You use the <client> element for registering both well-known objects and client-activated objects. Within this tag, you add an <activated> tag that specifies the same information required for programmatic configuration.

Let’s modify the JobClient.exe.config file to specify the JobNotes type as a client-activated object:

<configuration>
   <system.runtime.remoting>
      <application name="JobClient">
         <client>
            <wellknown 
               type="JobServerLib.JobServerImpl, JobServerLib" 
               url="http://localhost:4000/JobURI" />
         </client>
         <client url = "http://localhost:4000">
            <activated type="JobServerLib.JobNotes, JobServerLib"/>
         </client>
         <channels>
            <channel ref="http" port="0" />
         </channels>
      </application>
   </system.runtime.remoting>
</configuration>

You need to add a <client> element that specifies the URL of the activation endpoint by using the url attribute. The <client> element contains a child, the <activated> element. Because the JobClient application is using only one client-activated type, the configuration file contains only one <activated> element entry. If your application needs to activate several types at the same endpoint, you’ll have several <activated> element entries—one for each type under the same <client> element. Likewise, if you need to activate different types at different endpoints, you’ll have multiple <client> elements—one for each endpoint. You specify the type of the client-activated object by using the <activated> element’s type attribute. In the previous code listing, we could have added the <activated> element to the existing <client> element that contains the <wellknown> element, but it’s not necessary.

Configuring the Server for .NET Remoting Client-Activated Objects

Now we need to modify the JobServer application to configure the JobNotes type as a client-activated object. Once again, we can configure the application for .NET Remoting either programmatically or by using a configuration file.

Programmatic Configuration

The RemotingConfiguration class provides the RegisterActivatedServiceType method to allow you to programmatically configure a type for client activation. You use this method to configure the JobServer.exe host application to host JobNotes class instances as client-activated objects by adding the following line of code to the JobServer host application’s Main method:

RemotingConfiguration.RegisterActivatedServiceType(typeof(JobNotes));

When the JobServer application executes this line of code, it registers the JobNotes type as a client-activated object. This means that the host will accept client activation requests for the JobNotes type. Upon receiving an activation request, the Remoting infrastructure instantiates an instance of the JobNotes class.

Configuration File

You can also use a configuration file to configure a server host application for client-activated objects. You use the <activated> element to register a specific type with the .NET Remoting infrastructure as a client-activated object. You can modify the JobServer.exe.config file to register the JobNotes class as a client-activated object by adding the <activated> tag, as shown in the following code listing:

<configuration>
  <system.runtime.remoting>
    <application name="JobServer">
      <service>
        <wellknown mode="Singleton" 
          type="JobServerLib.JobServerImpl, JobServerLib" 
          objectUri="JobURI" />
        <activated type="JobServerLib.JobNotes, JobServerLib" />
      </service>
      <channels>
        <channel ref="http" port="4000" />
      </channels>
    </application>
  </system.runtime.remoting>
</configuration>

Adding a Sponsor to the Lease

At this point, we’ve implemented and configured the JobNotes class as a client-activated object. It’s now appropriate to consider the lifetime requirements for the JobNotes class.

Chapter 2 discussed how the .NET Remoting infrastructure uses a lease-based system to control the lifetime of remote objects. The System.Marshal­ByRefObject class provides a virtual method named InitializeLifetimeService that deriving classes can override to change the default lease values, thereby controlling how long the remote object instance lives.

The default implementation of the InitializeLifetimeService method returns an instance of an ILease implementation with default values. The default InitialLeaseTime property value is 5 minutes, which means that the object’s lease won’t expire for 5 minutes. The default value for the RenewOnCallTime property is 2 minutes. If the lease is due to expire within the time specified by RenewOnCallTime, calling a method extends the lease by setting its CurrentLeaseTime to the RenewOnCallTime property. This means that each time a method on this object is called, its lease can be extended to the value of the RenewOnCallTime property, provided the lease’s remaining time is less than the value of the RenewOnCallTime property. The default value for the SponsorshipTimeout property is 2 minutes. Thus, if the lease has any sponsors when it expires, calls to the ISponsor.Renewal method will time out after 2 minutes if the sponsor doesn’t respond.

To put this in perspective, recall that the JobServerImpl class provided an override for the InitializeLifetimeService method that returned null, indicating that the object instance should live indefinitely, until the host application terminated. For well-known objects in Singleton mode, such as JobServerImpl, having an indefinite lifetime makes sense.

However, for client-activated objects such as JobNotes, having an indefinite lifetime doesn’t make sense because instances of the JobNotes class don’t need to hang around after the client application shuts down. But if we need to persist the notes data, we’ll need to implement a mechanism allowing disconnected JobNotes instances to serialize their state to a persistent store before being garbage collected. Then when a client application activates a new instance of the JobNotes class, the constructor will deserialize the previously stored state information. In this case, we’ll probably want to reimplement the JobNotes class to support a SingleCall well-known object activation model. But this requirement isn’t necessary for this sample application.

Suppose, however, that you do want to make the JobNotes instances hang around for longer than the default lease time. To do so, you need to override the InitializeLifetimeService method to obtain and initialize the object’s lease with values other than the default.

Initializing the Lease

The JobNotes class overrides the InitializeLifetimeService method inherited from System.MarshalByRefObject. As discussed in the “Implementing the JobServer Application” section, the .NET Remoting infrastructure calls this method to obtain lease information for a remote object. The JobServerImpl class implementation of this method returns null, telling the .NET Remoting infrastructure that the object instance should live indefinitely. However, for the JobNotes class, there’s no need to allow the object to live indefinitely. The following code listing shows how the JobNotes class overrides the default implementation of the InitializeLifetimeService method to modify the initial lease values that control its lifetime:

public override Object InitializeLifetimeService()
{
    ILease lease = (ILease)base.InitializeLifetimeService();
    if ( LeaseState.Initial == lease.CurrentState )
    {
        lease.InitialLeaseTime   = TimeSpan.FromMinutes(4);
        lease.SponsorshipTimeout = TimeSpan.FromMinutes(1);
        lease.RenewOnCallTime    = TimeSpan.FromMinutes(3);
    }

    return lease;
}

In implementing the JobNotes version of the InitializeLifetimeService method, we obtain the lease for this instance by calling the base class’s implementation of InitializeLifetimeService. To set the lease’s values, the lease must be in the LeaseState.Initial state. If the lease isn’t in this state, attempts to set the values will result in an exception.

Implementing the ISponsor Interface

To take full advantage of the lease-based lifetime mechanism provided by .NET Remoting, you can create a sponsor by implementing the ISponsor interface and registering the sponsor with the lease for a remote object. When the lease expires, the runtime will call the ISponsor.Renewal method on any registered sponsors, giving each sponsor an opportunity to renew the lease.

For the JobClient application, you can make the Form1 class a sponsor by having it derive from and implement the ISponsor interface, as the following code shows:

public TimeSpan Renewal(ILease lease)
{
    return TimeSpan.FromMinutes(5);
}

The ISponsor.Renewal method returns a System.TimeSpan instance representing 5 minutes, which has the effect of renewing the lease for 5 minutes. Depending on your application needs, you might want to use a longer or shorter time span. Using a longer time span results in less frequent calls to the ISponsor.Renewal method and therefore less network traffic—especially if the client application sponsored many client-activated object instances. The trade-off, of course, is that the client-activated object instances can exist on the server for longer than needed—potentially monopolizing much-needed resources. Using a shorter time span reduces the amount of time these so-called zombie objects hang around but results in more frequent calls to the ISponsor.Renewal method and therefore produces more network traffic.

Registering the Sponsor

With the ISponsor implementation complete, you can now register the Form1 instance as a sponsor for the JobNotes instance’s lease. You do this by first obtaining the ILease reference on the remote object and then calling the ILease.Register method, which passes the ISponsor interface of the sponsor. To demonstrate this, add the following code to the Form1 constructor:

ILease lease = (ILease) RemotingServices.GetLifetimeService(m_JobNotes);
lease.Register((ISponsor) this);

Now when the JobClient application starts and creates the JobNotes instance, the lease for the JobNotes instance will have an initial lease time of 4 minutes. This is because the JobNotes class overrides the InitializeLifetimeService method, setting the InitialLease time property to 4 minutes. If the client doesn’t call any methods on the JobNotes instance for these first 4 minutes, the lease will expire and the runtime will call the ISponsor.Renewal method on the Form1 class, which renews the lease, keeping the JobNotes instance alive for 5 more minutes.