Click here to Skip to main content
15,887,477 members
Articles / Programming Languages / C#
Article

Peer Collaboration - Contacts

Rate me:
Please Sign up or sign in to vote.
4.00/5 (2 votes)
24 May 20069 min read 23.4K   246   20  
Contact Management using Microsoft's Peer-to-Peer Collaboration technology in Windows Vista.

Background

This article demonstrates contact management using Microsoft's new Peer-to-Peer Collaboration technology in Windows Vista. It shows how you can add People Near Me as contacts, import and export contact data to a file, invite contacts to collaboration, and show the current user's contact and presence information.

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#. 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.

PeerContactCollection

A new Contacts property has been added to the PeerCollab class which returns a collection of the contacts previously added or imported by the current Windows user account (peer).

C#
public PeerContactCollection Contacts
{
  get { return new PeerContactCollection(); }
}

The collection implements the IEnumerable interface. The Reset method of this interface calls the underlying PeerCollabEnumContacts API to retrieve the list of contacts. The PEER_CONTACT data structure returned during the enumeration is wrapped by the PeerContact class.

Importing a Contact

The PeerContactCollection class provides a static (Shared) Import method to import contact information stored in a file. This method reads the XML fragment and calls the Add method which uses the underlying PeerCollabAddContact API to add the contact.

C#
public static PeerContact Add(string Xml)
{
  IntPtr ptr;
  uint hr = PeerCollabNative.PeerCollabAddContact(Xml, out ptr);
  if (hr != 0) throw new PeerCollabException(hr);

  PeerContact contact = new PeerContact((PEER_CONTACT)
     Marshal.PtrToStructure(ptr, typeof(PEER_CONTACT)));
  PeerCollabNative.PeerFreeData(ptr);

  contact.Watch = true;
  return contact;
}

public static PeerContact Import(string Path)
{
  if (Path == null || Path == string.Empty)
      throw new ArgumentException("Invalid argument", "Path");

  string xml;
  using (StreamReader sr = new StreamReader(Path))
  {
    xml = sr.ReadToEnd();
  }

  return Add(xml);
}

Notice how the Add method automatically sets Watch to true to begin monitoring the contact's presence.

Deleting a Contact

The PeerContactCollection includes two Delete methods to delete a contact. The first method deletes the current contact in the enumeration. The second method deletes a given contact. In both cases, the underlying PeerCollabDeleteContact API is called.

C#
public void Delete()
{
  PeerContact contact = (PeerContact)contacts.Current;
  PeerContactCollection.Delete(contact);
}

public static void Delete(PeerContact Contact)
{
  uint hr = PeerCollabNative.PeerCollabDeleteContact(
                             Contact.data.pwzPeerName);
  if (hr != 0) throw new PeerCollabException(hr);
}

PeerContact

The PeerContact class represents specific information about a contact including its unique peer name, nick name, display name, and e-mail address. It also includes a boolean flag to indicate whether changes to this contact should be monitored. The other interesting property of a contact is WatcherPermission. When set to Allowed, it allows my presence information to be sent to the contact. When Blocked, the other contact is blocked from seeing my presence. Setting any of the contact properties will update the contact information locally. This allows you to rename details of a contact as they change, or make it easier to distinguish a contact in a large list. None of these changes are sent back to the original contact.

C#
public class PeerContact
{
  internal PEER_CONTACT data;
  private PeerEndPointCollection endpoints;

  internal PeerContact(PEER_CONTACT contact)
  {
      data = contact;
      data.credentials.cbData = 0;
      data.credentials.pbData = IntPtr.Zero;
  }

  public string PeerName
  {
    get { return data.pwzPeerName; }
  }

  public string NickName
  {
    get { return data.pwzNickName; }
    set { data.pwzNickName = value; Update(); }
  }

  public string DisplayName
  {
    get { return data.pwzDisplayName; }
    set { data.pwzDisplayName = value; Update(); }
  }

  public string EmailAddress
  {
    get { return data.pwzEmailAddress; }
    set { data.pwzEmailAddress = value; Update(); }
  }

  public bool Watch
  {
    get { return data.fWatch; }
    set { data.fWatch = value; Update(); }
  }

  public PeerWatchPermission WatcherPermission
  {
    get { return data.WatcherPermissions; }
    set { data.WatcherPermissions = value; Update(); }
  }
}

Exporting a Contact

The PeerContact class includes an Export method to export the details of the current contact to a file. The underlying PeerCollabExportContact API is used to generate an XML fragment that is stored in the file.

C#
public string GetXML()
{
  string xml;
  uint hr = 
   PeerCollabNative.PeerCollabExportContact(data.pwzPeerName, 
   out xml);
  if (hr != 0) throw new PeerCollabException(hr);

  return xml;
}

public void Export(string Path)
{   
  if (Path == null || Path == string.Empty)
      throw new ArgumentException("Invalid argument", "Path");

  using (StreamWriter sw = new StreamWriter(Path))
  {
      sw.Write(GetXML());
  }
}

The GetXML method allows the raw XML encoded details of the contact to be retrieved. This is useful when an application builder wants to exchange contact information without using a file. The details and encrypted credentials of the contact are encoded into an XML string as shown next:

XML
<CONTACTINFO>
  <PeerName>8c2f560d9ac6059b26748275d8f1caca86ced091</PeerName>
  <NickName>Adrian</NickName>
  <DisplayName>Adrian Moore</DisplayName>
  <EmailAddress>test@test.com</EmailAddress>
  <Credentials>
    MIIC4QYJKoZIhvcNAQcCoIIC0jCCAs4CAQExADALBgkqhkiG9w0BBwGgggK2MIIC
    sjCCAhugAwIBAgIQsk2lEKspw7xEiB+Fk0LKUjANBgkqhkiG9w0BAQUFADAZMRcw
    ...
  </Credentials>
</CONTACTINFO>

Contact EndPoints

The remaining property of the PeerContact class is EndPoints. This property returns a collection of the active endpoints associated with the contact. Of course, if the current user is not in the remote contact's list or is blocked by the remote contact, no endpoints will be returned.

C#
public PeerEndPointCollection Endpoints
{
  get { return new PeerEndPointCollection(this); }
}

The collection implements the IEnumerable interface. The Reset method of this interface calls the underlying PeerCollabEnumEndpoints API to retrieve the list of endpoints. The PEER_ENDPOINT data structure returned during the enumeration is passed, along with the contact, to the PeerContactEndPoint class.

C#
public class PeerContactEndPoint : PeerEndPoint
{
  private PeerContact contact;

  internal PeerContactEndPoint(PeerContact Contact, PEER_ENDPOINT info)
      : base(info)
  {
    contact = Contact;
  }

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

The PeerContactEndPoint class includes a property to return the list of registered applications associated with the endpoint. This class also includes Invite and SendInvite methods to send an invitation to the contact to start a registered application to collaborate. The Invite method uses the underlying PeerCollabInviteContact API to send an invitation and wait for a response. To avoid waiting, use the SendInvite method to asynchronously send an invitation and receive the response via an event.

C#
public PeerInvitationResponse Invite(PeerInvitation Invite)
{
  IntPtr ptr;
  uint hr = PeerCollabNative.PeerCollabInviteContact(
            ref contact.data, 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;
}

public void SendInvite(PeerInvitation Invitation)
{
  if (worker == null) worker = new InviteWorker(this);
  CancelInvite();
  worker.Invite(Invitation);
}

The worker class used in my previous article for inviting People Near Me has been updated to handle asynchronously sending invitations to contacts by using the underlying PeerCollabAsyncInviteContact API.

C#
internal void Invite(PeerInvitation Invite)
{
  uint hr;
  if (contact == null)
    hr = PeerCollabNative.PeerCollabAsyncInviteEndpoint(ref endpoint.data, 
         ref Invite.data, 
         sendEvent.SafeWaitHandle.DangerousGetHandle(), 
         out hInvitation);
  else if (endpoint == null)
  // send to all endpoints of the contact
    hr = PeerCollabNative.PeerCollabAsyncInviteContact(ref contact.data, 
         IntPtr.Zero, ref Invite.data, 
         sendEvent.SafeWaitHandle.DangerousGetHandle(), 
         out hInvitation);
  else
    hr = PeerCollabNative.PeerCollabAsyncInviteContact(ref contact.data, 
         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);
}

Other New PeerCollab Properties

A few other properties have been added to the PeerCollab class in order to expose several remaining features of the collaboration APIs.

The contact information for the current Windows user account can be retrieved using the MeContact property. Indirectly, this property uses the underlying PeerCollabGetContact API to retrieve the contact details for the current user.

C#
public PeerContact MeContact
{
  get 
  {
    PeerContact contact = GetContact(@"Me");
    if (contact.Watch == false)
    {
      contact.Watch = true;
      contact.WatcherPermission = PeerWatchPermission.Allowed;
      contact.Update();
    }

    return contact;
  }
}

public PeerContact GetContact(string PeerName)
{
  uint hr;
  IntPtr ptr;

  if (PeerName == null || PeerName == 
             string.Empty || PeerName.ToUpper() == "ME")
      hr = PeerCollabNative.PeerCollabGetContact(IntPtr.Zero, out ptr);
  else
      hr = PeerCollabNative.PeerCollabGetContact(PeerName, out ptr);

  if (hr != 0) throw new PeerCollabException(hr);

  PeerContact contact = new PeerContact(
    (PEER_CONTACT)Marshal.PtrToStructure(ptr, typeof(PEER_CONTACT)));
  PeerCollabNative.PeerFreeData(ptr);

  return contact;
}

The friendly endpoint name for the current user can be accessed using the EndPointName property. This property wraps the underlying PeerCollabGetEndpointName and PeerCollabSetEndpointName APIs. Any endpoint name would typically indicate the device you are currently using (PC, LAPTOP, PDA, etc.), and is used by other contacts to locate your current presence.

The presence information for the current user can be accessed using the Presence property. This property returns a PeerPresence class which wraps the underlying PEER_PRESENSE_INFO data structure.

C#
private PeerPresence presence = new PeerPresence();

public PeerPresence Presence
{
  get
  {
    PeerSignInOption current = Status;
    if (current == PeerSignInOption.NearMe || 
            current == PeerSignInOption.All)
        presence.Refresh();
    return presence;
  }
}

The PeerPresence class includes two properties that indicate the current user's status (Online, OutToLunch, etc.) and a short message to provide further details of their status.

C#
public class PeerPresence
{
    internal PEER_PRESENCE_INFO data;

    internal PeerPresence()
    {
        data = new PEER_PRESENCE_INFO();
        data.status = PeerPresenceStatus.Idle;
        data.pwzDescriptiveText = string.Empty;
    }

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

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

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

    internal void Update()
    {
        uint hr = PeerCollabNative.PeerCollabSetPresenceInfo(ref data);
        if (hr != 0) throw new PeerCollabException(hr);
    }

    internal void Refresh()
    {
        IntPtr ptr;
        uint hr = PeerCollabNative.PeerCollabGetPresenceInfo(IntPtr.Zero, out ptr);
        if (hr != 0) throw new PeerCollabException(hr);

        data = (PEER_PRESENCE_INFO)Marshal.PtrToStructure(ptr, 
                                    typeof(PEER_PRESENCE_INFO));
        PeerCollabNative.PeerFreeData(ptr);
    }
}

To refresh the details of the MeContact, the internal Refresh method is used which calls the underlying PeerCollabGetPresenceInfo API. Any changes to the status or description of the current user's presence is sent to any contact's watching using the PeerCollabSetPresenceInfo API.

New PeerCollab Events

The PeerCollab class has three new events which fire when contact, presence, or application information changes.

ContactChanged

The underlying PEER_EVENT_ENDPOINT_CHANGED and PEER_EVENT_MY_ENDPOINT_CHANGED notifications produce a PEER_EVENT_ENDPOINT_CHANGED_DATA data structure which is decoded and results in the new ContactChanged event. The ContactChanged event fires when details about a contact are changed or the current user's contact list is updated. Three types of changes can occur:

  • Added occurs when a contact is added to the current user's contact list.
  • Updated occurs when contact information is updated locally, either for the current user or a remote contact (name, nick name, or e-mail address etc.).
  • Deleted occurs when a contact is deleted from the current user's contact list.

PresenceChanged

The underlying PEER_EVENT_ENDPOINT_PRESENCE_CHANGED and PEER_EVENT_MY_PRESENCE_CHANGED notifications produce a PEER_EVENT_PRESENCE_CHANGED_DATA data structure which is decoded and results in the new PresenceChanged event. The PresenceChanged event fires when the presence of the current user or a remote contact being monitored changes. Three types of changes can occur:

  • Added occurs when endpoint information is requested for a contact by the current user since first signing into peer collaboration.
  • Updated occurs when presence status is updated for the current user or a remote contact (Away, Online, OnThePhone, etc.).
  • Deleted occurs when the current user or a remote contact signs out of peer collaboration.

ApplicationChanged

The underlying PEER_EVENT_ENDPOINT_APPLICATION_CHANGED and PEER_EVENT_MY_APPLICATION_CHANGED notifications produce a PEER_EVENT_APPLICATION_CHANGED_DATA data structure which is decoded and results in a new ApplicationChanged event. The ApplicationChanged event fires when the applications are registered, unregistered, or updated. Three types of changes can occur:

  • Added occurs when an application is registered by the current user or a remote contact.
  • Updated occurs when the details of a registered application are updated by the current user or a remote contact.
  • Deleted occurs when the an application is unregistered by the current user or a remote contact.

Using the Sample Application

The sample application requires Windows Vista to run. It is recommended to create a few user accounts and use the Switch User feature to run multiple copies of the application. Also, is recommended to unzip the sample application into a location that all users can access, such as c:\users\public.

The People Near Me tab allows you to see users on the same subnet that are signed into peer collaboration.

Select a user and click the Add To Contacts button, to add the person to your local contacts list. The button will be dimmed if the user is already added to your contact list.

C#
if (listBox1.SelectedIndex == -1) return;
PeerPeopleNearMe pnm = (PeerPeopleNearMe)listBox1.SelectedItem;

try
{
    PeerContact contact = pnm.EndPoint.AddToMyContacts();
    listBox2.Items.Add(contact);
    button1.Enabled = false;
    LogMessage(@"Contact Add", "Completed");
}
catch (PeerCollabException ex)
{
    LogMessage(@"Contact Add", ex.Message);
}

The Contacts tab allows you to manage your list of contacts.

The top-left list box shows your current list of contacts. Click a name to show the current list of endpoints for the contact (typically 1). The property grid in the General sub-tab shows the current properties for the contact.

C#
if (listBox2.SelectedIndex == -1) return;
PeerContact contact = (PeerContact)listBox2.SelectedItem;
propertyGrid3.SelectedObject = contact;

foreach (PeerContactEndPoint endpoint in contact.Endpoints)
{
    listBox3.Items.Add(endpoint);
}

In the General sub-tab, click the Delete button to delete the currently selected contact.

C#
if (listBox2.SelectedIndex == -1) return;
PeerContact contact = (PeerContact)listBox2.SelectedItem;
PeerContactCollection.Delete(contact);

Clicking the Export button displays the standard dialog to select the location of a file to store the contact information. Use the c:\users\public folder to easily share contact information between users on the same computer.

C#
if (listBox2.SelectedIndex == -1) return;
PeerContact contact = (PeerContact)listBox2.SelectedItem;

DialogResult result = saveFileDialog1.ShowDialog();
if (result == DialogResult.OK)
{
    try
    {
        contact.Export(saveFileDialog1.FileName);
        LogMessage("Export", "Completed");
    }
    catch (Exception ex)
    {
        LogMessage("Export", ex.Message);
    }
}

Clicking the Import button displays the standard dialog to select a previously exported contact file to import.

C#
DialogResult result = openFileDialog1.ShowDialog();
if (result == DialogResult.OK)
{
    try
    {
        PeerContactCollection.Import(openFileDialog1.FileName);
        RefreshContacts();
        LogMessage("Import", "Completed");
    }
    catch (Exception ex)
    {
        LogMessage("Import", ex.Message);
    }
}

Click an endpoint in the top-middle list box to show the current list of registered applications:

C#
if (listBox3.SelectedIndex == -1) return;
PeerContactEndPoint endpoint = (PeerContactEndPoint)listBox3.SelectedItem;

foreach (PeerApplication app in endpoint.Applications)
{
    listBox5.Items.Add(app);
}

On the Invite sub-tab, enter a short message to send to the contact you are going to invite.

Click the Invite (Wait) button to send an invitation and wait for the response. The sample application will freeze until the remote contact responds.

C#
if (listBox3.SelectedIndex == -1 || listBox5.SelectedIndex == -1)
    return;

PeerContactEndPoint endpoint = (PeerContactEndPoint)listBox3.SelectedItem;
PeerApplication app = (PeerApplication)listBox5.SelectedItem;

PeerInvitation invitation = new PeerInvitation(app, textBox1.Text);
try
{
    propertyGrid5.SelectedObject = null;
    SetResponse(endpoint.Invite(invitation));
}
catch (Exception ex)
{
    LogMessage("Invite", ex.Message);
}

Click the Invite button to send an invitation asynchronously.

C#
if (listBox3.SelectedIndex == -1 || listBox5.SelectedIndex == -1)
    return;

PeerContactEndPoint endpoint = (PeerContactEndPoint)listBox3.SelectedItem;
PeerApplication app = (PeerApplication)listBox5.SelectedItem;

PeerInvitation invitation = new PeerInvitation(app, textBox1.Text);
try
{
    propertyGrid5.SelectedObject = null;
    endpoint.InviteResponse += new 
        PeerEndPoint.ResponseHandler(endpoint_InviteResponse);
    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.

C#
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.

C#
PeerContactEndPoint endpoint = 
        (PeerContactEndPoint)listBox3.SelectedItem;
try
{
    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 Me tab shows the details about the "Me" contact.

Clicking the Export button displays the standard dialog to select the location of a file to store the contact information.

C#
PeerContact contact = (PeerContact)propertyGrid1.SelectedObject;
contact.Export(saveFileDialog1.FileName);
LogMessage("Export", "Completed");

The Presence tab show details about the current user's presence.

Change the properties to update the user's presence information and signed-in status.

Finally, the list box at the bottom of the window shows diagnostic messages. Double-click this list to clear it.

Points of Interest

Contacts added using peer collaboration are located under the current user's Contacts folder (c:\users\<user name>\Contacts). Nice to see they are using a standard, Windows supported format for exchanging contact information and how this ties into their existing vCard technology.

I was surprised to see that contact details can be shared by anyone. That is, when I share you my contact details, I'm not sharing them specifically with you. You can take my details and share them with any of your contacts without me knowing. Its very much like a business card. So be careful not to include any personal information when exchanging contact information.

Links to Resources

I have found the following resource(s) to be very useful in understanding peer-to-peer collaboration:

Conclusion

I hope you have found this article interesting. The next article will complete the series by describing the one remaining feature of peer collaboration: Objects.

If you have suggestions for other topics, please leave a comment and 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


Written By
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 --