23 Sample Windows Forms Application for .NET Clients

This is a step-by-step user guide that explains how to create a simple Windows Forms Application that uses the Coherence for .NET library.

23.1 General Instructions

Developing and configuring a Windows Forms Application that uses Coherence for .NET requires five basic steps:

  1. Create a Windows Application Project

  2. Add a Reference to the Coherence for .NET Library

  3. Create an App.config File

  4. Create Coherence for .NET Configuration Files

  5. Create and Design the Application

  6. Implement the Application

23.2 Create a Windows Application Project

To create a new Windows Application, follow these steps:

  1. Go to the File->New->Project... tab in Visual Studio 2005.

  2. In the New Project window choose the Visual C# project type and Windows Application template. Enter the name, location (full path where you want to store your application), and solution for your project.

    Figure 23-1 illustrates the New Project window with the name, location, and solution for the project.

    Figure 23-1 New Project Window

    Description of Figure 23-1 follows
    Description of "Figure 23-1 New Project Window"

  3. Click OK.

    Visual Studio should have created the following files: Program.cs, Form1.cs and Form1.Designer.cs. Figure 23-2 illustrates the Solution Explorer with the created project files

    Figure 23-2 Solution Explorer with the Created Project Files

    Description of Figure 23-2 follows
    Description of "Figure 23-2 Solution Explorer with the Created Project Files"

  4. Rename these files if you want.

    In this example they have been renamed to ContactCacheClient.cs, ContactForm.cs, and ContactForm.Designer.cs respectively.

23.3 Add a Reference to the Coherence for .NET Library

To use the Coherence for .NET library in your .NET application, you must first add a reference to the Coherence.dll library.

Adding a reference to the Coherence.dll library:

  1. In your project go to Project->Add Reference... or right click References in the Solution Explorer and choose Add Reference...

  2. In the Add Reference window that appears choose the Browse tab and find the Coherence.dll library on your file system. Figure 23-3 illustrates the .dll files in the Add Reference window.

    Figure 23-3 Add Reference Window

    Description of Figure 23-3 follows
    Description of "Figure 23-3 Add Reference Window"

  3. Click OK.

23.4 Create an App.config File

To correctly configure the Coherence for .NET library, you must create an App.config XML file that contains the appropriate file names for each configuration file used by the library.

  1. Right-click the project in the Solution Explorer and choose the Add->New Item... tab.

  2. In the Add New Item window select the Application Configuration File.

    Figure 23-4 illustrates the contents of the Add New Item window.

    Figure 23-4 Add New Item Window

    Description of Figure 23-4 follows
    Description of "Figure 23-4 Add New Item Window"

  3. Click OK.

Example 23-1 illustrates a sample valid App.config configuration file.

Example 23-1 Sample App.config File

<?xml version="1.0"?>

<configuration>
  <configSections>
    <section name="coherence" type="Tangosol.Util.CoherenceConfigHandler, Coherence"/>
  </configSections>
  <coherence>
    <cache-factory-config>coherence.xml</cache-factory-config>
    <cache-config>cache-config.xml</cache-config>
    <pof-config>pof-config.xml</pof-config>
  </coherence>
</configuration>

In <configSections> you must specify a class that handles access to the Coherence for .NET configuration section.

Elements within the Coherence for .NET configuration section are:

  • cache-factory-config—contains the path to a configuration descriptor used by the CacheFactory to configure the (IConfigurableCacheFactory and Logger) used by the CacheFactory.

  • cache-config—contains the path to a cache configuration descriptor which contains the cache configuration described earlier (see "Configuring Coherence*Extend on the Client"). This cache configuration descriptor is used by the DefaultConfigurableCacheFactory.

  • pof-config—contains the path to a configuration descriptor used by the ConfigurablePofContext to register custom types used by the application.

23.5 Create Coherence for .NET Configuration Files

Example 23-2 illustrates a sample coherence.xml configuration file

Example 23-2 Sample coherence.xml File for .NET

<?xml version="1.0"?>

<coherence xmlns="http://schemas.tangosol.com/coherence">
  <logging-config>
    <destination>ContactCache.log</destination>
    <severity-level>5</severity-level>
    <message-format>{date} &lt;{level}&gt; (thread={thread}): {text}</message-format>
    <character-limit>8192</character-limit>
  </logging-config>
</coherence>

Example 23-3 illustrates a sample cache-config.xml configuration file.

Example 23-3 Sample cache-config.xml File for .NET

<?xml version="1.0"?>

<cache-config xmlns="http://schemas.tangosol.com/cache">
  <caching-scheme-mapping>
    <cache-mapping>
      <cache-name>dist-contact-cache</cache-name>
      <scheme-name>extend-direct</scheme-name>
    </cache-mapping>
  </caching-scheme-mapping>

  <caching-schemes>
    <remote-cache-scheme>
      <scheme-name>extend-direct</scheme-name>
      <service-name>ExtendTcpCacheService</service-name>
      <initiator-config>
        <tcp-initiator>
          <remote-addresses>
            <socket-address>
              <address>localhost</address>
              <port>9099</port>
            </socket-address>
          </remote-addresses>
        </tcp-initiator>

        <outgoing-message-handler>
          <request-timeout>30s</request-timeout>
        </outgoing-message-handler>

      </initiator-config>
    </remote-cache-scheme>
  </caching-schemes>
</cache-config>

Example 23-4 illustrates a sample pof-config.xml configuration file.

Example 23-4 Sample pof-config.xml File for .NET

<?xml version="1.0"?>

<pof-config xmlns="http://schemas.tangosol.com/pof">
  <user-type-list>
    <!-- include all "standard" Coherence POF user types -->
    <include>assembly://Coherence/Tangosol.Config/coherence-pof-config.xml</include>

    <!-- include all application POF user types -->
    <user-type>
      <type-id>1001</type-id>
      <class-name>ContactCache.Windows.ContactInfo, ContactCacheClient</class-name>
    </user-type>
  </user-type-list>
</pof-config>

Having created these configuration files, everything is now in place to connect to a Coherence cluster and perform all operations supported by Coherence for .NET.

23.6 Create and Design the Application

Next, you must add controls to your Windows form. This example shows you how to store objects into a INamedCache, read from the cache, query the cache, remove an item from the cache, and clear the cache. For this we're going to use buttons that will raise events when clicked, a couple of TextBox components for editing objects, and a DataGridView for displaying the current contents of a INamedCache. In this example we're going to work with just a ContactInfo user type, but a similar approach can be used with any other user defined type.

To add controls in your application follow these steps:

  1. Go to View->Toolbox.

  2. In the Toolbox window choose the controls you want to use and drag them on the Windows form.

  3. For each control, right-click it, choose Properties tab, and set the necessary properties.

Figure 23-5 illustrates what the Contact Cache Info application UI should look after you have finished the previous steps.

Figure 23-5 Contact Cache Client UI

Description of Figure 23-5 follows
Description of "Figure 23-5 Contact Cache Client UI"

23.7 Implement the Application

The first step in the implementation of the example Windows application is to create a ContactInfo class that implements the IPortableObject interface.

Example 23-5 Sample Class that Implements IPortableObject

public class ContactInfo : IPortableObject
{
    private string name;
    private string street;
    private string city;
    private string state;
    private string zip;

    public ContactInfo()
    { }

    public ContactInfo(string name, string street, string city, string state, string zip)
    {
        this.name   = name;
        this.street = street;
        this.city   = city;
        this.state  = state;
        this.zip    = zip;
    }

    public void ReadExternal(IPofReader reader)
    {
        name   = reader.ReadString(0);
        street = reader.ReadString(1);
        city   = reader.ReadString(2);
        state  = reader.ReadString(3);
        zip    = reader.ReadString(4);
    }

    public void WriteExternal(IPofWriter writer)
    {
        writer.WriteString(0, name);
        writer.WriteString(1, street);
        writer.WriteString(2, city);
        writer.WriteString(3, state);
        writer.WriteString(4, zip);
    }

    // property definitions omitted for brevity
}

Before the application can start handling events, we must bind the DataGridView control with a data source object:

  1. In the Toolbox window choose the BindingSource object and drag it onto the form.

  2. Set its properties. Enter contactsBindingSource into the Name field and then set its data source by clicking the arrow button on the right end of the DataSource field. In the drop down window choose Add Project Data Source... and the Data Source Configuration Wizard will appear. Chose Object and find the ContactInfo class in your project.

    Figure 23-6 Using Data Source Wizard to Bind a Control to a Data Source

    Description of Figure 23-6 follows
    Description of "Figure 23-6 Using Data Source Wizard to Bind a Control to a Data Source"

  3. The final step is to bind the DataGridView control to the contactBindingSource. This is done by simply choosing the contactsBindingSource in the drop down window in the DataSource field of the DataGridView properties window. This is illustrated in Figure 23-7.

    Figure 23-7 Choosing a Data Source to Bind to the Control

    Description of Figure 23-7 follows
    Description of "Figure 23-7 Choosing a Data Source to Bind to the Control"

Now we have bound contactsBindingSource to our DataGridView control and all further interaction with the data, including navigating, sorting, filtering, and updating, is accomplished with calls to the BindingSource component. We also need IFilter and CacheEventFilter fields to manage filtering and a WindowsFormsCacheListener field used to ensure that any event handling code that must run as a response to a cache event is executed on the UI thread. For this to work, we'll have to delegate methods for each cache event we're handling and then register a listener with the cache by using the AddCacheListener() method. This is explained in more details in Chapter 20, "Special Considerations—Windows Forms Applications for .NET Clients". In the constructor, we will also obtain the INamedCache that we're using in the application by using the CacheFactory.GetCache() static method and initialize the ComboBox used for choosing the search attribute.

Example 23-6 Adding Listeners

/// <summary>
/// Named cache.
/// </summary>
private INamedCache cache;

/// <summary>
/// Listener that allows end users to handle Coherence cache events,
/// which are always raised from a background thread.
/// </summary>
private WindowsFormsCacheListener listener;

/// <summary>
/// Evaluate the specified extracted value.
/// </summary>
private IFilter filter;

/// <summary>
/// Wrapper filter, used by listeners.
/// </summary>
private CacheEventFilter cacheEventFilter;

/// <summary>
/// Search pattern.
/// </summary>
private string pattern;

/// <summary>
/// Default constructor.
/// </summary>
public ContactForm()
{
    listener = new WindowsFormsCacheListener(this);
    listener.EntryInserted += new CacheEventHandler(AddRow);
    listener.EntryUpdated  += new CacheEventHandler(UpdateRow);
    listener.EntryDeleted  += new CacheEventHandler(DeleteRow);

    cache = CacheFactory.GetCache("dist-contact-cache");
    cache.AddCacheListener(listener);

    InitializeComponent();
    InitializeComboBox();
}

/// <summary>
/// Initialize <b>ComboBox</b> with attribute names.
/// </summary>
/// <remarks>
/// Choosing attribute from the ComboBox allows to search for given
/// pattern in choosen entry attribute inside the named cache.
/// </remarks>
private void InitializeComboBox()
{
    cmbAttribute.Items.Add("Name");
    cmbAttribute.Items.Add("Street");
    cmbAttribute.Items.Add("City");
    cmbAttribute.Items.Add("State");
    cmbAttribute.Items.Add("Zip");

    cmbAttribute.SelectedIndex = 0;
}

As with any other Windows application, most of the remaining implementation has to do with event handling. Since each component in the Windows form can raise an event, event handlers must be created to handle each event. Event handlers in Visual Studio can be added to your application by following these steps:

  1. Right-click the Window component for which you'd like to implement an event handler and choose Properties.

  2. In the upper toolbar of the Properties window, select the lighting button and all events that the component can raise will be displayed.

    Figure 23-8 Properties Window

    Description of Figure 23-8 follows
    Description of "Figure 23-8 Properties Window"

  3. Choose the event you want to handle and double-click it. Visual Studio will add the necessary code to your application to enable you to handle the event. Next, you must implement the empty event handler method.

Example 23-7 illustrates the event code in the sample Windows application:

Example 23-7 Adding Events

/// <summary>
/// Load form event handler.
/// </summary>
/// <param name="sender">
/// The source of the event.
/// </param>
/// <param name="e">
/// An <b>EventArgs</b> that contains no event data.
/// </param>
private void ContactForm_Load(object sender, EventArgs e)
{
    RefreshContactsGrid(true);
}
/// <summary>
/// Closed form event handler.
/// </summary>
/// <remarks>
/// Removes the event handlers.
/// </remarks>
/// <param name="sender">
/// The source of the event.
/// </param>
/// <param name="e">
/// An <b>EventArgs</b> that contains no event data.
/// </param>
private void ContactForm_FormClosed(object sender, FormClosedEventArgs e)
{
    cache.RemoveCacheListener(listener, cacheEventFilter);
}

/// <summary>
/// Enter cell event handler for the <b>addressDataGridView</b>.
/// </summary>
/// <remarks>
/// Refreshes the <b>TextBox</b>es with data from selected
/// <b>addressDataGridView</b> row.
/// </remarks>
/// <param name="sender">
/// The source of the event.
/// </param>
/// <param name="e">
/// An <b>EventArgs</b> that contains no event data.
/// </param>
private void addressDataGridView_CellEnter(object sender, DataGridViewCellEventArgs e)
{
    DataGridViewCellCollection cells = addressDataGridView.CurrentRow.Cells;

    txtName.Text   = (string) cells[0].Value;
    txtStreet.Text = (string) cells[1].Value;
    txtCity.Text   = (string) cells[2].Value;
    txtState.Text  = (string) cells[3].Value;
    txtZip.Text    = (string) cells[4].Value;
}

/// <summary>
/// Click event handler for <b>Put</b> button.
/// </summary>
/// <remarks>
/// Stores the <see cref="ContactInfo"/> data entered in
/// <b>TextBox</b>es into the INamedCache.
/// </remarks>
/// <param name="sender">
/// The source of the event.
/// </param>
/// <param name="e">
/// An <b>EventArgs</b> that contains no event data.
/// </param>
private void btnPut_Click(object sender, EventArgs e)
{
    String name  = txtName.Text;
    ContactInfo contact = new ContactInfo(txtName.Text,
                                          txtStreet.Text,
                                          txtCity.Text,
                                          txtState.Text,
                                          txtZip.Text);
    cache.Insert(name, contact);
}

/// <summary>
/// Click event handler for the <b>Remove</b> button.
/// </summary>
/// <remarks>
/// Removes the <see cref="ContactInfo"/> mapped by the current
/// Name <b>TextBox</b> value. If there is no such entry in the
/// <b>INamedCache</b>, a simple warning box is displayed.
/// </remarks>
/// <param name="sender">
/// The source of the event.
/// </param>
/// <param name="e">
/// An <b>EventArgs</b> that contains no event data.
/// </param>
private void btnRemove_Click(object sender, EventArgs e)
{
    cache.Remove(txtName.Text);
    ResetTextBoxes();
}

/// <summary>
/// Click event handler for the <b>Clear</b> button.
/// </summary>
/// <remarks>
/// Clears the <b>INamedCache</b>.
/// </remarks>
/// <param name="sender">
/// The source of the event.
/// </param>
/// <param name="e">
/// An <b>EventArgs</b> that contains no event data.
/// </param>
private void btnClear_Click(object sender, EventArgs e)
{
    cache.RemoveCacheListener(listener, cacheEventFilter);
    cache.Clear();
    cache.AddCacheListener(listener, cacheEventFilter, false);

    contactsBindingSource.Clear();
    ResetTextBoxes();
}

/// <summary>
/// Click event handler for <b>Refresh</b> button.
/// </summary>
/// <remarks>
/// Refreshes the <b>addressDataGridView</b>, filtering named cache
/// entries by a given attribute and string pattern. If empty string
/// is provided as a pattern all entries in the named cache will be
/// accounted and displayed.
/// </remarks>
/// <param name="sender">
/// The source of the event.
/// </param>
/// <param name="e">
/// An <b>EventArgs</b> that contains no event data.
/// </param>
private void btnRefresh_Click(object sender, EventArgs e)
{
    string newPattern = txtPattern.Text;
    string attribute = (string) cmbAttribute.SelectedItem;

    if (!newPattern.Equals(pattern))
    {
        pattern = newPattern;
        cache.RemoveCacheListener(listener, cacheEventFilter);

        if (pattern != String.Empty)
        {
            IValueExtractor extractor = new ReflectionExtractor("get" + attribute);
            filter = new LikeFilter(extractor, pattern, '\\', false);
            cacheEventFilter = new CacheEventFilter(CacheEventFilter.CacheEventMask.All
                                                    | CacheEventFilter.CacheEventMask.UpdatedEntered
                                                    | CacheEventFilter.CacheEventMask.UpdatedLeft,
                                                    filter);
        }
        else
        {
            filter = null;
            cacheEventFilter = null;
        }
        cache.AddCacheListener(listener, cacheEventFilter, false);
    }
    RefreshContactsGrid(true);
}

/// <summary>
/// Click event handler for <b>SelectIndexChanged</b> event.
/// </summary>
/// <remarks>
/// Resets the pattern string to Refresh button click event
/// handler would work properly.
/// </remarks>
/// <param name="sender">
/// The source of the event.
/// </param>
/// <param name="e">
/// An <b>EventArgs</b> that contains no event data.
/// </param>
private void cmbAttribute_SelectedIndexChanged(object sender, EventArgs e)
{
    pattern = "";
}

We also have to write cache event handlers, as delegated in the constructor. This is illustrated in Example 23-8:

Example 23-8 Adding Cache Event Handlers

/// <summary>
/// Event handler for  <see cref="ICacheListener.EntryInserted"/>
/// event.
/// <param name="sender">
/// The source of the event.
/// </param>
/// <param name="args">
/// An <see cref="CacheEventArgs"/>.
/// </param>
private void AddRow(object sender, CacheEventArgs args)
{
    contactsBindingSource.Add(args.NewValue);
}

/// <summary>
/// Event handler for  <see cref="ICacheListener.EntryUpdated"/>
/// event.
/// </summary>
/// <param name="sender">
/// The source of the event.
/// </param>
/// <param name="args">
/// An <see cref="CacheEventArgs"/>.
/// </param>
public void UpdateRow(object sender, CacheEventArgs args)
{
    int index = contactsBindingSource.IndexOf(args.OldValue);
    if (index < 0)
    {
        // updated entered
        contactsBindingSource.Add(args.NewValue);
    }
    else
    {
        if (SatisfiesFilter(args.NewValue))
        {
            contactsBindingSource[index] = args.NewValue;
        }
        else
        {
            contactsBindingSource.RemoveAt(index);
        }
    }
}

/// <summary>
/// Event handler for <see cref="ICacheListener.EntryDeleted"/>
/// event.
/// </summary>
/// <param name="sender">
/// The source of the event.
/// </param>
/// <param name="args">
/// An <see cref="CacheEventArgs"/>.
/// </param>
public void DeleteRow(object sender, CacheEventArgs args)
{
    contactsBindingSource.Remove(args.OldValue);
}

Example 23-9 illustrates helper methods used by the event handlers in the previous example:

Example 23-9 Adding Helper Methods for Event Handlers

/// <summary>
/// Resets all of the text boxes on the form.
/// </summary>
private void ResetTextBoxes()
{
    txtName.Text   = "";
    txtStreet.Text = "";
    txtCity.Text   = "";
    txtState.Text  = "";
    txtZip.Text    = "";
}

/// <summary>
/// Initialize <b>ComboBox</b> with attribute names.
/// </summary>
/// <remarks>
/// Choosing attribute from the ComboBox allows to search for given
/// pattern in choosen entry attribute inside the named cache.
/// </remarks>
private void InitializeComboBox()
{
    cmbAttribute.Items.Add("Name");
    cmbAttribute.Items.Add("Street");
    cmbAttribute.Items.Add("City");
    cmbAttribute.Items.Add("State");
    cmbAttribute.Items.Add("Zip");

    cmbAttribute.SelectedIndex = 0;
}

/// <summary>
/// Queries the object with specified filter criteria.
/// </summary>
/// <param name="obj">
/// An object to which the test is applied.
/// </param>
/// <returns>
/// <b>true</b> if the test passes, <b>false</b> otherwise.
/// </returns>
private bool SatisfiesFilter(object obj)
{
    IFilter clientFilter = new LikeFilter(new ReflectionExtractor((string) cmbAttribute.SelectedItem),
                                          pattern, '\\', false);
    return clientFilter.Evaluate(obj);
}

/// <summary>
/// Refreshes the contacts table.
/// </summary>
/// <param name="updateContacts">
/// Flag specifying whether to query against cache to get
/// the most recent data or not.
/// </param>
private void RefreshContactsGrid(bool updateContacts)
{
    if (updateContacts)
    {
        RefreshContacts();
    }
    contactsBindingSource.ResetBindings(false);
}

/// <summary>
/// Refreshes the contacts table with the most recent data within the
/// cache.
/// </summary>
private void RefreshContacts()
{
    contactsBindingSource.Clear();
    ICollection cacheEntries = (filter == null ? cache.Values : cache.GetEntries(filter));
    foreach (object entry in cacheEntries)
    {
        if (entry is DictionaryEntry)
        {
            contactsBindingSource.Add(((DictionaryEntry) entry).Value);
        }
        else
        {
            contactsBindingSource.Add(entry);
        }
    }
}