UnifySquare Logo


Nav Accent Bar
From technical articles to tid-bits of important news and information, stay up to date on the latest UC happenings. Unify Square Blog
Helping you deploy the world's leading platform for Unified Communications.
 

In this post, I’m going to demonstrate how to add automatic reconnection logic to UCMA applications, which is otherwise not well documented.

In an ideal world, one’s custom OCS 2007 R2 middle-tier applications never lose their connections to OCS front-end servers.  In the real world, however, unplanned network and server outages will disrupt these connections.  If you do not want to be responsible for manually restarting your UCMA 2.0 applications each time this happens, you need to add automatic reconnection logic to these applications.

How to do this is not fully documented.  And, as far as I know, there are no sample applications showing how to

1) Detect that you have lost the connection to the front-end server, and

2) Re-establish the connection to the front end server. 

So, I shall provide some code samples in this blog that help clarify how to accomplish these two important tasks.

In order to understand the solution, you need to understand how UCMA signals an application connection, disconnection and failure.  In particular, these three points are key:

1) The connection to the OCS front-end server is created when you establish the application endpoint via the LocalEndpoint.EndEstablish() method.  Any failure establishing the connection will be thrown by this method.

2) When the local endpoint is disconnected from the OCS front-end server, its state will transition to Terminating and then Terminated.

3) An attempt to establish a call using a disconnected endpoint via the Call.EndEstablish() method will throw a InvalidOperationException exception. 

Here are the code samples that you can add to your UCMA 2.0 application to detect a lost connection and re-establish it. 

Step 1: Detect that the application has lost its connection to the OCS front-end server

1.1) Register your local endpoint for state changes:

    LocalEndpoint _endpoint;

    ….

    _endpoint.StateChanged += new EventHandler<LocalEndpointStateChangedEventArgs>(_endpoint_StateChanged);

1.2) Implement the _endpoint_StateChanged event handler:

    /// <summary>
    /// Will be true if we need to re-establish the endpoint's connection to the front end server.
    /// </summary>
    bool needReConnect;

    /// <summary>
    /// Called when endpoint state changes.
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="args"></param>
    void _endpoint_StateChanged(object sender, LocalEndpointStateChangedEventArgs args)
    {
        try
        {
            if (args.State == LocalEndpointState.Terminated)
            {
                Console.WriteLine("endpoint state is now terminated.");

                // Endpoint lost connection to OCS server. Need to re-establish our endpoint.
                needReConnect = true;
            }
            else if (args.State == LocalEndpointState.Reestablishing)
            {
                Console.WriteLine("endpoint state is re-establishing.");
            }
            else if (args.State == LocalEndpointState.Established)
            {
                Console.WriteLine("endpoint state is now established.");

                needReConnect = false;
           }
        }
        catch (Exception e)
        {
            HandleException(e);
        }
    }

Step 2: Re-establish the connection with the front-end server

2.1) Before making your next call to the OCS server, add the following code:

    …

    // Do we need to re-establish our connection with the OCS front end server?
    if (needReConnect)
    {
        needReConnect = (ReestablishEndpoint() ? false : true);
    }

    if (false == needReConnect)
    {
        // Continue on with call, etc.
    }

    // Otherwise, try again the next time around

    …

2.2) Implement the ReestablishEndpoint() method.

    /// <summary>
    /// Returns true if get reconnected, false otherwise
    /// </summary>
    /// <returns>bool</returns>
    private static bool ReestablishEndpoint()
    {
        bool success = false;

        try
        {
            // Terminate the endpoint.
            // Terminate the platform.
            ShutdownPlatform();

            // Re-initialize the platform and endpoint
            InitPlatform();

            success = true;
        }
        catch (Exception e)
        {
            Console.WriteLine("Re-establish failed.");
            HandleException(e);
        }

        return success;
    }

2.3) Implement the ShutdownPlatform() method.

    CollaborationPlatform _collabPlatform;

    /// <summary>
    /// Uninitialize the UCMA platform.
    /// </summary>
    private static void ShutdownPlatform()
    {
        …

        // Terminate the endpoint.
       if (_endpoint != null)
        {

            …
            _endpoint.StateChanged -= _endpoint_StateChanged;
            _endpoint.EndTerminate(_endpoint.BeginTerminate(null, null));
            _endpoint = null;
        }

        if (_collabPlatform != null)
        {
            _collabPlatform.EndShutdown(_collabPlatform.BeginShutdown(null, null));
           _collabPlatform = null;
        }
    }

2.4) Implement the InitPlatform() method.

        /// <summary>
        /// Initializes the UCMA platform and endpoint.
        /// </summary>
        private static void InitPlatform()
        {
            …

                _collabPlatform = new CollaborationPlatform(platformSettings);

                Console.Write("Starting CollaborationPlatform... ");
                _collabPlatform.EndStartup(_collabPlatform.BeginStartup(null, null));
                Console.WriteLine("Complete");

      …

                _endpoint = new UserEndpoint(_collabPlatform, settings); // or ApplicationEndpoint, as appropriate

                Console.Write("Establishing Endpoint... ");
                _endpoint.EndEstablish(_endpoint.BeginEstablish(null, _endpoint));
                Console.WriteLine("Complete");

                // Register for state changes
                _endpoint.StateChanged += new EventHandler<LocalEndpointStateChangedEventArgs>(_endpoint_StateChanged);

     …

    }

Note that the local endpoint state must be Idle when BeginEstablish() is called. 

For this reason, this sample terminate both the local endpoint and the collaboration platform before trying to re-establish the endpoint’s connection with the OCS front-end server. 

Finally, it’s worth noting that you can catch the InvalidOperationException exception when attempting to establish a new call with the local endpoint.  This allows you to log a message with more complete context than the one that the collaboration platform provides:

Call call;

call.EndEstablish(call.BeginEstablish(null, null));

catch (InvalidOperationException e)
{
    // NOTE:
    // If the endpoint has been disconnected from the front end server, then the error message text is
    // "Endpoint is currently terminating or already terminated"

    if (e.ToString().Contains("Endpoint is currently terminating or already terminated"))
    {

        // Just in case we do not get the state change event on the local endpoint…
        // We need to re-establish a connection with the front end server.
        needReConnect = true;

        // TODO: Log a more informative error.
    }
}

Note that we are currently using this code in several of our internal UCMA 2.0 applications and the applications are successfully reconnecting after OCS front end server re-starts.

That is it!  Let me know if you find this useful!  Have fun coding…


August 19, 2009 07:24 by katyle Permalink | Comments (11) | Comment RSS RSS Button Image


The release of Office Communication Server 2007 R2 from Microsoft at the beginning of January 2009 was followed not long after by the release of a comprehensive book titled “Programming for Unified Communications with Microsoft Office Communications Server 2007 R2” in May.  The purpose of the book is to give developers both a reference and a road map for developing applications for OCS 2007 R2.  The authors, Rui Maximo, Kurt De Ding, Vishwa Ranjan, Chris Mayo and Oscar Newkerk do an excellent job of describing, in detail, how to develop an application which leverages OCS 2007 R2 from the ground up.

One of the things that I really like about this book is that it starts from the beginning and takes you through the end of the initial development cycle, taking into consideration the fact that this may be a platform that you have not used before. 

First, the book describes how to setup your machine to develop with the OCS 2007 R2 SDKs.  It details the hardware and software requirements and provides guidance on how to setup a test topology.

The above cannot be emphasized enough.  Office Communication Server is a complex product that is normally installed and maintained by an IT professional.  If you are a classically educated developer, you may not have had experience setting up many of the pre-requisites for OCS.  These pre-requisites include setting up a Domain Controller and DNS Server, installing a domain Certificate Authority (CA) for generated client and server certificates, creating necessary DNS SRV records, and guiding you through the multi-step OCS 2007 R2 setup program.  These tasks are all detailed in Chapter 9, “Preparing the UC Development Environment”.  I highly recommend that all developers whom are new to OCS read this chapter, especially if tasks such as “create and configure the desired user accounts in AD DS” scare you, as they did me.

The book will also help you to be aware of what SDKs exist for OCS 2007 R2 and help you to decide what SDK is best suited for your project.  Note that there are six SDKs to choose from.  The overview for the SDKs is in Chapter 2, “Microsoft Unified Communications APIs Foundation”.  I recommend reading this chapter before starting your first OCS project, and then referring to it every time that you start designing another.

Of course, the book also contains a chapter or chapters dedicated to each of the SDKs, each of which is very helpful.

Finally, the last highlight of the book is Chapter 10, “Debugging a Unified Communications Application”.  This is my second favorite chapter of the book (Chapter 9, “Preparing the UC Development Environment” being my number one favorite).  This chapter describes the various logging tools available with the OCS 2007 R2 SDKs and shows you how to use them.  This chapter also explains the exception hierarchy and gives examples of how to make use of the exception classes in your exception handling code.  I believe that many of the items covered in Chapter 10 are only touched on in the OCS 2007 R2 SDK documentation and may thus be unused by many developers.

How do I get this book, you ask?  It is available from Amazon.com via the following link:

http://www.amazon.com/gp/product/0735626235/ref=s9_sims_gw_s0_p14_i1?pf_rd_m=ATVPDKIKX0DER&pf_rd_s=center-2&pf_rd_r=0KXJAGNNF4X40SZ6XG9P&pf_rd_t=101&pf_rd_p=470938631&pf_rd_i=507846

Disclaimer:

The author of this review currently works with one of the authors, and is acquainted with one of the others.  My enthusiasm for the book, however, is genuine.  I wrote my first one and one-half UCMA 2.0 applications before reading this book, using the MSDN and SDK documentation and samples as my main reference.  Although these were “adequate”, they were not excellent.  I could have saved myself a lot of time and trouble by reading this book first, cover-to-cover, and then referring to the MSDN and SDK documentation purely as a API reference guide (which is all that they are really meant to be).

Have fun coding!

Katy Lynn McCullough-Leonard


June 26, 2009 08:34 by katyle Permalink | Comments (129) | Comment RSS RSS Button Image


One of the samples in the UCMA 2.0 SDK for Office Communication Server 2007 is the "ConferenceEscalation” sample application.  The UCMA 2.0 SDK samples demonstrate how to use the new Microsoft.Rtc.Collaboration name space to easily perform many collaboration activities, without having an in-depth understanding of how OCS uses the SIP protocol.

Let’s take a closer look at the “ConferenceEscalation” sample.  This sample sets up a peer-to-peer audio call between two user endpoints.  Next, the first user endpoint creates an ad-hoc (unscheduled) conference. Then, the first endpoint calls the new Conversation.BeginEscalateToConference() and Conversation.EndEscalateToConference() methods to automatically transfer (escalate) the second endpoint’s conversation to the ad-hoc conference.

Unfortunately, the “ConferenceEscalation” sample does not work as shipped with OCS 2007 R2.  Instead, a Microsoft.Rtc.Signalling.OperationFailureException is thrown by the EndEscalateToConference() method.  The failure reason is that this media provider does not support escalation.  And, if you read the fine print in the UCMACore2.0 help file for OCS 2007 R2, you will indeed find that the Conversation EscalateToConference methods are only supported for the Instant Messaging modality.  Indeed, as an exercise, if you change the AudioVideoCall object to an InstantMessagingCall object, and register to receive incoming InstantMessagingCall events, the sample will work.

The code that I will share with you, however, shows how to successfully transfer an existing audio/video call to a conference call using the UCMA 2.0 SDK.  Accomplishing this involves the following steps:

  1. Create a new conversation.
  2. Create a conference using the new conversation.
  3. Join the conference.
  4. Call into the conference (establish a call with the conference MCU).
  5. Transfer the original P2P call to the conference audio/video MCU.
  6. Terminate the original P2P call, since it is now defunct.

The following code is a modification of the code in the Escalate To Conference sample application from the UCMA 2.0 SDK for OCS 2007 R2.  First, of course, you must download and install the UCMA 2.0 SDK.  Follow the Microsoft instructions for installing carefully.  There are several pre-requisites and the SDK setup itself is a two-part install. 

Note that Chris Mayo’s blog – “Unified Communications Development” at http://blogs.msdn.com/cmayo/ contains lots of information on getting setup to do UCMA 2.0 development. 

Once you have the SDK installed, navigate to the “Program Files\Microsoft Office Communications Server 2007 R2\UCMA SDK 2.0\UCMACore\Sample Applications\Collaboration\QuickStarts\ConferenceEscalation” folder.  Take a look at the original EscalateToConference.cs file.  Build the sample and run it, noting the exception. 

Then, try changing the AudioVideoCall object and related callbacks to be an InstantMessagingCall object.  Run the sample again and note that this succeeds.

Finally, follow the instructions below to modify the code in the EscalateToConference.cs file.  Make sure that you change the server and user settings to match your environment.  Pay particular attention to the Run() and EndJoinConference() methods:

  1. Add the following namespaces to the EscalateToConference.cs file:
    • using System.Collections.Generic;
    • using Microsoft.Rtc.Collaboration.Conferencing;
    • using Microsoft.Rtc.Collaboration.ConferenceManagement;
  2. Add the following global variables before the Main() method:
    • private AutoResetEvent _waitForFlowActive = new AutoResetEvent(false);
    • private AudioVideoCall audioVideoCall;   // original AV call
  3. Replace the existing Run() method with the following:

              public void Run()
              {
                  // A helper class to take care of platform and endpoint setup and cleanup.

                  UCMADemoHelper odh = new UCMADemoHelper();

                  // Prepare and instantiate the platform.
                  _collabPlatform = odh.InitalizePlatform(_applicationName, _transportType);

                  // Create a user endpoint, using the network credential object defined above.
                  UserEndpoint callerEndpoint = odh.InitalizeRegisteredUserEndpoint(_collabPlatform, _callerUserURI, _userServer, _callerCredential);

                  // Create a second user endpoint, using the network credential object defined above.
                  UserEndpoint calleeEndpoint = odh.InitalizeRegisteredUserEndpoint(_collabPlatform, _calleeUserURI, _userServer, _calleeCredential);

                  // Here, we are accepting an Audio Video call only.
                  // If the incoming call is not an Audio Video call (for example, a custom (Foo) Call,
                  // then it will not get raised to the application. UCMA 2.0 handles this silently by having the call types register
                  // for various modalities (as part of the extensibility framework). The appropriate action (accepting the call)
                  // will be handled in the handler assigned to the method call below.
                  calleeEndpoint.RegisterForIncomingCall<AudioVideoCall>(On_AudioVideoCall_Received);

                  // Setup the call and conversation objects for the initial call (IM), and place the call (synchronously).
                  Conversation conversation = new Conversation(callerEndpoint);
                  audioVideoCall = new AudioVideoCall(conversation);
                  audioVideoCall.BeginEstablish(_calleeUserURI , null, EndCallEstablish, audioVideoCall);

                  // Force synchronization to ensure that the call is now complete.
                  _waitForCallAccept.WaitOne();
                  _waitForCallEstablish.WaitOne();

                  Console.WriteLine("");
                  Console.WriteLine(" Beginning conference creation and escalation...");
                  Console.WriteLine("");

                  //
                  // START OF MODIFIED CODE.
                  //

                  // Create a new conversation and call with the Audio Video MCU.
                  Conversation confConversation = new Conversation(callerEndpoint);

                  confConversation.ConferenceSession.StateChanged += new EventHandler<StateChangedEventArgs<ConferenceSessionState>>(ConferenceSession_StateChanged);

                  // Create and join an ad-hoc conference with two participants
                  ConferenceParticipantInformation[] participants = new ConferenceParticipantInformation[2];

                  participants[0] = new ConferenceParticipantInformation(_calleeUserURI, ConferencingRole.Attendee);
                  participants[1] = new ConferenceParticipantInformation(_callerUserURI, ConferencingRole.Leader);

                  // create an ad-hoc conference (added to sample to show more conferencing features)
                  Conference conference = CreateAdHocConference("Ad-hoc Conference", callerEndpoint, McuType.AudioVideo, participants);

                  ConferenceJoinInformation confJoinInfo = new ConferenceJoinInformation(new RealTimeAddress(conference.ConferenceUri));

                  confConversation.ConferenceSession.BeginJoin(confJoinInfo, EndJoinConference, confConversation.ConferenceSession);

                  //Wait for call to be transfered.
                  _waitForConferenceEscalation.WaitOne();

                  // Note that the original peer-to-peer conversation is now defunct.
                  // Go ahead and shut it down.
                  // check the call state on the audioVideoCall object
                  audioVideoCall.EndTerminate(audioVideoCall.BeginTerminate(null, null));

                  // just so don't have to change the rest of the code
                  conversation = confConversation;    

                  Console.WriteLine("");
                  Console.WriteLine(" Beginning conference command and control..." );
                  Console.WriteLine("");

        // END OF MODIFIED CODE

                  // Promote a participant to leader; Leaders can lock and unlock the conference, as well as possessing the ability
                  // to eject and control other participants, and mute participants (in an Audio conference).
                  // For purposes of the demonstration, choose an arbitrary conferene participant, here, the first.

                  ConversationParticipant target = null;

                  if (conversation.RemoteParticipants.Count >= 1)
                  {
                      target = conversation.RemoteParticipants[0];
                  }
                  else
                  {
                      // TODO: Error handling is left to the reader.
                  }

                  Console.WriteLine("User " + target.UserAtHost + " is currently an " + target.Role + ".");

                  // Note: This is the naive synch implementation, and is not generally suitable for production code.

        // It is only used here for brevity.
        conversation.ConferenceSession.EndModifyRole(conversation.ConferenceSession.BeginModifyRole(target, ConferencingRole.Leader, null, null));
                  Console.WriteLine("User " + target.UserAtHost + " is now a " + target.Role + ".");

                  Console.WriteLine("The conference lock's state is currently " + conversation.ConferenceSession.IsLocked + ".");
                  // Locking the conference prevents new users from joining the conference, unless explicitly called into the

      // conference through the dialout API.


                  conversation.ConferenceSession.EndLockConference(conversation.ConferenceSession.BeginLockConference(null,null));
                  Console.WriteLine("The conference lock's state is now " + conversation.ConferenceSession.IsLocked + ".");

                  // On the AudioVideoMCU, a leader can mute remote, or local participants at will.
                  // This line will mute the target's endpoint until Unmuted, or the call is torn down.
                  ParticipantEndpoint targetPE = null;
                  if (conversation.ConferenceSession.AudioVideoMcuSession.GetRemoteParticipantEndpoints()[0] != null)
                  {
                      targetPE = conversation.ConferenceSession.AudioVideoMcuSession.GetRemoteParticipantEndpoints()[0]; //Again, we assume that there is the participant from above present.
                  }
                  else
                  {
                      // TODO: Error handling is left to the reader.
                  }
                  conversation.ConferenceSession.AudioVideoMcuSession.EndMute(conversation.ConferenceSession.AudioVideoMcuSession.BeginMute(targetPE, null, null));

                  // Now, eject the participant, and then shut down the platform.

                  Console.WriteLine("The conference currently has " + conversation.ConferenceSession.GetRemoteParticipantEndpoints().Count + " attendees.");
                  //Ejection can only be performed by leaders of the conference.
                  conversation.ConferenceSession.EndEject(conversation.ConferenceSession.BeginEject(target, null, null));
                  Console.WriteLine("The conference now has " + conversation.ConferenceSession.GetRemoteParticipantEndpoints().Count + " attendees.");

                  Console.WriteLine("");
                  Console.WriteLine("Success!");
                  Console.WriteLine("");

                  Console.WriteLine("Now shutting down the platform...");
                  odh.ShutdownPlatform(_collabPlatform);

                  Console.WriteLine("");
                  Console.WriteLine("Press any key to exit");
                  Console.ReadLine();
              }

              /// <summary>
              /// Added by Unify Square just to show more conferencing features.
              /// </summary>
              /// <param name="conferenceName"></param>
              /// <param name="localEndpoint"></param>
              /// <param name="mcuType"></param>
              /// <param name="participants"></param>
              /// <returns></returns>
              Conference CreateAdHocConference(String conferenceName, LocalEndpoint localEndpoint, String mcuType, IList<ConferenceParticipantInformation> participants)
              {
                  Conference conference = null;

                  // Set up the conference properties
                  ConferenceScheduleInformation conferenceScheduleInformation = new ConferenceScheduleInformation(); 
                  conferenceScheduleInformation.AdmissionPolicy = ConferenceAdmissionPolicy.OpenAuthenticated; 
                  conferenceScheduleInformation.IsPasscodeOptional = true; 
                  conferenceScheduleInformation.Passcode = ConferenceServices.GeneratePasscode();
                  conferenceScheduleInformation.Description = conferenceName; // The verbose description of the conference.
                  conferenceScheduleInformation.ExpiryTime = System.DateTime.Now.AddHours(2); // delete time
                  conferenceScheduleInformation.ConferenceId = ConferenceServices.GenerateConferenceId();

                  // add our participants
                  foreach (ConferenceParticipantInformation participantInfo in participants)
                  {
                      conferenceScheduleInformation.Participants.Add(participantInfo);
                  }

                  // These two lines assign a set of modalities from the available MCUs to the conference.
                  // Custom modalities (and their corresponding MCUs) may be added at this time.
                  ConferenceMcuInformation conferenceMCU = new ConferenceMcuInformation(mcuType);
                  conferenceScheduleInformation.Mcus.Add(conferenceMCU);

                  // Now that the setup object is complete, schedule the conference using the conference services
                  // Note: the conference organizer is considered a leader of the conference by default.
                  conference = localEndpoint.ConferenceServices.EndScheduleConference(localEndpoint.ConferenceServices.BeginScheduleConference(conferenceScheduleInformation, null, null));

                  return conference;
              }

  4. Add the following two callback methods to determine when the new audio/video flow is configured and active.

    // Called when flow configured

    void conferenceAVCall_AudioVideoFlowConfigurationRequested(object sender, AudioVideoFlowConfigurationRequestedEventArgs args)
    {
        // flow is configured
        // wait for the flow state to go to active.
        args.Flow.StateChanged += new EventHandler<MediaFlowStateChangedEventArgs>(Flow_StateChanged);

        _waitForFlowActive.Set();   // go ahead and set now
    }

    // Called when flow state changes

    void Flow_StateChanged(object sender, MediaFlowStateChangedEventArgs args)
    {
        if (args.State == MediaFlowState.Active)
        {
            // flow has gone active
            _waitForFlowActive.Set();
        }
    }

  5. Replace the EndJoinConference() method with this method:

            private void EndJoinConference(IAsyncResult ar)
            {
                ConferenceSession confSession = ar.AsyncState as ConferenceSession;
                try
                {
                   confSession.EndJoin(ar);

                }
                catch (OperationFailureException opFailEx)
                {
                    // OperationFailureException: Indicates failure to connect the call to the remote party.
                    // It is left to the application to perform real error handling here.
                    Console.WriteLine(opFailEx.ToString());
                }
                catch (RealTimeException realTimeEx)
                {
                    // RealTimeException may be thrown on media or link-layer failures, or call rejection (FailureResponseException)
                    // It is left to the application to perform real error handling here.
                    Console.WriteLine(realTimeEx.ToString());
                }


                // Method which is NOT working for audio calls.
    //            confSession.Conversation.BeginEscalateToConference(EndEscalateConference, confSession.Conversation);

                // START OF MODIFIED CODE.

                // Call into the conference ourselves.
                AudioVideoCall conferenceAVCall = new AudioVideoCall(confSession.Conversation);

                // Register for call backs.
                conferenceAVCall.AudioVideoFlowConfigurationRequested += new EventHandler<AudioVideoFlowConfigurationRequestedEventArgs>(conferenceAVCall_AudioVideoFlowConfigurationRequested);
                conferenceAVCall.StateChanged += _call_StateChanged;

                Console.WriteLine("");
                Console.WriteLine("Establishing call with audio/video MCU");

                // Call the audio/video MCU.
                // Do this sync just to reduce lines of code for this sample
                conferenceAVCall.EndEstablish(conferenceAVCall.BeginEstablish(null, null, null));

                // We would wait for the flow to be configured for real code.
                _waitForFlowActive.WaitOne();

                Console.WriteLine("");
                Console.WriteLine("Transfering existing call to Audio Video MCU");

                // We have now joined the conference on this conversation.
                // Add our other endpoint to the conference by transfering
                // their call to the conference audio video MCU.
                // Note that the conference audio video MCU is the third-party
                // that all conference attendees communicate with.
                // The MCU "mixes" the participants speech and multiplexes it to
                // all attendees.
                // Make this synchronous just to reduce sample lines of code.
                confSession.AudioVideoMcuSession.EndTransfer(confSession.AudioVideoMcuSession.BeginTransfer(audioVideoCall, null, null, null));

                // Note that the flow becomes active after we transfer the call

                Console.WriteLine("");
                Console.WriteLine("Call has been successfully transfered");

                // set the event for both the caller and the callee,
                // since the callee is automatically transfered
                _waitForConferenceEscalation.Set();

                // END OF MODIFIED CODE.
            }

 

Have fun!

Katy Lynn McCullough-Leonard

 


May 12, 2009 09:30 by Katyle Permalink | Comments (20) | Comment RSS RSS Button Image


Privacy  |  Contact  |  Terms of Use  |  www.unifysquare.com | Copyright © 2009 Unify2  -  All rights reserved.
Microsoft and the Microsoft Logos are trademarks of Microsoft, Inc. Unify2 is a trademark of Unify Square, Inc.