Monday, March 28, 2011

How to be culturally sensitive with numbers!


Earnest Rutherford from Wikipedia. Image credits.

As a developer, it is important to be culturally conscientious, specifically when it comes to the formatting and parsing of dates and numbers. A fact which I obviously overlooked recently when a user noticed some map coordinates that were displayed many orders of magnitude larger than they should have been. In this post I would like to present the issue encountered and how to resolve it.

Here is the scenario. A third party service returns a map coordinate in decimal degrees as a string. For clarity, this is represented by the following constant.

const string COORDINATE = "-117.182";

And the goal is to convert this string representation to number and assigned it to the following variable.

double longitude;

This number’s format and my computer’s locale are both set to English (United States) or “en-US”. This means that “-117.182” is easily converted to a double as shown below.

double.TryParse(
    COORDINATE,
    out longitude);
MessageBox.Show(longitude.ToString());

image

Under-the-hood, the .NET framework is using the computer’s current locale to guide the number conversion and subsequent display. So, the TryParse above is actually identical to the following:

double.TryParse(
    COORDINATE,
    NumberStyles.Float | NumberStyles.AllowThousands,
    new CultureInfo("en-US"),
    out longitude);
MessageBox.Show(longitude.ToString(new CultureInfo("en-US")));

image

Now, problems occur when a number formatted using “en-US” is parsed on a computer set to a different locale such as Spanish (or “es-ES”).

double.TryParse(
    COORDINATE,
    NumberStyles.Float | NumberStyles.AllowThousands,
    new CultureInfo("es-ES"),
    out longitude);
MessageBox.Show(longitude.ToString(new CultureInfo("es-ES")));

image

In Spain, the convention is to use a “.” as a thousand delimiter rather than a decimal marker as in the US. This results in the number being one thousand times larger than it should be (or originally intended to be).

We cannot change the third party service but the solution is to explicitly convert the string to a number using the “en-US” or the more generic invariant culture formatter.

double.TryParse(
    COORDINATE,
    NumberStyles.Float | NumberStyles.AllowThousands,
    CultureInfo.InvariantCulture,
    out longitude);
MessageBox.Show(longitude.ToString(new CultureInfo("es-ES")));

image

Now that the string is correctly parsed as a double it can be re-displayed using the current (or any other) locale.

In summary:

  1. Whenever possible serialize dates and numbers to strings using the invariant culture, and
  2. Never assume that dates and numbers are serialized with the current locale.

Monday, March 21, 2011

How to sort List<T> using an Anonymous Method


Gottfried Wilhem Leibniz (from wikipedia)

This post illustrates how to use an anonymous method to perform “in line” sorting of a generic list. Let’s start with the definition of a “person” object.

public class Person  {
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

And in our project we have created a generic list of “person” objects called “people”. The following code create the people variable and populates it with well known physicists.

// Create a list of people
List<Person> people = new List<Person>();
people.Add(new Person() { FirstName = "Gottfried", LastName = "Leibniz" });
people.Add(new Person() { FirstName = "Marie", LastName = "Curry" });
people.Add(new Person() { FirstName = "Albert", LastName = "Einsten" });
people.Add(new Person() { FirstName = "Isaac", LastName = "Newton" });
people.Add(new Person() { FirstName = "Niels", LastName = "Bohr" });

Next, let’s display the “before” list in a popup. This code uses a lambda expression in a LINQ expression to concatenate the names of our favorite physicists.

// Display "before" list of people
string before = string.Empty;
people.ForEach(
    new Action<Person>(
        p => before += string.Format(
            "{0} {1} {2}",
            p.FirstName,
            p.LastName,
            Environment.NewLine
        )
    )
);
MessageBox.Show(before);

image

Now, let’s sort the list of physicists based on their first name. This code block associates an anonymous method to the List’s Sort method.

// Sort on *FirstName*
people.Sort(
    delegate(Person x, Person y) {
        if (x == null) {
            if (y == null) { return 0; }
            return -1;
        }
        if (y == null) { return 0; }
        return x.FirstName.CompareTo(y.FirstName);
    }
);

To verify that the code worked…

// Display "after" list of people
string after = string.Empty;
people.ForEach(
    new Action<Person>(
        p => after += string.Format(
            "{0} {1} {2}",
            p.FirstName,
            p.LastName,
            Environment.NewLine
        )
    )
);
MessageBox.Show(after);

image

This technique offers a number of advantages, namely:

  • No need to create a separate static method,
  • Source types (eg “person”) do not need to support IComparable,
  • It binds sorting logic tightly with the sorting method.

One possible disadvantage is that it may result is code duplication. For example, if “sort by first name” is frequently used it may be prudent to add this code as a static method as shown below.

public class Person  {
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public static int SortByFirstName(Person x, Person y){
        if (x == null) {
            if (y == null) { return 0; }
            return -1;
        }
        if (y == null) { return 0; }
        return x.FirstName.CompareTo(y.FirstName);
    }
}

This static method would then be referenced as shown below.

// Sort on *FirstName*
people.Sort(Person.SortFirstName);

Friday, March 18, 2011

Bing Web Search in C#

image

This blog post contains a C# wrapper of the Bing API (Microsoft’s internet search engine). In my previous post I presented a similar wrapper for Google’s Custom Search API, successor to the Web Search API.  I was frankly less than impressed with Google custom search API as it appears to be engineered to favor specific themes and/or domains.

To use use this code below you must first create a Live ID and request an App ID.  This page provided more details.

How to use:

If you have a .NET 4.0 WPF application, like this:

<Window x:Class="WpfApplication6.MainWindow"
       xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
       xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
       Title="MainWindow" Height="350" Width="525">
    <Grid>
        <DataGrid x:Name="myDataGrid" />
    </Grid>
</Window>

Then all you need to do to populate the window with internet search results about Albert Einstein is to called the asynchronous search method on BingSearch .

public partial class MainWindow : Window {
    public MainWindow() {
        InitializeComponent();

        this.Loaded += (s, e) => {
            BingSearch search = new BingSearch() {
                AppId = "<Your App ID>"
            };
            search.SearchCompleted += (a, b) => {
                this.myDataGrid.ItemsSource = b.Response.Web.Results;
            };
            search.SearchAsync("Albert Einstein");               
        };
    }
}

Below is the source code to BingSearch, a class that provides both a synchronous and an asynchronous method to search the internet using Bing.

public class BingSearch {
    public BingSearch() {
        this.Count = 50;
        this.Offset = 0;
        this.Sources = "Web";
        this.Adult = BingSafeLevel.Moderate;
        this.Options = "DisableLocationDetection";
        this.WebOptions = "DisableHostCollapsing+DisableQueryAlterations";
    }
    //
    // PROPERTIES
    //
    public string AppId { get; set; }
    public string Sources { get; set; }
    public int Count { get; set; }
    public int Offset { get; set; }
    public BingSafeLevel Adult { get; set; }
    public string Options { get; set; }
    public string WebOptions { get; set; }
    //
    // EVENTS
    //
    public event EventHandler<BingSearchEventArgs> SearchCompleted;
    //
    // METHODs
    //
    protected void OnSearchCompleted(BingSearchEventArgs e) {
        if (this.SearchCompleted != null) {
            this.SearchCompleted(this, e);
        }
    }
    public BingSearchResponse Search(string search) {
        //
        UriBuilder builder = this.BuilderQuery(search);

        // Submit Request
        WebClient w = new WebClient();
        string result = w.DownloadString(builder.Uri);
        if (string.IsNullOrWhiteSpace(result)) { return null; }

        // Desearealize from JSON to .NET objects
        Byte[] bytes = Encoding.Unicode.GetBytes(result);
        MemoryStream memoryStream = new MemoryStream(bytes);
        DataContractJsonSerializer dataContractJsonSerializer =
new DataContractJsonSerializer(typeof(BingWebResponse));
        BingWebResponse bingWebResponse =
dataContractJsonSerializer.ReadObject(memoryStream) as BingWebResponse;
        memoryStream.Close();

        return bingWebResponse.SearchResponse;
    }
    public void SearchAsync(string search) {
        // Get URL
        UriBuilder builder = this.BuilderQuery(search);

        // Submit Request
        WebClient w = new WebClient();
        w.DownloadStringCompleted += (a, b) => {
            // Check for errors
            if (b == null) { return; }
            if (b.Error != null) { return; }
            if (string.IsNullOrWhiteSpace(b.Result)) { return; }

            // Desearealize from JSON to .NET objects
            Byte[] bytes = Encoding.Unicode.GetBytes(b.Result);
            MemoryStream memoryStream = new MemoryStream(bytes);
            DataContractJsonSerializer dataContractJsonSerializer =
new DataContractJsonSerializer(typeof(BingWebResponse));
            BingWebResponse bingWebResponse =
dataContractJsonSerializer.ReadObject(memoryStream) as BingWebResponse;
            memoryStream.Close();

            // Raise Event
            this.OnSearchCompleted(
                new BingSearchEventArgs() {
                    Response = bingWebResponse.SearchResponse
                }
            );
        };
        w.DownloadStringAsync(builder.Uri);
    }
    private UriBuilder BuilderQuery(string search) {
        // Build Query
        string query = string.Empty;
        query += string.Format("AppId={0}", this.AppId);
        query += string.Format("&Query={0}", search);
        query += string.Format("&Sources={0}", this.Sources);
        query += string.Format("&Adult={0}", this.Adult);
        query += string.Format("&Options={0}", this.Options);
        query += string.Format("&Web.Count={0}", this.Count);
        query += string.Format("&Web.Offset={0}", this.Offset);
        query += string.Format("&Web.Options={0}", this.WebOptions);
        query += string.Format("&JsonType={0}", "raw");

        // Construct URL
        UriBuilder builder = new UriBuilder() {
            Scheme = Uri.UriSchemeHttp,
            Host = "api.bing.net",
            Path = "json.aspx",
            Query = query
        };

        // Return URL
        return builder;
    }
}

public enum BingSafeLevel {
    Off, Moderate, Strict
}

public class BingSearchEventArgs : EventArgs {
    public BingSearchResponse Response { get; set; }
}

[DataContract]
public class BingWebResponse {
    [DataMember(Name = "SearchResponse")]
    public BingSearchResponse SearchResponse { get; set; }
}

[DataContract]
public class BingSearchResponse {
    [DataMember(Name = "Version")]
    public string Version { get; set; }
    [DataMember(Name = "Query")]
    public BingQuery Query { get; set; }
    [DataMember(Name = "Web")]
    public BingWeb Web { get; set; }
}

[DataContract]
public class BingQuery {
    [DataMember(Name = "SearchTerms")]
    public string SearchTerms { get; set; }
}

[DataContract]
public class BingWeb {
    [DataMember(Name = "Total")]
    public int Total { get; set; }
    [DataMember(Name = "Offset")]
    public int Offset { get; set; }
    [DataMember(Name = "Results")]
    public List<BingResult> Results { get; set; }
}

[DataContract]
public class BingResult {
    [DataMember(Name = "Title")]
    public string Title { get; set; }
    [DataMember(Name = "Description")]
    public string Description { get; set; }
    [DataMember(Name = "Url")]
    public string Url { get; set; }
    [DataMember(Name = "DisplayUrl")]
    public string DisplayUrl { get; set; }
    [DataMember(Name = "DateTime")]
    public string DateTimeOriginal { get; set; }
    // Convert Date
    public DateTime? DateTimeConverted {
        get {
            if (string.IsNullOrWhiteSpace(this.DateTimeOriginal)) { return null; }
            DateTime d;
            if (!DateTime.TryParse(this.DateTimeOriginal, out d)) { return null; }
            return d;
        }
    }
}

Friday, March 4, 2011

Google Custom Search in C#

This post contains a C# class to search the internet using Google’s custom search API. Before using this class you must first create a Google account (link) and generate an API key (link).

Unfortunately, this code cannot be used directly from a Silverlight application because of security limitations (see cross domain policy). If you need this capability in a Silverlight web application I would suggest performing the search via an ASP.NET proxy (license permitting).

In this sample, I called the Google search API from a WPF application, but it would also work in a WinForms or ASP.NET application as suggested above.

Here is the XAML of the Window that displays the results of the Google search.

<Window x:Class="ESRI.PrototypeLab.MapServiceHarvester.MainWindow"
       xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
       xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
       Height="600"
       Width="800"
       >
    <Grid>
<DataGrid x:Name="DataGridResults" AutoGenerateColumns="True" />
    </Grid>
</Window>

Here is the code behind that makes that performs the internet search and displays the results in the datagrid defined above.

public partial class MainWindow : Window {
    public MainWindow() {
        InitializeComponent();

        this.Loaded += (s, e) => {
            GoogleSearch search = new GoogleSearch() {
                Key = "<enter your key here>",
                CX = "013036536707430787589:_pqjad5hr1a"
            };
            search.SearchCompleted += (a, b) => {
                this.DataGridResults.ItemsSource = b.Response.Items;
            };
            search.Search("gis");
        };
    }
}

And here is the class that calls Google’s custom search API, desterilizes the JSON response and returns the result as single .NET objects.

public class GoogleSearch {
    public GoogleSearch() {
        this.Num = 10;
        this.Start = 1;
        this.SafeLevel = SafeLevel.off;
    }
    //
    // PROPERTIES
    //
    public string Key { get; set; }
    public string CX { get; set; }
    public int Num { get; set; }
    public int Start { get; set; }
    public SafeLevel SafeLevel { get; set; }
    //
    // EVENTS
    //
    public event EventHandler<SearchEventArgs> SearchCompleted;
    //
    // METHODs
    //
    protected void OnSearchCompleted(SearchEventArgs e) {
        if (this.SearchCompleted != null) {
            this.SearchCompleted(this, e);
        }
    }
    public void Search(string search) {
        // Check Parameters
        if (string.IsNullOrWhiteSpace(this.Key)) {
            throw new Exception("Google Search 'Key' cannot be null");
        }
        if (string.IsNullOrWhiteSpace(this.CX)) {
            throw new Exception("Google Search 'CX' cannot be null");
        }
        if (string.IsNullOrWhiteSpace(search)) {
            throw new ArgumentNullException("search");
        }
        if (this.Num < 0 || this.Num > 10) {
            throw new ArgumentNullException("Num must be between 1 and 10");
        }
        if (this.Start < 1 || this.Start > 100) {
            throw new ArgumentNullException("Start must be between 1 and 100");
        }
                            
        // Build Query
        string query = string.Empty;
        query += string.Format("q={0}", search);
        query += string.Format("&key={0}", this.Key);
        query += string.Format("&cx={0}", this.CX);
        query += string.Format("&safe={0}", this.SafeLevel.ToString());
        query += string.Format("&alt={0}", "json");
        query += string.Format("&num={0}", this.Num);
        query += string.Format("&start={0}", this.Start);
           
        // Construct URL
        UriBuilder builder = new UriBuilder() {
            Scheme = Uri.UriSchemeHttps,
            Host = "www.googleapis.com",
            Path = "customsearch/v1",
            Query = query
        };

        // Submit Request
        WebClient w = new WebClient();
        w.DownloadStringCompleted += (a, b) => {
            // Check for errors
            if (b == null) { return; }
            if (b.Error != null) { return; }
            if (string.IsNullOrWhiteSpace(b.Result)) { return; }

            // Desearealize from JSON to .NET objects
            Byte[] bytes = Encoding.Unicode.GetBytes(b.Result);
            MemoryStream memoryStream = new MemoryStream(bytes);
            DataContractJsonSerializer dataContractJsonSerializer =
new DataContractJsonSerializer(typeof(GoogleSearchResponse));
            GoogleSearchResponse googleSearchResponse =
dataContractJsonSerializer.ReadObject(memoryStream) as GoogleSearchResponse;
            memoryStream.Close();

            // Raise Event
            this.OnSearchCompleted(
                new SearchEventArgs() {
                    Response = googleSearchResponse
                }
            );
        };
        w.DownloadStringAsync(builder.Uri);

    }
}

public enum SafeLevel { off, medium, high }

public class SearchEventArgs : EventArgs {
    public GoogleSearchResponse Response { get; set; }
}

[DataContract]
public class GoogleSearchResponse {
    [DataMember(Name = "kind")]
    public string Kind { get; set; }
    [DataMember(Name = "url")]
    public Url Url { get; set; }
    [DataMember(Name = "queries")]
    public Queries Queries { get; set; }
    [DataMember(Name = "context")]
    public Context Context { get; set; }
    [DataMember(Name = "items")]
    public List<Item> Items { get; set; }
}

[DataContract]
public class Url {
    [DataMember(Name = "type")]
    public string Type { get; set; }
    [DataMember(Name = "template")]
    public string Template { get; set; }
}

[DataContract]
public class Queries {
    [DataMember(Name = "nextPage")]
    public List<Page> NextPage { get; set; }
    [DataMember(Name = "request")]
    public List<Page> Request { get; set; }
}

[DataContract]
public class Page {
    [DataMember(Name = "title")]
    public string Title { get; set; }
    [DataMember(Name = "totalResults")]
    public int Request { get; set; }
    [DataMember(Name = "searchTerms")]
    public string SearchTerms { get; set; }
    [DataMember(Name = "count")]
    public int Count { get; set; }
    [DataMember(Name = "startIndex")]
    public int StartIndex { get; set; }
    [DataMember(Name = "inputEncoding")]
    public string InputEncoding { get; set; }
    [DataMember(Name = "outputEncoding")]
    public string OutputEncoding { get; set; }
    [DataMember(Name = "safe")]
    public string Safe { get; set; }
    [DataMember(Name = "cx")]
    public string CX { get; set; }
}

[DataContract]
public class Context {
    [DataMember(Name = "title")]
    public string Title { get; set; }
    [DataMember(Name = "facets")]
    public List<List<Facet>> Facets { get; set; }
}

[DataContract]
public class Facet {
    [DataMember(Name = "label")]
    public string Label { get; set; }
    [DataMember(Name = "anchor")]
    public string Anchor { get; set; }
}

[DataContract]
public class Item {
    [DataMember(Name = "kind")]
    public string Kind { get; set; }
    [DataMember(Name = "title")]
    public string Title { get; set; }
    [DataMember(Name = "htmlTitle")]
    public string HtmlTitle { get; set; }
    [DataMember(Name = "link")]
    public string Link { get; set; }
    [DataMember(Name = "displayLink")]
    public string DisplayLink { get; set; }
    [DataMember(Name = "snippet")]
    public string Snippet { get; set; }
    [DataMember(Name = "htmlSnippet")]
    public string HtmlSnippet { get; set; }
    [DataMember(Name = "cacheId")]
    public string CacheId { get; set; }
    //[DataMember(Name = "pagemap")] *** Cannot deserialize JSON to .NET! ***
    //public Pagemap Pagemap { get; set; }
}

[DataContract]
public class Pagemap {
    [DataMember(Name = "metatags")]
    public List<Dictionary<string, string>> Metatags { get; set; }
}

[DataContract]
public class Metatag {
    [DataMember(Name = "creationdate")]
    public string Creationdate { get; set; }
    [DataMember(Name = "moddate")]
    public string Moddate { get; set; }
}

And lastly, here is the result.

image

The only issue I had with this sample was trying to deserialize the JSON associated with the “pagemap” property (see commented out code above). Any tips would be appreciated.

Wednesday, March 2, 2011

P2P Collaboration in ArcMap

Most collaboration technologies require a central server to manager communication. Email, web browsing and instant messaging all require a server to handle communication between clients. This post re-introduces a sample originally published in May of 2008 that facilitates server-less map collaboration or “peer-to-peer geo-collaboration”.

The new add-in for ArcGIS 10 can be downloaded (with source code) from here.

How to Install:

Download the contribution from the code gallery (link), unzip the file and double click on file with the esriAddIn extension. This will launch the Esri add-in installer, click Install Add-In

How to Uninstall:

The add-in can be removed by clicking Customize > Add-In Manager, selecting P2P Collaboration from the list of installed add-ins and clicking Delete this Add-In.

How to Use:

First, display the collaboration toolbar by clicking Customize > Toolbars > P2P Collaboration. Clicking on the only button on the toolbar will display the p2p collaboration dockable window as shown below.

 image

Before you can collaborate with other peers you must first connect to a mesh (or peer cloud).  A mesh is virtual network comprising of two or more peers.  When the Connect button is clicked the following dialog is displayed.

image

The Username is a friendly name that you want to be identified on the mesh as.  By default, a password is not used. If you specify a password then you will ONLY be able to "see" other peers that used the same password. Using a password is a good method of excluding unwanted peers.

Most P2P applications are considered to be examples of hybrid P2P technology. This is because most rely on some sort of server interaction, such as a DNS server.  In the case of this add-in, a centralized resource is required for peers to find other peers. This central resource is called a peer resolver.

The p2p collaboration add-in supports two resolver types:

  1. PNRP
    Peer Name Resolution Protocol (or PNRP) is a proprietary technology by Microsoft. The add-in supports PNRP 2.0 which is installed by default on computers running Microsoft Windows Vista and an optional install for Microsoft Windows XP SP2.  Unfortunately PNRP is not supported on Microsoft Windows 2003/2008.
  2. Custom
    This is the address of a custom peer resolver running on your network. Details on how to configure and start a custom peer resolver are detailed below. The purpose of the resolver is to exchange IP addresses (and ports) of other peers. All subsequent communication is done on a peer-to-peer basis.

Within a few seconds on connecting you should see the names of other peers appearing in the list. The user in bold is you.

 image

There are five ways you can collaborate with other peers:

  1. Chatting
    To chat click the Chat tab and start typing. It is important to note that chatting is communal, that is, all peers see all text messages. 
    image
  2. Publishing geo-referenced screenshots
    Returning to the Users tab, if you right click on another peer's name you will see the following context menu appear. Clicking on Publish Map will send your current map display to the selected user.  The selected user (i.e. Jim) will automatically receive a new raster layer in his map document. 
    image
  3. Requesting geo-referenced screenshots
    In the example above, Jim sent a screenshot to Bob. This would have required Bob to make an explicit request to Jim. The p2p collaboration add-in has the capability of requesting a screenshots from other peers without them knowing! To covertly request a screenshot, select Request Map in the context menu. To stop other peers from harvesting screenshots (or "maps") from your computer, unchecked the Map > Share option from the main menu.
    image
  4. Add, removing, editing shared graphics and ink
    Shared graphics is probably the most useful feature of the add-in. All peers can add, remove, edit graphics collectively. A line or box added by one peer can be moved or deleted by another peer. Any graphic from the Drawing toolbar or ink from the Tablet toolbar is supported.

    image  

    By default, all graphics/ink that is added to the map are shared with all other peers. If you want to add graphics/ink to your map without it being shared then you can disable sharing by clicking Graphics > Share
    image 
  5. Share navigation
    From a screenshot above you may have noticed a entry in the peer context menu called Zoom to Map.  This will change your map extent to be same as the peer you selected.  Essentially you can see what areas other people are looking at.  However there is a significantly more advanced feature called shared navigation that allows one peer to control the map display of other peers.  The controlling peer (aka the master) must first enable extent sharing from the main menu (Extents > Share). 
    image

    Other peers (aka slaves) must then subscribe to the master peer’s map extent.  To subscribe to another peers extents click Follow in the peer context menu as shown below. 
    image

If you choose to use PNRP as your organization’s peer resolver then please remember that this is only supported on Microsoft Window XP SP2, Microsoft Windows Vista and Microsoft Windows 7. Another disadvantage of PNRP is that it requires partial support for IPv6. If your network and routes do not support this protocol then PNRP may not function correctly. On Vista, PNRP 2.0 is already installed and running. However, PNRP, by default is not installed on Microsoft Windows XP. To install PNRP on Microsoft Windows XP follow these steps:

  1. In the Control Panel, double-click Add or Remove Programs.
  2. In the Add or Remove Programs dialog box, click Add/Remove Windows Components.
  3. In the Windows Components Wizard, select the "Networking Services" check box and click "Details".
  4. Check the "Peer-to-Peer" check box and click "OK".
  5. Click "Next" in the Windows Components Wizard.
  6. When the installation completes, click "Finish".
  7. From a command shell prompt, start the PNRP service with the following command: net start pnrpsvc.

After installing PNRP on Microsoft Windows XP, there is one more step. The version of PNRP that is included with Microsoft Windows XP is not compatible with Microsoft Windows Vista. To upgrade PNRP 1.0 to the Vista compatible PNRP 2.0 you need to install KB920342 from here.

For greater compatibility, your organization may choose to run a custom peer resolver on your network. The downloadable contribution includes a sample custom peer resolver that is specifically designed for the ArcMap add-in. The customer peer resolver is named ESRI.PrototypeLab.P2P.PeerResolver.exe and is located in the following subdirectory of the download.
ESRI.PrototypeLab.P2P\ESRI.PrototypeLab.P2P.PeerResolver\bin\Release

image

The for best results, leave the Server name as localhost and the Protocol set to Tcp. If there is an obvious port conflict then change it to something else. For example, port 80 might be used by a web server. To start the peer resolver service click Start.  If you close the application by clicking the "X" button then the application will continue running in the windows system tray as shown below.

image

When the peer resolver is minimized to the system tray, use the right click menu (or “context menu”) to start, stop, open or close the resolver.

Lastly, to assist with configuring and testing your setup, the download includes a simple test app. This file is called ESRI.PrototypeLab.P2P.Test.exe and is located here:
ESRI.PrototypeLab.P2P\ESRI.PrototypeLab.P2P.Test\bin\Release

image