Программирование на .net, обмениваемся опытом

0 голосов
спросил 06 Март, 10 от TDenis (42,620 баллов) в категории Программные продукты Esri
Предлагаю здесь обмениваться опытом людям, которые программируют в ArcGIS под .Net или просто работают с ArcObjects. Думаю, это может быть интересно не только новичкам, но и более продвинутым разработчикам.

-----------------------------------

Попробую немного затронуть тему интерфейсов. Примеры на C#, но и на VB.Net многое выглядит аналогично.

Использование методов расширения (Extension methods).
Один из основных вопросов у начинающих разработчиков под ArcGIS связан с огромным числом интерфейсов. Их действительно много и их приходится запоминать, чтобы не копаться каждый раз в справке, ища соответствующий интерфейс по названию метода или по списку интерфейсов, которые реализует рассматриваемый объект. Но это только полбеды. Допустим, вы запомнили те интерфейсы, с которыми вы работаете наиболее часто. Но это не избавляет вас от того, что вам приходится постоянно приводить эти интерфейсы один к другому (которые иногда еще и находится в различных сборках). Конечно, в рамках архитектуры системы наличие всех этих интерфейсов, группирующих методы по функционалу, может выглядеть красиво и смотреться все это будет отлично проработанным решением, но в итоге программировать становится не всегда удобно. Хочется попробовать немного упростить работу.

К примеру, когда вы работаете с геометрией, вам приходится постоянно приводить интерфейсы один к другому, например к IProximityOperator, ICurve, ITopologicalOperator, IArea.
Вместо этого можно подключить свою библиотеку с готовыми методами расширения для "основных" интерфейсов. Для полигонов (PolygonClass) - под "основным" методом я подразумеваю IPolygon, для точек (PointClass) - IPoint и т.д.
И в этом случае получается не просто библиотека, а библиотека, которой действительно удобно пользоваться.
Для того, чтобы найти, скажем, центр масс полигона, вам больше не придется делать нечто вроде:

IArea pArea = pPolygon as IArea;

return pArea.Centroid;

вместо этого можно будет написать:
return pPolygon.GetCentroid();

где GetCentroid() - это метод расширения для интерфейса IPolygon, т.е. не стандартный метод от ESRI, а наш собственный, но привязанный к IPolygon.

Кстати говоря, в данном случае всё равно далеко не всем интуитивно понятно, что IArea - это тот самый интерфейс для поиска центра.
Здесь я добавил расширяющий метод для IGeometry (для точки он вернет себя же, для линии и полигона - центр). Т.к. IPolygon наследует IGeometry, то никакого приведения не требуется.

Интерфейс IPolygon можно расширить методом GetArea(), возвращающим значение площади, этот метод мне приходится тоже использовать достаточно часто. Останется перенести туда же метод LabelPoint и тогда можно забыть про IArea насовсем.

Методы вроде StartEditing(), StopEditOperation() можно прицепить к IFeatureLayer, скрывая логику получения Workspace.
К IFeatureCursor или ICursor можно добавить метод Release(), тогда для освобождения ресурсов не потребуется вспоминать и явно обращаться к Marshal, а достаточно будет работать именно с IFeatureCursor.
К IMap можно прицепить FindLayer(), чтобы не вспоминать про метод FindMapLayer() в IGPUtilities из соответствующего пространства имен.
И т.д.

Пример метода расширения:

public static double GetDistance(this IGeometry thisGeometry, IGeometry pGeometry)

{
    return ((IProximityOperator)thisGeometry).ReturnDistance(pGeometry);
}


Ничего сложного, обычный статический метод с добавлением ключевого слова this для первого параметра, тип которого мы расширяем. И как видно из примеров, это будет не классический статический (static/shared) метод, а именно метод для экземпляров.

В выпадающем меню интеллисенса метод будет помечен специальным значком, и вы сразу сможете отличить ваш метод от стандартных. Можно давать методу комментарий на родном языке, что удобно.

Понятно, что нельзя абсолютно все пихать в "основные" интерфейсы, иначе с тем же успехом можно обходиться и вовсе без интерфейсов, а использовать класс напрямую. В этом случае получается огромный список методов и свойств, выбирать из такого списка довольно сложно (создайте экземпляр того же PolygonClass и посмотрите сколько у него разных методов). Поэтому в "основных" интерфейсах должно быть только то, что вы используете очень часто.

Конечно, в расширении есть и свои минусы. Это особенность языка C# 3.0 и официально для работы этих методов требуется framework 3.5. Хотя можно заставить их работать с более старыми версиями framework, используя вот такой хак:
http://www.c-sharpcorner.com/UploadFile/pcurnow/extmethods03242008070853AM/extmethods.aspx.

Т.е. нужно лишь добавить следующий код:
namespace System.Runtime.CompilerServices

{
    [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Assembly)]    
    public sealed class ExtensionAttribute : Attribute
    {
    }
}


Но есть и более важная проблема - расширяющие методы могут внести некоторую мешанину в "стройную" систему стандартных интерфейсов.
Перевешивают ли эти минусы в вашем случае - решайте сами.
Но использование такого подхода позволит вам реже обращаться к специализированным интерфейсам, позволит значительно быстрее писать код, который к тому же будет более простой и короткий, по которому будет легче ориентироваться, проводить рефакторинг.

Особенно это актуально, когда вам в очередной раз необходимо быстренько решить несложную, но достаточно типовую задачу - когда, например, требуется создать новую кнопку для ArcMap, которая бы находила на карте слой по его имени, проходила бы по всем записям слоя, проводила бы некоторый анализ и записывала бы куда-то результат или выводила отчет. Думаю многие писали подобное не раз. Действия часто одни и те же.

Дополнительно про расширяющие методы можно почитать тут:
http://msdn.microsoft.com/ru-ru/library/bb383977.aspx
http://msdn.microsoft.com/ru-ru/magazine/cc163317.aspx (для vb)
http://en.wikipedia.org/wiki/Extension_method

-------------------------------

Вопрос опытным разработчикам.
А что делаете вы при создании нового проекта первым делом? Создаёте и подключаете ли вы какие-нибудь собственные шорткаты, сниппеты именно для arcobjects? Пользуетесь ли вы стандартными? Пишете ли вы предварительно юнит-тесты для своего кода, непосредственно работающего с ArcObjects? С помощью mock-объектов или как?
Что делаете вы, чтобы упростить свою работу?

18 Ответы

0 голосов
ответил 11 Март, 10 от TDenis (42,620 баллов)
Получение IApplication

Для ряда операций требуется объект, реализующий IApplication.
Если вы пишете, к примеру, кнопку для ArcMap, то вы получаете объект, реализующий этот интерфейс, в методе OnCreate, который вызывается при создании этой самой кнопки. В этом методе данный объект присваивается закрытому полю m_application.

Какие же это могут быть операции, которым нужен IApplication?
Самое очевидное - требуется получить IMxDocument, а от него IMap.
Другой случай - когда вы хотите открыть рабочее пространство на редактирование. Вам нужен этот объект для поиска расширения esriEditor.Editor - ведь чтобы найти его, необходимо вызвать метод FindExtensionByCLSID.
Хотя пользоваться данным расширением не обязательно, поскольку редактировать можно и с помощью IWorkspaceEdit, но, тем не менее, именно этот способ является рекомендованным для создания сессий редактирования внутри ArcMap (см. например ESRI Developer Summit 2006, Hitchhiker's Guide to the Geodatabase).

Можно таскать этот IApplication с собой сквозь методы, и неважно как глубоко.
Когда это надоест - можно сделать статическое свойство, и при создании кнопки присвоить ему значение m_application, и затем получать это значение из любого места программы.
Когда надоест не забывать присваивать значение при создании каждой кнопки - то добавляем в нашу библиотеку вот такой класс:

public static class Current

{
    public static IApplication Application
    {
        get
        {
            Type pType = Type.GetTypeFromCLSID(typeof(AppRefClass).GUID);
            System.Object obj = Activator.CreateInstance(pType);
            return obj as IApplication;
        }
    }

    public static IMxDocument MxDocument
    {
        get
        {
            IApplication pApplication = Current.Application;
            return pApplication.Document as IMxDocument;
        }
    }

    public static IActiveView ActiveView
    {
        get
        {
            IMxDocument pMxDocument = Current.MxDocument;
            return pMxDocument.ActiveView;
        }
    }

    public static IMap FocusMap
    {
        get
        {
            IMxDocument pMxDocument = Current.MxDocument;
            return pMxDocument.FocusMap;
        }
    }
}


И чтобы получить IApplication в любом месте программы потребуется написать всего лишь:
var pApplication = Current.Application;


а чтобы получить активную карту пишем:
var pMap = Current.FocusMap;


При этом, конечно, не стоит поддаваться искушению делать глобальным всё подряд. Об этом, к примеру, предупреждают такие камрады как Мартин Фаулер, Кент Бек, Вард Каннингем, Джошуа Кириевски.
Так что не чересчур ли вышенаписанное для вас, сможете ли вы мириться с подобной глобальностью - решайте сами :)
    
0 голосов
ответил 12 Март, 10 от TDenis (42,620 баллов)
ArcObjects и LINQ

Задача.
Допустим у нас есть полигональный слой Polygons, объекты которого разбиты по группам - каждому объекту приписан тип (целочисленное поле Type).
Для каждой группы необходимо посчитать суммарную площадь входящих в неё полигонов.
Затем для каждого полигона необходимо вычислить отношение его площади к общей площади группы, в которую он входит. Полученное значение необходимо записать в поле NormArea.

Для решения подобных задач обработки данных в .NET 3.5 есть ряд удобных средств, например технология LINQ.
Только вот ArcObjects пока не предоставляет никаких средств для поддержки этой технологии.

Поэтому предлагаю ещё немного побаловаться и сделать следующее:

1. Добавить поддержку индексаторов, чтобы вместо:
int fldArea = feature.FindField("Area")

double area = (double)feature.get_Value(fldArea);

писать:
double area = (double)feature["Area"];

2. добавить поддержку оператора foreach для курсоров, чтобы вместо:
int fldArea = pFeatureCursor.FindField("Area");

IFeature pFeature = pFeatureCursor.NextFeature();
while (pFeature != null)
    totalArea += (double)feature.get_Value(fldArea);
Marshal.ReleaseComObject(pFeatureCursor);

писать:
foreach (var feature in features)

    totalArea += (double)feature["Area"];

3. добавить поддержку запросов LINQ, как с помощью синтаксиса, облегчающего восприятие:
var ids = from feature in features

          where (double)feature["Area"] > 100
          orderby feature["Name"] descending
          select (string)feature["Name"];

так и в форме расширяющих методов с лямбда-выражениями:
var ids = features.Where(f => (double) f["Area"] > 100).OrderByDescending(f => f["Name"]).Select(f => (string) f["Name"]);

4. добавить поддержку нескольких идущих подряд запросов к одному курсору:
double totalArea = 0.0;

foreach (var feature in features)
    totalArea += (double)feature["Area"];

var ids = from feature in features
    where (double)feature["Area"] > 100
    orderby feature["Name"] descending
    select (string)feature["Name"];

5. Добавить поддержку оператора using для курсора:
using (var features = featureLayer.SearchEx(null))

{
    ...
}

6. Забыть про необходимость вызова метода ReleaseComObject для курсора в случае перебора как с помощью foreach (даже если вы выходите из цикла с помощью break), так и с помощью запросов LINQ. Даже если вы не используете оператор using.

Итак...
Менять стандартные объекты мы не можем. Поэтому будем оборачивать (декорировать) их новыми объектами, попутно добавляя новый фунционал.

Начнём с интерфейса IFeatureLayer, добавим ему расширяющий метод:
public static class IFeatureLayerExtensions

{
    public static IFeatureCursorEx SearchEx(this IFeatureLayer pFeatureLayer, IQueryFilter filter)
    {
        return new FeatureCursorEx(pFeatureLayer.FeatureClass, filter, false);
    }
}

То же самое можно проделать и с IFeatureCursor.

Здесь мы ввели новый интерфейс IFeatureCursorEx и объект FeatureCursorEx.
IFeatureCursorEx обернёт IFeatureCursor, а так же потребует реализацию интерфейса IEnumerable<T>. Это необходимо для поддержки перебора оператором foreach и методами LINQ (п.2 и п.3).
Вместе с тем, мы объявим необходимость реализации интерфейса IDisposable, который позволит вызывать код очистки ресурсов при завершении работы с объектом (п.5 и п.6).
public interface IFeatureCursorEx : IFeatureCursor, IEnumerable<FeatureEx>, IEnumerator<FeatureEx>, IDisposable

{
}

Что касается реализации.
Для возможности "перезапуска" курсора мы должны сохранить исходные IFeatureClass, IQueryFilter и параметр Recycling.
Должны автоматически инициализировать стандартный курсор, когда он нужен, иметь возможность его автоматически перезапускать, корректно удалять его после прохода по всем записям, а также при уничтожении самого объекта.

Не буду копировать сюда весь код класса, потому что он достаточно объемный, хоть и не сложный, и ссылка на исходник приведена далее. Рассмотрим лишь реализацию метода получения очередного объекта:
public IFeature NextFeature()

{
    _current = null;

    if (_isAttached == false)
        Attach();

    IFeature pFeature = _cursor.NextFeature();
    if (pFeature != null)
        _current = new FeatureEx(pFeature);
    else
        Detach();

    return _current;
}

Проверяем, подключен (инициализирован) ли курсор. Если нет, то инициализируем (п.4).
Если следующий объект оказывается равен null, то это означает, что записи кончились. В этом случае мы отключаем (удаляем) курсор (п.5, п.6).

Заметьте, что мы возвращаем созданный объект FeatureEx.
FeatureEx обёртывает IFeature с целью добавления индексаторов (п.1).
Класс FeatureEx обеспечивает поддержку сразу двух типов индексаторов, числового:
public object this[int index]

{
    get
    {
        return this.get_Value(index);
    }
    set
    {
        this.set_Value(index, value);
    }
}

и текстового:
public object this[string name]

{
    get
    {
        return this.get_Value(this.Fields.FindField(name));
    }
    set
    {
        this.set_Value(this.Fields.FindField(name), value);
    }
}

Заметьте, этот индексатор каждый раз заново определяет номер поля. Для увеличения производительности попробуйте кэшировать значения либо с помощью обобщенного Dictionary<TKey, TValue>, либо, например, с помощью HybridDictionary.

Поскольку объект, реализующий IFeature может реализовывать ещё ряд интерфейсов, которые мы оборачивать не собираемся, то наша обёртка должна обеспечивать возможность получения исходного объекта (свойство Feature).
public IFeature Feature

{
    get
    {
        return _feature;
    }
    private set
    {
        _feature = value;
    }
}

Ну а все прочие свойства просто оборачиваются:
public IObjectClass Class

{
    get { return _feature.Class; }
}


Всё.
Вернёмся
0 голосов
ответил 15 Март, 10 от pooperec (10,820 баллов)
Про расширение и удобства всё правильно написал, но ИМХО это личное дело каждого кому, что, как и куда использовать =)

А вот типкаст я всёже склонен использовать вида (Делфи):
var
p : IPolygon;
Begin
   (p as IGeometry).методы IGeometry
End;

Но более безопасно с точки зрения отлова и предотвращения ошибок:
var
x : IGeometry;
p : IPolygon;
Begin
  p.QueryInterface(IGeometry, x);
End;

0 голосов
ответил 15 Март, 10 от TDenis (42,620 баллов)
Про расширение и удобства всё правильно написал, но ИМХО это личное дело каждого кому, что, как и куда использовать =)

Так ведь я же и не заставляю =)
Лично мне стало надоедать, я написал как можно выкрутиться в .net, может кому интересно будет.
Но более безопасно с точки зрения отлова и предотвращения ошибок:
var
x : IGeometry;
p : IPolygon;
Begin
  p.QueryInterface(IGeometry, x);
End;

В C# знаю два метода приведения. Первый:
(IPolygon)pGeometry
если не приводится - кидает exception.
второй:
pGeometry as IPolygon
работает только для ссылочных типов, и если не получается привести - просто возвращает null.

А в чем смысл QueryInterface, чем он безопасен?
    
0 голосов
ответил 15 Март, 10 от pooperec (10,820 баллов)
> А в чем смысл QueryInterface, чем он безопасен?

В Делфи приведение object as IPolygon выдает ошибку тайпкаста, а QueryInterface при ошибке просто выдаст null.

Так что, как оказываеться, не принципиально =)
0 голосов
ответил 31 Март, 10 от BodyZ (460 баллов)
Получение IApplication
А какие модули необходимо подключить к проекту для получения IApplication. У меня выбивает ошибку, т.е не понимает что такое IApplication.
0 голосов
ответил 31 Март, 10 от TDenis (42,620 баллов)
Мне кажется, вы каким-то не тем путём идёте) Потому что обычно такого вопроса не возникает.

Вообще есть специальная штуковина под названием ESRI Library Locator. Показывает какие объекты в каких сборках.
0 голосов
ответил 21 Апр, 10 от BodyZ (460 баллов)
Подскажите каким образом можно к существующему слою добавить программно точку. Заранее спасибо
0 голосов
ответил 21 Апр, 10 от pooperec (10,820 баллов)
pFeature:=pFeatureLayer.FeatureClass.CreateFeature;
pFeature.Shape:=pGeometry;
pFeature.Store;
0 голосов
ответил 21 Апр, 10 от BodyZ (460 баллов)
А нельзя ли поподробнее. Может есть ссылка на исходник, где выполняется программное добавление точки к существующему слою. Спасибо
Добро пожаловать на сайт Вопросов и Ответов, где вы можете задавать вопросы по GIS тематике и получать ответы от других членов сообщества.
...