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.
Developing and configuring a Windows Forms Application that uses Coherence for .NET requires five basic steps:
To create a new Windows Application, follow these steps:
Go to the File->New->Project... tab in Visual Studio 2005.
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.
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
Rename these files if you want.
In this example they have been renamed to ContactCacheClient.cs
, ContactForm.cs
, and ContactForm.Designer.cs
respectively.
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:
In your project go to Project->Add Reference... or right click References in the Solution Explorer and choose Add Reference...
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.
Click OK.
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.
Right-click the project in the Solution Explorer and choose the Add->New Item... tab.
In the Add New Item window select the Application Configuration File.
Figure 23-4 illustrates the contents of the Add New Item window.
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.
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} <{level}> (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.
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:
Go to View->Toolbox.
In the Toolbox window choose the controls you want to use and drag them on the Windows form.
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.
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:
In the Toolbox window choose the BindingSource
object and drag it onto the form.
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
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
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.
/// <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:
Right-click the Window component for which you'd like to implement an event handler and choose Properties.
In the upper toolbar of the Properties window, select the lighting button and all events that the component can raise will be displayed.
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:
/// <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); } } }