Graphical Clients

An important objective for any graphical application is to maintain a responsive user interface. The event loops of many common graphical programming tool kits run single-threaded, and any delays in application-specific code have a noticeable and negative impact on the user experience. Applications often have no choice but to create a separate thread for any operation that has the potential to block.

By default, an Ice invocation uses synchronous semantics similar to that of a regular in-process function call: the calling thread blocks until the invocation completes. However, a synchronous Ice invocation carries additional risk of blocking because of the network activity that Ice performs behind the scenes. As an example, the Ice run time may need to establish a connection to the server, which can take some time. Even if the connection is already established due to an earlier invocation, the seemingly simple act of sending a message over an open connection can also block unexpectedly (for example, due to problems in the network).

Applications such as our graphical chat clients that require non-blocking semantics must instead use Ice's asynchronous programming model, called Asynchronous Method Invocation (AMI). Although this model requires some extra effort, the benefit is worth it: Ice guarantees that asynchronous invocations will never block the calling thread. As a result, graphical applications can safely make Ice invocations from the event loop thread without adversely affecting the user interface.

Ice provides the Dispatcher facility to give applications control over the thread with which request dispatches are executed. The graphical clients in the chat demo use this facility so that calls on ChatRoomCallback objects can update the user interface directly.

Creating a Session

One of the first tasks of a push client is establishing a session with the Glacier2 router. This process is greatly simplified by using the Glacier2 helper classes included with Ice, and our graphical clients make extensive use of these classes. (The code shown below was adapted from the Java graphical client, but the C# and C++ codes are similar.)

Before we can create a session, we need to configure the dispatcher to ensure that request dispatches are executed in the UI thread:

initData.dispatcher = (runnable, connection) -> SwingUtilities.invokeLater(runnable);

As you can see, the dispatcher implementation simply delegates to Swing's invokeLater function.

Next, the client creates an instance of Glacier2.SessionFactoryHelper and passes it a callback object:

Glacier2.SessionCallback callback = ...;
_factory = new Glacier2.SessionFactoryHelper(initData, callback);

The callback is invoked to notify the application about significant events in the lifecycle of a Glacier2 session.

Now we can use the factory to initiate the creation of a session:

Glacier2.SessionHelper session = _factory.connect(userName, password);

The client invokes  connect with the user-supplied account information. The return value is a new Glacier2.SessionHelper object that is responsible for managing the session and for invoking the client's SessionCallback object when necessary. For example, since the establishment of a session occurs asynchronously to avoid blocking the thread that calls connect, the client cannot begin using the session until its connected callback method is invoked to indicate that the request completed successfully:

public void
connected(SessionHelper session)
    throws SessionNotExistException
{
    // ...
    _chat = Chat.ChatSessionPrx.uncheckedCast(_session.session());
    Chat.ChatRoomCallbackPrx callback = Chat.ChatRoomCallbackPrx.uncheckedCast(
        _session.addWithUUID(new ChatRoomCallbackI(coordinator)));
    _chat.setCallbackAsync(callback).whenCompleteAsync(...);
}

The SessionHelper method session returns a proxy for the newly-created Glacier2 session object. We down-cast this proxy to the derived interface Chat::ChatSession that we saw earlier.

Next the code creates the callback object that will receive events from the chat room. There's a lot happening in this short snippet of code. First, we instantiate our callback servant class ChatRoomCallbackI and pass it to the addWithUUID method. This method registers our servant with an object adapter intended especially for callback objects, creating that object adapter if it doesn't already exist. addWithUUID also ensures that the object identity associated with the servant includes a UUID and complies with Glacier2's conventions for callback objects. Finally, we narrow the resulting callback proxy to the type Chat::ChatRoomCallback using an unchecked cast.

The last step is to join the chat room, which we accomplish by invoking setCallback on the session. We use an asynchronous invocation here to avoid blocking.

.NET Client

The user interface of the .NET chat client is built using Windows Presentation Foundation (WPF),. The chat client's graphical elements are arranged using Extensible Application Markup Language (XAML), while the application logic is implemented in C#. To avoid blocking .NET's event loop, the program uses asynchronous invocations to communicate with the chat server. For example, the code below shows how the client invokes an asynchronous version of the send operation to publish a newly-typed chat message:

public async void sendMessage(string message)
{
    ...
    try
    {
        long timestamp = await _chat.sendAsync(message);
        userSayEvent(timestamp, _username, message);
    }
    catch(Chat.InvalidMessageException ex)
    {
        appendMessage("<system-message> - " + ex.reason + Environment.NewLine);
    }
    catch(Exception)
    {
        destroySession();
    }
}

The sendAsync method accepts the chat message as its only argument and returns a .NET Task. The code then awaits the task and .NET will schedule a continuation to run after the task completes. Upon completion, we dispatch the event or handle the error as we do for a synchronous invocation. The call to sendAsync is made from a WPF thread and the continuation will be executed using the WPF thread because the WPF SynchronizationContext schedules the continuations using its dispatcher, and therefore it's safe to modify GUI components from there. 

C++ Client

The user interface of the C++ chat client is built using the Qt Framework. To avoid blocking Qt's event loop, the program uses asynchronous invocations to communicate with the chat server. For example, the code below shows how the client invokes an asynchronous version of the send operation to publish a newly-typed chat message:

shared_ptr<ChatSessionPrx> session = ...
string message = ...
chat->sendAsync(message, [this, message](long long timestamp)
                         { 
                             _coordinator->userSayEvent(timestamp, _username, message); 
                         },
                         [this](std::exception_ptr eptr)
                         {
                             try
                             {
                                 rethrow(eptr);
                             }
                             catch(const Chat::InvalidMessageException& e)
                             {
                                 ostringstream os;
                                 os << " - " << e.reason;
                                 _coordinator->appendMessage(os.str());
                             }
                             catch(const Ice::Exception&)
                             {
                                 _coordinator->destroySession(ex);
                              }
                         });

Ice calls the response callback when the invocation completes successfully; the parameters of this method correspond to the return value and output parameters of the Slice operation. In the example above, response receives the time stamp assigned by the server. Note that the response and exception callbacks are executed in the UI thread because we installed a Dispatcher, so it's safe for these functions to modify the user interface.