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