Что-то все больше меня уносит в сторону ПО, работающего со всякими online-сервисами. Вон было недавно о Google Translate и Yahoo! Maps, а сегодня буду рассказывать о том как работать с сервисом хостинга фотографий Flickr на .NET Compact Framework.

Итак, предметом нашего внимания сегодня будет библиотека Flickr.NET, которую можно найти на CodePlex. Flickr.NET – это библиотека с открытым исходным кодом, есть версия для .NET Compact Framework. Вот ее мы и будем использовать.

Для начала Создаем новый проект для Smart Device на C#.

Затем распаковываем исходный код Flickr.NET в папку с созданным решением (рядом с папкой, в которой находится исходный код только что созданного проекта).

После этого добавляем проект FlickrNetCF в решение

Добавляем ссылку на проект FlickrNetCF в список зависимостей нашего приложения (с помощью Project -> Add Reference).

Ну вот, теперь можно работать.

Рисуем вот такую форму:

На форме:

  • Поле ввода запроса для поиска изображений
  • Кнопка поиска
  • Список (ListView) для просмотра информации о найденных изображениях
  • Кнопка открытия формы для просмотра полноразмерного изображения

Теперь можно заняться, собсьвенно, работой с Flickr. Для работы с сервисом в библиотеке FlickrNetCF есть класс Flickr. Для работы ему необходимы:

  • Application Key
  • Secret Key

Получить эти два ключа можно вот по этому адресу.

Создаем объект для работы с сервисом

using FlickrNet;
...
Flickr _flickr = new Flickr();
_flickr.ApiKey = "<здесь будет ваш API Key>";
_flickr.ApiSecret = "<здесь будет ваш Secret Key>";

Теперь нам неплохо бы обеспечить поиск изображений и отображение превью, а также информации об изображении. Загрузка превью – процесс длительный, поэтому обрабатывать информацию, полученную от сервиса будем в отдельном потоке:

Thread _downloadThread;
string _searchText = "";
...
private void StartDownloadingPreviws()
    {
    if (_downloadThread != null)
    {
        _downloadThread.Abort();
    }
    photoInfoListView.Items.Clear();
    photoThumbnailList.Images.Clear();
    _searchText = searchTextBox.Text;
    _downloadThread = new Thread(new ThreadStart(DownloadPhotosDelegate));
    _downloadThread.Start();
}

Для поиска изображений в классе Flickr предусмотрен метод PhotosSearchText(), который в качестве параметра принимает строку поискового запроса. Именно этот метод мы и будем использовать для получения информации о найденных фотографиях. Метод PhotosSearchText() возвращает объект класса Photos, из которого мы можем получить всю информацию о найденных изображениях (свойство PhotoCollection класса Photos возвращает массив объектов Photo, каждый из которых содержит полную информацию об одном изображении, например, название, ссылку на превью, ссылку на полноразмерное изображение и др.)

void DownloadPhotosDelegate()
{
    try
    {
        Photos _photos = _flickr.PhotosSearchText(_searchText);
        foreach (Photo photo in _photos.PhotoCollection)
        {
            MemoryStream photoStream = new MemoryStream();
            WebRequest request = HttpWebRequest.Create(photo.SquareThumbnailUrl);
            WebResponse response = request.GetResponse();
            MemoryStream stream = new MemoryStream();
            Stream responseStream = response.GetResponseStream();
            int readCount = 0;
            byte[] buffer = new byte[1024];
            while ((readCount =
                responseStream.Read(buffer, 0, buffer.Length)) > 0)
            {
                stream.Write(buffer, 0, readCount);
            }
            response.Close();

            Bitmap bmp = new Bitmap(stream);
            string originalURL = photo.LargeUrl;
            string title = photo.Title;
            Invoke(new AddItemDelegate(AddItemToList),
                new object[] { bmp, title,  originalURL})
        }
    }
    catch (ThreadAbortException)
    {
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message);
    }
}

Т.к. в .NET неглавный поток не может работать с элементами GUI, то добавление превью изображений в список нам необходимо делать в отдельном методе, который будет вызываться посредством Invoke():

Invoke(new AddItemDelegate(AddItemToList), new object[] { bmp, title,  originalURL});
...
private delegate void AddItemDelegate(Bitmap bmp, string name, string originalURL);

Bitmap ResizeBitmap(Bitmap b, System.Drawing.Size size)
{
    Bitmap result = new Bitmap(size.Width, size.Height);
    using (Graphics g = Graphics.FromImage((Image)result))
        g.DrawImage(b, 
            new Rectangle(0, 0, result.Width, result.Height), 
            new Rectangle(0, 0, b.Width, b.Height), 
            GraphicsUnit.Pixel);
    return result;
}

void AddItemToList(Bitmap bmp, string name, string originalURL)
{
    try
    {
        photoThumbnailList.Images.Add(bmp);
        ListViewItem newItem = new ListViewItem(name);
        newItem.ImageIndex = photoThumbnailList.Images.Count - 1;
        newItem.Tag = originalURL;
        photoInfoListView.Items.Add(newItem);
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message);
    }
}

ОК, с главной формой пока закончили. теперь приступим к созданию формы для просмотра полноразмерного изображения.
Добавляем в проект новую форму. На нее кладем Panel (Dock = Fill, AutoScroll = true), на Panel кладем PictureBox.
Теперь нам нужно обеспечить загрузку изображения в отдельном потоке. Подход приблизительно такой же как и в главной форме:

private Thread _downloader;
private string _imageURL = "";
Bitmap _sourceBitmap;

public string ImageURL
{
    get
    {
        return _imageURL;
    }
    set
    {
        _imageURL = value;
    }
}

public ImageViewForm()
{
    InitializeComponent();
}

void OnImageDownloaded(Bitmap bmp)
{
    _sourceBitmap = bmp;
    imageBox.Image = bmp;
    imageBox.Width = bmp.Width;
    imageBox.Height = bmp.Height;
}

private delegate void ImageDownloadedDelegate(Bitmap bmp);

void DownloadImage()
{
    try
    {
        do
        {
            if (ImageURL == null || ImageURL.Length == 0) break;
            WebRequest request = HttpWebRequest.Create(ImageURL);
            WebResponse response = request.GetResponse();
            Bitmap bmp = new Bitmap(response.GetResponseStream());
            response.Close();
            Invoke(new ImageDownloadedDelegate(OnImageDownloaded),
                new object[] { bmp });
        }
        while (false);
    }
    catch (ThreadAbortException)
    {
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message);
    }
}

public void StartDownloading()
{
    _downloader = new Thread(new ThreadStart(DownloadImage));
    _downloader.Start();
}

private void ImageViewForm_Closing(object sender, CancelEventArgs e)
{
    if (_downloader != null)
    {
        _downloader.Abort();
    }
}

Теперь пишем обработчик нажатия кнопки открытия формы просмотра полноразмерного изображения в главной форме

private void showPictureButton_Click(object sender, EventArgs e)
{
    do
    {
        if (photoInfoListView.SelectedIndices.Count == 0) break;
        ListViewItem item = photoInfoListView.Items[photoInfoListView.SelectedIndices[0]];
        if (item == null) break;
        string url = (item.Tag as string);
        if (url == null || url.Length == 0) break;
        ImageViewForm dlg = new ImageViewForm();
        dlg.ImageURL = url;
        dlg.StartDownloading();
        dlg.ShowDialog();
    }
    while (false);
}

Ну вот, собственно и все. В результате мы получим вот такое приложение:


Исходный код тестового приложения, а также исполняемый файл можно скачать здесь.

Previous ArticleNext Article
Технический директор IT-Dimension, компании-разработчика кросс-платформенного программного обеспечения

Leave a Reply

Your email address will not be published. Required fields are marked *

К.

Кроссплатформенная разработка для мобильных с Xamarin

Введение

Совсем недавно компания Xamarin анонсировала выход новой версии своего инструментария для кроссплатформенной разработки мобильных приложений, но вменяемых статей на русском по этой тематике так и нет. На Хабре появился небольшой обзор, не связанный с кодингом, там же была пара попыток рассказать об этом чуть подробнее, но дальше процесса создания Hello World приложения дело не зашло. А жаль. В этот раз мы попробуем исправить это досадное недоразумение.

Начало работы

Инсталлятор Xamarin устанавливает плагин к Visual Studio, который позволяет разрабатывать приложения для популярных мобильных платформ в привычном разработчику окружении. Также устанавливается отдельная среда разработки Xamarin Studio, которая, судя по всему, является модифицированной версией MonoDevelop. Мне привычнее работать в Visual Studio поэтому примеры в этой статье будут показаны с использованием именно этой среды разработки.

После установки в Visual Studio добавляются шаблоны проектов для мобильных приложений под Android и iOS (поддерживается создание как специализированных приложений для iPad и iPhone, так и универсальных приложений, а также приложений, использующих OpenGL). Для создания приложений для iOS в Visual Studio прийдется, правда, заплатить. Этот функционал доступен либо в режиме trial, либо в business-версии инструментария, а это стоит $999 в год.

После создания проекта мы получаем все тот же API для каждой платформы, который мы имеем при нативной разработке, но синтаксис будет на C#, к тому же есть возможность использовать базовые типы .NET Framework, синтаксический сахар и прочие плюшки .NET.

Разработка для Android

После создания Android-проекта мы получаем набор файлов, в котором есть класс главного окна и набор ресурсных файлов. После длительной работы в Eclipse немного раздражает название папок в PascalCase, но к этому можно довольно быстро привыкнуть. Также есть отличия в работе с файлами ресурсов. Для того, чтобы встроенный дизайнер окон понимал файлы ресурсов с лайаутами, у них расширение изменено на .AXML вместо привычного .XML в Eclipse. Это довольно сильно раздражает, особенно если рисовать лайауты в Eclipse, а потом переносить в Visual Studio в случае если Eclipse’овский дизайнер окон больше нравится.

Встроенный дизайнер окон мне лично показался неудобным. Медленный, часто валит всю IDE, я так и не понял, как в нем по-простому переключиться между XML-видом и UI. Здесь уж точно можно сказать что писано чужими для хищников. Я для себя решил, что в Eclipse мне удобнее, привычнее, да и на экран ноутбука в Eclipse помещается больше полезной информации. Может кому-то дизайнер Visual Studio и понравится больше, на вкус и цвет фломастеры разные.

Activities

Код на C# для Mono for Android очень схож с кодом на Java.

namespace XamarinDemo.Android
{
    [Activity(Label = "XamarinDemo.Android", MainLauncher = true, Icon = "@drawable/icon")]
    public class MainActivity : Activity
    {
        int count = 1;

        protected override void OnCreate(Bundle bundle)
        {
            base.OnCreate(bundle);
            SetContentView(Resource.Layout.Main);
            Button button = FindViewById<Button>(Resource.Id.MyButton);
            button.Click += delegate { button.Text = string.Format("{0} clicks!", count++); };
        }
    }
}

Отличия, которые сразу видны:

  • Для регистрации activity не надо ничего прописывать в манифесте. Для этого используется аннотация [Activity]
  • Для того, чтобы сделать activity стартовой, необходимо для аннотации [Activity] указать параметр MainLauncher. Это будет равносильно заданию action = android.intent.action.MAIN и category = android.intent.category.LAUNCHER в манифесте
  • Основные методы жизненного цикла activity (да и вобще все методы) имеют названия в PascalCase.
Элементы управления и обработка событий

Для получения ссылок на элементы управления, как и в Java-версии, используется метод FindViewById(). Очень порадовало наличие generic-версии этого метода, который возвращает объект нужного типа и позволяет избавиться от c-cast’ов.
Вместо listener’ов, а точнее, в дополнение к ним, для подключения обработчиков событий используются делегаты. Можно использовать и listener’ы, но это, во-первых, не .NET way, во-вторых, требует написания большего количества кода, даже по сравнению с Java. К тому же делегатов можно подключить несколько:

Button button = FindViewById<Button>(Resource.Id.MyButton);
button.Click += delegate { button.Text = string.Format("{0} clicks!", count++); };
button.Click += (o, e) => 
    {
        Toast.MakeText(this, string.Format("{0} clicks!", count++), ToastLength.Long).Show(); 
    };

Не со всеми обработчиками событий в виде делегатов дела обстоят радужно. В одном из проектов обнаружилась проблема с событием View.ViewTreeObserver.GlobalLayout, у которого при использовании оператора -= не отключались делегаты-обработчики. Да вообще никак не отключались. Пришлось использовать IOnGlobalLayoutListener.

Использование Java-библиотек в .NET-проекте

В Mono for Android есть возможность использовать существующие JAR-файлы в приложении на C#. Для этих целей предусмотрен специальный тип проекта: Java Binding Library.
Для того, чтобы использовать JAR-библиотеку необходимо:

  • Создать проект Java Binding Library в решении.
  • В папку Jars положить нужный JAR-файл (в JAR-библиотеке не должно быть AndroidManifest.xml, ибо наличие этого файла может повлечь ряд неприятных последствий. Подробнее об этом можно узнать здесь).
  • Указать Build Action для JAR-файла как EmbeddedJar.
  • В проекте Android-приложения добавить в References проект созданной Java Binding Library.

После этого можно использовать классы из JAR-файла. Необходимо учитывать, что имена пакетов в C# и в исходном Java-коде могут немного отличаться. Так, например, пакет com.example.androiddemolib из Java-кода будет переименован на Com.Example.Androiddemolib (то есть будет преобразован в PascalCase).
Неплохую инструкцию по использованию Java-библиотек можно почитать здесь.

Работа с базами данных

Для работы с базами данных в Mono for Android предусмотрены используются классы пространства имен Mono.Data.Sqlite (используются для доступа к базам данных SQLite) и System.Data.SqlClient (для доступа к Microsoft SQL Server). Классы пространства имен System.Data.SqlClient доступны только в Business-редакции инструментов разработки. Можно также использовать классы-обертки над родным Java API для Android и сторонние разработки, например sqlite-net, в котором доступен асинхронный вариант API.
Пользоваться доступным API довольно просто. Все очень схоже с разработкой для настольных ПК:

namespace XamarinDemo.Android
{
    [Activity(Label = "XamarinDemo.Android", 
        MainLauncher = true, Icon = "@drawable/icon")]
    public class MainActivity : Activity
    {
        SqliteConnection GetConnection(string path)
        {
            SqliteConnectionStringBuilder builder = 
                new SqliteConnectionStringBuilder();
            if (!File.Exists(path))
            {
                FileInfo info = new FileInfo(path);
                if (!Directory.Exists(info.Directory.FullName))
                {
                    Directory.CreateDirectory(info.Directory.FullName);
                }
                SqliteConnection.CreateFile(path);
            }
            builder.DataSource = path;
            return new SqliteConnection(builder.ToString());
        }

        protected override void OnCreate(Bundle bundle)
        {
            base.OnCreate(bundle);
            SetContentView(Resource.Layout.Main);

            try
            {
                string path = GetDatabasePath("xamarindemo.sqlite").Path;
                SqliteConnection connection = GetConnection(path);
                connection.Open();
                SqliteCommand command = connection.CreateCommand();
                command.CommandType = CommandType.Text;
                command.CommandText = "CREATE TABLE IF NOT EXISTS DemoTable(" +
                    "id INTEGER AUTO_INCREMENT PRIMARY KEY NOT NULL" +
                    ", name VARCHAR(32))";
                command.ExecuteNonQuery();
                connection.Close();
            }
            catch (Exception e)
            {
                System.Diagnostics.Debug.WriteLine(e.ToString());
            }
        }
    }
}

Разработка для iOS

Элементы управления и обработчики событий

Создание элементов управления в коде ничем не отличается от аналогичной работы в Objective-C. Обработчики событий можно навешивать также как и в Android – с помощью делегатов. Также можно добавлять обработчики как в Objective-C через селекторы.

namespace XamarinDemo.iOS
{
    public class MyViewController : UIViewController
    {
        UIButton button;
        …

        public MyViewController()
        {
        }

        public override void ViewDidLoad()
        {
            base.ViewDidLoad();
            …
            button = UIButton.FromType(UIButtonType.RoundedRect);
            …
            button.AddTarget(this, 
            	new Selector("ButtonTouchInside"), 
            	UIControlEvent.TouchUpInside);
            …
            button.TouchUpInside += (object sender, EventArgs e) =>
            {
                button.SetTitle(String.Format(
            		"clicked {0} times", numClicks++), UIControlState.Normal);
            };
            …

            View.AddSubview(button);
        }

        [Export("ButtonTouchInside")]
        void OnButtonTouchInside()
        {
            Console.WriteLine("Hello!");
        }

    }
}
Использование нативных библиотек

В Xamarin iOS, также как и для Android, доступна возможность использования нативных библиотек. Для этих целей есть специальный тип проекта – iOS Binding Project, в который можно добавлять статические библиотеки, после чего они будут слинкованы вместе с основным проектом.

В общем виде, для использования нативной библиотеки в C# проекте необходимо сделать следующее:

  • Создать проект статической библиотеки в XCode.
  • Если планируете отлаживать C# проект в симуляторе, то в настройках проекта статической библиотеки необходимо добавить архитектуру i386 в список Valid Architectures.
  • Написать нужную логику (допустим у нас есть один экспортируемый класс):
    @interface iOSDemoClass : NSObject
    - (NSInteger) addData: (NSInteger) value1 andOtherValue: (NSInteger) value2;
    @end
    …
    @implementation iOSDemoClass
    -(NSInteger) addData: (NSInteger) value1 andOtherValue: (NSInteger) value2
    {
        return value1 + value2;
    }
    @end
    
    
  • Закрыть проект в Xcode
  • Собрать раздельно статическую библиотеку для нужных архитектур (например таким образом):
    xcodebuild -project iOSDemoLib.xcodeproj \
    -target iOSDemoLib -sdk iphonesimulator \
    -configuration Release clean build
    xcodebuild -project iOSDemoLib.xcodeproj \
    -target iOSDemoLib -sdk iphoneos -arch armv7 \
    -configuration Release clean build
    
  • Сделать универсальную библиотеку для нескольких платформ из библиотек, получившихся на предыдущем шаге:
    lipo -create -output ../iOSDemoLib.Binding/libiOSDemoLib.a ../libiOSDemoLib-i386.a ../libiOSDemoLib-iphone-armv7.a
    
  • Создать проект iOS Binding Project в C#-решении и добавить туда универсальную статическую библиотеку. После этого будет автоматически создан файл [libname].linkwith.cs с правилами линковки, которые выглядят приблизительно так:
    [assembly: LinkWith ("libiOSDemoLib.a", LinkTarget.Simulator | LinkTarget.ArmV7, ForceLoad = true, Frameworks = "Foundation")]
    
  • В файле ApiDefinition.cs (создается автоматически) прописать правила привязки нативных классов к .NET-типам (обращаю внимание на то что комплексные имена методов должны содержать двоеточия в местах, где в Objective-C должны быть параметры метода. То есть в методе с двумя параметрами будет два двоеточия):
    namespace iOSDemoLib.Binding
    {
        [BaseType (typeof(NSObject))]
        interface iOSDemoClass 
        {
            [Export("addData:andOtherValue:")]
            int AddDataAndOtherValue(int value1, int value2);
        }
    }
    
  • В проекте приложения, в котором планируется использовать библиотеку, добавить проект iOS Binding в References.
  • Собрать, запустить, быть счастливым.

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

Работа с базами данных

Для работы с базами данных в iOS используются те же пространства имен, что и в Android. API, соответственно, такой же. Небольшая разница может быть в мелочах. Например, для получения пути к файлу базы данных SQLite в Android есть специальный API-вызов:

string path = GetDatabasePath("xamarindemo.sqlite").Path;

В iOS же нужно использовать стандартные средства .NET:

string path = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.Personal), 
       "xamarindemo.sqlite");
Удаленная отладка iOS приложений из Visual Studio

В Xamarin 2.0 есть возможность отладки мобильных приложений для iOS прямо из Visual Studio. Для того, чтобы это стало возможно, необходимо иметь в локальной сети Mac с установленным XCode, iOS SDK и Xamarin Studio. Никаких дополнительных настроек производить не надо, достаточно при открытии iOS проекта в Visual Studio выбрать нужный build-сервер из списка доступных.

К сожалению данный подход не заработал с виртуальной машиной, запущенной на том же компьютере что и Visual Studio. Хотя с обычным Mac’ом в локальной сети все замечательно работает. Причин или объяснений этому пока найти не удалось, пытаюсь общаться с разработчиками по этому поводу.

Также не очень понятно, как организовывать UI-тестирование и проверку работоспособности приложения из Visual Studio. Симулятор запускается на Mac’е, похоже, что без VNC здесь не обойтись.

Кроссплатформенные библиотеки классов (Portable Class Libraries)

Xamarin предоставляют возможность создания библиотек, которые могут быть использованы (в виде кода или готовых сборок) сразу для нескольких платформ. Такие библиотеки называются Portable Class Library (PCL).

Для разработки подобных библиотек используется специальный урезанный вариант .NET Runtime и это чудесный инструмент для разработчика, значимость которого трудно переоценить, но здесь тоже не все так просто. По умолчанию в Visual Studio нельзя указать Android и iOS в качестве поддерживаемых платформ для PCL-проекта. Но это не значит, что Visual Studio сразу становится бесполезной в этом плане.

Проблему можно решить путем создания XML-файлов в папке C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETPortable\v4.0\Profile\Profile104\SupportedFrameworks для x64 систем или в папке C:\Program Files\Reference Assemblies\Microsoft\Framework\.NETPortable\v4.0\Profile\Profile104\SupportedFrameworks для x86 систем.
Android файл MonoAndroid,Version=v1.6+.xml

<?xml version="1.0" encoding="utf-8"?>
<Framework DisplayName="Mono for Android"
  Identifier="MonoAndroid"
  Profile="*"
  MinimumVersion="1.6"
  MaximumVersion="*" />

iOS файл VSMonoTouch,Version=v1.0+.xml

<?xml version="1.0" encoding="utf-8"?>
<Framework DisplayName="VS MonoTouch"
  Identifier=".NETFramework"
  Profile="*"
  MinimumVersion="1.0"
  MaximumVersion="1.0" />

После создания этих двух файлов и перезапуска Visual Studio можно будет указывать Android и iOS в качестве поддерживаемых платформ для новых PCL-проектов или добавить эти платформы в существующие проекты через Properties -> Library -> Target Framework.
Но это еще не все нюансы.

Если вы хотите поддержку Android и iOS в PCL-проекте, то придётся отказаться от поддержки Xbox 360 и Windows Phone 7 (зато можно поддерживать Windows Phone 7.5+). Для Xbox придётся создавать еще один проект и в нем добавлять файлы из существующего PCL-проекта в виде ссылок. Или, как вариант, в одном PCL-проекте оставить все платформы от Microsoft (включая Xbox 360), а в другом оставить только iOS и Android.

Есть еще такая проблема что PCL-проект, добавленный из Visual Studio под Windows не будет участвовать в сборке решения, если открыть его в Xamarin Studio под OS X. Проект будет неактивен и будет выводиться сообщение (not built in active configuration). Решается эта проблема удалением проекта из решения и добавлением заново.

Новые возможности языка и поддержка C#5

В разработке для Android актуальным является вынесение работы с файлами, базами данных, сетью и т.д., то есть с длительными операциями, в отдельный поток. В C# 5 для реализации асинхронных операций предусмотрены специальные возможности, а именно async/await. К сожалению, в текущей версии Mono for Android и MonoTouch этих возможностей нет. Из-за этого многие довольно интересные библиотеки нельзя использовать в том виде, который для них предусмотрен. Например для работы с асинхронным API библиотеки sqlite-net приходится делать несколько финтов ушами. Радует то, что эти возможности должны стать доступны через несколько месяцев с переходом Xamarin на C# 5.

Дополнительные компоненты и библиотеки

Xamarin помимо, собственно, продажи инструментария для разработки ПО, открыли магазин по продаже сторонних компонент, многие из которых действительно очень упрощают жизнь разработчику. В магазине доступны как платные, так и бесплатные библиотеки и темы оформления. Полезность, например, бесплатных Xamarin.Auth, Xamarin.Social и Xamarin.Mobile трудно переоценить. Есть возможность публиковать собственные компоненты в этом магазине.

Неприятные моменты

Из проблемных моментов наиболее заметными являются:

  • Жуткие тормоза при отладке Android-приложений, когда на довольно мощной машине с Core i7 и 8 GB RAM при останове на брейкпоинте Visual Studio зависает на 10-15 секунд. Подобное может повторяться на каждом шаге отладчика.
  • Отсутствие в списке устройств, доступных для отладки приложений, некоторых устройств, подключенных к компьютеру (которые, тем не менее, видны через adb devices и на которых Eclipse отлично запускает приложения)
  • Произвольные отпадания отладчика и необходимость перезапуска приложения в связи с этим.
  • Отсутствие статей и учебных материалов (да, есть какие-то материалы на официальном сайте, но они покрывают не все популярные задачи)
  • Наличие багов, о которых узнаешь ВНЕЗАПНО! после общения на форуме. Хотя на том же официальном форуме разработчики довольно бодро отвечают на вопросы и вообще молодцы.
  • Цена на Business-редакцию очень кусается. К тому же лицензия на год, а не навсегда (немного скрашивает ситуацию наличие скидок при покупке лицензии сразу для нескольких платформ).

Приятные моменты

  • При покупке лицензии сразу для нескольких платформ или сразу для нескольких рабочих мест, предусмотрена скидка.
  • С выходом новой версии, в которой будет поддержка async\await разработка действительно станет проще и будет возможность использовать кучу полезных библиотек, не меняя их код.

Выводы

Инструментарий от Xamarin таки да, действительно работает и если вы планируете разрабатывать несколько приложений в год или же планируемая прибыль от разрабатываемого приложения больше чем $2k, то Xamarin SDK явно может облегчить вам жизнь и избавить от двойной работы над кодом для каждой платформы.

С другой же стороны, для Indy-разработчика цена в $1k для каждой платформы мне, например, кажется чрезмерной т.к. есть много нюансов, которые необходимо знать и\или прочувствовать на себе перед началом разработки, есть баги, которые неизвестно в каком режиме будут исправляться и неизвестно насколько их наличие может замедлить разработку конкретно вашего проекта.

Немного клёвых ссылочек!

UPD: Уточнение по поводу стоимости лицензии:

Лицензия бессрочная, но включает в себя один год бесплатных обновлений. Я им писал в саппорт за разъяснениями. Вот их ответ

Your license is perpetual. You have to purchase a renewal every year to get new updates.

К.

Кросс-платформенная разработка — Windows Mobile и Windows (.NET Compact Framework, C#)

В этот раз статья от Андрея Коновалова о том, как сделать мультиплатформенный проект в Visual Studio с использованием .NET Compact Framework.
Не так много разработчиков осознают, что разрабатывая приложения для платформы Windows Mobile с использованием Compact Framework, у них существуют шансы собрать это же приложение под десктоп версию Windows! Я и сам об этом долгое время только задумывался, предполагая, что подобная возможность есть, но не рассматривал её как нечто, хоть сколько-нибудь реальное.

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

Во-первых, во-вторых, в-третьих…

Во-первых, необходимо изначально создавать приложение для Windows Mobile (т.е. это основная платформа). Это действительно важно. Причин несколько, но основная заключается в том, что Compact Framework на то и компактный, что там существенно меньше классов и свойств у классов. Т.е. совместимость с десктопом есть, но односторонняя, т.е. только в сторону десктопа.

Во-вторых, нужно понимать, что отличия в приложении всё-таки будут и их придётся программировать отдельно. Например, стандартное меню, находящееся внизу на Windows Mobile автоматически перемещается наверх, и там Cancel и More выглядят не очень привлекательно. Далее, на десктопе в принципе нет InputPanel. Т.е. по сути нужно быть готовым к инструкциям компилятору #if #else #endif.

В-третьих, надо также готовиться к тому, что будут некоторые ограничения, касающиеся дизайна форм. А именно, нельзя открывать форму визуальным редактором при десктопе, выбранном в качестве текущего таргета — сразу же в *.Designer.cs налетит множество свойств, не поддерживаемых мобильным фреймворком — придётся откатывать.

В-четвёртых, придётся вручную править *.csproj файлы и видеть в Solution Explorer-е жёлтые треугольники — это нормально 🙂

В пятых, не все сборки и неймспесы на 100% работают на десктопе. Например, я совершенно не уверен в том, что SQL Server Compact собирается на десктопе. Не проверял, поэтому не обещаю. Точно знаю, что с SQLite всё хорошо (хотя и придётся попотеть немного).

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

Попробуем разобраться с основными тонкостями.

Desktop Target

Начнём с того, что у нас должен быть некоторый проект, созданный для Compact Framework. Создадим новый таргет через Build->Configuration Manager:

Windows Mobile and Windows - Configuration ManagerПосле этого добавим символ условной компиляции в свойствах проекта:

Windows Mobile and Windows - Project SettingsНачало положено. Посмотрим теперь, что делать, чтобы обеспечить подключение правильных сборок в нужном таргете. По умолчанию в нашем csproj файле нет никаких разделений по таргетам:

 <ItemGroup>
  <Reference Include="mscorlib" />
  <Reference Include="System" />
  <Reference Include="System.Data" />
  <Reference Include="System.Drawing" />
  <Reference Include="System.Windows.Forms" />
  <Reference Include="System.Xml" />
 </ItemGroup>

Чтобы иметь полный контроль, необходимо организовать примерно следующий финт:

<ItemGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
  <Reference Include="mscorlib">
   <Private>False</Private>
  </Reference>
  <Reference Include="Microsoft.WindowsMobile, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35">
   <SpecificVersion>False</SpecificVersion>
   <HintPath>..\..\..\..\..\Program Files\Windows Mobile 6 SDK\Managed Libraries\Microsoft.WindowsMobile.dll</HintPath>
  </Reference>
  <Reference Include="Microsoft.WindowsMobile.Status, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35">
   <SpecificVersion>False</SpecificVersion>
   <HintPath>..\..\..\..\..\Program Files\Windows Mobile 6 SDK\Managed Libraries\Microsoft.WindowsMobile.Status.dll</HintPath>
  </Reference>
  <Reference Include="Microsoft.WindowsCE.Forms">
   <Private>True</Private>
  </Reference>
  <Reference Include="System" />
  <Reference Include="System.Data">
   <Private>False</Private>
  </Reference>
  <Reference Include="System.Windows.Forms" />
  <Reference Include="System.Drawing" />
  <Reference Include="System.Data.SQLite, Version=1.0.60.0, Culture=neutral, PublicKeyToken=1fdb50b1b62b4c84, processorArchitecture=MSIL">
   <Private>True</Private>
   <HintPath>..\..\..\..\..\Program Files\SQLite.NET\bin\CompactFramework\SQLite.Interop.060.DLL</HintPath>
  </Reference>
 </ItemGroup>

 <ItemGroup Condition=" '$(Configuration)|$(Platform)' == 'Desktop|AnyCPU' ">
  <Reference Include="mscorlib">
   <Private>False</Private>
   <HintPath>C:\Windows\Microsoft.NET\Framework\v2.0.50727\mscorlib.dll</HintPath>
  </Reference>
  <Reference Include="System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089, processorArchitecture=x86">
   <HintPath>C:\Windows\Microsoft.NET\Framework\v2.0.50727\System.dll</HintPath>
  </Reference>
  <Reference Include="System.Windows.Forms">
   <HintPath>C:\Windows\Microsoft.NET\Framework\v2.0.50727\System.Windows.Forms.dll</HintPath>
  </Reference>
  <Reference Include="System.Drawing">
   <HintPath>C:\Windows\Microsoft.NET\Framework\v2.0.50727\System.Drawing.dll</HintPath>
  </Reference>
  <Reference Include="System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089, processorArchitecture=x86" />
  <Reference Include="System.Data.SQLite, Version=1.0.60.0, Culture=neutral, PublicKeyToken=db937bc2d44ff139, processorArchitecture=x86">
   <Private>True</Private>
   <HintPath>..\..\..\..\..\Program Files\SQLite.NET\bin\System.Data.SQLite.DLL</HintPath>
  </Reference>
 </ItemGroup>

Как видно, мы разделяем блоки ItemGroup по таргетам и указываем разные пути до сборок. Признаюсь, что именно здесь я возился дольше всего в моём проекте. Правильные версии выдирались прямо из сообщений об ошибках компиляции 😉 Т.е. компилятор ругался, что не находятся такие-то нужные сборки и я подставлял правильные значения. И именно здесь я несколько раз порывался бросить всё, т.к. гуглятся подобные ошибки очень плохо.

В результате хитрых манипуляций с csproj файлом получается такая ерунда в Solution Explorer:
Windows Mobile and Windows - Warnings
Это нормально, т.к. Visual Studio не совсем пригодна к таким извращениям (хотя и позволяет их в итоге).

#if #endif

Когда сборки подключаются правильные, мы имеем возможность для каждой платформы использовать тот максимум, который каждая из платформ поддерживает. Напомню, однако, что автоматическая кодогенерация дизайнера форм нам тут всё портит. Поэтому необходимо, по возможности, сначала всё надизайнить, а потом уже править только руками — ведь при перегенерации *.Designer.cs, студия не сохраняет наши правки и добавления #if endif участков.

Немного хитрый момент. Мой проект поддерживает как QVGA, так и VGA разрешение, однако, в Designer.cs у форм свойство ClientSize всегда соответствует QVGA разрешению. На десктопе же иметь окно размером 240х268 как-то неправильно, особенно, когда есть VGA-скин. Поэтому в конструкторе после InitializeComponent() я помещаю участок условной компиляции:

Size vertSize = new Size(480, 560);
Size horisSize = new Size(640, 480);
[…]
InitializeComponent();
#if Desktop
this.ClientSize = vertSize;
this.FormBorderStyle = FormBorderStyle.Fixed3D;
this.MaximizeBox = false;
this.MouseWheel += new MouseEventHandler(MainController_MouseWheel);
#endif

Как видно, у меня объявлено две переменные типа Size. Зачем это нужно? Просто-напросто, у меня по F9 происходит переключение ClientSize, для эмуляции смены ориентации экрана. Полезно, когда необходимо протестировать OnResize. Да и в конце концов, есть же нетбуки с 800х480, для них ландшафтная ориентация — единственно возможная, чтобы всё поместилось на экране 🙂

Также видно, что MouseWheel тоже обрабатывается только на десктопе.

System.Diagnostics.Conditional

Есть удобный способ указывать, для какого таргета собирать некоторый метод:

[Conditional("Desktop")]
public void SomeDesktopMethod()
{
}

В данном способе хорошо то, что вызовы данного метода могут существовать в коде, но при наличии данного атрибута, вызовы будут просто проигнорированы на других таргетах! Альтернатива с использованием #if #endif предполагает, что везде, где нужен вызов, необходимо проставить проверку, чтобы компилировать или не компилировать вызов на нужной платформе.

Отладка на десктопе

Существует два способа отладки. Первый (неудобный) заключается в том, мы идём в bin\Desktop\, запускаем exe-файл и потом в студии говорим Debug — Attatch to process. Этот способ сначала кажется единственно возможным. Но! Есть мега-хак, неофициальный и неподдерживаемый способ (который, тем не менее, работает как в VS2005, так и в VS2008). Способ следующий:

  • В студии открыть Tools/Options, далее в дереве выбрать Device Tools/Devices.
  • В выпадающем меню выбрать платформу, для которой необходимо организовать отладку на десктопе. Это будет необходимо повторить для всех платформ, где необходимо.
  • Далее нужно выбрать любой из эмуляторов и нажать [Save As…]. Удобно назвать копию “My Computer”.
  • Теперь надо закрыть студию и открыть %USERPROFILE%\Local Settings\Application Data\Microsoft\CoreCon\1.0\conman_ds_platform.xsl файл в текстовом редакторе.
  • В файле необходимо найти элемент, соответствующий свежесозданному “дивайсу”
  • Далее добавляем следующую строку — true сразу после тега .
  • Сохраняем conman_ds_platform.xsl и перезапускаем студию

Ну вот, теперь нам доступен отладчик и все вкусности десктопной отладки.

Заключение

Чтож, в статье перечислены основные подводные камни, с которыми я столкнулся в процессе сборки своего проекта под Windows. Далее всё было делом техники — ловил исключения и разбирался в их причине. Среди них были не найденные пути, которые, очевидно, на десктопе отличаются и т.д., но это всё уже было ничто, по сравнению с начальными проблемами.

PS. Почти всё, что описано выше, я выстрадал в результате долгих поисков, и вот, в самом конце, когда я искал способ отладки на десктопе, я натолкнулся на шикарную статью Дениела Моса о кросс-платформенной компиляции для Compact Framework 🙂 Моя статья ни в коем случае не является переводом, однако, не могу не дать на неё ссылку.

Оригинал статьи на Хабре.