Thursday, July 28, 2011

How to create a drop target for layers in ArcMap?

This post as originally published 3/16/2007 on Microsoft Live Spaces.

How to create a drop target for layers in ArcMap?

Drag and drop is a convenient and intuitive method of exchanging data between windows (and applications). ESRI applications like ArcCatalog and ArcMap have drop and drop capabilities, for example, you can drag layer into the Buffer geoprocessing tool window (see above).

Using the sample code below you can add this capability to your .NET application, that is, you can make your window/control a drop target for layers dragged from the ArcMap table of contents. This code can be used in a window hosted by an ESRI application such as an ArcMap dockable window or as a standalone application.

To enable this feature, a utility class called EsriDataObject is provided below to assist with the deserialization of dropped objects from ArcMap.

The first code sample demonstrates how to use EsriDataObject in a .NET Form. The code will display the concatenate list of layer names that were dropped on the form. The code assumes there is Form called form1 and a TextBox called textBox1.

How to use "EsriDataObject" in a .NET Form.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using ESRI.ArcGIS.Carto;

namespace MyNamespace {
    public partial class Form1 : Form {
        public Form1() {
            InitializeComponent();
            this.textBox1.AllowDrop = true;
            this.textBox1.DragEnter +=
new DragEventHandler(this
.textBox1_DragEnter); this.textBox1.DragOver +=
new DragEventHandler(this
.textBox1_DragOver); this.textBox1.DragDrop +=
new DragEventHandler(this
.textBox1_DragDrop); } private void textBox1_DragEnter(object sender, DragEventArgs e) { e.Effect = EsriDataObject.IsValid(e.Data) ?
DragDropEffects.All : DragDropEffects
.None; } private void textBox1_DragOver(object sender, DragEventArgs e) { e.Effect = EsriDataObject.IsValid(e.Data) ?
DragDropEffects.All : DragDropEffects
.None; } private void textBox1_DragDrop(object sender, DragEventArgs e) { EsriDataObject esriDataObject =
EsriDataObject
.ConvertToEsriDataObject(e.Data); foreach (ILayer layer in esriDataObject.LayerCollection) { this.textBox1.Text += layer.Name + " "; } } } }

Sample 2: Source code to "EsriDataObject".

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Windows.Forms;
using ESRI.ArcGIS.Carto;
using ESRI.ArcGIS.ArcMapUI;
using ESRI.ArcGIS.esriSystem;

namespace MyNamespace {
    public class EsriDataObject {
        private const string DATAOBJECT_ESRILAYERS = "ESRI Layers";
        private const string INTERFACE_ILAYER =
"{34C20002-4D3C-11D0-92D8-00805F7C28B0}"
; private const string INTERFACE_ITABLEPROPERTY =
"{4657D951-5FFB-11D3-9F6C-00C04F6BC886}"; private readonly List<ILayer> _lc = new List<ILayer>(); private readonly List<ITableProperty> _tpc = List<ITableProperty>(); // // PROPERTIES // public List<ILayer> LayerCollection { get { return this._lc; } } public List<ITableProperty> TablePropertyCollection { get { return this._tpc; } } // // STATIC METHODS // public static bool IsValid(IDataObject dataObject) { return dataObject.GetDataPresent(DATAOBJECT_ESRILAYERS); } public static EsriDataObject ConvertToEsriDataObject(
IDataObject
dataObject) { // EsriDataObject esriDataObject = new EsriDataObject(); // Exit if dropped object is invalid if (EsriDataObject.IsValid(dataObject)) { // Get Byte Array from DataObject object esriLayers = dataObject.GetData(DATAOBJECT_ESRILAYERS); MemoryStream memoryStream = (MemoryStream)esriLayers; byte[] bytes = memoryStream.ToArray(); // Load Byte Array into a Stream (ESRI Wrapper of IStream) IMemoryBlobStreamVariant memoryBlobStreamVariant =
new MemoryBlobStreamClass
(); memoryBlobStreamVariant.ImportFromVariant(bytes); IMemoryBlobStream2 memoryBlobStream =
(
IMemoryBlobStream2
)memoryBlobStreamVariant; IStream stream = (IStream)memoryBlobStream; // Load Stream into an ESRI ObjectStream IObjectStream objectStream = new ObjectStreamClass(); objectStream.Stream = stream; // Get Number of Layers in Dropped Object byte pv; uint cb = sizeof(int); uint pcbRead; objectStream.RemoteRead(out pv, cb, out pcbRead); int count = Convert.ToInt32(pv); // Define Guids Guid guidLayer = new Guid(INTERFACE_ILAYER); Guid guidTable = new Guid(INTERFACE_ITABLEPROPERTY); // Get Dropped Layers for (int i = 0; i < count; i++) { object o = objectStream.LoadObject(ref guidLayer, null); ILayer layer = (ILayer)o; esriDataObject.LayerCollection.Add(layer); } // Get Dropped TableProperties for (int i = 0; i < count; i++) { object o = objectStream.LoadObject(ref guidTable, null); if (o == null) { continue; } ITableProperty tableProperty = (ITableProperty)o; esriDataObject.TablePropertyCollection.Add(tableProperty); } } return esriDataObject; } } }

Additional information:

Dragging and dropping objects from ArcCatalog is a lot easier than ArcMap. In ArcMap, a dragged object is packaged as an "ESRI Layer" object in the DataObject. The code above describes how to unpack an "ESRI Layer" object. In ArcCatalog, a dragged object is packaged as an "ESRI Names" object in the DataObject. Fortunately, there are ArcObjects available to pack and unpack this object.

To create an "ESRI Names" object use the following:
INameFactory.PackageNames Method

To unpackage an "ESRI Names" object use:
INameFactory.UnpackageNames Method

Lastly, here is an online example showing how to unpackage an "ESRI Names" object:
esriSystem NameFactory Example

Tuesday, July 26, 2011

Adding a WPF RichTextBox hyperlink at specific character positions?


Adding hyperlinks to a WPF RichTextBox can be achieved at design time in XAML or programmatically at run time.
Hyperlinks are created programmatically via the hyperlink constructor by parsing either the start and end TextPointers or InLine objects. These constructors are not particularly developer friendly unless the developer is programmatically populating a RichTextBox’s FlowDocument from Runs (see here).
In a recent project I need to add a hyperlink for text between predefined character index. For example, I needed a hyperlink for the text located between character positions 100 and 110.  The following extension method enables this.
RichTextBox extension:
namespace ESRI.PrototypeLab.Sample {
    public static class Extensions {
        public static Hyperlink ToHyperlink(this RichTextBox rtb, int start, int end) {
            TextPointer p1 = rtb.ToTextPointer(start);
            TextPointer p2 = rtb.ToTextPointer(end);
            if (p1 == null || p2 == null) { return null; }
            return new Hyperlink(p1, p2);
        }
        public static TextPointer ToTextPointer(this RichTextBox rtb, int index) {
            int count = 0;
            TextPointer position = rtb.Document.ContentStart;
            while (position != null) {
                if (position.GetPointerContext(LogicalDirection.Forward) ==
                    TextPointerContext.Text) {
                    string textRun = position.GetTextInRun(LogicalDirection.Forward);
                    int length = textRun.Length;
                    if (count + length > index) {
                        return position.GetPositionAtOffset(index - count);
                    }
                    count += length;
                }
                position = position.GetNextContextPosition(LogicalDirection.Forward);
            }
            return null;
        }
    }
}
How to use:
Hyperlink hyperlink = this.RichTextBox.ToHyperlink(100, 110);
hyperlink.Click += (s, e) => {
    MessageBox.Show("Hyperlink Clicked");};

Wednesday, July 20, 2011

The App Interoperability Conundrum


I would argue that mobile computing began the early 20th century with the popularization of wristwatches. The wristwatch, like its ancestral pocket watch, allowed people to know, at any moment, what time it was. Mobile computers advanced significantly in 2007 when Apple introduced the iPhone. This device was not only a mobile communication device but it allowed people know, at any moment, where they are.
But I don’t want to discuss the historical importance of the iPhone, rather the paradigm shift in application design.
For the past twenty years or so, the personal computer (or PC) has dominated homes and offices throughout the world. The vast majority of PCs used some generation of Microsoft’s Windows operating system. Regardless of the criticism that Microsoft stifled innovation, Windows users have enjoyed one unquestioned privilege, interoperability.
Quite unconsciously, a PC user can perform information exchange between applications using operations like copy and paste or drag and drop. Not only can a user exchange information, like text or imagery, between applications like Internet Explorer, Excel and Word but also with third party applications. This interoperability is available because of two factors:
  1. All Windows applications understand the same data and file types, and
  2. Windows provides a built-in framework that software developers can use to enable the inter-application exchange of information.
It is without question that mobile devices will continue to proliferate in the consumer market with improvements in mobile infrastructure, device affordability and cultural acceptance. However I see an uneasy trend in mobile application, that is, mobile applications are becoming increasingly focused but less interoperability.
To illustrate this point, consider the following hypothetical example. I am in downtown Los Angeles, I’m hungry and need nourishment from a reputable establishment. On my iPhone I can use:
  1. Google Maps to find a nearby restaurant,
  2. MapQuest to get voice guided routing directions,
  3. Built-in camera to record my memorable meal,
  4. FourSquare to “check-in” and recommend this restaurant to my friends, and
  5. Yelp to post a review (and snapshot) of my dining experience.
In this workflow, the only interoperable elements were the street address I copied from (1) to (2) and the photo created in (3) and shared with (5).
Obviously this workflow could be optimized by using one app to perform two or more tasks like using Google Map to a find a restaurant and driving directions. But these five apps, in my hypothetical opinion, were the best in each task.
My criticism is not targeted at the number of app used during my lunch break but the lack of interoperability between them. For example:
  1. From Google Map, why is it not possible to send the restaurant address directly to my favorite routing apps?
  2. From the camera app, why is it not possible to share a photo directly with Yelp.
  3. From MapQuest, can I send the “route” to my friends so that they know where I am driving from?
  4. To rate (or “check-in” at) the restaurant with FourSquare and Yelp why did I have to locate the restaurant in each app? Why not just click the “get current establishment” button in each app?
These comments may seem like iPhone-bashing, but they are not. I am just bringing your attention the unintentional stovepiping caused by increasingly focused mobile apps.
To paraphrase Apple’s advertising slogan, there may be an “app for that” but what is lacking is the “app to app” interoperability. Again this is not a criticism of Apple or other mobile device vendors, just a mere observation of the consumer-driven “app paradigm”.
The "app paradigm” does have its advantages. Smaller focused apps with a handful of capabilities can be deployed quickly, updated frequently, serve niches and most importantly, be profitable. Many software developers have made a health living by serving the “long tail” with $0.99 apps.
But I predict that there may be an equilibrium shift in near future in which, due to the lack of app cohesion, that devices will eventually be ruled by a handful of super-apps. The foremost, without doubt, is Google. Google may (or should?) consolidate their shopping, social networking, mapping, translation, book and search services into a single super-app.