Click here to Skip to main content
Click here to Skip to main content

Peer Collaboration - Inviting

By , 1 May 2006
Rate this:
Please Sign up or sign in to vote.

Background

This article demonstrates inviting People Near Me using Microsoft's new peer-to-peer Collaboration technology in Windows Vista. It shows how you can detect other users signed in to People Near Me that have the sample application installed, and how you can invite that person to start the sample application.

Introduction

Microsoft's entire Peer-to-Peer technology is exposed through the latest Platform SDK as C/C++ API calls. However, the code in this article shows these APIs being used from .NET managed code using C#. The sample application adds several new classes which handle enumerating remotely registered applications, plus creation and sending invitations. While the classes hide the details of using Microsoft's peer-to-peer collaboration APIs (in order to simplify the programming model), the flow of unmanaged calls is outlined below.

Retrieving Information about People Near Me

The code since the previous article has been updated to distinguish a basic endpoint from an endpoint returned by People Near Me.

Basic EndPoint

The PeerEndPoint class represents a basic endpoint, and wraps the PEER_ENDPOINT data structure provided by the underlying API. A basic endpoint associates a friendly name with an IPv6 address and port number.

public class PeerEndPoint
{
  internal PEER_ENDPOINT data;

  internal PeerEndPoint(PEER_ENDPOINT info) 
  { 
    data = info;
  }

  public string Name
  {
    get { return data.pwzEndpointName; }
  }

  public IPEndPoint Address
  {
    get
    {
       return new IPEndPoint(new IPAddress(data.address.sin6.sin6_addr, 
              data.address.sin6.sin6_scope_id), 
              data.address.sin6.sin6_port);
    }
  }
}

People Near Me EndPoint

A remote peer signed into People Near Me publishes information via properties of the PeerCollab class. Other remote peers can use a new PeerPNMEndPoint class to monitor these changes.

Let's suppose Homer is signed into People Near Me and Bart wants to monitor him. The new PeerPNMEndPoint class includes functions that allow Bart to refresh Homer's endpoint information, show his presence, show the applications he has registered, and invite him to collaborate.

Refreshing an EndPoint

For some reason, Microsoft chose to use a polling scheme to monitor changes in People Near Me endpoints. In order to see the latest information about a remote peer signed into People Near Me, you must call the Refresh method of a PNM endpoint first. The Refresh method wraps the underlying PeerCollabRefreshEndpointData API which initiates the request and waits.

public void Refresh()
{
  PeerCollab.EndpointRefreshRequests.Add(this);
  uint hr = 
    PeerCollabNative.PeerCollabRefreshEndpointData(ref data);
  if (hr == 0)
  {
    RefreshEvent.WaitOne();
    hr = hresult;
    hresult = 0;
  }
  PeerCollab.EndpointRefreshRequests.Remove(this);
  if (hr != 0) throw new PeerCollabException(hr);
}

Once the new endpoint data is received, a PEER_EVENT_REQUEST_STATUS_CHANGED event fires. Using the basic endpoint in the event, the PeerPNMEndPoint class of the original caller is matched and notified of the event.

internal static List<PeerPNMEndPoint> 
         EndpointRefreshRequests = 
         new List<PeerPNMEndPoint>();

private void HandleEventRequestStatusChanged(IntPtr evptr)
{
  PEER_EVENT_REQUEST_STATUS_CHANGED_DATA data = 
    (PEER_EVENT_REQUEST_STATUS_CHANGED_DATA)
    Marshal.PtrToStructure(evptr, 
    typeof(PEER_EVENT_REQUEST_STATUS_CHANGED_DATA));
  PeerPNMEndPoint endpoint = new PeerPNMEndPoint(
     (PEER_ENDPOINT)Marshal.PtrToStructure(
     data.pEndpoint, typeof(PEER_ENDPOINT)));

  foreach (PeerPNMEndPoint item in EndpointRefreshRequests)
  {
    if (item.Address.ToString() == endpoint.Address.ToString())
    {
        item.RefreshCompleted(data.hrChange);
        break;
    }
  }
}

The internal RefreshCompleted function handles the event by saving the HRESULT of the request and notifying the waiting thread. If the waiting thread sees a non-zero HRESULT, an exception is thrown.

internal void RefreshCompleted(uint hresult)
{
  this.hresult = hresult;
  RefreshEvent.Set();
}

Presence

The Presence property wraps the underlying PeerCollabGetPresenceInfo API.

public PeerPresence Presence
{
  get
  {
    IntPtr ptr;
    uint hr = PeerCollabNative.PeerCollabGetPresenceInfo(ref data, out ptr);
    if (hr != 0) throw new PeerCollabException(hr);

    PeerPresence presence = new PeerPresence(
      (PEER_PRESENCE_INFO)Marshal.PtrToStructure(ptr, 
      typeof(PEER_PRESENCE_INFO)));
    PeerCollabNative.PeerFreeData(ptr);

    return presence;
  }
}

Presence indicates the status of the endpoint and a message. The PeerPresense class wraps the underlying PEER_PRESENCE_INFO data structure.

public class PeerPresence
{
  internal PEER_PRESENCE_INFO data;

  internal PeerPresence()
  {
    data = new PEER_PRESENCE_INFO();
    data.status = PeerPresenceStatus.Offline;
    data.pwzDescriptiveText = @"Offline";
  }

  internal PeerPresence(PEER_PRESENCE_INFO info)
  {
    data = info;
  }

  public PeerPresenceStatus Status
  {
    get { return data.status; }
    set { data.status = value; }
  }

  public string Description
  {
    get { return data.pwzDescriptiveText; }
    set { data.pwzDescriptiveText = value; }
  }
}

The following list shows the Presence states, and is consistent with those used by the existing Instant Messenger type applications:

Offline The user is current unavailable.
OutToLunch The user is currently "out to lunch" and unable to respond.
Away The user is away and unable to respond.
BeRightBack The user has stepped away from the application and will participate soon.
Idle The user is idling.
Busy The user is busy and does not wish to be disturbed.
OnThePhone The user is currently on the phone and does not wish to be disturbed.
Online The user is actively participating in the peer collaboration network.

Applications

The Applications property of PeerPNMEndPoint returns a collection of the application's registered by the remote peer that has a scope of NearMe or All.

public PeerApplicationCollection Applications
{
  get { return new PeerApplicationCollection(data); }
}

The collection implements the IEnumerable interface. The Reset method of this interface calls the underlying PeerCollabEnumApplications API to retrieve the list of applications registered at the remote peer's endpoint. The PEER_APPLICATION data structure returned during the enumeration is wrapped by the PeerApplication class.

public class PeerApplication
{
  internal PEER_APPLICATION data;

  internal PeerApplication(PEER_APPLICATION info)
  {
    data = info;
  }

  public Guid ID
  {
    get { return data.id; }
  }

  public string Description
  {
    get { return data.pwzDescription; }
  }
}

Inviting

An invitation indicates a short message from the sender plus the application that the sender wants the remote peer to launch. The underlying PEER_INVITATION data structure is wrapped by the PeerInvitation class. An instance of this class is passed when calling the invite methods.

public class PeerInvitation
{
  internal PEER_INVITATION data;

  public PeerInvitation(PeerApplication Application, string Message) 
  {
    data = new PEER_INVITATION();
    data.applicationId = Application.ID;
    data.pwzMessage = Message;
  }

  internal PeerInvitation(PEER_INVITATION info)
  {
    data = info;
  }

  public Guid ApplicationId
  {
    get { return data.applicationId; }
  }

  public string Message
  {
    get { return data.pwzMessage; }
  }
}

The Invite method of the PeerPNMEndPoint class sends a synchronous invitation to a remote peer. It wraps the underlying PeerCollabInviteEndpoint API.

public PeerInvitationResponse Invite(PeerInvitation Invite)
{
  IntPtr ptr;
  uint hr = PeerCollabNative.PeerCollabInviteEndpoint(ref data, 
            ref Invite.data, out ptr);
  //hr = PEER_E_CANNOT_CONNECT - other user 
  //is not accepting invitations
  if (hr != 0) throw new PeerCollabException(hr);

  PeerInvitationResponse response = new PeerInvitationResponse(
     (PEER_INVITATION_RESPONSE)Marshal.PtrToStructure(ptr, 
     typeof(PEER_INVITATION_RESPONSE)));
  PeerCollabNative.PeerFreeData(ptr);

  return response;
}

Note that the function does not return until the remote peer accepts or declines the invitation. If the remote peer dismisses the invitation, it may be several minutes before this function times out and returns. To avoid this, a SendInvite method is provided which calls the underlying PeerCollabAsyncInviteEndpoint API to send the invitation asynchronously.

internal void Invite(PeerInvitation Invite)
{
    uint hr = PeerCollabNative.PeerCollabAsyncInviteEndpoint(
              ref endpoint.data, 
              ref Invite.data, 
              sendEvent.SafeWaitHandle.DangerousGetHandle(), 
              out hInvitation);
    //hr = PEER_E_CANNOT_CONNECT - other user 
    //is not accepting invitations
    if (hr != 0) throw new PeerCollabException(hr);
}

When the remote peer accepts or declines the invitation, a worker thread is started which calls the underlying PeerCollabGetInvitationResponse API to retrieve the response.

privaterInviteWorker(object xdata, bool timedOut)
{
  IntPtr ptr;
  uint hr = 
   PeerCollabNative.PeerCollabGetInvitationResponse(hInvitation, 
   out ptr);

  PeerInvitationResponse response;
  if (ptr == IntPtr.Zero)
    response = new PeerInvitationResponse();
  else
    response = new PeerInvitationResponse(
               (PEER_INVITATION_RESPONSE)
               Marshal.PtrToStructure(ptr, 
               typeof(PEER_INVITATION_RESPONSE)));

  PeerInviteResponseEventArgs args = new 
       PeerInviteResponseEventArgs(response);
  PeerCollabNative.PeerFreeData(ptr);

  endpoint.RaiseInviteResponse(args);
  Close();
}

This response is passed to an internal method to raise a InviteResponse event on the original PeerPNMEndPoint class that sent the invitation.

public delegate void ResponseHandler(object sender, 
       PeerInviteResponseEventArgs e);
public event ResponseHandler InviteResponse;

internal void RaiseInviteResponse(PeerInviteResponseEventArgs args)
{
  if (InviteResponse != null)   InviteResponse(this, args);
}

Note that if the remote peer does not allow people to send invitations, a PEER_E_CANNOT_CONNECT exception is immediately raised for either invite methods.

Regardless of whether the invitation was synchronous or asynchronous, the PEER_INVITATION_RESPONSE data structure that is returned is wrapped by the PeerInvitationResponse class.

public class PeerInvitationResponse
{
  private PEER_INVITATION_RESPONSE response;

  internal PeerInvitationResponse()
  {
    response = new PEER_INVITATION_RESPONSE();
    response.hrExtendedInfo = PeerCollabNative.PEER_E_CANNOT_CONNECT;
    response.action = PeerInvitationResponseAction.Error;
  }

  internal PeerInvitationResponse(PEER_INVITATION_RESPONSE Response)
  {
    response = Response;
    if (response.hrExtendedInfo != 0 && response.pwzMessage == null)
    {
      PeerCollabException ex = new 
         PeerCollabException(response.hrExtendedInfo);
      response.pwzMessage = ex.Message;
    }
  }

  public PeerInvitationResponseAction Action
  {
    get { return response.action; }
  }

  public string Message
  {
    get { return response.pwzMessage; }
  }

  public string ExtendedInfo // HRESULT
  {
    get { return Microsoft.VisualBasic.Conversion.Hex(
                           response.hrExtendedInfo); }
  }
}

To make the response more useful, the wrapper class converts any error code to its string form and stores it in a message. Also, the HRESULT is returned as a hex string to make it easier to report and lookup.

Handling an Invitation

When an invitation is received, Windows Vista pops up a window displaying the invitation. If the user account is idle, or notifications are turned off, the window appears in the task bar.

Otherwise, if notifications are enabled or the user is active, the notification appears as toast in the bottom, right corner.

When an application is launched as a result of accepting an invitation, use the LaunchInfo property of the PeerCollab class to retrieve information about the remote peer who sent the invitation.

public PeerLaunchInfo LaunchInfo
{
  get
  {
      IntPtr ptr;
      uint hr = PeerCollabNative.PeerCollabGetAppLaunchInfo(out ptr);
      if (hr == 0x80070490) return null;
      if (hr != 0) throw new PeerCollabException(hr);

      PeerLaunchInfo info = new PeerLaunchInfo(
         (PEER_APP_LAUNCH_INFO)Marshal.PtrToStructure(ptr, 
         typeof(PEER_APP_LAUNCH_INFO)));
      PeerCollabNative.PeerFreeData(ptr);
      return info;
    }
}

The underlying PEER_LAUNCH_INFO data structure is wrapped by the PeerLaunchInfo class.

public class PeerLaunchInfo
{
  private PeerEndPoint endpoint;
  private PeerInvitation invitation;

  internal PeerLaunchInfo(PEER_APP_LAUNCH_INFO info)
  {
    endpoint = new PeerEndPoint((PEER_ENDPOINT)
               Marshal.PtrToStructure(info.pEndpoint, 
               typeof(PEER_ENDPOINT)));
    invitation = new PeerInvitation((PEER_INVITATION)
                 Marshal.PtrToStructure(info.pInvitation, 
                 typeof(PEER_INVITATION)));
  }

  public PeerEndPoint Endpoint
  {
    get { return endpoint; }
  }

  public PeerInvitation Invitation
  {
    get { return invitation; }
  }
}

This class includes properties about the details of the original invitation and the endpoint of the remote peer application that sent the invitation.

Using the Sample Application

The folder containing the software contains a command line tool called register.exe that allows an application to be registered with peer collaboration. Use the run.bat batch file to register the invite sample. The application.exe file is a visual registration tool, and is taken from my previous article about registering applications. Finally, sample.exe allows you to interactively see People Near Me and issue invitations. All of the software requires Windows Vista to run.

Start sample.exe to see a window similar to the image at the beginning of the article. The first tab allows you to interact with other remote peers signed into People Near Me. The top-left list box shows the names of remote peers signed into People Near Me. Click a name to refresh the endpoint and show the list of applications registered with a scope of NearMe or All by the remote peer.

pnm.EndPoint.Refresh();
foreach (PeerApplication app in pnm.EndPoint.Applications)
{
  listBox2.Items.Add(app);
}

Click an application in the top-right list box to show its properties in the property grid below.

PeerApplication app = (PeerApplication)listBox2.SelectedItem;
propertyGrid1.SelectedObject = app;

Before issuing an invitation, enter a short message in the Message text box. Click the Invite (Wait) button to send an invitation and wait for the response. The sample application will freeze until the remote peer responds.

PeerPeopleNearMe pnm = (PeerPeopleNearMe)listBox1.SelectedItem;
PeerApplication app = (PeerApplication)listBox2.SelectedItem;

PeerInvitation invitation = new PeerInvitation(app, textBox1.Text);
try
{
  PeerInvitationResponse response = pnm.EndPoint.Invite(invitation);
  propertyGrid2.SelectedObject = response;
  LogMessage("Invite", "Completed");
}
catch (Exception ex)
{
  LogMessage("Invite", ex.Message);
}

Click the Invite button to send an invitation asynchronously.

PeerPeopleNearMe pnm = (PeerPeopleNearMe)listBox1.SelectedItem;
PeerApplication app = (PeerApplication)listBox2.SelectedItem;

PeerInvitation invitation = new PeerInvitation(app, textBox1.Text);
try
{
  propertyGrid2.SelectedObject = null;
  pnm.EndPoint.InviteResponse += new 
    PeerPNMEndPoint.ResponseHandler(EndPoint_InviteResponse);
  pnm.EndPoint.SendInvite(invitation);
  LogMessage("Invite", "Completed");
}
catch (Exception ex)
{
  LogMessage("Invite", ex.Message);
}

The InviteResponse event will fire when a response is given or the request times out.

void EndPoint_InviteResponse(object sender, PeerInviteResponseEventArgs e)
{
  LogMessage("Invite", e.Response.Message);
  propertyGrid2.SelectedObject = e.Response;
}

At any point before the remote peer responds, you can click the Cancel button to cancel the request. Canceling the request removes the invitation message from the remote peer's desktop.

PeerPeopleNearMe pnm = (PeerPeopleNearMe)listBox1.SelectedItem;
try
{
  pnm.EndPoint.CancelInvite();
  LogMessage("Cancel", "Completed");
}
catch (Exception ex)
{
  LogMessage("Cancel", ex.Message);
}

The response to an invitation is shown in the bottom property grid. The list box at the bottom of the window shows diagnostic messages. Double-click this list to clear it.

If the sample application is launched as a result of accepting an invitation, the second Launch Info tab shows the details of the original invitation and the endpoint of the application that sent the invitation. The sample application was registered with a "/e" command-line option to indicate when it is launched via an invitation.

PeerLaunchInfo info = collab.LaunchInfo;
if (info != null)
{
  propertyGrid3.SelectedObject = info.Invitation;
  propertyGrid4.SelectedObject = info.Endpoint;
}

A future article will show how these details can be used to initiate collaboration.

Point of Interest

The API documentation for the February CTP of Windows Vista is lacking a lot of information about the error codes returned by the various APIs and how the APIs should be used together. Hopefully, this will be improved before the final SDK is released.

Links to Resources

I have found the following resources to be very useful in understanding peer-to-peer collaboration:

Conclusion

I'll be writing more articles to describe the remaining features of Peer-to-Peer Collaboration in the coming weeks and months, so stay tuned. If you have suggestions for other topics, please leave a comment. Don't forget to vote.

History

  • Initial revision.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here

About the Author

Adrian_Moore
Web Developer
Canada Canada
Adrian Moore is the Development Manager for the SCADA Vision system developed by ABB Inc in Calgary, Alberta.
 
He has been interested in compilers, parsers, real-time database systems and peer-to-peer solutions since the early 90's. In his spare time, he is currently working on a SQL parser for querying .NET DataSets (http://www.queryadataset.com).
 
Adrian is a Microsoft MVP for Windows Networking.

Comments and Discussions

 
-- There are no messages in this forum --
| Advertise | Privacy | Mobile
Web01 | 2.8.140415.2 | Last Updated 1 May 2006
Article Copyright 2006 by Adrian_Moore
Everything else Copyright © CodeProject, 1999-2014
Terms of Use
Layout: fixed | fluid